网络通道、协议与连接
本页说明客户端与服务器之间“一个 UDP 包里装了什么”、可靠与不可靠数据如何共存、消息如何分发到处理函数,以及客户端从发起连接到进入游戏要经历的签入(signon)状态。实体状态的编码放在 实体复制。
相关源码文件#
- 协议定义:
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、100 | net_Tick、net_StringCmd、net_SetConVar、net_SignonState | common/netmessages.proto:111-122 |
CLC_Messages(客户端→服务器) | 8–20 | clc_Move(携带 CUserCmd)、clc_BaselineAck、clc_ListenEvents、clc_FileCRCCheck | common/netmessages.proto:206-220 |
SVC_Messages(服务器→客户端) | 8–38 | svc_ServerInfo、svc_SendTable、svc_ClassInfo、svc_CreateStringTable、svc_UserMessage、svc_GameEvent、svc_PacketEntities、svc_TempEntities | common/netmessages.proto:332-363 |
CCLCMsg_Move 只有三个字段:备份命令数、新命令数、以及一段按位编码的 data:common/netmessages.proto:233-238。也就是说 Protobuf 只是外壳,CUserCmd 本身仍用位流增量编码,实体属性同理(CSVCMsg_PacketEntities.entity_data,见 实体复制)。
2. 包结构:CNetChan::SendDatagram#
[out seq:32][in seq:32][flags:8][checksum:16?][in reliable state:8][choked:8?]
[reliable subchannel 数据?][附加 datagram][unreliable 流][voice 流?][NOP 填充]按代码顺序:
- 远端地址为空(demo 回放或假客户端)时不真正发送,只递增序号:
engine/net_chan.cpp:1996-2006。 - 若可靠流有新数据,先把它切成片段(fragments)放入待发队列:
engine/net_chan.cpp:2013-2022。 - 写入出/入序号、预留 flags 与可选校验和、入方向可靠状态字节;有被“扣住”(choked)的包时附带数量并置
PACKET_FLAG_CHOKED:engine/net_chan.cpp:2032-2053。 SendSubChannelData若写入了可靠数据则置PACKET_FLAG_RELIABLE:engine/net_chan.cpp:2055-2058。- 依次追加调用方传入的 datagram(快照)、不可靠流、(PC 上)语音流;放不下时只打印并丢弃该部分:
engine/net_chan.cpp:2065-2095。 - 不足最小可路由长度时用
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 表示无效包):
engine/net_chan.cpp:2858-2876。 - 通知消息处理器
PacketStart,处理可靠片段,然后ProcessMessages解析剩余的普通消息:engine/net_chan.cpp:2908-2941。 - 通知
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_SetConVar | SV_ReplicateConVarChange | engine/sv_main.cpp:1503-1515 |
| 游戏事件 | svc_GameEvent(序列化后发给监听的客户端) | CGameEventManager::SerializeEvent | engine/GameEventManager.cpp:610 |
| 用户消息 | svc_UserMessage | server.dll SendUserMessage → engine->SendUserMessage | game/server/gameinterface.cpp:4031-4034 |
| 每帧快照 | svc_PacketEntities + svc_TempEntities + 声音 | CBaseClient::SendSnapshot | engine/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。