RAGE 网络传输层:连接、套接字、NAT 穿透与消息
本页解释 rage/base/src/net(命名空间 rage,日志通道 ragenet)如何把 UDP 数据报变成游戏可用的“按通道、可靠/不可靠、带事件回调”的 P2P 连接。GTA V 多人模式的一切点对点流量(会话管理消息、对象同步、语音、文本聊天、时间同步)都经过这里的 netConnectionManager。上层的会话(snSession/CNetworkSession)见 network-sessions-and-transitions.md,在线服务(ROS、presence、matchmaking)见 rline-online-services.md,对象同步与带宽管理见 ../17-network-sync/network-object-model.md 和 ../17-network-sync/bandwidth-players-and-debug.md。
安全边界:本页只到“模块边界 / 消息类型 / 状态机 / 容量常量”层面。加密、密钥交换、DTLS、relay 线格式只写存在与位置,不写算法细节、密钥材料或服务端点。
背景与术语#
| 术语 | 代码实体 | 含义 |
|---|---|---|
| 端点 (endpoint) | netEndpoint、EndpointId | 同一远端地址上所有通道的集合;地址可在直连/relay/peer relay 之间重分配 |
| 连接 (connection) | netConnection / Cxn、int cxnId | 某端点某通道上的有序可靠流,带独立序列号、ACK、重传和超时 |
| 通道 (channel) | ChannelId,0–15 | 逻辑复用:每端点每通道至多一个连接;14、15 被底层保留 |
| 包 (packet) | netPacket | 一个 UDP 数据报,含若干 bundle |
| 束 (bundle) | netBundle | 一个通道的帧集合 + ACK 信息 |
| 帧 (frame) | netFrame | 应用发送的最小单元(一次 Send()),可靠帧自带序号 |
| 消息 (message) | netMessage + NET_MESSAGE_DECL | 帧负载中用位流序列化的强类型结构 |
| 隧道 (tunnel) | netTunneler/netTunnelRequest | 建立连接前的 NAT 穿透 + relay 回退 + 密钥交换 |
设计说明文档 NetOverview.doc、LiveOverview.doc、RNL.ppt、TODO.txt 也在该目录中,但属于设计意图证据,本页以源码为准。
相关源码文件#
| 文件 | 作用 |
|---|---|
net/net.h、net/net.cpp | 全局常量/枚举、netInit/netUpdate/netShutdown、net 线程池、netRandom |
net/connectionmanager.h/.cpp(≈9.8k 行) | netConnectionManager、netEndpoint、Cxn、发送 Worker 线程、Pack/Unpack、事件分发 |
net/connection.h/.cpp | netConnection:序列号、ACK 位图、重传、keepalive、超时 |
net/packet.h/.cpp、net/netsequence.h、net/policies.h | 包/束/帧二进制布局、netSequence、连接与通道策略 |
net/netsocket*.cpp/.h、net/netsocketmanager.*、net/nethardware*.cpp | 平台套接字、主套接字 + 对称套接字复用、接收线程 |
net/netaddress.*、net/peeraddress.*、net/netpeerid.*、net/netrelaytoken.* | IP/端口地址、三类 netAddress、netPeerAddress |
net/tunneler.*、net/tunneler_ice.*(≈10k 行) | 隧道状态机、类 ICE 的候选地址连通性检查 |
net/natdetector.*、net/natupnp.*、net/natpcp.* | NAT 类型检测、UPnP/PCP 端口映射 |
net/relay.*、net/connectionrouter.* | relay/presence 服务器通信、P2P 中继路由 |
net/message.h、net/messageids.h、net/event.h | 消息声明宏、全局消息 ID 表、连接事件 |
net/compression.*、net/timesync.*、net/lag.*、net/peercomplainer.* | 包压缩、网络时钟、延迟估计、对端投诉/踢出 |
net/bandwidth.*、net/netratelimit.*、net/netallocator.* | 包记录器、令牌桶、连接管理器分配器 |
net/crypto.*、net/keyexchange.*、net/netnonce.* | 加密/随机数/密钥交换(只写位置) |
net/http.*、net/tcp.*、net/task.*、net/task2.*、net/status.h、net/transaction.* | HTTP/TCP 客户端、异步任务框架、netStatus、请求/响应事务(供 rline/snet 使用) |
game/network/Network.cpp、game/network/Live/LiveManager.cpp | 游戏侧初始化、通道委托、超时策略、tunables |
分层总览#
证据:通道委托注册 src/dev_ng/game/network/Network.cpp:1740-1744;连接管理器成员 m_Router/m_Tunneler/m_Compression/m_Channels/socket 与 relay 委托 src/dev_ng/rage/base/src/net/connectionmanager.h:1749-1794;Init 中向 socket manager 和 relay 注册并发委托 src/dev_ng/rage/base/src/net/connectionmanager.cpp:1332-1353;接收线程先交给 netRelay::Accept,未消费才派发给连接管理器 src/dev_ng/rage/base/src/net/netsocketmanager.cpp:1340-1350;隧道状态机调用 ICE 与 key exchange src/dev_ng/rage/base/src/net/tunneler.cpp:1179-1271、src/dev_ng/rage/base/src/net/tunneler.cpp:1461-1485。NAT 检测与隧道的关系是推断(ICE 使用 NAT 信息与端口映射器),未逐行追踪。
全局生命周期:谁初始化什么#
netInit / netUpdate / netShutdown#
netInit() 带引用计数(s_NetInitCount),只在第一次调用时依次初始化:net 线程池 → netTcp → netCrypto(NX 除外)→ netHardware → netRelay(需要 socket manager)→ netResolver(仅 !GTA_VERSION)→ netNatDetector → HTTP 拦截器(可选)→ netTask → netTask2 src/dev_ng/rage/base/src/net/net.cpp:43-85。netUpdate() 每帧轮询 netHardware、netRelay、netNatDetector、netTask、netTask2 src/dev_ng/rage/base/src/net/net.cpp:87-103,netShutdown() 在计数归零时逆序关闭 src/dev_ng/rage/base/src/net/net.cpp:105-132。GTA V 的 forceinclude 定义 GTA_VERSION 501(如 src/dev_ng/rage/base/src/forceinclude/win64_final.h:524),因此 netResolver 在本游戏中被编译排除。
注意 netConnectionManager 不由 netInit 创建;它是游戏对象,由 CNetwork 持有。
游戏侧装配顺序#
CLiveManager::Init先创建网络堆、rline 堆和sm_RlineAllocator,然后new netSocketManager并Init(sm_RlineAllocator, NETWORK_CPU_AFFINITY, port),随后netInit(sm_RlineAllocator, sm_SocketMgr),再把CLiveP2pKeys提供的 HMAC salt 交给连接管理器src/dev_ng/game/network/Live/LiveManager.cpp:453-525。之后才rlInit(...)使用主套接字src/dev_ng/game/network/Live/LiveManager.cpp:874。CNetwork::Init(INIT_CORE)在网络堆中分配连接管理器专用堆SCXNMGR_HEAP_SIZE(PC/Durango/Orbis 为 650 KB,其他平台 128 KBsrc/dev_ng/game/network/Network.cpp:635-639),用netMemAllocator把其中约 407 KB 切成 12 个固定块桶(16…960 字节),其余交给简单分配器,且SetQuitOnFail(false),即连接管理器自行处理分配失败src/dev_ng/game/network/Network.cpp:1693-1707。- 随后
sm_CxnMgr.Init(allocator, DEFAULT_ENDPOINT_POOL_HINT, DEFAULT_CXN_POOL_HINT, sktMgr, CXN_MGR_CPU_AFFINITY, NULL, NULL, NULL):端点提示 =MAX_NUM_ACTIVE_PLAYERS、连接提示 = 端点 ×2src/dev_ng/game/network/Network.cpp:274-275;CXN_MGR_CPU_AFFINITY = 0(非负 → 多线程模式)src/dev_ng/game/network/Network.cpp:813;压缩分配器和字典文件名均为NULLsrc/dev_ng/game/network/Network.cpp:1718-1728。池大小提示之后可被 tunablesENDPOINT_POOL_HINT/CXN_POOL_HINT调整src/dev_ng/game/network/Network.cpp:1242-1245。 ResetConnectionPolicies(GetTimeoutTime())设置所有通道默认连接策略;然后为 16 个通道各注册一个绑定到CNetwork::OnNetEvent的Delegate,并创建CNetworkSession、CNetworkParty、语音会话等src/dev_ng/game/network/Network.cpp:1730-1760。- 每帧
CLiveManager依次sm_SocketMgr->Update()、netUpdate()、rlUpdate()src/dev_ng/game/network/Live/LiveManager.cpp:1687-1693;连接管理器自身的Update()由CNetwork::Update驱动(计时条名"netConnectionManager",src/dev_ng/game/network/Network.cpp:2149)。CNetwork的完整生命周期见 network-sessions-and-transitions.md。
线程模型#
| 线程 | 创建位置 | 做什么 |
|---|---|---|
| 主线程(游戏更新) | — | netConnectionManager::Update():MT 模式下只做压缩统计更新、Dispatch() 事件、UpdateTunnelRequests();netUpdate()/rlUpdate() |
[RAGENET] Send(48 KB 栈) | Worker::Init | 循环调用 Send()(Pack→压缩→加密→发包)并更新 16 个通道限速器,然后在 m_SendSema 上等待 clamp(m_MinSendInterval, 10, 250) ms |
| socket manager 接收线程(64 KB 栈) | netSocketManager::Init → ProcessSockets | select 最多等 1 s;收包后先给 netRelay::Accept,否则作为并发事件派发给连接管理器(解密/解压/入队都在此线程) |
| net 线程池(4 线程) | netThreadPoolInit | 供 netTask2 等执行阻塞工作;Durango/Orbis 交替绑定 CPU 1/2,其他平台 CPU 0 |
证据:主线程 Update() src/dev_ng/rage/base/src/net/connectionmanager.cpp:4123-4164;Worker 线程创建与循环 src/dev_ng/rage/base/src/net/connectionmanager.cpp:696-703、src/dev_ng/rage/base/src/net/connectionmanager.cpp:765-806;栈大小 src/dev_ng/rage/base/src/net/connectionmanager.cpp:54;接收线程常量 src/dev_ng/rage/base/src/net/netsocketmanager.h:339-344、select 超时注释 src/dev_ng/rage/base/src/net/netsocketmanager.cpp:1192-1200;线程池 src/dev_ng/rage/base/src/net/net.h:307-329、src/dev_ng/rage/base/src/net/net.cpp:134-157。
同步:连接管理器所有共享状态由 m_Cs(NET_CS_SYNC 宏)保护,委托列表另有 m_CsDlgt 以降低死锁风险 src/dev_ng/rage/base/src/net/connectionmanager.h:1752-1773。非 __NO_OUTPUT 构建下 netCriticalSectionToken 包装了锁计数与等待/持有抖动统计,__NO_OUTPUT 下退化为 sysCriticalSectionToken src/dev_ng/rage/base/src/net/connectionmanager.h:430-474。接收路径刻意在解密、解压之后才进入临界区,以免阻塞主线程和发送线程 src/dev_ng/rage/base/src/net/connectionmanager.cpp:3769-3779。主线程是否在整个 Dispatch()+UpdateTunnelRequests() 期间持锁由 tunable CXN_ACQUIRE_CRIT_SEC_FOR_FULL_MAIN_UPDATE 控制 src/dev_ng/rage/base/src/net/connectionmanager.cpp:4151-4164、src/dev_ng/game/network/Network.cpp:1223。
寻址:netSocketAddress → netAddress → netPeerAddress#
netIpV4Address/netIpV6Address/netIpAddress/netSocketAddress:IP 与 IP:port 值类型src/dev_ng/rage/base/src/net/netaddress.h:48、src/dev_ng/rage/base/src/net/netaddress.h:558。双栈只在 iOS 打开(IPv4_IPv6_DUAL_STACK (RSG_IOS))src/dev_ng/rage/base/src/net/net.h:54-63。netAddress是“相对路由的地址”,Type有TYPE_DIRECT、TYPE_RELAY_SERVER、TYPE_PEER_RELAY三种;relay 形式由“代理地址 + relay token + 目标地址”组成,peer relay 形式由“下一跳地址 + 目标netPeerId”组成src/dev_ng/rage/base/src/net/netaddress.h:737-791。netPeerAddress是可在 presence/matchmaking 中发布的“如何找到我”描述:netPeerId、rlGamerHandle、relay 服务器地址 + relay token、公网地址、私网地址、NAT 类型src/dev_ng/rage/base/src/net/peeraddress.h:326-336。NET_PEER_ADDR_CANDIDATES仅RSG_GGP(Stadia)启用,可携带至多 4 个平台 P2P 候选地址src/dev_ng/rage/base/src/net/peeraddress.h:18-32、src/dev_ng/rage/base/src/net/peeraddress.h:79-82。netPeerId是 64 位、每次启动都会变化的本机 ID(注释明确与rlPeerInfo中的 ID 不同,且不得持久化)src/dev_ng/rage/base/src/net/netpeerid.h:15-35。
netConnectionManager 用三张侵入式 map 同时按 EndpointId、netAddress、netPeerId 索引活动端点 src/dev_ng/rage/base/src/net/connectionmanager.h:1625-1628、src/dev_ng/rage/base/src/net/connectionmanager.h:1727-1741,因此当对端从直连切换到 relay(或反之)时可以 ReassignEndpoint 而不打断上层连接。
连接管理器的数据模型#
池与容量#
| 项 | 值 | 证据 |
|---|---|---|
| 端点/连接池上限 | MAX_ENDPOINTS_IN_POOL = MAX_CXNS_IN_POOL = 0xFFFF(保证连接索引 ≤ 16 位),端点按 4 个一批增长 | connectionmanager.h:352-359 |
| 存储 | netDynArray<netEndpoint,4,0xFFFF> m_EpPile + 空闲表 m_EpPool;netDynArray<Cxn,4,0xFFFF> m_CxnPile + m_CxnPool | connectionmanager.h:1721-1747 |
| 通道数 | NET_MAX_CHANNELS = 16,应用可用 14(0–13),NET_TUNNELER_CHANNEL_ID = 14、NET_RESERVED_CHANNEL_ID = 15 | net.h:200-217 |
| 端点超时 | 无收发 60 s、无连接 30 s、地址保持 3 s、坏 QoS 阈值 4 s、接收队列 80% | connectionmanager.h:290-297 |
| pending close 最长时间 | Cxn::DEFAULT_PENDING_CLOSE_MAX_TIME = 1000 ms | connectionmanager.h:61-64 |
| 默认策略 | 压缩开启、全局发送间隔 0、坏 QoS 内存阈值 10% | connectionmanager.h:361-375 |
(表中短路径均指 src/dev_ng/rage/base/src/net/。)
GTA V 使用的通道号定义在 fwnet/nettypes.h:语音 2、游戏 7、party 8、会话语音 11、会话活动(transition)12、会话游戏 13 src/dev_ng/rage/framework/src/fwnet/nettypes.h:105-121。
netEndpoint#
每个端点持有 Cxn* m_Cxns[NET_MAX_CHANNELS](可发送的连接)、m_CxnList(可接收的连接,含 pending close)、无连接帧队列 m_CxnlessQ、挂起的 netTunnelRequest*、relay 回退地址 m_RelayAddr、m_PeerAddr、发送节流 m_PackTimeout/m_SendInterval、超时、跳数 m_HopCount(直连 1,relay 服务器 2,peer relay ≥2)src/dev_ng/rage/base/src/net/connectionmanager.h:242-321。QoS 原因枚举包括连接超时、长时间未收包、最老未确认帧过旧、RTT 高、套接字接收队列、连接内存 src/dev_ng/rage/base/src/net/connectionmanager.h:212-227;NET_CXNMGR_COLLECT_STATS 恒为 1,所以每端点统计(重复帧、乱序、压缩前后字节数等)总是收集 src/dev_ng/rage/base/src/net/connectionmanager.h:33-38。
Channel#
每通道有令牌桶式出站限速器 m_OutboundLimiter(单位放大 1000 倍)、通道策略、该通道新连接的默认连接策略、包记录器列表、委托列表、超限标志 src/dev_ng/rage/base/src/net/connectionmanager.h:1656-1709。
帧化:packet → bundle → frame#
UDP 数据报 (≤ netSocket::MAX_BUFFER_SIZE = 1024 B)
└─ [relay/peer-relay 头, 视路由而定] + [P2P 加密封装]
└─ netPacket 头: size(16b) + flags(8b: RELAY/COMPRESSED/ALT_SEND_PORT/SECURE_ADDR)
└─ netBundle × N(每通道一个)
头: size | flags(SYN/ACK/TERM/OUT_OF_BAND) | channelId(4b) | firstUnreliableSeq(16b) | ack(16b) | 乱序 ACK 位图(≥16b, 补齐到字节)
└─ netFrame × M
头: size | sendFlags(RELIABLE) | seqDep(16b) | [seq(16b) 仅可靠帧]
└─ 负载(通常是一个 netMessage)证据:MAX_BUFFER_SIZE = 1024 src/dev_ng/rage/base/src/net/netsocket.h:108-111;包标志 src/dev_ng/rage/base/src/net/packet.h:21-29;包头仅 size+flags,且 Xbox VDP 要求开头为 16 位长度 src/dev_ng/rage/base/src/net/packet.h:407-421;束注释、标志与位布局 src/dev_ng/rage/base/src/net/packet.h:449-507;束头必须按字节对齐以最大化乱序 ACK 位 src/dev_ng/rage/base/src/net/packet.h:542-546;帧布局与“不可靠帧省略序号” src/dev_ng/rage/base/src/net/packet.h:553-598。图中的位宽标注是按 sizeof(netSequence)、datBitsNeeded<NET_MAX_CHANNEL_ID> 等定义推导的,size 字段的位宽未展开。
要点:
- 不可靠帧按严格序号顺序打进 bundle,因此只在 bundle 头存第一个不可靠帧序号,每帧省 2 字节;可靠帧可能因重传乱序,所以自带
seq(同上引用)。 - 最大帧负载由
netPacketStorage::MAX_SIZEOF_P2P_PAYLOAD = 1024 − 最大 relay 头 − 双重加密开销逐层减去包头、束头、可靠帧头推得src/dev_ng/rage/base/src/net/packet.h:349-371、src/dev_ng/rage/base/src/net/packet.h:397-400。netMessage::MAX_BYTE_SIZEOF_MESSAGE = netFrame::MAX_BYTE_SIZEOF_PAYLOADsrc/dev_ng/rage/base/src/net/message.h:288-289。源码未给出最终字节数,具体值需按位宽计算(本页未计算)。 - P2P 加密开销上限 29 字节,经他人 peer relay 时因双重加密翻倍;支持 DTLS 的平台另有 66 字节 DTLS 开销上限,取最大值
src/dev_ng/rage/base/src/net/net.h:71-107。 - 包的“明文尾部”:
netPacket::GetSize()可能小于缓冲区长度,加密部分之后可附加不加密数据src/dev_ng/rage/base/src/net/connectionmanager.cpp:3664-3672;发送端只压缩加密部分,并把明文尾部拷贝到压缩包之后src/dev_ng/rage/base/src/net/connectionmanager.cpp:4844-4881。
可靠性:netConnection#
状态与序列号#
netConnection::State:STATE_LISTEN → STATE_SYN_SENT / STATE_SYN_RECEIVED → STATE_ESTABLISHED → STATE_CLOSED(注释:不得改顺序)src/dev_ng/rage/base/src/net/connection.h:44-52。Open() 后连接处于 LISTEN,只接受带 SYN 位的 bundle 来同步入站序号 src/dev_ng/rage/base/src/net/connection.h:91-113。netSequence 为 u16,比较用带回绕的 netSeqLt src/dev_ng/rage/base/src/net/netsequence.h:22、src/dev_ng/rage/base/src/net/connection.h:393-399。
连接内部队列 src/dev_ng/rage/base/src/net/connection.h:454-485:
| 成员 | 作用 |
|---|---|
m_NewQ | 新入队帧 |
m_ResendQ | 按重发时间排序的已发帧 |
m_Unacked | 按序号索引的未确认可靠帧 |
m_OoRcvd | 乱序到达、等待补洞的入站帧 |
m_RcvdBits | 相对 m_InboundSeq 的乱序接收位图(0 = 缺口) |
m_OutboundSeq / m_InboundSeq | 下一个出站序号 / 期望入站序号 |
接收窗口 RCV_WIN = netBundle::BIT_SIZEOF_UO_ACK_BITS:只能确认窗口内的乱序帧,发送方据此提前移出重传队列 src/dev_ng/rage/base/src/net/connection.h:333-339。
Pack 规则(发送侧,运行在发送线程)#
netConnection::Pack 先从重传队列打包,直到遇到“还不到重发时间”的可靠帧,再从新队列打包,防止新帧饿死重传;未 ESTABLISHED 时只发重传队列(其中含 SYN 帧)src/dev_ng/rage/base/src/net/connection.cpp:669-674。若接收线程自上次 pack 后尚未处理过数据,则不允许重发(避免误判丢包)src/dev_ng/rage/base/src/net/connection.cpp:714-726。超过最大发送次数即记为超时;若大半个超时窗口内什么也没收到,则超时原因记为“无 keepalive”而非“未确认帧” src/dev_ng/rage/base/src/net/connection.cpp:755-791。不可靠帧发送一次后即丢弃 src/dev_ng/rage/base/src/net/connection.cpp:835-849。未建立连接时不发送纯 ACK bundle;所有发送都兼作 keepalive src/dev_ng/rage/base/src/net/connection.cpp:908-952。
主线程卡死检测:如果主线程超过 P2P 超时的 90% 没有调用 Update(),发送线程会向对端发送 remote timeout 请求并本地排队超时,以便把对端的投诉遥测归因于本机卡顿 src/dev_ng/rage/base/src/net/connection.cpp:860-897。超时原因枚举含 UNACKED_FRAMES、NO_KEEPALIVES、LOCAL_STALL、REMOTE_STALL、REMOTE_SUSPENDED、FORCED src/dev_ng/rage/base/src/net/connection.h:54-64。
策略默认值#
| 策略 | 默认 | 证据 |
|---|---|---|
超时 m_TimeoutInterval | 8000 ms(0 = 永不超时,同时关闭 keepalive) | policies.h:21-81 |
| keepalive 间隔 | 1000 ms | 同上 |
| 可靠帧重发间隔 | 250 ms | 同上 |
| 最大发送次数 | ceil(timeout / resend) = 32 | 同上 |
| pending close 最大发送次数(超时为 0 时) | 50 | 同上 |
| 通道出站带宽上限 | 0 = 不限;硬上限 1 MB/s(“DON'T INCREASE”) | policies.h:109-143 |
| 通道压缩 / 总保持 keepalive | 开 / 关 | 同上 |
游戏侧:CNetwork::DEFAULT_CONNECTION_TIMEOUT/KEEP_ALIVE_INTERVAL 取上表默认值 src/dev_ng/game/network/Network.cpp:198-199,可被 tunables NETWORK_TIME_OUT/NETWORK_KEEP_ALIVE_INTERVAL 覆盖并立即 ResetConnectionPolicies src/dev_ng/game/network/Network.cpp:2001-2008,非 Final 构建还可用 -notimeouts/-timeout=秒 src/dev_ng/game/network/Network.cpp:5254-5271。ResetConnectionPolicies 同时修改所有通道的默认策略和所有现存连接,并重算会话注册策略 src/dev_ng/game/network/Network.cpp:5410-5427。
关闭语义#
netCloseType:NET_CLOSE_GRACEFULLY(引用计数归零后进入 PENDING_CLOSE,直到所有可靠帧被确认、ACK 发完)、NET_CLOSE_IMMEDIATELY(归零后立即关闭)、NET_CLOSE_FINALLY(无视引用计数)src/dev_ng/rage/base/src/net/net.h:155-173。远端关闭通过 bundle 的 FLAG_TERM 表达 src/dev_ng/rage/base/src/net/connectionmanager.cpp:3952-3956。
发送路径#
证据:Send() 中“可靠帧总入队,不可靠帧在通道超限时丢弃”、失败时回收内存重试、NET_SEND_IMMEDIATE 唤醒发送线程 src/dev_ng/rage/base/src/net/connectionmanager.cpp:2539-2563;Send() 主循环 src/dev_ng/rage/base/src/net/connectionmanager.cpp:4817-4890;SendPacket 的 peer relay 与 relay 分支 src/dev_ng/rage/base/src/net/connectionmanager.cpp:6990-7076。
套接字错误处理:WOULD_BLOCK、INVALID_ARG、NO_BUFFERS 被忽略(持续则由超时处理),其他错误为该端点每个连接排队 netEventConnectionError(CXNERR_SEND_ERROR);pending close 连接直接立即关闭 src/dev_ng/rage/base/src/net/connectionmanager.cpp:4895-4978。
SendOutOfBand() 发送不需要连接的帧(bundle 带 FLAG_OUT_OF_BAND),例如时间同步 ping/pong;SendTunnelingPacket()/SendReservedChannelPacket() 用于隧道和保留通道 src/dev_ng/rage/base/src/net/connectionmanager.h:1072-1150。
接收路径#
- 接收线程
ProcessSockets读取主套接字/对称套接字,OnReceive先让netRelay::Accept消费 relay/presence 包,其余经并发委托派发netSocketEventPacketReceivedsrc/dev_ng/rage/base/src/net/netsocketmanager.cpp:1236-1350。relay 转发来的 P2P 包则由netRelay以NET_RELAYEVENT_PACKET_RECEIVED派发(注释称 relay 头已由relay.cpp剥离)。 HandleReceivedPacket:对直连包总是P2pDecrypt,对 relay 包仅在未使用“安全 relay”时解密;然后按来源分流——relay 服务器 →Unpack;带RELAY标志的直连包 →HandlePeerRelayPacket(转发或终点);普通直连 →Unpacksrc/dev_ng/rage/base/src/net/connectionmanager.cpp:3633-3694。Unpack:必要时解压 → 加锁 → 按地址找活动端点;找不到时查挂起端点(隧道进行中,可OverrideRequest切换到实际来源地址),或按 relay 地址匹配并把端点切到 relaysrc/dev_ng/rage/base/src/net/connectionmanager.cpp:3761-3812。- 对每个 bundle:调用通道包记录器;非 OOB bundle 若通道上没有连接,则仅当该通道
StartListening()过、bundle 带 SYN 且首帧合法时,排队NET_EVENT_CONNECTION_REQUESTED;关闭的连接丢弃;FLAG_TERM表示远端终止src/dev_ng/rage/base/src/net/connectionmanager.cpp:3898-3956。OOB bundle 若在保留通道且为 relay 地址变更消息则内部处理,不交给游戏src/dev_ng/rage/base/src/net/connectionmanager.cpp:4027-4044。 - 收到包后重置该端点所有连接的超时计时器并更新最后接收时间
src/dev_ng/rage/base/src/net/connectionmanager.cpp:4068-4077。帧以事件形式进入m_EventQ,等主线程Dispatch()。
事件与分发#
netEvent 类型:CONNECTION_REQUESTED、CONNECTION_ESTABLISHED、CONNECTION_ERROR、CONNECTION_CLOSED、FRAME_RECEIVED、ACK_RECEIVED、BANDWIDTH_EXCEEDED、OUT_OF_MEMORY,均用 NET_EVENT_DECL(AutoId)声明 src/dev_ng/rage/base/src/net/event.h:82-105、src/dev_ng/rage/base/src/net/event.h:173-401。
Dispatch() 每次只处理调用时队列中已有的事件数(防止接收线程持续入队导致无限循环);对 ESTABLISHED/ERROR 事件先在锁内更新路由表与调用方的 netStatus,再 DispatchEvent 给通道委托和订阅者 src/dev_ng/rage/base/src/net/connectionmanager.cpp:5046-5110。委托接口被注释为“已弃用、正向 producer/consumer (Subscribe) 过渡” src/dev_ng/rage/base/src/net/connectionmanager.h:1760-1766;Delegate 不可复制,以免数组搬移后注册地址失效 src/dev_ng/rage/base/src/net/connectionmanager.h:399-428。
游戏侧 CNetwork::OnNetEvent 只处理致命 OOM:语音通道忽略;会话语音通道让语音会话 Bail;游戏/会话活动/会话游戏通道则 CNetwork::Bail(BAIL_NETWORK_ERROR, BAIL_CTX_NETWORK_ERROR_OUT_OF_MEMORY) src/dev_ng/game/network/Network.cpp:6564-6635。帧事件由各子系统自己的委托消费(snSession、玩家管理器、语音、文本聊天、netTimeSync 等)。
消息定义与注册#
- 消息是普通结构体,在类内写
NET_MESSAGE_DECL(Name, ID)和NET_MESSAGE_SER(bb, msg) { ... };宏生成MSG_ID()、Export/Import,并通过AUTOID_DECL_ID把消息挂进netMessageAutoId 族src/dev_ng/rage/base/src/net/message.h:84-118;.cpp中放NET_MESSAGE_IMPL(=AUTOID_IMPL)src/dev_ng/rage/base/src/net/message.h:155-160。用法示例见头文件注释src/dev_ng/rage/base/src/net/message.h:183-209。 __NO_OUTPUT构建不生成MSG_NAME()/GetMsgName()(“security: remove strings from Final builds”)src/dev_ng/rage/base/src/net/message.h:83-96。- 线格式头:3 字母签名
MSG_SIG(NET_MAKE_SIG3('M','S','G'))+ 1 位标志 + 8 或 16 位 ID;IsMessage()靠签名识别负载是否为消息src/dev_ng/rage/base/src/net/message.h:255-289、src/dev_ng/rage/base/src/net/message.h:309-330。 - 全局消息 ID 表
messageids.h中的enum netMessageId为所有平台/配置强制指定 ID,从而让不同 exe(如 final logging 与零售版、Steam 与非 Steam)网络兼容,并避免依赖 AutoId 构造顺序;ID 必须 < 256,可以有空洞src/dev_ng/rage/base/src/net/messageids.h:13-35。表中同时包含 RAGE 底层消息(ICE、密钥交换、时间同步、路由、投诉)与游戏/会话消息(如MSG_TRANSITION_TO_GAME_NOTIFY、CMSG_JOIN_RESPONSE、SN_MSG_SESSION_MEMBER_IDS)src/dev_ng/rage/base/src/net/messageids.h:37-177。这意味着新增任何网络消息都必须改动 rage 头文件。
建立连接:隧道、ICE 与 relay 回退#
API#
OpenTunnel(peerAddr, ...)→ 得到可用netAddress(直连或 relay)src/dev_ng/rage/base/src/net/connectionmanager.cpp:1553;EstablishConnection(epId, channel)/OpenConnection(peerAddr|epId, channel, status)主动建连;StartListening(channel)后对方的 SYN 产生CONNECTION_REQUESTED,游戏用AcceptConnection接受或DenyConnection拒绝src/dev_ng/rage/base/src/net/connectionmanager.h:560-752。
netTunnelRequest 状态机#
证据:头文件中的 dot 状态图与 State 枚举 src/dev_ng/rage/base/src/net/tunneler.h:232-280;实现中每个 STATE_X 都有对应的“进行中” STATE_XING 子状态 src/dev_ng/rage/base/src/net/tunneler.cpp:1179-1271。netTunnelType(LAN/ONLINE)、netTunnelReason(matchmaking 查询、直接查询、QoS、建立会话、P2P 等)、错误码分组(direct 0x1000xxxx、relay 0x2000xxxx、LAN/key exchange 0x3000xxxx,“与报告系统一致,不得重排”)见 src/dev_ng/rage/base/src/net/tunneler.h:52-112。
- relay 地址发现:先查本地缓存,否则通过
rlPresence::GetAttributesForGamer读取对方 presence 属性中的 relay 地址(只含 relay token);非原生 gamer handle 跳过 presence 查询src/dev_ng/rage/base/src/net/tunneler.cpp:490-517、src/dev_ng/rage/base/src/net/tunneler.cpp:555-609。 - 密钥交换:
netKeyExchange::ExchangeKeys(...)在直连或 relay 路径上进行;成功后UpdateP2pKey记录该 peer 的 P2P 密钥src/dev_ng/rage/base/src/net/tunneler.cpp:669、src/dev_ng/rage/base/src/net/tunneler.cpp:260。消息为NET_KEY_EXCHANGE_SESSION_OFFER/ANSWER。具体协议不在本页展开。
netIceTunneler(直连阶段)#
netTunneler::NativeOpenTunnel 无平台分支地委托给静态 netIceTunneler(src/dev_ng/rage/base/src/net/tunneler.cpp:1461-1485),后者借用 ICE(RFC 5245)术语:本地/远端候选(host、server reflexive 等)、候选对状态 WAITING/IN_PROGRESS/SUCCEEDED/FAILED、检查表、触发检查队列、有效列表、黑名单,以及针对端口保持型对称 NAT 的 SymmetricSocket 和 SymmetricInference src/dev_ng/rage/base/src/net/tunneler_ice.cpp:341-365、src/dev_ng/rage/base/src/net/tunneler_ice.cpp:637-666、src/dev_ng/rage/base/src/net/tunneler_ice.cpp:1428-1447。消息为 netIceSessionOffer/Answer/Ping/Pong/PortOpener/RelayRouteCheck/DebugInfo(messageids.h 中 NET_ICE_SESSION_*)。
容量常量 src/dev_ng/rage/base/src/net/tunneler_ice.cpp:137-157、src/dev_ng/rage/base/src/net/tunneler_ice.cpp:190-197:
| 常量 | LEAN | 非 LEAN |
|---|---|---|
MAX_ACTIVE_ICE_SESSIONS(须 ≥ MAX_NUM_PLAYERS) | 64 | 64 |
MAX_CANDIDATES_PER_PEER | 5 | 10 |
MAX_CANDIDATE_PAIRS | 8 | 25 |
MAX_ACTIVE_TRANSACTIONS_PER_SESSION | 10 | 30 |
MAX_BLACKLISTED_ADDRESSES_PER_SESSION | 1 | 3 |
| 每事务最大重试 / 单会话最长时间 | 8 / 20 s | 8 / 20 s |
ICE_TUNNELER_LEAN 在非 __OPTIMIZED 构建(或 RSG_LEAN_CLIENT)下为 1 src/dev_ng/rage/base/src/net/tunneler_ice.cpp:7-11——即调试构建反而使用更小的表。ICE 相关 tunables(如 NUM_NAT_FAILS_BEFORE_WARNING)在 CNetworkTunablesListener::OnTunablesRead(src/dev_ng/game/network/Network.cpp:1104)中下发 src/dev_ng/game/network/Network.cpp:1248-1250。
NAT 检测与端口映射#
netNatDetector(全静态)检测 NAT 类型并在可用时通过 UPnP(natupnp.*)/PCP(natpcp.*)设置端口转发 src/dev_ng/rage/base/src/net/natdetector.h:634-687。发现流程状态:等待 ROS 在线 → 取 NAT 发现服务器列表 → 等待端口映射器 → 发现 NAT 类型 → 测量 UDP 绑定超时 → 成功/失败 src/dev_ng/rage/base/src/net/natdetector.h:73-86。netNatInfo 记录映射方式(端点无关/相关)、过滤模式、端口分配策略(保持/连续/随机)、UDP 超时测量状态 src/dev_ng/rage/base/src/net/natdetector.h:88-136。对外暴露的粗粒度类型 netNatType 为 UNKNOWN/OPEN/MODERATE/STRICT,注释要求与会话服务器上的取值一致 src/dev_ng/rage/base/src/net/net.h:244-255。PC 上 Social Club DLL 从游戏取得 NAT 信息再 SetLocalNatInfo src/dev_ng/rage/base/src/net/natdetector.h:665-666。NET_DISABLE_NAT_DETECTION/NET_DISABLE_RELAY_SERVER 只在 RGSC DLL、Launcher、iOS、Android 下为真 src/dev_ng/rage/base/src/net/net.h:65-69。
netSocketManager 的“对称套接字”机制正是为端口保持型对称 NAT 设计:为单一对端单独开 socket,使首个映射端口可预测 src/dev_ng/rage/base/src/net/netsocketmanager.h:80-107;最多 64 个 socket 端点,socket 创建失败重试间隔上限 90 s src/dev_ng/rage/base/src/net/netsocketmanager.h:303、src/dev_ng/rage/base/src/net/netsocketmanager.h:344。
Relay 与 P2P 中继#
netRelay(全静态)与 relay 服务器通信转发 P2P 包,并经同一通道接收 ROS 的实时消息(presence 服务器);presence 通信在Init()后即开始,relay 只在StartRelay()(开始网络游戏时)后开始src/dev_ng/rage/base/src/net/relay.h:150-200。事件:地址获得/变更、包、消息、relay pongsrc/dev_ng/rage/base/src/net/relay.h:31-41。relay 地址变更时连接管理器通过保留通道向对端发送MsgCxnRelayAddrChanged(SendRelayAddressChangedMsg/OnRelayEventAddressChanged)src/dev_ng/rage/base/src/net/connectionmanager.cpp:7413-7442。- “安全 relay”(
netRelay::IsUsingSecureRelays())下 relay 包使用 relay token 而非明文地址(NET_PACKETFLAG_SECURE_ADDR),且 relay 路径不再做 P2P 加密src/dev_ng/rage/base/src/net/connectionmanager.cpp:3644-3660、src/dev_ng/rage/base/src/net/packet.h:21-29。SUPPORT_DTLS覆盖 PC/Durango/SCE 等平台,用于加密经 relay/presence 服务器的流量src/dev_ng/rage/base/src/net/net.h:71-74。本页不描述 relay 线格式字段。 netConnectionRouter:直连不可达时把流量经其他已连接 peer 转发(P2P relay),减少对 relay 服务器依赖,并把断开的直连改道;使用单调序列号防环src/dev_ng/rage/base/src/net/connectionrouter.h:32-44。NET_PEER_RELAY_DESTINATION_ONLY = 1:中间节点不应答路由请求src/dev_ng/rage/base/src/net/connectionrouter.h:16-18。路由消息为NET_ROUTE_REQUEST/REPLY/ERR/CHANGE_*。连接建立事件时端点被加入路由器src/dev_ng/rage/base/src/net/connectionmanager.cpp:5077-5082,路由器在发送线程每个 pack 周期更新一次src/dev_ng/rage/base/src/net/connectionmanager.cpp:4828-4830。
带宽、QoS 与压缩#
- 通道限速:
Channel::Update每次按delta(≤1000ms) × limit补充令牌,上限limit×1000src/dev_ng/rage/base/src/net/connectionmanager.cpp:885-922;CheckExceededBandwidth在令牌为负时排队一次netEventBandwidthExceeded并置标志src/dev_ng/rage/base/src/net/connectionmanager.cpp:7303-7344。不可靠帧在超限时被丢弃,可靠帧不受影响(见发送路径)。游戏层的动态带宽分配由 fwnetnetBandwidthMgr通过Get/SetChannelPolicies与包记录器完成,见 ../17-network-sync/bandwidth-players-and-debug.md。 - 包记录器:
netPacketRecorder/netBandwidthRecorder/netMessageHistogram可挂在 socket 或通道上src/dev_ng/rage/base/src/net/bandwidth.h:21-98。 - 发送间隔:全局
SetGlobalSendInterval与每端点m_SendInterval;发送线程按所有端点最小间隔休眠(10–250 ms)src/dev_ng/rage/base/src/net/connectionmanager.cpp:788-805。 - QoS:端点 QoS(见上)与整体 QoS(内存、带宽)由 tunables
ENDPOINT_BAD_QOS_THRESHOLD_MS、CXN_MGR_ENABLE_OVERALL_QOS控制src/dev_ng/game/network/Network.cpp:1229-1231。 - 压缩:在加密前对包负载压缩,只有确实变小才置
NET_PACKETFLAG_COMPRESSEDsrc/dev_ng/rage/base/src/net/connectionmanager.cpp:6688-6713。netCompression的设计是“Oodle Network + 离线训练的静态字典”(注释称节省 40–50% 带宽)src/dev_ng/rage/base/src/net/compression.h:24-34,但NET_USE_OODLE在GTA_VERSION != 0时为 0src/dev_ng/rage/base/src/net/compression.cpp:24-32,此时Compress退化为逐包datCompresssrc/dev_ng/rage/base/src/net/compression.cpp:1113-1118,Init也忽略字典文件名src/dev_ng/rage/base/src/net/compression.cpp:701-707。由于 GTA V 的 forceinclude 定义GTA_VERSION 501,且游戏传入的字典为NULL,GTA V 使用的是无字典的datCompress。
时间同步:netTimeSync#
客户端/服务器模型:服务器以 FLAG_SERVER 启动、应答 ping;客户端以 FLAG_CLIENT 启动、定期用 OOB 帧发送 ping。服务器回 pong 时携带“服务器时间”(而非本地时间),以支持“先当客户端后成为服务器”的主机迁移场景;客户端以 serverRecvTime + RTT/2 估计服务器时间并维护偏移 src/dev_ng/rage/base/src/net/timesync.cpp:266-295、src/dev_ng/rage/base/src/net/timesync.cpp:426-478。ping 间隔从 2 s 起每收到 pong 翻倍到 60 s;RTT > 300 ms 的 pong 被拒绝(最多拒 10 次),≤ 100 ms 总接受 src/dev_ng/rage/base/src/net/timesync.h:45-62、src/dev_ng/rage/base/src/net/timesync.h:100-103。GetTime() 保证同步后时间不倒退 src/dev_ng/rage/base/src/net/timesync.cpp:297-326。time token 用于在会话切换时只接受当前主机的时间(Start 参数注释 src/dev_ng/rage/base/src/net/timesync.h:69-104)。
游戏中唯一实例是 CNetwork::sm_networkClock src/dev_ng/game/network/Network.cpp:698、src/dev_ng/game/network/Network.cpp:717,在 NETWORK_GAME_CHANNEL_ID 上运行:会话主机在 HostSession 中以服务器身份启动(经 transition 进入时改为 Restart(FLAG_SERVER))src/dev_ng/game/network/Sessions/NetworkSession.cpp:7231-7268,客户端在加入/主机迁移时以 FLAG_CLIENT 指向主机端点启动或重启(如 src/dev_ng/game/network/Sessions/NetworkSession.cpp:17862)。transition 启动时,非原始主机用主机下发的时间与 token 直接以服务器身份播种时钟 src/dev_ng/game/network/Sessions/NetworkSession.cpp:9829-9846。
其他传输层服务#
| 组件 | 职责 | 游戏使用 |
|---|---|---|
netPeerComplainer | 对端向“服务器”(会话主机)投诉无法通信的 peer;服务器等待 m_BootDelay(默认 10 s)汇总投诉后,按投诉数降序回调踢出候选;投诉每 1 s 重发以抵抗丢包与主机迁移 peercomplainer.h:23-120 | CNetworkSession::m_PeerComplainer[ST_Max](每种会话一份,“用于修复破损的 peer 网格”)src/dev_ng/game/network/Sessions/NetworkSession.h:1614-1616 |
netTransactor | 在连接上做请求/响应事务,按通道注册请求处理器 transaction.h:362-411 | snet 的加入等任务 src/dev_ng/rage/suite/src/snet/tasks.h:125-132 |
netLag / netLagPingMsg | 简单延迟估计,默认 2 s ping lag.h:60-68 | 未见游戏引用(未穷尽检索) |
netTokenBucket 等 | 令牌桶限速(netratelimit.h)netratelimit.h:43-134 | HTTP 请求追踪等 |
netMemAllocator | 连接管理器专用分配器:≤1024 B 小块用固定桶减少碎片 netallocator.h:18-48 | CNetwork::Init |
netStatus | 线程安全异步状态对象 status.h:17-47 | 全部异步 API |
netTask/netTask2 | 异步任务框架;netTask2 可把阻塞工作放进 net 线程池 task2.h:183-204 | rline 任务基础,见 rline-online-services.md |
netHttpRequest/netTcp | HTTP(S) 客户端(GET/POST/PUT/DELETE/HEAD、过滤器、分块编码)与异步 TCP http.h:31-65、tcp.h:62-80;ConnectReason 枚举标记每类 TCP 连接的用途(Cloud、UGC、Matchmaking、Presence…)net.h:257-305 | ROS/云/遥测 |
netRttp | 带版本/流/keepalive 标志的 RTTP 协议 rttp.h:79-108 | 仅被 rline/deprecated/rockon 引用(有意略过) |
(表中短路径均指 src/dev_ng/rage/base/src/net/。)
加密与随机数(只写位置)#
netCrypto(crypto.h):随机字节生成等;内部基于 wolfSSL(CheckWolfSslSizes)src/dev_ng/rage/base/src/net/crypto.h:151-160。netP2pCrypt/netP2pCryptContext:每 peer 的 P2P 包加密与完整性校验,由netConnectionManager::P2pEncrypt/P2pDecrypt调用src/dev_ng/rage/base/src/net/crypto.h:249-261、src/dev_ng/rage/base/src/net/connectionmanager.cpp:6748。游戏在启动时为其设置一个 salt(来源CLiveP2pKeys,见上文装配顺序)。netKeyExchange(keyexchange.*):隧道阶段的密钥协商src/dev_ng/rage/base/src/net/keyexchange.h:25-39。netRandom:快速线性同余 RNG,初始化后丢弃第一个值以避免相近种子下的偏差,SetRandomSeed/GetIntCrypto走netCryptosrc/dev_ng/rage/base/src/net/net.cpp:179-277。- PC Final 构建禁止 RageNet 工程开启 RTTI(编译期
#error)src/dev_ng/rage/base/src/net/net.cpp:33-37。更高层的安全架构见 ../23-debug-security/security-overview.md。
平台与构建差异#
| 维度 | 差异 | 证据 |
|---|---|---|
| 套接字实现 | netsocket_pc/_orbis/_ps3/_xbox.cpp、nethardware_pc/_orbis/_ps3/_xbox.cpp;Durango 另有 durango/xblhttp.*、xblstatus.*、xbltask.* 封装 XBL HTTP/异步 | 目录清单 |
| 跨平台联机 | NET_CROSS_PLATFORM_MP (0):GTA V 不支持跨平台多人 | net.h:51-52 |
| presence 路由 | SC_PRESENCE = __RGSC_DLL || !RSG_PC:主机上 rlPresence 直接由 rlScPresence 服务,PC 上经 Social Club DLL | net.h:109-119 |
| 模拟丢包/延迟 | FAKE_DROP_LATENCY = __BANK;-netrealworld 参数(非 Final) | netsocket.h:18、net.cpp:26-60 |
| 默认端口 | CNetwork::GetSocketPort():Final 与非 Final 使用不同默认值,非 Final 可 -socketport 覆盖 | src/dev_ng/game/network/Network.cpp:1607-1630 |
| 调试 | BANK 构建提供 BankEstablishConnection 等 RAG 控件;非 Final 可禁用不活跃端点回收 | connectionmanager.h:1378-1393、src/dev_ng/game/network/Network.cpp:1736-1738 |
(表中 net.h/net.cpp/netsocket.h/connectionmanager.h 均位于 src/dev_ng/rage/base/src/net/。)
约束与边界行为#
- 内存耗尽是常态化处理路径:连接管理器堆
SetQuitOnFail(false);入队失败先ReclaimMem()(丢弃不可靠帧)再重试,关键分配失败排队netEventOutOfMemory;致命 OOM 时游戏 bail(见事件与分发)。ReclaimAllMem后所有连接被视为失步,应关闭src/dev_ng/rage/base/src/net/connection.h:301-318。 - 监听是按通道的显式开关:未
StartListening的通道上收到 SYN 会被丢弃(日志提示 “Did you forget to call StartListening()?”)。 - 不可靠帧只在本通道未超限时入队;可靠帧即使超限也入队,但其最大发送次数决定超时(默认约 8 s)。
- 连接 ID 复用:关闭的连接保留 ID(
Send注释),调用方需通过IsOpen/IsPendingOpen检查状态src/dev_ng/rage/base/src/net/connectionmanager.cpp:2529-2537。 netPeerId与rlPeerInfoID 不同,跨层比较时要注意(见寻址)。- 消息 ID 表的兼容性约束:修改
netMessageId值会破坏与其他 exe 的网络兼容。
未决问题 / 证据不足#
- 未逐行阅读
netIceTunneler的候选收集与连通性检查流程、netConnectionRouter的路由发现算法、netRelay的服务器选择与 ping 机制;本页只给出类型、状态与容量。 - 未计算最终可用帧负载字节数(需要展开
datBitsNeeded与 relay 头位宽)。 netHardware的各平台初始化(如 PSN/XBL 网络栈要求)未展开。