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 本节要点

  1. 统一抽象:解析与心跳等能力挂在 BaseChannel;各协议 Channel 嵌入复用。
  2. WebSocketgorilla/websocket 为帧语义,通过 wsConn 等适配为 Read/Write 字节流,方可复用固定头解析。
  3. 业务侧:同一套 handler 表可挂在不同 Server 初始化路径上;切换协议主要改监听与构造。
  4. 差异下沉:握手、窗口、MTU、Origin 校验等留在各协议包,不污染通用解析代码。

2.5 节把 read 循环、RingBuffer 与 ParseFromRingBuffer 串成完整的粘包/拆包路径。

results matching ""

    No results matching ""