CD Code Digest Hub /GTA V · v2

RAGE 网络传输层:连接、套接字、NAT 穿透与消息

74 分钟阅读 22 个章节 3 张图

本页解释 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/.cppnetConnection:序列号、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 持有。

游戏侧装配顺序#

  1. 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。
  2. CNetwork::Init(INIT_CORE) 在网络堆中分配连接管理器专用堆 SCXNMGR_HEAP_SIZE(PC/Durango/Orbis 为 650 KB,其他平台 128 KB src/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。
  3. 随后 sm_CxnMgr.Init(allocator, DEFAULT_ENDPOINT_POOL_HINT, DEFAULT_CXN_POOL_HINT, sktMgr, CXN_MGR_CPU_AFFINITY, NULL, NULL, NULL):端点提示 = MAX_NUM_ACTIVE_PLAYERS、连接提示 = 端点 ×2 src/dev_ng/game/network/Network.cpp:274-275;CXN_MGR_CPU_AFFINITY = 0(非负 → 多线程模式)src/dev_ng/game/network/Network.cpp:813;压缩分配器和字典文件名均为 NULL src/dev_ng/game/network/Network.cpp:1718-1728。池大小提示之后可被 tunables ENDPOINT_POOL_HINT/CXN_POOL_HINT 调整 src/dev_ng/game/network/Network.cpp:1242-1245。
  4. ResetConnectionPolicies(GetTimeoutTime()) 设置所有通道默认连接策略;然后为 16 个通道各注册一个绑定到 CNetwork::OnNetEvent 的 Delegate,并创建 CNetworkSession、CNetworkParty、语音会话等 src/dev_ng/game/network/Network.cpp:1730-1760。
  5. 每帧 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 → ProcessSocketsselect 最多等 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_CxnPoolconnectionmanager.h:1721-1747
通道数NET_MAX_CHANNELS = 16,应用可用 14(0–13),NET_TUNNELER_CHANNEL_ID = 14、NET_RESERVED_CHANNEL_ID = 15net.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 msconnectionmanager.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#

text
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_PAYLOAD src/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_TimeoutInterval8000 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。

接收路径#

  1. 接收线程 ProcessSockets 读取主套接字/对称套接字,OnReceive 先让 netRelay::Accept 消费 relay/presence 包,其余经并发委托派发 netSocketEventPacketReceived src/dev_ng/rage/base/src/net/netsocketmanager.cpp:1236-1350。relay 转发来的 P2P 包则由 netRelay 以 NET_RELAYEVENT_PACKET_RECEIVED 派发(注释称 relay 头已由 relay.cpp 剥离)。
  2. HandleReceivedPacket:对直连包总是 P2pDecrypt,对 relay 包仅在未使用“安全 relay”时解密;然后按来源分流——relay 服务器 → Unpack;带 RELAY 标志的直连包 → HandlePeerRelayPacket(转发或终点);普通直连 → Unpack src/dev_ng/rage/base/src/net/connectionmanager.cpp:3633-3694。
  3. Unpack:必要时解压 → 加锁 → 按地址找活动端点;找不到时查挂起端点(隧道进行中,可 OverrideRequest 切换到实际来源地址),或按 relay 地址匹配并把端点切到 relay src/dev_ng/rage/base/src/net/connectionmanager.cpp:3761-3812。
  4. 对每个 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。
  5. 收到包后重置该端点所有连接的超时计时器并更新最后接收时间 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 把消息挂进 netMessage AutoId 族 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)6464
MAX_CANDIDATES_PER_PEER510
MAX_CANDIDATE_PAIRS825
MAX_ACTIVE_TRANSACTIONS_PER_SESSION1030
MAX_BLACKLISTED_ADDRESSES_PER_SESSION13
每事务最大重试 / 单会话最长时间8 / 20 s8 / 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 pong src/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×1000 src/dev_ng/rage/base/src/net/connectionmanager.cpp:885-922;CheckExceededBandwidth 在令牌为负时排队一次 netEventBandwidthExceeded 并置标志 src/dev_ng/rage/base/src/net/connectionmanager.cpp:7303-7344。不可靠帧在超限时被丢弃,可靠帧不受影响(见发送路径)。游戏层的动态带宽分配由 fwnet netBandwidthMgr 通过 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_COMPRESSED src/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 时为 0 src/dev_ng/rage/base/src/net/compression.cpp:24-32,此时 Compress 退化为逐包 datCompress src/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-120CNetworkSession::m_PeerComplainer[ST_Max](每种会话一份,“用于修复破损的 peer 网格”)src/dev_ng/game/network/Sessions/NetworkSession.h:1614-1616
netTransactor在连接上做请求/响应事务,按通道注册请求处理器 transaction.h:362-411snet 的加入等任务 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-134HTTP 请求追踪等
netMemAllocator连接管理器专用分配器:≤1024 B 小块用固定桶减少碎片 netallocator.h:18-48CNetwork::Init
netStatus线程安全异步状态对象 status.h:17-47全部异步 API
netTask/netTask2异步任务框架;netTask2 可把阻塞工作放进 net 线程池 task2.h:183-204rline 任务基础,见 rline-online-services.md
netHttpRequest/netTcpHTTP(S) 客户端(GET/POST/PUT/DELETE/HEAD、过滤器、分块编码)与异步 TCP http.h:31-65、tcp.h:62-80;ConnectReason 枚举标记每类 TCP 连接的用途(Cloud、UGC、Matchmaking、Presence…)net.h:257-305ROS/云/遥测
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 走 netCrypto src/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 DLLnet.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 与 rlPeerInfo ID 不同,跨层比较时要注意(见寻址)。
  • 消息 ID 表的兼容性约束:修改 netMessageId 值会破坏与其他 exe 的网络兼容。

未决问题 / 证据不足#

  • 未逐行阅读 netIceTunneler 的候选收集与连通性检查流程、netConnectionRouter 的路由发现算法、netRelay 的服务器选择与 ping 机制;本页只给出类型、状态与容量。
  • 未计算最终可用帧负载字节数(需要展开 datBitsNeeded 与 relay 头位宽)。
  • netHardware 的各平台初始化(如 PSN/XBL 网络栈要求)未展开。