1. 2.4 多协议接入
在读缓冲与线协议确定之后,接入层问题变成:不同传输承载(TCP、WebSocket、KCP 等)能否共用同一套读解析与 Channel 逻辑。本节概括 zhenyi-base 里「适配为 net.Conn + 嵌入 BaseChannel」的做法(类型与包名以仓库为准)。
1.1. 2.4.1 问题:多协议意味着多套代码?
一个实时应用可能需要同时支持多种协议:
| 协议 | 适用场景 |
|---|---|
| TCP | 服务端间通信、游戏客户端 |
| WebSocket | 浏览器端、H5 游戏、Web 管理后台 |
| KCP | 弱网环境、对延迟敏感的游戏 |
如果每种协议写一套独立的网络层,协议解析、粘包处理、连接管理、心跳检测……全部重复。
1.2. 2.4.2 核心思路:适配器模式
做法:为每种承载提供一个满足 net.Conn 读写契约的适配层,使 BaseChannel 只依赖字节流语义。
┌──────────────────────────────────────────┐
│ BaseChannel │ ← 统一连接抽象
│ (协议解析、RingBuffer、心跳) │
├──────────┬──────────┬────────────────────┤
│ TCP │ WS │ KCP │
│ ztcp │ zws │ zkcp │
│ Channel │ Channel │ Channel │
├──────────┼──────────┼────────────────────┤
│ net.Conn │ wsConn │ kcp.UDPSession │ ← 全部实现 net.Conn
└──────────┴──────────┴────────────────────┘
三种协议各有自己的 Channel 实现,但都嵌入 znet.BaseChannel:
// TCP
type Channel struct {
*znet.BaseChannel // 嵌入,直接复用
}
// WebSocket
type Channel struct {
*znet.BaseChannel // 嵌入,直接复用
}
// KCP
type Channel struct {
*znet.BaseChannel // 嵌入,直接复用
}
1.3. 2.4.3 最难适配的:WebSocket
TCP 和 KCP 本身就是 net.Conn,不需要适配。
WebSocket 不同——gorilla/websocket 的 *websocket.Conn 不是 net.Conn。它是消息模式的(一帧一帧地读),而我们需要流模式的(和 TCP 一样按字节读)。
zhenyi 写了一个适配器 wsConn,把消息模式转成流模式:
type wsConn struct {
c *websocket.Conn
readBuf []byte // 帧剩余数据缓存
}
func (w *wsConn) Read(p []byte) (int, error) {
// 如果上一帧还有剩余数据,先消费完
if len(w.readBuf) > 0 {
n := copy(p, w.readBuf)
w.readBuf = w.readBuf[n:]
return n, nil
}
// 读取下一帧
mt, data, err := w.c.ReadMessage()
// 只接受二进制帧,忽略其他类型
n := copy(p, data)
if n < len(data) {
w.readBuf = data[n:] // 一帧装不下,缓存剩余部分
}
return n, nil
}
func (w *wsConn) Write(p []byte) (int, error) {
w.c.WriteMessage(websocket.BinaryMessage, p) // 包装成二进制帧发送
return len(p), nil
}
为什么要适配成流模式?
因为 BaseChannel 的协议解析(固定头 + 变长体)是按字节流设计的。WebSocket 帧和 TCP 字节流是两种不同的数据模型,适配成流模式后,协议解析层完全不需要知道底层是什么协议。
1.4. 2.4.4 业务层无感知
适配完成后,业务层的代码是这样的:
// TCP 服务器
tcpSrv := ztcp.NewServer(":8001", handlers)
tcpSrv.Server(ctx)
// WebSocket 服务器
wsSrv := zws.NewServer(":8002", handlers)
wsSrv.Server(ctx)
// KCP 服务器
kcpSrv := zkcp.NewServer(":8003", handlers)
kcpSrv.Server(ctx)
handlers 完全一样,包括 OnAccept(连接建立回调)和 OnRead(消息到达回调)。业务逻辑写在 handlers 里,不管底下是 TCP 还是 WebSocket。
这意味着:
- 同一个业务逻辑可以同时跑在三种协议上
- 新增协议只需要写一个适配器,不用改业务代码
- 切换协议只需要改一行初始化代码
1.5. 2.4.5 每种协议的差异点
虽然业务层统一了,但每种协议的底层配置还是不同的:
1.5.1. TCP
// 标准 TCP,不需要额外配置
ln, _ := net.Listen("tcp", ":8001")
特点:稳定可靠,适合大多数场景。
1.5.2. WebSocket
// 基于 HTTP 升级
upgrader := websocket.Upgrader{
HandshakeTimeout: 10 * time.Second,
ReadBufferSize: 1024,
WriteBufferSize: 1024,
}
特点:能穿透防火墙和代理,浏览器原生支持。注意生产环境要校验 CheckOrigin,防止跨站 WebSocket 攻击。
1.5.3. KCP
// 基于 UDP,需要手动配置参数
conn.SetNoDelay(1, 20, 2, 1) // 启用无延迟模式
conn.SetWindowSize(128, 128) // 窗口大小
conn.SetACKNoDelay(true) // ACK 不延迟
conn.SetMtu(1200) // MTU 大小
特点:基于 UDP 的可靠传输,通过牺牲带宽换取低延迟。适合弱网游戏。但 KCP 的参数调优比较复杂,需要根据网络环境调整。
1.6. 2.4.6 本节要点
- 统一抽象:解析与心跳等能力挂在
BaseChannel;各协议 Channel 嵌入复用。 - WebSocket:
gorilla/websocket为帧语义,通过wsConn等适配为Read/Write字节流,方可复用固定头解析。 - 业务侧:同一套 handler 表可挂在不同
Server初始化路径上;切换协议主要改监听与构造。 - 差异下沉:握手、窗口、MTU、Origin 校验等留在各协议包,不污染通用解析代码。
2.5 节把 read 循环、RingBuffer 与 ParseFromRingBuffer 串成完整的粘包/拆包路径。