CD Code Digest Hub /CS:GO

网络通道、协议与连接

15 分钟阅读 11 个章节 1 张图

本页说明客户端与服务器之间“一个 UDP 包里装了什么”、可靠与不可靠数据如何共存、消息如何分发到处理函数,以及客户端从发起连接到进入游戏要经历的签入(signon)状态。实体状态的编码放在 实体复制。

相关页面:引擎总览 · 主循环与 Tick · 输入与预测

相关源码文件#

  • 协议定义:common/netmessages.proto、common/netmessages_signon.h
  • 通道:engine/net_chan.cpp、engine/net_chan.h、engine/net_ws.cpp
  • 连接:engine/baseclientstate.cpp、engine/baseserver.cpp、engine/baseclient.cpp、engine/sv_client.cpp
  • 客户端发送:engine/cl_main.cpp

1. 消息目录(Protobuf)#

引擎层网络消息以 Protobuf message 定义,分成三组编号:

组编号代表消息证据
NET_Messages(双向)0–7、100net_Tick、net_StringCmd、net_SetConVar、net_SignonStatecommon/netmessages.proto:111-122
CLC_Messages(客户端→服务器)8–20clc_Move(携带 CUserCmd)、clc_BaselineAck、clc_ListenEvents、clc_FileCRCCheckcommon/netmessages.proto:206-220
SVC_Messages(服务器→客户端)8–38svc_ServerInfo、svc_SendTable、svc_ClassInfo、svc_CreateStringTable、svc_UserMessage、svc_GameEvent、svc_PacketEntities、svc_TempEntitiescommon/netmessages.proto:332-363

CCLCMsg_Move 只有三个字段:备份命令数、新命令数、以及一段按位编码的 data:common/netmessages.proto:233-238。也就是说 Protobuf 只是外壳,CUserCmd 本身仍用位流增量编码,实体属性同理(CSVCMsg_PacketEntities.entity_data,见 实体复制)。

2. 包结构:CNetChan::SendDatagram#

text
[out seq:32][in seq:32][flags:8][checksum:16?][in reliable state:8][choked:8?]
[reliable subchannel 数据?][附加 datagram][unreliable 流][voice 流?][NOP 填充]

按代码顺序:

  1. 远端地址为空(demo 回放或假客户端)时不真正发送,只递增序号:engine/net_chan.cpp:1996-2006。
  2. 若可靠流有新数据,先把它切成片段(fragments)放入待发队列:engine/net_chan.cpp:2013-2022。
  3. 写入出/入序号、预留 flags 与可选校验和、入方向可靠状态字节;有被“扣住”(choked)的包时附带数量并置 PACKET_FLAG_CHOKED:engine/net_chan.cpp:2032-2053。
  4. SendSubChannelData 若写入了可靠数据则置 PACKET_FLAG_RELIABLE:engine/net_chan.cpp:2055-2058。
  5. 依次追加调用方传入的 datagram(快照)、不可靠流、(PC 上)语音流;放不下时只打印并丢弃该部分:engine/net_chan.cpp:2065-2095。
  6. 不足最小可路由长度时用 NOP 消息填充,并把尾部补到字节边界:engine/net_chan.cpp:2097-2130。

可靠传输使用 8 个“子通道”交替确认(stop-and-wait 位翻转):engine/net_chan.h:33。接收端在 PACKET_FLAG_RELIABLE 时读取子通道号与各流片段,成功后翻转对应位,再检查各流是否收齐:engine/net_chan.cpp:2910-2931。

设计含义(推断):快照是“要么整体进包、要么整体丢弃”的不可靠数据,源码注释称之为 “snapshot behavior”:engine/net_chan.cpp:2061-2064。丢包由下一次快照的差量基准机制修复,而不是重传。

3. 发送一条消息:SendNetMsg#

SendNetMsg 根据消息自身的 IsReliable() 或调用方的 bForceReliable 选择可靠流,语音走独立流;写缓冲溢出时可按 net_droponsendoverflow 断开连接:engine/net_chan.cpp:2965-2995。消息类型与处理对象的绑定通过 RegisterMessage 按类型号登记:engine/net_chan.cpp:3011-3020。

4. 接收:ProcessPacket#

  1. 来源地址与远端不符则丢弃;更新流量统计;解析包头(返回 -1 表示无效包):engine/net_chan.cpp:2858-2876。
  2. 通知消息处理器 PacketStart,处理可靠片段,然后 ProcessMessages 解析剩余的普通消息:engine/net_chan.cpp:2908-2941。
  3. 通知 PacketEnd;客户端若在录制 demo,则把本包记录下来:engine/net_chan.cpp:2944-2951。

客户端侧消息处理对象是 CClientState(CBaseClientState 的子类):engine/client.h:68;服务端每个连接对应一个 CGameClient,例如 clc_Move 的处理函数是 CGameClient::CLCMsg_Move:engine/sv_client.cpp:253。

5. 连接握手(无连接包阶段)#

证据:客户端发出 A2S_GETCHALLENGE:engine/baseclientstate.cpp:1451;服务器分派 A2S_GETCHALLENGE、C2S_CONNECT 等无连接包:engine/baseserver.cpp:936-970;ReplyChallenge 写 S2C_CHALLENGE:engine/baseserver.cpp:1631、engine/baseserver.cpp:1656;客户端解析挑战(含 Steam 认证字段与溢出检查):engine/baseclientstate.cpp:1673-1706,随后在 HandleDeferredConnection 中 SendConnectPacket:engine/baseclientstate.cpp:2195、engine/baseclientstate.cpp:2273、engine/baseclientstate.cpp:789-808;服务器 ConnectClient:engine/baseserver.cpp:519;客户端收到 S2C_CONNECTION 且加密期望一致时 FullConnect:engine/baseclientstate.cpp:1651-1671、engine/baseclientstate.cpp:1138。若服务器尚未收到客户端的任何通道包,会在发送阶段重发 S2C_CONNECTION:engine/sv_main.cpp:2197-2202。

6. 签入状态(signon)#

状态含义(源码注释)服务器动作
CHALLENGE(1)挑战阶段,仅无连接包—
CONNECTED(2)网络通道已就绪校验连接、放宽超时以便加载地图,标记待发 ServerInfo
NEW(3)已收到服务器信息与字符串表发送签入数据(含 svc_ClassInfo);单人时安装字符串表镜像
PRESPAWN(4)已收到签入缓冲SpawnPlayer
SPAWN(5)准备接收实体包ActivatePlayer,停止 HLTV 回放
FULL(6)收到首个非差量包,完全连接收紧超时,读档时 FinishRestore,通知插件 ClientFullyConnect
CHANGELEVEL(7)服务器换图中停止 HLTV 回放

证据:状态定义 common/netmessages_signon.h:7-14;通用处理 engine/baseclient.cpp:335-376;游戏客户端扩展处理 engine/sv_client.cpp:942-986;签入数据发送 engine/sv_client.cpp:1194-1240。

7. 客户端发包节奏#

CL_Move 在每个 tick 的输入阶段运行:

  • 对非回环连接(或开启 host_limitlocal 时),不满足“到达 m_flNextCmdTime、通道可发送、最后一个 tick”三个条件时只生成命令、不发送;被扣住的命令计入 chokedcommands 并调用 SetChoked:engine/cl_main.cpp:2767-2813。
  • 发包时:CL_SendMove 打包“新命令 + 2 条备份命令”(数量受 MAX_NEW_COMMANDS/MAX_BACKUP_COMMANDS 限制),任一 WriteUsercmdDeltaToBuffer 失败则整条消息不发:engine/cl_main.cpp:2684-2734。
  • 连接质量差(通道即将超时)时请求非差量全量更新:engine/cl_main.cpp:2820-2868。
  • 发送 net_Tick 确认收到的服务器 tick,然后 SendDatagram,并按 cl_cmdrate 计算下次发送时间;签入期间固定每秒 5 包:engine/cl_main.cpp:2892-2924。

服务器收到 clc_Move 后的检查见 输入与预测。

8. 其他承载在通道上的游戏数据#

数据通道消息生产者证据
ConVar 复制net_SetConVarSV_ReplicateConVarChangeengine/sv_main.cpp:1503-1515
游戏事件svc_GameEvent(序列化后发给监听的客户端)CGameEventManager::SerializeEventengine/GameEventManager.cpp:610
用户消息svc_UserMessageserver.dll SendUserMessage → engine->SendUserMessagegame/server/gameinterface.cpp:4031-4034
每帧快照svc_PacketEntities + svc_TempEntities + 声音CBaseClient::SendSnapshotengine/baseclient.cpp:1481-1512

9. 网络字符串表#

字符串表是服务器向客户端增量同步“字符串 → 可选数据”字典的机制,用于预缓存列表、实例基线、用户信息等。服务器在建表时创建引擎自带的表:可下载文件、模型/通用/声音/贴花预缓存、实例基线、光照样式、用户信息、动态模型、服务器启动数据,并把纯净服务器白名单写入表中:engine/sv_main.cpp:587-656;游戏 DLL 再创建自己的表:game/server/gameinterface.cpp:1675-1709。

每次发送快照时,在写实体之前先写字符串表的增量更新(本地后门模式下已在打包阶段直接更新):engine/baseclient.cpp:1466-1473、engine/networkstringtable.cpp:2045。单表增量写入与客户端解析入口:engine/networkstringtable.cpp:946、engine/networkstringtable.cpp:1117。服务器在每帧发送后把字符串表 tick 推进 1,确保之后的修改计入下一帧:engine/sv_main.cpp:3413-3416。实体的“实例基线”就存放在 INSTANCE_BASELINE 表中,写 EnterPVS 时缺失基线会报错:engine/sv_main.cpp:630-631、engine/sv_ents_write.cpp:640。

约束与边界#

  • 每帧的网络套接字处理在 NET_RunFrame 中进行,它同时驱动 RCON 与匹配框架的 RunFrame:engine/net_ws.cpp:3640-3667。
  • net_ws.cpp 中的 Steam 数据报中继(SDR)、通道加密密钥交换与 RCON 协议见 服务器运维面;客户端发来的 net_StringCmd 如何分派与限制见 输入与命令。