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(newHttpServerCodec())// 第1层编解码.addLast(newHttpObjectAggregator(65536))// 第2层消息聚合.addLast(newHttpServerHandler());// 第3层业务处理各层职责深度解析第1层 — HttpServerCodec这是HttpRequestDecoderHttpResponseEncoder的组合。它将原始字节解码为 HTTP 消息对象。由于 HTTP 消息可能分多个 TCP 段到达解码器会产出多个消息部件第2层 — HttpObjectAggregator将上述分散的HttpRequest 多个HttpContent聚合成一个完整的FullHttpRequest。参数65536是最大内容长度64KB超过则返回413 Request Entity Too Large。聚合前[HttpRequest] [HttpContent] [LastHttpContent] 聚合后[FullHttpRequest] ← 包含完整的请求头 请求体第3层 — HttpServerHandler继承SimpleChannelInboundHandlerFullHttpRequest直接处理完整的 HTTP 请求。步骤二构建 HTTP 响应// 关键实现逻辑来自 HttpServer.javaFullHttpResponserespnewDefaultFullHttpResponse(HttpVersion.HTTP_1_1,// HTTP 版本HttpResponseStatus.OK,// 状态码 200Unpooled.copiedBuffer(body,StandardCharsets.UTF_8)// 响应体);// 必须设置的三个响应头resp.headers().set(HttpHeaderNames.CONTENT_TYPE,text/html; charsetUTF-8);resp.headers().set(HttpHeaderNames.CONTENT_LENGTH,resp.content().readableBytes());resp.headers().set(HttpHeaderNames.CONNECTION,HttpHeaderValues.KEEP_ALIVE);为什么必须设置 Content-LengthHTTP/1.1 的持久连接Keep-Alive需要知道消息边界。如果不设置 Content-Length客户端无法判断响应是否完整接收可能导致连接挂起或数据截断。Connection: Keep-Alive 的作用 允许同一个 TCP 连接复用处理多个 HTTP 请求避免每次请求都要重新建立 TCP 连接三次握手的开销。步骤三请求路由当前实现的局限当前实现是全路由匹配——所有请求都进入同一个 Handler。生产环境需要实现路由分发// 生产环境的路由设计思路OverrideprotectedvoidchannelRead0(ChannelHandlerContextctx,FullHttpRequestreq){Stringurireq.uri();if(/api/users.equals(uri)){handleGetUsers(ctx,req);}elseif(/api/health.equals(uri)){handleHealthCheck(ctx,req);}else{// 404ctx.writeAndFlush(createResponse(NOT_FOUND,Not Found));}}2.2 WebSocket 服务端实现详解步骤一握手过程协议升级WebSocket 连接的建立依赖一次 HTTP 握手整个过程由WebSocketServerProtocolHandler自动完成Sec-WebSocket-Key/Accept 的安全机制 客户端发送随机 Base64 编码的 16 字节 Key服务端将其与固定 GUID258EAFA5-E914-47DA-95CA-C5AB0DC85B11拼接后做 SHA-1 哈希再 Base64 编码。这不是加密而是防止缓存代理服务器误判。步骤二Pipeline 五层架构解析// 来自 WebSocketServer.java 的完整 Pipeline 配置ChannelPipelinepipelinech.pipeline();// 第1层HTTP 编解码器// 职责处理握手阶段的 HTTP 请求/响应编解码// 握手完成后该 Handler 自动切换到 WebSocket 帧编解码模式pipeline.addLast(newHttpServerCodec());// 第2层HTTP 消息聚合器// 职责将 HttpRequest HttpContent 聚合为 FullHttpRequest// 握手完成后不再处理数据但必须保留WebSocketServerProtocolHandler 依赖它pipeline.addLast(newHttpObjectAggregator(65536));// 第3层大数据流写入处理器// 职责支持 ChunkedInput可分块发送大文件/大数据// 避免一次性将大消息加载到内存防止 OOMpipeline.addLast(newChunkedWriteHandler());// 第4层WebSocket 协议处理器核心// 职责// ① 自动完成握手检测 /ws 路径的 Upgrade 请求// ② 握手完成后动态修改 Pipeline// - 移除不再需要的 Handler// - 添加 WebSocketFrameDecoder入站帧解码// - 添加 WebSocketFrameEncoder出站帧编码// ③ 自动处理 Ping/Pong 帧心跳// ④ 自动处理 Close 帧优雅关闭pipeline.addLast(newWebSocketServerProtocolHandler(/ws));// 第5层业务处理器// 职责只处理业务帧Text/Binary不需要关心协议细节pipeline.addLast(newWebSocketHandler());关键设计洞察WebSocketServerProtocolHandler是整个实现的核心。它在握手成功后会动态修改 Pipeline移除 HTTP 相关的 Handler替换为 WebSocket 帧处理器。这种运行时 Pipeline 重组是 Netty 灵活性的体现。步骤三帧类型处理// 来自 WebSocketHandler.javaOverrideprotectedvoidchannelRead0(ChannelHandlerContextctx,WebSocketFrameframe){if(frameinstanceofTextWebSocketFrametextFrame){// 处理文本帧StringtexttextFrame.text();ctx.writeAndFlush(newTextWebSocketFrame(回复: text));}else{// 未处理的帧类型BinaryWebSocketFrame 等log.warn(不支持的帧类型: {},frame.getClass().getSimpleName());}}WebSocket 帧类型完整对照表帧类型Java 类操作码说明处理方文本帧TextWebSocketFrame0x1UTF-8 文本数据业务 Handler二进制帧BinaryWebSocketFrame0x2任意二进制数据业务 Handler关闭帧CloseWebSocketFrame0x8关闭连接请求ProtocolHandler 自动处理Ping帧PingWebSocketFrame0x9心跳探测ProtocolHandler 自动回复 PongPong帧PongWebSocketFrame0xA心跳响应ProtocolHandler 自动处理延续帧ContinuationWebSocketFrame0x0分片消息的后续帧需要手动处理分片步骤四连接生命周期管理// 连接建立OverridepublicvoidchannelActive(ChannelHandlerContextctx)throwsException{log.info(WebSocket 连接建立: {},ctx.channel().remoteAddress());// 生产环境注册到连接管理器、发送欢迎消息等}// 连接断开OverridepublicvoidchannelInactive(ChannelHandlerContextctx)throwsException{log.info(WebSocket 连接断开: {},ctx.channel().remoteAddress());// 生产环境清理资源、通知其他用户、持久化状态等}// 异常处理OverridepublicvoidexceptionCaught(ChannelHandlerContextctx,Throwablecause){log.error(WebSocket 异常: {},cause.getMessage());ctx.close();// 必须关闭否则可能泄漏}三、性能优化具体措施与效果3.1 当前实现中的性能设计优化点实现方式效果NIO 非阻塞 IONioEventLoopGroupNioServerSocketChannel单线程管理数千连接主从 ReactorBossGroup(1) WorkerGroup(CPU*2)连接接收与IO处理分离池化内存分配Netty 默认PooledByteBufAllocator减少 GC 压力分配速度提升 3-5 倍直接内存Netty 默认优先使用 DirectByteBufIO 传输零拷贝减少一次内存复制Keep-AliveConnection: KEEP_ALIVE复用 TCP 连接省去三次握手开销3.2 可进一步优化的方向3.2.1 性能维度// ① 添加 IdleStateHandler 实现心跳超时检测pipeline.addFirst(newIdleStateHandler(120,60,0,TimeUnit.SECONDS));// ② 启用 WebSocket 压缩permessage-deflate 扩展// 可减少 60%-80% 的文本消息传输体积pipeline.addLast(newWebSocketServerCompressionHandler());pipeline.addLast(newWebSocketServerProtocolHandler(/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 合并 flushpipeline.addLast(newFlushConsolidationHandler());3.2.2 可扩展性维度// ① 路由化 WebSocket 路径// 支持多个 WebSocket 端点/ws/chat、/ws/notify 等newWebSocketServerProtocolHandler(/ws,null,true,65536);// ② 连接管理器生产级设计publicclassConnectionManager{privatefinalChannelGroupallChannelsnewDefaultChannelGroup(ws-connections,GlobalEventExecutor.INSTANCE);privatefinalMapString,ChanneluserChannelsnewConcurrentHashMap();publicvoidaddChannel(StringuserId,Channelchannel){allChannels.add(channel);userChannels.put(userId,channel);}publicvoidbroadcast(WebSocketFrameframe,StringexcludeUserId){for(Channelch:allChannels){ch.writeAndFlush(frame.retainedDuplicate());}}}// ③ 消息分发机制// 根据帧类型路由到不同的业务处理器if(frameinstanceofTextWebSocketFrame){// JSON 解析 → 消息路由 → 业务处理}elseif(frameinstanceofBinaryWebSocketFrame){// 二进制数据处理文件传输、图片等}3.2.3 安全性维度// ① 握手鉴权自定义 WebSocketServerProtocolHandler 拦截握手请求pipeline.addLast(newWebSocketServerProtocolHandler(/ws,null,true,65536){Overridepublicvoidhandshook(ChannelHandlerContextctx,FullHttpRequestreq){// 从握手请求中提取 Token 进行鉴权Stringtokenreq.headers().get(Authorization);if(!authenticate(token)){ctx.close();// 拒绝非法连接}}});// ② Origin 校验防止跨站 WebSocket 劫持// 在 WebSocketServerProtocolHandler 之前添加校验 Handlerpipeline.addLast(newChannelInboundHandlerAdapter(){OverridepublicvoidchannelRead(ChannelHandlerContextctx,Objectmsg){if(msginstanceofFullHttpRequestreq){Stringoriginreq.headers().get(Origin);if(!ALLOWED_ORIGINS.contains(origin)){req.release();ctx.close();return;}}ctx.fireChannelRead(msg);}});// ③ TLS/SSL 加密// 在 Pipeline 最前面添加 SslHandlerSslContextsslCtxSslContextBuilder.forServer(cert,key).build();pipeline.addFirst(sslCtx.newHandler(ch.alloc()));// ④ 消息大小限制防 DoS// WebSocketServerProtocolHandler 的 maxFramePayloadLength 参数newWebSocketServerProtocolHandler(/ws,null,true,1024*64);// 最大 64KB四、常见问题与解决方案4.1 开发阶段常见问题问题现象根因解决方案握手失败 404浏览器报404 Not FoundWebSocket 路径不匹配确保WebSocketServerProtocolHandler的路径参数与客户端请求路径一致消息乱码中文显示为乱码TextWebSocketFrame 默认 UTF-8但响应头未设置编码确认Content-Type: text/html; charsetUTF-8连接频繁断开客户端反复重连未处理心跳NAT/防火墙超时断开添加IdleStateHandler 心跳 Ping/Pong 机制内存持续增长堆外内存泄漏WebSocketFrame 未释放SimpleChannelInboundHandler自动释放若用ChannelInboundHandlerAdapter需手动frame.release()大消息发送失败OutOfDirectMemoryError大消息一次性写入超出缓冲区使用ChunkedWriteHandlerChunkedInput分块发送HTTP 响应不完整浏览器显示部分页面未设置 Content-Length始终设置CONTENT_LENGTH或使用Transfer-Encoding: chunked4.2 生产环境排查要点五、技术难点深度分析5.1 Pipeline 动态重组核心技术难点WebSocketServerProtocolHandler内部实现了一个精妙的设计握手成功后动态修改 Pipeline。为什么需要动态重组握手阶段需要 HTTP 编解码器处理 HTTP 请求/响应握手完成后协议切换为 WebSocket 帧格式需要帧编解码器如果不移除 HTTP 相关 Handler会导致后续帧数据被错误处理5.2 HttpObjectAggregator 的内存风险// 危险设置过大的聚合上限newHttpObjectAggregator(1024*1024*100);// 100MB// 攻击者发送超大请求体 → 服务端内存暴涨 → OOM// 正确做法根据业务需要设置合理上限newHttpObjectAggregator(65536);// 64KB适合大多数场景5.3 WebSocket 分片消息处理当消息较大时WebSocket 协议支持将其拆分为多个帧发送发送方 原始消息: Hello World (11 bytes) 分片发送: [TextFrame(fin0, Hello )] [ContinuationFrame(fin1, World)] 接收方需要手动处理分片拼接 if (frame instanceof TextWebSocketFrame) { if (frame.isFinalFragment()) { // 完整消息直接处理 } else { // 第一个分片缓存并等待后续帧 } } else if (frame instanceof ContinuationWebSocketFrame) { // 后续分片拼接到缓冲区 if (frame.isFinalFragment()) { // 最后一个分片处理完整消息 } }注意WebSocketServerProtocolHandler默认不处理分片需要业务层自行实现。5.4 优雅关闭与资源清理// 正确的关闭流程// 1. 服务端发送 Close 帧携带状态码和原因ctx.writeAndFlush(newCloseWebSocketFrame(1000,Server shutting down));// 2. 等待客户端确认关闭// 客户端收到 Close 帧后回复 Close 帧然后双方关闭 TCP 连接// 3. 设置关闭超时防止客户端不响应导致连接挂起newWebSocketServerProtocolHandler(/ws,null,true,65536,10000);// 第5个参数 10000 10秒关闭超时​