5-Netty WebSocket 与 HTTP
一、技术架构设计思路
1.1 为什么在 Netty 上实现 HTTP/WebSocket?
传统 Java Web 开发中,HTTP 服务通常由 Tomcat/Jetty 等 Servlet 容器承载。但在以下场景中,基于 Netty 自建 HTTP/WebSocket 服务更有优势:
| 场景 |
Servlet 容器 |
Netty 自建 |
| 长连接高并发(10万+) |
线程模型受限,连接数瓶颈明显 |
单线程管理数千连接,轻松支撑 |
| 自定义协议 |
需要额外扩展 |
原生支持,Pipeline 灵活组合 |
| 内存占用敏感 |
JVM 堆内对象多 |
直接内存 + 池化分配,更省内存 |
| 需要精细控制连接生命周期 |
受 Servlet 规范约束 |
完全掌控 Channel 的每个阶段 |
典型应用:Netty 是 Spring WebFlux、gRPC-Java、Dubbo 3.x 等框架的底层网络层。
1.2 整体架构设计
1.3 HTTP 与 WebSocket 的本质区别
二、关键代码逻辑实现步骤
2.1 HTTP 服务端实现详解
步骤一:Pipeline 三层架构
ch.pipeline()
.addLast(new HttpServerCodec()) // 第1层:编解码
.addLast(new HttpObjectAggregator(65536)) // 第2层:消息聚合
.addLast(new HttpServerHandler()); // 第3层:业务处理
各层职责深度解析:
第1层 --- HttpServerCodec :这是 HttpRequestDecoder + HttpResponseEncoder 的组合。它将原始字节解码为 HTTP 消息对象。由于 HTTP 消息可能分多个 TCP 段到达,解码器会产出多个消息部件:
第2层 --- HttpObjectAggregator :将上述分散的 HttpRequest + 多个 HttpContent 聚合成一个完整的 FullHttpRequest。参数 65536 是最大内容长度(64KB),超过则返回 413 Request Entity Too Large。
聚合前:[HttpRequest] [HttpContent] [LastHttpContent]
聚合后:[FullHttpRequest] ← 包含完整的请求头 + 请求体
第3层 --- HttpServerHandler :继承 SimpleChannelInboundHandler<FullHttpRequest>,直接处理完整的 HTTP 请求。
步骤二:构建 HTTP 响应
// 关键实现逻辑(来自 HttpServer.java)
FullHttpResponse resp = new DefaultFullHttpResponse(
HttpVersion.HTTP_1_1, // HTTP 版本
HttpResponseStatus.OK, // 状态码 200
Unpooled.copiedBuffer(body, StandardCharsets.UTF_8) // 响应体
);
// 必须设置的三个响应头
resp.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=UTF-8");
resp.headers().set(HttpHeaderNames.CONTENT_LENGTH, resp.content().readableBytes());
resp.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
为什么必须设置 Content-Length? HTTP/1.1 的持久连接(Keep-Alive)需要知道消息边界。如果不设置 Content-Length,客户端无法判断响应是否完整接收,可能导致连接挂起或数据截断。
Connection: Keep-Alive 的作用: 允许同一个 TCP 连接复用处理多个 HTTP 请求,避免每次请求都要重新建立 TCP 连接(三次握手)的开销。
步骤三:请求路由(当前实现的局限)
当前实现是"全路由匹配"------所有请求都进入同一个 Handler。生产环境需要实现路由分发:
// 生产环境的路由设计思路
@Override
protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest req) {
String uri = req.uri();
if ("/api/users".equals(uri)) {
handleGetUsers(ctx, req);
} else if ("/api/health".equals(uri)) {
handleHealthCheck(ctx, req);
} else {
// 404
ctx.writeAndFlush(createResponse(NOT_FOUND, "Not Found"));
}
}
2.2 WebSocket 服务端实现详解
步骤一:握手过程(协议升级)
WebSocket 连接的建立依赖一次 HTTP 握手,整个过程由 WebSocketServerProtocolHandler 自动完成:
Sec-WebSocket-Key/Accept 的安全机制 : 客户端发送随机 Base64 编码的 16 字节 Key,服务端将其与固定 GUID 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 拼接后做 SHA-1 哈希再 Base64 编码。这不是加密,而是防止缓存代理服务器误判。
步骤二:Pipeline 五层架构解析
// 来自 WebSocketServer.java 的完整 Pipeline 配置
ChannelPipeline pipeline = ch.pipeline();
// 第1层:HTTP 编解码器
// 职责:处理握手阶段的 HTTP 请求/响应编解码
// 握手完成后,该 Handler 自动切换到 WebSocket 帧编解码模式
pipeline.addLast(new HttpServerCodec());
// 第2层:HTTP 消息聚合器
// 职责:将 HttpRequest + HttpContent 聚合为 FullHttpRequest
// 握手完成后不再处理数据,但必须保留(WebSocketServerProtocolHandler 依赖它)
pipeline.addLast(new HttpObjectAggregator(65536));
// 第3层:大数据流写入处理器
// 职责:支持 ChunkedInput,可分块发送大文件/大数据
// 避免一次性将大消息加载到内存,防止 OOM
pipeline.addLast(new ChunkedWriteHandler());
// 第4层:WebSocket 协议处理器(核心)
// 职责:
// ① 自动完成握手(检测 /ws 路径的 Upgrade 请求)
// ② 握手完成后,动态修改 Pipeline:
// - 移除不再需要的 Handler
// - 添加 WebSocketFrameDecoder(入站帧解码)
// - 添加 WebSocketFrameEncoder(出站帧编码)
// ③ 自动处理 Ping/Pong 帧(心跳)
// ④ 自动处理 Close 帧(优雅关闭)
pipeline.addLast(new WebSocketServerProtocolHandler("/ws"));
// 第5层:业务处理器
// 职责:只处理业务帧(Text/Binary),不需要关心协议细节
pipeline.addLast(new WebSocketHandler());
关键设计洞察 :WebSocketServerProtocolHandler 是整个实现的核心。它在握手成功后会动态修改 Pipeline,移除 HTTP 相关的 Handler,替换为 WebSocket 帧处理器。这种"运行时 Pipeline 重组"是 Netty 灵活性的体现。
步骤三:帧类型处理
// 来自 WebSocketHandler.java
@Override
protected void channelRead0(ChannelHandlerContext ctx, WebSocketFrame frame) {
if (frame instanceof TextWebSocketFrame textFrame) {
// 处理文本帧
String text = textFrame.text();
ctx.writeAndFlush(new TextWebSocketFrame("回复: " + text));
} else {
// 未处理的帧类型(BinaryWebSocketFrame 等)
log.warn("不支持的帧类型: {}", frame.getClass().getSimpleName());
}
}
WebSocket 帧类型完整对照表:
| 帧类型 |
Java 类 |
操作码 |
说明 |
处理方 |
| 文本帧 |
TextWebSocketFrame |
0x1 |
UTF-8 文本数据 |
业务 Handler |
| 二进制帧 |
BinaryWebSocketFrame |
0x2 |
任意二进制数据 |
业务 Handler |
| 关闭帧 |
CloseWebSocketFrame |
0x8 |
关闭连接请求 |
ProtocolHandler 自动处理 |
| Ping帧 |
PingWebSocketFrame |
0x9 |
心跳探测 |
ProtocolHandler 自动回复 Pong |
| Pong帧 |
PongWebSocketFrame |
0xA |
心跳响应 |
ProtocolHandler 自动处理 |
| 延续帧 |
ContinuationWebSocketFrame |
0x0 |
分片消息的后续帧 |
需要手动处理分片 |
步骤四:连接生命周期管理
// 连接建立
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
log.info("WebSocket 连接建立: {}", ctx.channel().remoteAddress());
// 生产环境:注册到连接管理器、发送欢迎消息等
}
// 连接断开
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
log.info("WebSocket 连接断开: {}", ctx.channel().remoteAddress());
// 生产环境:清理资源、通知其他用户、持久化状态等
}
// 异常处理
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
log.error("WebSocket 异常: {}", cause.getMessage());
ctx.close(); // 必须关闭,否则可能泄漏
}
三、性能优化具体措施与效果
3.1 当前实现中的性能设计
| 优化点 |
实现方式 |
效果 |
| NIO 非阻塞 IO |
NioEventLoopGroup + NioServerSocketChannel |
单线程管理数千连接 |
| 主从 Reactor |
BossGroup(1) + WorkerGroup(CPU*2) |
连接接收与IO处理分离 |
| 池化内存分配 |
Netty 默认 PooledByteBufAllocator |
减少 GC 压力,分配速度提升 3-5 倍 |
| 直接内存 |
Netty 默认优先使用 DirectByteBuf |
IO 传输零拷贝,减少一次内存复制 |
| Keep-Alive |
Connection: KEEP_ALIVE |
复用 TCP 连接,省去三次握手开销 |
3.2 可进一步优化的方向
3.2.1 性能维度
// ① 添加 IdleStateHandler 实现心跳超时检测
pipeline.addFirst(new IdleStateHandler(120, 60, 0, TimeUnit.SECONDS));
// ② 启用 WebSocket 压缩(permessage-deflate 扩展)
// 可减少 60%-80% 的文本消息传输体积
pipeline.addLast(new WebSocketServerCompressionHandler());
pipeline.addLast(new WebSocketServerProtocolHandler("/ws", null, true));
// 第三个参数 true 表示启用 permessage-deflate 压缩
// ③ 配置 TCP 参数优化
.childOption(ChannelOption.TCP_NODELAY, true) // 禁用 Nagle,减少延迟
.childOption(ChannelOption.SO_KEEPALIVE, true) // OS 级心跳保活
.childOption(ChannelOption.SO_RCVBUF, 65536) // 接收缓冲区 64KB
.childOption(ChannelOption.SO_SNDBUF, 65536) // 发送缓冲区 64KB
// ④ 使用 FlushConsolidationHandler 合并 flush
pipeline.addLast(new FlushConsolidationHandler());
3.2.2 可扩展性维度
// ① 路由化 WebSocket 路径
// 支持多个 WebSocket 端点:/ws/chat、/ws/notify 等
new WebSocketServerProtocolHandler("/ws", null, true, 65536);
// ② 连接管理器(生产级设计)
public class ConnectionManager {
private final ChannelGroup allChannels =
new DefaultChannelGroup("ws-connections", GlobalEventExecutor.INSTANCE);
private final Map<String, Channel> userChannels = new ConcurrentHashMap<>();
public void addChannel(String userId, Channel channel) {
allChannels.add(channel);
userChannels.put(userId, channel);
}
public void broadcast(WebSocketFrame frame, String excludeUserId) {
for (Channel ch : allChannels) {
ch.writeAndFlush(frame.retainedDuplicate());
}
}
}
// ③ 消息分发机制
// 根据帧类型路由到不同的业务处理器
if (frame instanceof TextWebSocketFrame) {
// JSON 解析 → 消息路由 → 业务处理
} else if (frame instanceof BinaryWebSocketFrame) {
// 二进制数据处理(文件传输、图片等)
}
3.2.3 安全性维度
// ① 握手鉴权:自定义 WebSocketServerProtocolHandler 拦截握手请求
pipeline.addLast(new WebSocketServerProtocolHandler("/ws", null, true, 65536) {
@Override
public void handshook(ChannelHandlerContext ctx, FullHttpRequest req) {
// 从握手请求中提取 Token 进行鉴权
String token = req.headers().get("Authorization");
if (!authenticate(token)) {
ctx.close(); // 拒绝非法连接
}
}
});
// ② Origin 校验(防止跨站 WebSocket 劫持)
// 在 WebSocketServerProtocolHandler 之前添加校验 Handler
pipeline.addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
if (msg instanceof FullHttpRequest req) {
String origin = req.headers().get("Origin");
if (!ALLOWED_ORIGINS.contains(origin)) {
req.release();
ctx.close();
return;
}
}
ctx.fireChannelRead(msg);
}
});
// ③ TLS/SSL 加密
// 在 Pipeline 最前面添加 SslHandler
SslContext sslCtx = SslContextBuilder.forServer(cert, key).build();
pipeline.addFirst(sslCtx.newHandler(ch.alloc()));
// ④ 消息大小限制(防 DoS)
// WebSocketServerProtocolHandler 的 maxFramePayloadLength 参数
new WebSocketServerProtocolHandler("/ws", null, true, 1024 * 64); // 最大 64KB
四、常见问题与解决方案
4.1 开发阶段常见问题
| 问题 |
现象 |
根因 |
解决方案 |
| 握手失败 404 |
浏览器报 404 Not Found |
WebSocket 路径不匹配 |
确保 WebSocketServerProtocolHandler 的路径参数与客户端请求路径一致 |
| 消息乱码 |
中文显示为乱码 |
TextWebSocketFrame 默认 UTF-8,但响应头未设置编码 |
确认 Content-Type: text/html; charset=UTF-8 |
| 连接频繁断开 |
客户端反复重连 |
未处理心跳,NAT/防火墙超时断开 |
添加 IdleStateHandler + 心跳 Ping/Pong 机制 |
| 内存持续增长 |
堆外内存泄漏 |
WebSocketFrame 未释放 |
SimpleChannelInboundHandler 自动释放;若用 ChannelInboundHandlerAdapter 需手动 frame.release() |
| 大消息发送失败 |
OutOfDirectMemoryError |
大消息一次性写入超出缓冲区 |
使用 ChunkedWriteHandler + ChunkedInput 分块发送 |
| HTTP 响应不完整 |
浏览器显示部分页面 |
未设置 Content-Length |
始终设置 CONTENT_LENGTH 或使用 Transfer-Encoding: chunked |
4.2 生产环境排查要点
五、技术难点深度分析
5.1 Pipeline 动态重组(核心技术难点)
WebSocketServerProtocolHandler 内部实现了一个精妙的设计:握手成功后动态修改 Pipeline。
为什么需要动态重组?
- 握手阶段需要 HTTP 编解码器处理 HTTP 请求/响应
- 握手完成后,协议切换为 WebSocket 帧格式,需要帧编解码器
- 如果不移除 HTTP 相关 Handler,会导致后续帧数据被错误处理
5.2 HttpObjectAggregator 的内存风险
// 危险:设置过大的聚合上限
new HttpObjectAggregator(1024 * 1024 * 100); // 100MB!
// 攻击者发送超大请求体 → 服务端内存暴涨 → OOM
// 正确做法:根据业务需要设置合理上限
new HttpObjectAggregator(65536); // 64KB,适合大多数场景
5.3 WebSocket 分片消息处理
当消息较大时,WebSocket 协议支持将其拆分为多个帧发送:
发送方:
原始消息: "Hello World" (11 bytes)
分片发送: [TextFrame(fin=0, "Hello ")] [ContinuationFrame(fin=1, "World")]
接收方需要手动处理分片拼接:
if (frame instanceof TextWebSocketFrame) {
if (frame.isFinalFragment()) {
// 完整消息,直接处理
} else {
// 第一个分片,缓存并等待后续帧
}
} else if (frame instanceof ContinuationWebSocketFrame) {
// 后续分片,拼接到缓冲区
if (frame.isFinalFragment()) {
// 最后一个分片,处理完整消息
}
}
注意:WebSocketServerProtocolHandler 默认不处理分片,需要业务层自行实现。
5.4 优雅关闭与资源清理
// 正确的关闭流程
// 1. 服务端发送 Close 帧(携带状态码和原因)
ctx.writeAndFlush(new CloseWebSocketFrame(1000, "Server shutting down"));
// 2. 等待客户端确认关闭
// 客户端收到 Close 帧后回复 Close 帧,然后双方关闭 TCP 连接
// 3. 设置关闭超时(防止客户端不响应导致连接挂起)
new WebSocketServerProtocolHandler("/ws", null, true, 65536, 10000);
// 第5个参数 10000 = 10秒关闭超时