5-Netty WebSocket 与 HTTP

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 三层架构
java 复制代码
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 响应
java 复制代码
// 关键实现逻辑(来自 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。生产环境需要实现路由分发:

java 复制代码
// 生产环境的路由设计思路
@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 五层架构解析
java 复制代码
// 来自 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 灵活性的体现。

步骤三:帧类型处理
java 复制代码
// 来自 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 分片消息的后续帧 需要手动处理分片
步骤四:连接生命周期管理
java 复制代码
// 连接建立
@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 性能维度
java 复制代码
// ① 添加 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 可扩展性维度
java 复制代码
// ① 路由化 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 安全性维度
java 复制代码
// ① 握手鉴权:自定义 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 的内存风险

java 复制代码
// 危险:设置过大的聚合上限
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 优雅关闭与资源清理

java 复制代码
// 正确的关闭流程
// 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秒关闭超时

相关推荐
浩瀚地学9 小时前
【面试算法笔记】0302-哈希表-哈希表实现
java·经验分享·笔记·算法·面试
AC赳赳老秦9 小时前
时间开销自动统计:OpenClaw 记录工作任务时长、分析时间分配、给出优化建议
java·大数据·开发语言·python·自动化·deepseek·openclaw
名侦探7229 小时前
构建 Agent 的五大难点与解决方案
java·大数据·人工智能
Demons_kirit9 小时前
图库原理(以阿里云OSS为案例)
java·阿里云·云计算
SimonKing9 小时前
那个号称"对标 Spring"的国产框架 Solon,到底行不行?
java·后端·程序员
Lethehong9 小时前
飞算Java:从需求梳理到风险闭环,搭建云盾企业信息安全与权限管理平台
java·开发语言
霸道流氓气质10 小时前
SpringBoot中使用字典驱动的动态路由示例
java·spring boot·后端
学计算机的计算基10 小时前
操作系统八股文:进程与线程全面梳理(附调度算法+IPC+锁机制)
java·算法
wuqingshun31415910 小时前
请描述简单工厂模式的工作原理
java·简单工厂模式
大不点wow10 小时前
Spring 中 @Bean、@Component 与 @Configuration 的作用及底层逻辑
java·spring