WebSocket 接入层
概述
微信小游戏、HTML5 游戏、部分 App 内嵌游戏无法直接使用裸 TCP,WebSocket 是它们与服务器通信的标准通道:基于 HTTP 握手、运行在 80/443 端口、天然穿透绝大多数防火墙。本文讲解 WebSocket 协议要点、Netty 集成方式、文本/二进制帧处理与心跳保活,并给出与自定义游戏协议结合的最佳实践。
一、WebSocket 协议基础
1.1 为什么小游戏选 WebSocket
| 对比 | TCP | WebSocket |
|---|---|---|
| 浏览器/小游戏支持 | 不支持 | 原生支持 |
| 连接建立 | 三次握手 | HTTP 升级握手 |
| 传输方向 | 全双工 | 全双工 |
| 端口 | 自定义 | 80/443,易过防火墙 |
| 数据格式 | 裸字节 | 分帧(含掩码等开销) |
结论:
微信小游戏 API 只有 WebSocket
休闲小游戏客户端基本都走 WebSocket
后端仍是 Netty,只是接入层多一层 WS 协议1.2 握手过程
1. 客户端发 HTTP Upgrade 请求:
GET /game-ws HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: xxx
Sec-WebSocket-Version: 13
2. 服务器计算 Sec-WebSocket-Accept 返回 101:
HTTP/1.1 101 Switching Protocols
Sec-WebSocket-Accept: yyy
3. 连接升级为 WebSocket,之后按 WS 帧通信握手意义:
一次 HTTP 请求完成协议升级
复用 80/443 端口与现有网络设施
可挂在 Nginx / CDN 后面1.3 帧类型
| 帧类型 | 说明 | 游戏用途 |
|---|---|---|
TextFrame | 文本帧(UTF-8) | JSON 消息、日志 |
BinaryFrame | 二进制帧 | 自定义二进制协议消息 |
PingFrame | 心跳请求 | 保活 |
PongFrame | 心跳响应 | 保活 |
CloseFrame | 关闭连接 | 主动断开 |
ContinuationFrame | 分片续帧 | 大消息分片 |
游戏关注:
BinaryFrame 放自定义二进制协议(复用 Magic+Opcode+Length)
Ping/Pong 做 WS 层保活
也可不用 WS 层心跳,用业务心跳(见后文)二、Netty 集成 WebSocket
2.1 核心组件 WebSocketServerProtocolHandler
Netty 提供了完整的 WebSocket 服务端处理器,自动处理握手、帧解析、关闭、Ping/Pong。
java
// pipeline 装配
pipeline.addLast(new HttpServerCodec()); // HTTP 编解码(握手前)
pipeline.addLast(new HttpObjectAggregator(65536)); // 聚合 HTTP 请求
pipeline.addLast(new WebSocketServerProtocolHandler("/game-ws")); // WS 协议处理
pipeline.addLast(new WebSocketFrameHandler()); // 自定义帧处理器各 Handler 职责:
HttpServerCodec 处理握手前的 HTTP 请求
HttpObjectAggregator 把分块的 HTTP 消息聚合完整
WebSocketServerProtocolHandler
处理 Upgrade 握手、帧编解码、Ping/Pong、关闭帧
握手成功后会移除 HttpServerCodec(自动)2.2 握手事件处理
java
public class WebSocketFrameHandler extends SimpleChannelInboundHandler<WebSocketFrame> {
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {
if (evt == WebSocketServerProtocolHandler.ServerHandshakeStateEvent.HANDSHAKE_COMPLETE) {
// 握手完成:这里可做连接校验、会话创建
ctx.pipeline().remove(HttpServerCodec.class);
ctx.pipeline().remove(HttpObjectAggregator.class);
} else {
ctx.fireUserEventTriggered(evt);
}
}
}握手完成点:
真正的"连接建立"事件(WS 场景替代 channelActive 的时机)
在这里创建 Session / 记录连接
可校验握手路径与 token2.3 路径与鉴权
路径控制:
WebSocketServerProtocolHandler("/game-ws") 只处理该路径
其他路径返回 404 / 直接拒绝
不同玩法可拆不同路径(/game-lobby、/game-battle)
握手鉴权:
token 可放 URL 参数(wss://host/game-ws?token=xxx)
握手完成后立刻校验,失败发 CloseFrame 关闭三、文本与二进制帧处理
3.1 帧类型分发
java
public class WebSocketFrameHandler extends SimpleChannelInboundHandler<WebSocketFrame> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, WebSocketFrame frame) {
if (frame instanceof BinaryWebSocketFrame) {
handleBinary(ctx, (BinaryWebSocketFrame) frame);
} else if (frame instanceof TextWebSocketFrame) {
handleText(ctx, (TextWebSocketFrame) frame);
} else if (frame instanceof PingWebSocketFrame) {
ctx.writeAndFlush(new PongWebSocketFrame(frame.content().retain()));
} else if (frame instanceof CloseWebSocketFrame) {
ctx.close();
}
}
}处理原则:
Binary → 解析自定义协议消息 → 走业务分发
Text → 一般只允许控制类消息(JSON 配置/日志上报)
Ping → 自动回 Pong(也可交给 Handler 统一处理)
Close → 触发下线流程3.2 二进制帧内复用自定义协议
推荐方案:WS 帧内封装自定义二进制协议
BinaryFrame.content() 就是"魔法头 + Opcode + Length + Body"
WS 帧本身有边界(一帧一条消息),无粘包问题
Length 仍然保留:帧内校验 + 多消息聚合容错
Pipeline 设计:
HttpServerCodec → HttpObjectAggregator
→ WebSocketServerProtocolHandler
→ 自定义二进制解码器(content → GameMessage)
→ 业务 Handler注意:WebSocket 帧边界
WS 每帧自带长度,天然解决粘包
但应用层 Length 建议保留:
帧分片(ContinuationFrame)需要重组
未来切回 TCP 时协议可复用3.3 文本帧的 JSON 场景
Text 帧适用场景:
运维指令(后台推送)
调试面板
非核心低频消息
与二进制核心协议并存,互不干扰四、心跳保活
4.1 两种心跳方案
方案一:WS 协议层 Ping/Pong
服务器周期发 Ping,客户端回 Pong:
由 WebSocketServerProtocolHandler 自动处理
连接层保活,业务无感知方案二:业务层心跳消息(推荐)
用自定义协议的心跳 Opcode:
客户端周期发心跳消息
服务器读空闲检测(IdleStateHandler)超时断开
与 TCP 方案的 IdleStateHandler 完全一致选型建议:
小游戏环境建议业务层心跳:
微信后台可能休眠,业务心跳更可控
心跳同时可携带客户端时间、版本等信息
与纯 TCP 服务器共用一套心跳逻辑4.2 IdleStateHandler 复用
WebSocket 场景下 pipeline 里加 IdleStateHandler:
IdleStateHandler(60, 0, 0) → 读空闲 60s 断开
客户端心跳 30s 一次 → 正常连接永不断开
与二进制解码器、业务 Handler 共存
心跳不进入业务分发,在网关拦截五、性能与稳定性
5.1 参数调优
| 项 | 建议 |
|---|---|
| HttpObjectAggregator 上限 | 64KB 足够(握手无大请求) |
| WS 帧最大长度 | 与协议 maxFrameLength 一致 |
| 心跳周期 | 30s,读空闲阈值 60s(两倍) |
| 内存 | 用 PooledByteBufAllocator(默认) |
5.2 与 Nginx/CDN 协同
前置 Nginx 场景:
Nginx 配 WebSocket 反代:
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
Nginx 与上游保持心跳,避免空闲断连
wss 由 Nginx 层 TLS 终结5.3 常见问题
| 现象 | 原因 | 处理 |
|---|---|---|
| 握手一直失败 | 路径不匹配 / 未配置协议升级 | 核对路径与 Nginx 配置 |
| 连接空闲就被断 | 网关/负载均衡空闲超时 | 心跳周期小于空闲阈值 |
| 帧内容解析乱 | 忘在 WS 层做二进制解码 | 增加解码器 Handler |
| 断线频繁 | 移动网络切换 | 客户端指数退避重连 |
六、小结
WebSocket 是微信小游戏与 HTML5 游戏接入服务器的事实标准:通过一次 HTTP 升级握手建立全双工长连接,Netty 的 WebSocketServerProtocolHandler 把握手、帧解析、Ping/Pong 全部内置。接入层的正确姿势是在二进制帧内复用自定义二进制协议——WS 帧解决边界,内部字节仍按 Magic+Opcode+Length 解析,这样未来切 TCP 通道协议零改动。心跳用业务层消息配合 IdleStateHandler 读超时,与纯 TCP 方案保持一致。WebSocket 与 TCP 只是接入通道不同,通道之上的会话、分发、业务逻辑完全复用。