基于KCP的帧同步服务器实现

前置知识链接:

什么是KCP?

什么是帧同步?


以下是 go 代码实现

1. 基础KCP服务端初始化

package main

import (
    "github.com/xtaci/kcp-go/v5"
    "golang.org/x/crypto/pbkdf2"
    "crypto/sha1"
    "log"
    "time"
)

func main() {
    // KCP加密配置(可选)
    key := pbkdf2.Key([]byte("demo_pass"), []byte("demo_salt"), 1024, 32, sha1.New)
    block, _ := kcp.NewAESBlockCrypt(key)

    // 启动KCP服务端监听
    listener, err := kcp.ListenWithOptions(":8888", block, 10, 3)
    if err != nil {
        log.Fatal(err)
    }
    log.Println("帧同步服务端已启动,监听端口8888")

    // 帧同步主循环管理
    go frameLoop()

    for {
        conn, err := listener.AcceptKCP()
        if err != nil {
            log.Println("客户端连接失败:", err)
            continue
        }
        go handleClient(conn)
    }
}

关键点

  • 使用kcp.ListenWithOptions初始化服务端,支持加密和FEC纠错。
  • 103参数分别代表数据分片和纠错分片数量,提升抗丢包能力。

2. 客户端连接管理

type ClientSession struct {
    conn     *kcp.UDPSession
    lastFrame uint32
    inputQueue [][]byte // 存储客户端操作指令
}

var clients sync.Map // 全局客户端会话映射

func handleClient(conn *kcp.UDPSession) {
    session := &ClientSession{
        conn: conn,
    }
    clients.Store(conn.RemoteAddr(), session)

    // 设置KCP参数优化延迟
    conn.SetNoDelay(1, 10, 2, 1) // 快速模式
    conn.SetWindowSize(4096, 4096)

    buf := make([]byte, 4096)
    for {
        n, err := conn.Read(buf)
        if err != nil {
            clients.Delete(conn.RemoteAddr())
            return
        }
        // 收集客户端输入(如移动、攻击指令)
        session.inputQueue = append(session.inputQueue, buf[:n])
    }
}

关键点

  • 使用SetNoDelay(1,10,2,1)开启KCP快速模式,降低RTO重传延迟。
  • 每个客户端维护输入队列,确保帧同步时按顺序处理。

3. 帧同步主循环

func frameLoop() {
    ticker := time.NewTicker(33 * time.Millisecond) // 30帧/秒
    defer ticker.Stop()
    var currentFrame uint32 = 0

    for range ticker.C {
        currentFrame++
        var frameData [][]byte

        // 收集所有客户端当前帧输入
        clients.Range(func(_, value interface{}) bool {
            session := value.(*ClientSession)
            if len(session.inputQueue) > 0 {
                input := session.inputQueue[0]
                frameData = append(frameData, input)
                session.inputQueue = session.inputQueue[1:]
            }
            return true
        })

        // 广播帧数据(包含当前帧号+所有操作)
        broadcastFrame(currentFrame, frameData)
    }
}

func broadcastFrame(frame uint32, data [][]byte) {
    // 封装帧数据(Protobuf/JSON)
    payload := encodeFrameData(frame, data)

    // 向所有客户端发送
    clients.Range(func(_, value interface{}) bool {
        session := value.(*ClientSession)
        if _, err := session.conn.Write(payload); err != nil {
            log.Println("广播失败:", err)
        }
        return true
    })
}

关键点

  • 固定时间间隔(如33ms)驱动帧同步,确保所有客户端逻辑帧一致。
  • 使用encodeFrameData序列化帧数据,推荐Protobuf减少带宽占用。

4. 断线重连与状态快照

// 客户端重连时发送完整状态
func handleReconnect(session *ClientSession) {
    snapshot := getWorldSnapshot() // 获取当前游戏状态快照
    if _, err := session.conn.Write(snapshot); err != nil {
        log.Println("快照发送失败:", err)
    }
}

关键点

  • 断线重连时通过getWorldSnapshot生成全量状态,避免逐帧追补。
  • KCP的可靠传输确保快照数据完整到达。

5. 性能优化建议

  1. KCP参数调优
    conn.SetMtu(1350):避免IP分片,适配常见网络MTU。
    conn.SetStreamMode(true):流模式减少分片开销。
  2. 带宽控制
    • 使用conn.SetWindowSize(1024, 1024)限制发送/接收窗口,防止内存暴涨。
  3. 加密与压缩
    • 集成Snappy压缩算法减少帧数据大小。

滚动至顶部