ARTICLE · 1234294
Netty源码分析(22) — Netty 的 SSL/TLS 支持
Netty源码分析(22) — Netty 的 SSL/TLS 支持
一、背景与问题
兄弟们,聊到网络安全,SSL/TLS 绝对是绕不开的话题。在我们日常开发的互联网应用中,从 HTTP 升级到 HTTPS,从 WebSocket 的 wss:// 协议,几乎都离不开 SSL/TLS 加密。
但是问题来了:Netty 作为底层网络框架,它是怎么支持 SSL/TLS 加密的?
如果你用过 Netty 的 SSL 支持,第一反应肯定是往 pipeline 里加一个 SslHandler。没错,就这么简单。但你想过没有:
这个 SslHandler内部是怎么和 JDK 的 SSLEngine 打交道的?SSL 握手的过程在 Netty 里是怎么体现的? Netty 自带的 SslContext封装了什么?为什么有时候你用 openssl比 JDK 自带的 TLS 实现快那么多?
今天我们就来扒一扒 Netty 的 SSL/TLS 实现,从源码层面看看它到底干了什么。
先上张整体架构图,方便大家有个宏观认识:
上图为 SSLHandler 在 Netty 事件处理流程中的位置示意
二、核心源码分析
2.1 从 SslHandler 入手
SslHandler 是 Netty 处理 SSL/TLS 的核心 Handler。它继承自 ByteToMessageDecoder 和 ChannelOutboundHandlerAdapter,所以既能处理入站数据(解密),也能处理出站数据(加密)。
我们来看它的核心字段:
// io.netty.handler.ssl.SslHandlerpublicclassSslHandlerextendsByteToMessageDecoderimplementsChannelOutboundHandler {// 真正的 SSL 引擎 —— 所有加密解密都由它代理privatefinal SslEngine engine;// 最大打包大小,控制单次 SSL 加密的数据量privatefinalint maxPacketBufferSize;// 应用数据的最大缓冲区privatefinalint maxAppDataBufferSize;// 握手超时任务(ScheduledFuture),防止握手卡死privatelonghandshakeTimeoutMillis=10000;// 关闭超时privatelongcloseNotifyTimeoutMillis=3000;// 握手完成后的回调privatefinal Promise<Channel> handshakePromise = newDefaultPromise<>();// 数据是否已解密并准备传播到下一个 Handlerprivatefinal Queue<PendingWrite> pendingUnencryptedWrites;// ========= 核心方法 =========/** * 当数据从网络到达时,先由 decode() 解密, * 再传递给 pipeline 下游 */@Overrideprotectedvoiddecode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out)throws SSLException {// 把入站数据喂给 SSLEngine 进行解密 unwrap(ctx, in, out); }/** * 写出站数据时,拦截并进行加密 */@Overridepublicvoidwrite(ChannelHandlerContext ctx, Object msg, ChannelPromise promise)throws Exception {// 如果不是 ByteBuf,直接透传if (!(msg instanceof ByteBuf)) { ctx.write(msg, promise);return; }// 检查握手是否完成if (engine.isWrapNeededAfterHandshakeComplete()) {// 加密后发送 wrap(ctx, msg, promise); } else {// 握手还没完成,排队等待 pendingUnencryptedWrites.add(newPendingWrite((ByteBuf) msg, promise)); } }}这里有个关键点:decode() 是入站解码,unwrap() 方法负责把 SSL 加密的二进制数据解密成明文。write() 是出站拦截,wrap() 把明文加密成 SSL 数据包。
2.2 unwrap() — 解密的核心逻辑
unwrap() 是 SslHandler 里最核心的方法,我们来好好看看它是怎么解密数据的:
// io.netty.handler.ssl.SslHandler#unwrapprivatevoidunwrap( ChannelHandlerContext ctx, ByteBuf packet, int offset, int len, List<Object> out)throws SSLException {// 1. 将 Netty 的 ByteBuf 转换成 JDK NIO 的 ByteBuffer// SSLEngine 只认 JDK 的 ByteBufferfinalByteBufferin= packet.nioBuffer(offset, len);// 2. 循环:SSLEngine 可能一次 unwrap 不完所有数据while (true) {// 获取输出缓冲区(解密后的明文空间)ByteBufferout= allocateOutNetBuf(ctx, len);try {// 3. 核心调用:SSLEngine.unwrap()// 这个方法把 SSL/TLS 加密数据解密成明文SSLEngineResultresult= engine.unwrap(in, out);// 4. 处理 SSLEngine 的状态Statusstatus= result.getStatus();HandshakeStatushandshakeStatus= result.getHandshakeStatus();// 翻转输出缓冲区,准备读取 out.flip();switch (status) {case OK:// 解密成功,把明文 ByteBuffer 包装成 ByteBuf// 传递给 pipeline 的下一个 handlerif (out.hasRemaining()) {ByteBufdecoded= Unpooled.wrappedBuffer(out); out = null; // 防止 finally 释放 out.add(decoded); }break;case BUFFER_UNDERFLOW:// 数据不够,下次再处理break;case BUFFER_OVERFLOW:// 输出缓冲区太小,扩大后重试 out = ByteBuffer.allocate( engine.getSession().getApplicationBufferSize());break;case CLOSED:// 对方关闭了 SSL 连接 closeOutboundAndChannel(ctx, engine);return; }// 5. 处理握手状态if (handshakeStatus == HandshakeStatus.NEED_TASK) {// 需要执行握手任务(比如密钥协商)// 异步执行,避免阻塞 IO 线程 runDelegatedTasks(ctx, engine); } elseif (handshakeStatus == HandshakeStatus.NEED_UNWRAP || handshakeStatus == HandshakeStatus.NEED_WRAP) {// 继续握手或者准备加密数据返回 }// 6. 检查是否还有数据要处理if (!in.hasRemaining() || status == Status.BUFFER_UNDERFLOW) {break; } } catch (SSLException e) {// SSL 异常直接抛出去,触发 exceptionCaughtthrow e; } finally {if (out != null) {// 释放临时分配的 ByteBuffer ByteBufUtil.release(out); } } }// 7. 处理握手状态的特殊情况if (engine.getHandshakeStatus() == HandshakeStatus.NEED_WRAP) {// 需要加密握手数据发送给对方 wrap(ctx, Unpooled.EMPTY_BUFFER, ctx.newPromise()); }}2.3 wrap() — 加密的核心逻辑
加密和解密是对称的,出站数据走 wrap():
// io.netty.handler.ssl.SslHandler#wrapprivatevoidwrap(ChannelHandlerContext ctx, ByteBuf msg, ChannelPromise promise)throws SSLException {ByteBufout= ctx.alloc().buffer(maxPacketBufferSize);ByteBufin= msg;ByteBufferinBuffer= in.nioBuffer();ByteBufferoutBuffer= out.nioBuffer(0, maxPacketBufferSize);// 调用 SSLEngine.wrap() 加密SSLEngineResultresult= engine.wrap(inBuffer, outBuffer);// 更新写位置 out.writerIndex(outBuffer.position());// 根据结果决定下一步switch (result.getStatus()) {case OK:// 加密成功,把加密后的数据写出到 socket ctx.write(out, promise);break;case BUFFER_OVERFLOW:// 缓冲区不够,抛错或者扩大break;case CLOSED:// SSL 连接已关闭break; }// 如果引擎状态是 NEED_UNWRAP(握手中),切换处理if (result.getHandshakeStatus() == HandshakeStatus.NEED_UNWRAP) {// 尝试读取数据来继续握手 ctx.read(); }}2.4 SslContext — 工厂和生产配置
SslContext 是 Netty SSL 的配置中心和工厂,用来创建 SslHandler。Netty 提供了两个主要实现:
// io.netty.handler.ssl.SslContextpublicabstractclassSslContext implements ... {// 是否作为客户端(true)或服务端(false)privatefinalboolean isClient;// SSL 协议提供者(JDK / OpenSSL / BouncyCastle)privatefinal SslProvider provider;// 会话缓存,复用 SSL Session,避免重复握手privatefinal SSLSessionContext sessionContext;// 信任证书privatefinal TrustManagerFactory trustManagerFactory;// 应用协议协商(ALPN)—— HTTP/2 支持的关键privatefinal List<String> apn; // 创建 SslHandler 工厂方法publicfinal SslHandler newHandler(ByteBufAllocator allocator) {return newHandler(allocator, engine -> { }, // 可以传入回调来自定义 SSLEngine isClient ? engine.getSession().getPeerHost() : null); }// 实际构建 SSLEngineprotectedabstract SslEngine newEngine(ByteBufAllocator allocator);}// 典型的服务端 SSL 配置SslContextsslCtx= SslContextBuilder.forServer(certFile, keyFile) .sslProvider(SslProvider.OPENSSL) // 使用 OpenSSL .ciphers(Arrays.asList("TLS_AES_256_GCM_SHA384")) // 指定加密套件 .build();2.5 Netty 的 OpenSSL 集成 — 为什么更快?
Netty 通过 netty-tcnative 库(Tomcat Native 封装)实现了 OpenSSL 的集成。核心类是 OpenSslEngine:
// io.netty.handler.ssl.OpenSslEngine// 这个类本质上是对 OpenSSL 库的 JNI 调用封装publicfinalclassOpenSslEngineextendsSSLEngine {// 指向 openssl 原生 SSL* 对象的指针privatefinallong ssl;// openssl BIO(基本 IO)抽象privatefinallong bio;@Override@SuppressJava6Requirement(reason = "Usage guarded by java version check")protected SSLEngineResult unwrap(ByteBuffer src, ByteBuffer dst)throws SSLException {// 直接把数据送到 openssl 的 BIO 里处理intsslRead= SSL.readFromSSL(ssl, src, dst);// 检查 openssl 的返回值,映射到 SSLEngineResult.StatusreturnnewSSLEngineResult(...); }}为什么 OpenSSL 引擎比 JDK 的快?两个核心原因:
原生实现:加密算法运行在 C 层面,没有 JVM 和 JNI 边界开销 硬件加速:OpenSSL 能利用 CPU 的 AES-NI 指令集做硬件加速加密
三、原理分析
3.1 SSL/TLS 握手全过程
Netty 中的 SSL 握手是一个异步过程,和普通的网络 IO 一样,完全不会阻塞 IO 线程。我画个流程图:
客户端 服务端 | | | ---- ClientHello ----------> | 1. 客户端发起连接,发 ClientHello | | | <--- ServerHello ----------- | 2. 服务端回应,选择 TLS 版本和加密套件 | <--- Certificate ----------- | 3. 服务端发送证书 | <--- ServerHelloDone ------- | 4. 服务端说 "我发完了" | | | ---- ClientKeyExchange ----> | 5. 客户端生成 PreMasterSecret,用服务端公钥加密发送 | ---- ChangeCipherSpec -----> | 6. 通知服务端:"我要开始加密通信了" | ---- Finished -------------> | 7. 加密的握手完成消息 | | | <--- ChangeCipherSpec ------ | 8. 服务端也切换加密模式 | <--- Finished -------------- | 9. 握手完成! | | | ==== 之后所有数据都加密 ==== |在 Netty 源码中,握手过程被 SslHandler 自动管理。关键逻辑在 handshake() 方法:
// SslHandler#handshakepublic ChannelFuture handshake() {if (engine.getHandshakeStatus() != HandshakeStatus.NOT_HANDLED) {// 握手已经开始或已完成,直接返回return handshakePromise; }// 开始握手 engine.beginHandshake();// 触发真正的 SSL 握手流程// 这里会引发 SSLEngine 的 NEED_WRAP / NEED_UNWRAP 状态变化// SslHandler 在 decode/wrap 里自动响应这些状态 handshakePromise.addListener((ChannelFutureListener) future -> {if (future.isSuccess()) {// 握手成功,刷新排队的待发数据 flushPendingWrites(ctx); } });return handshakePromise;}3.2 设计模式分析
1. 装饰器模式SslHandler 像一个包装器,在原生的 TCP 流上包裹了一层 SSL 加密/解密逻辑。对于 pipeline 上下游的处理器来说,它们看到的是明文数据,SSL 层完全透明。
2. 观察者模式SslHandler 内部使用 Promise 来跟踪握手状态。外部通过添加 ChannelFutureListener 来监听握手完成事件。
3. 策略模式SslProvider 体现了策略模式:你可以选择 JDK SSL、OpenSSL 或 BouncyCastle 作为底层实现,而 SslHandler 的代码完全不需要改动。
四、完整可运行示例
咱们来一个完整的服务端-客户端 SSL 通信例子:
4.1 服务端代码
import io.netty.bootstrap.ServerBootstrap;import io.netty.channel.*;import io.netty.channel.nio.NioEventLoopGroup;import io.netty.channel.socket.SocketChannel;import io.netty.channel.socket.nio.NioServerSocketChannel;import io.netty.handler.ssl.SslContext;import io.netty.handler.ssl.SslContextBuilder;import io.netty.handler.ssl.SslProvider;import io.netty.handler.codec.LineBasedFrameDecoder;import io.netty.handler.codec.string.StringDecoder;import io.netty.handler.codec.string.StringEncoder;import java.nio.charset.StandardCharsets;import java.io.File;publicclassSecureEchoServer {publicstaticvoidmain(String[] args)throws Exception {// 用 OpenSSL 协议,生产环境一定要用 OpenSSL!SslContextsslCtx= SslContextBuilder.forServer(newFile("/path/to/server.crt"), // 证书newFile("/path/to/pkcs8.key") // 私钥 ) .sslProvider(SslProvider.OPENSSL) .build();EventLoopGroupboss=newNioEventLoopGroup(1);EventLoopGroupworker=newNioEventLoopGroup();try {ServerBootstrapb=newServerBootstrap(); b.group(boss, worker) .channel(NioServerSocketChannel.class) .childHandler(newChannelInitializer<SocketChannel>() {@OverrideprotectedvoidinitChannel(SocketChannel ch) {ChannelPipelinep= ch.pipeline();// 1. SSL Handler - 放在最前面! p.addLast(sslCtx.newHandler(ch.alloc()));// 2. 后面就跟普通 TCP 一样了// 因为数据已经被 SslHandler 解密成明文 p.addLast(newLineBasedFrameDecoder(1024)); p.addLast(newStringDecoder(StandardCharsets.UTF_8)); p.addLast(newStringEncoder(StandardCharsets.UTF_8));// 3. 业务 Handler p.addLast(newSimpleChannelInboundHandler<String>() {@OverrideprotectedvoidchannelRead0(ChannelHandlerContext ctx, String msg) { System.out.println("[Server] 收到: " + msg); ctx.writeAndFlush("[Echo] " + msg + " "); }@OverridepublicvoidchannelActive(ChannelHandlerContext ctx) { System.out.println("客户端连接: " + ctx.channel().remoteAddress()); }@OverridepublicvoidexceptionCaught(ChannelHandlerContext ctx, Throwable cause) { cause.printStackTrace(); ctx.close(); } }); } }) .option(ChannelOption.SO_BACKLOG, 128) .childOption(ChannelOption.TCP_NODELAY, true);ChannelFuturef= b.bind(8443).sync(); System.out.println("SSL 服务器启动在端口 8443"); f.channel().closeFuture().sync(); } finally { boss.shutdownGracefully(); worker.shutdownGracefully(); } }}4.2 客户端代码
import io.netty.bootstrap.Bootstrap;import io.netty.channel.*;import io.netty.channel.nio.NioEventLoopGroup;import io.netty.channel.socket.SocketChannel;import io.netty.channel.socket.nio.NioSocketChannel;import io.netty.handler.ssl.SslContext;import io.netty.handler.ssl.SslContextBuilder;import io.netty.handler.ssl.SslProvider;import io.netty.handler.codec.LineBasedFrameDecoder;import io.netty.handler.codec.string.StringDecoder;import io.netty.handler.codec.string.StringEncoder;import java.nio.charset.StandardCharsets;publicclassSecureEchoClient {publicstaticvoidmain(String[] args)throws Exception {// 客户端 SSL 上下文// 注意:客户端不需要私钥,只需要信任服务端的证书SslContextsslCtx= SslContextBuilder.forClient() .sslProvider(SslProvider.OPENSSL) .trustManager(newFile("/path/to/ca.crt")) // 信任的自签名 CA .build();EventLoopGroupgroup=newNioEventLoopGroup();try {Bootstrapb=newBootstrap(); b.group(group) .channel(NioSocketChannel.class) .handler(newChannelInitializer<SocketChannel>() {@OverrideprotectedvoidinitChannel(SocketChannel ch) {ChannelPipelinep= ch.pipeline();// 1. SSL Handler 必须放 pipeline 最前面 p.addLast(sslCtx.newHandler(ch.alloc(), "localhost", 8443));// 2. 编解码器(与服务器一致) p.addLast(newLineBasedFrameDecoder(1024)); p.addLast(newStringDecoder(StandardCharsets.UTF_8)); p.addLast(newStringEncoder(StandardCharsets.UTF_8));// 3. 业务 Handler p.addLast(newChannelInboundHandlerAdapter() {@OverridepublicvoidchannelActive(ChannelHandlerContext ctx) { System.out.println("SSL 连接已建立,开始发送数据...");// 发送多条消息,测试 SSL 加密for (inti=0; i < 5; i++) { ctx.writeAndFlush("Hello Netty SSL #" + i + " "); } }@OverridepublicvoidchannelRead(ChannelHandlerContext ctx, Object msg) { System.out.println("[Client] 收到回复: " + msg); }@OverridepublicvoidexceptionCaught(ChannelHandlerContext ctx, Throwable cause) { cause.printStackTrace(); ctx.close(); } }); } });ChannelFuturef= b.connect("localhost", 8443).sync(); f.channel().closeFuture().sync(); } finally { group.shutdownGracefully(); } }}4.3 自签名证书一键生成(开发环境用)
# 生成 CA 私钥和根证书openssl req -new -x509 -days 3650 -nodes \ -subj "/C=CN/ST=Beijing/L=Beijing/O=NettyDemo/CN=NettyRootCA" \ -keyout ca.key -out ca.crt# 生成服务端私钥和证书签名请求openssl genrsa -out server.key 2048openssl req -new -subj "/C=CN/ST=Beijing/L=Beijing/O=NettyDemo/CN=localhost" \ -key server.key -out server.csr# 用 CA 签发服务端证书openssl x509 -req -days 365 \ -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial \ -out server.crt# 把私钥转换成 PKCS#8 格式(Netty 要求)openssl pkcs8 -topk8 -nocrypt -in server.key -out pkcs8.key# 生成客户端信任的证书(CA)# 客户端 trustManager 指向 ca.crt 即可五、注意事项
5.1 常见坑
1. SslHandler 必须在 Pipeline 最前面
这是最最最容易踩的坑。SslHandler 处理的是加密后的二进制流,如果前面放了别的解码器,数据还没解密就被"误解"了。
// ❌ 错误:编解码器在 Ssl 前面p.addLast(newStringDecoder()); // 收到的是密文,解码乱码p.addLast(sslCtx.newHandler(ch.alloc()));// ✅ 正确:SslHandler 必须在最前面p.addLast(sslCtx.newHandler(ch.alloc()));p.addLast(newStringDecoder()); // 拿到的是解密后的明文2. SSL 握手是异步的,不要假设连接一建立就能发数据
// ❌ 可能踩坑publicvoidchannelActive(ChannelHandlerContext ctx) {// 这个时候 SSL 握手可能还没完成! ctx.writeAndFlush("数据");}// ✅ 正确做法:监听握手完成事件SslHandlersslHandler= ctx.pipeline().get(SslHandler.class);sslHandler.handshakeFuture().addListener(future -> {if (future.isSuccess()) {// 握手完成,可以发数据了 ctx.writeAndFlush("数据"); }});3. 性能选型:生产环境一定用 OpenSSL
JDK 自带的 SSL 实现性能差很多,尤其是高并发场景:
4. SSLEngine 的 BUFFER_OVERFLOW 和 BUFFER_UNDERFLOW
上层 Handler 不能假设一次 decode 就能拿到完整的一帧 SSL 数据。SslHandler 内部的 unwrap() 方法通过循环处理这点,但自定义 Handler 要注意:
BUFFER_UNDERFLOW:数据不够,下次继续BUFFER_OVERFLOW:输出缓冲区不够大,需要调整
5.2 性能优化建议
启用 Session 复用:SSL 握手很贵(尤其是 RSA 密钥交换),复用 Session 能大幅减少握手次数 调整最大数据包大小: SslHandler.setMaxPacketBufferSize()配合应用层的 MTU优先 TLS 1.3:1.3 只需要 1-RTT 就能完成握手,比 1.2 快一个来回 注意内存:SSL 加解密会产生额外的 ByteBuf 分配,监控堆外内存使用情况
// 开启 Session 缓存SslContextsslCtx= SslContextBuilder.forServer(...) .sessionCacheSize(10000) // Session 缓存容量 .sessionTimeout(300) // 5分钟超时 .sslProvider(SslProvider.OPENSSL) .build();5.3 适用场景
HTTPS 服务器:Netty 的 HTTP 协议栈 + SSL Handler,轻松实现 HTTPS 安全 RPC:内部服务之间的通信加密 WSS WebSocket:WebSocket 安全版本 MQTT over TLS:物联网安全通信 自研协议加密:私有协议的传输层加密
六、总结
Netty 的 SSL/TLS 支持本质上是对 JDK SSLEngine 的封装,核心是 SslHandler 这个处理器。它通过 wrap() 和 unwrap() 两个方法,在出站时加密、入站时解密,对 pipeline 上下文的 handler 完全透明。
关键点回顾:
SslHandler必须放在 pipeline 最前面握手是异步的,通过 handshakeFuture()监听完成事件生产环境务必用 OpenSSL 提供者(netty-tcnative) Session 复用 + TLS 1.3 = 高性能 SSL
理解了 SslHandler 和 SSLEngine 的交互机制后,你会发现 Netty 的 SSL/TLS 支持远比你想象的要优雅。它把复杂的加密协议细节全部封装在了一个 Handler 内部,让你能像写普通 TCP 应用一样写加密通信程序。
下一篇我们聊 Netty 的「零拷贝机制」,这个可以说是 Netty 性能老大哥级别的优化,敬请期待!















































































