Unity 网络
UDP 和 TCP
对比
特性 | UDP (用户数据报协议) | TCP (传输控制协议) |
---|---|---|
连接性 | 无连接 - 无需建立/拆除连接 | 面向连接 - 通信前需建立连接,结束后拆除连接 |
可靠性 | 不可靠 - 不保证交付、不保证顺序、不重传 | 可靠 - 保证交付、保证顺序、自动重传 |
传输单位 | 数据报 - 应用层消息边界保留 | 字节流 - 应用层消息边界可能不保留 |
流量控制 | 无内置 - 应用需自行处理 | 有 (滑动窗口) - 接收方控制发送速率 |
拥塞控制 | 无内置 - 应用需自行处理 (易导致拥塞崩溃) | 有 (多种算法) - 动态调整速率适应网络状况 |
头部开销 | 小 (8 字节) | 大 (通常 20 字节,可带选项更大) |
速度 | 快 - 无连接开销、无确认延迟、无重传 | 相对慢 - 连接管理、确认、重传、拥塞控制 |
复杂度 | 简单 | 复杂 |
典型应用 | DNS, DHCP, SNMP, VoIP, 视频流, 在线游戏, TFTP | HTTP, HTTPS, FTP, SMTP, SSH, Telnet, 文件传输 |
UDP (用户数据报协议) 详解
核心思想:
轻量级、尽最大努力交付: 它只做最基础的工作:复用/分用(端口号)和简单的错误检测(校验和)。它不提供任何可靠性保证。应用层需要自己处理丢包、乱序、重复等问题。
无连接: 每个 UDP 数据报都是独立的。发送方不需要先和接收方“握手”建立连接,直接发送数据报即可。接收方收到数据报时,只知道发送方的 IP 和端口,没有“连接”状态的概念。
面向报文: UDP 对应用层交下来的报文,既不合并,也不拆分,而是保留这些报文的边界。应用层交给 UDP 多长的报文,UDP 就原样发送,一次发送一个报文。接收方一次接收一个完整的 UDP 报文。
UDP 数据报结构 (头部 - 8 字节):
1 | 0 7 8 15 16 23 24 31 |
源端口 (16位): 发送方进程的端口号(可选,可设为0)。
目的端口 (16位): 接收方进程的端口号。
长度 (16位): UDP 数据报的总长度(头部 + 数据),最小为 8 字节(只有头部)。
校验和 (16位): 用于检测 UDP 头部和数据在传输中是否出错(覆盖头部、数据和伪头部)。注意: UDP 的校验和是可选的(IPv4),如果计算结果为 0,则用全 1 (0xFFFF) 表示。但在 IPv6 中是强制的。如果校验失败,数据报会被静默丢弃。
实现细节:
发送端:
应用层调用 sendto() 或 sendmsg() 系统调用,传入数据、目标 IP 和端口。
操作系统(内核)创建 UDP 数据报:添加 UDP 头部(源端口、目的端口、长度、计算校验和)。
将 UDP 数据报交给 IP 层处理(添加 IP 头部,路由等)。
接收端:
IP 层收到数据报,检查目的 IP 和协议字段 (17),如果匹配则交给 UDP 处理。
UDP 检查校验和(如果启用且非零)。失败则丢弃,无任何通知。
UDP 检查目的端口号,查找是否有绑定该端口的套接字。
如果找到匹配的套接字,数据报放入该套接字的接收缓冲区。
应用层调用 recvfrom() 或 recvmsg() 从接收缓冲区读取数据报(包含发送方的 IP 和端口)。
无状态: 内核只为 UDP 套接字维护一个接收缓冲区队列和一个发送缓冲区队列(虽然发送通常直接交给 IP)。没有连接状态、序列号、确认号、窗口大小等概念。
无重传/确认: 发送后即认为任务完成。接收端收到后不会发送任何确认给发送端(除非应用层协议自己定义)。
无流量/拥塞控制: 发送速率完全由应用层控制。如果发送过快,可能导致网络拥塞、路由器丢包或接收端缓冲区溢出(导致丢包)。
优点
开销小、速度快: 无连接建立/拆除延迟,无确认延迟,无重传延迟,头部小。
无连接状态: 服务器能支持更多活跃客户端(资源消耗少)。
应用层控制灵活: 应用可以根据需要实现自己的可靠机制、流量控制或拥塞控制(如 QUIC、RTP/RTCP),或者完全不需要这些机制(广播、实时应用)。
支持广播和多播: 可以一次发送给多个主机。
缺点:
不可靠: 数据可能丢失、重复、乱序到达。应用必须自己处理这些问题或容忍它们。
无拥塞控制: 不友好的应用可能引发或加剧网络拥塞。
TCP (传输控制协议) 详解
核心思想:
可靠、有序的字节流: 在不可靠的 IP 网络之上提供可靠的、面向连接的、字节流传输服务。它保证数据无差错、不丢失、不重复且按序到达。
面向连接: 通信双方在数据传输前必须建立逻辑连接(三次握手),传输结束后释放连接(四次挥手)。
全双工: 连接一旦建立,双方可以同时发送和接收数据。
流量控制: 使用滑动窗口机制,确保发送方不会发送过快导致接收方缓冲区溢出。
拥塞控制: 使用复杂的算法(如慢启动、拥塞避免、快速重传、快速恢复)探测网络容量并动态调整发送速率,避免网络拥塞崩溃,并尝试公平共享带宽。
TCP 段结构 (头部 - 通常 20 字节,可带选项)
1 | 0 1 2 3 |
源/目的端口 (16位): 标识发送和接收进程。
序列号 (32位): 核心字段之一。 表示本报文段所发送的数据的第一个字节在整个字节流中的序号。用于数据排序和确认。在连接建立时由双方随机初始化(ISN - Initial Sequence Number)。
确认号 (32位): 核心字段之一。 表示接收方期望收到的下一个字节的序号。即确认号 N 表示所有序号小于 N 的字节都已被正确接收。只有在 ACK 标志置位时此字段才有效。
数据偏移 (4位): TCP 头部的长度(以 32 位字为单位),因为头部有可变长度的选项。最小是 5(20 字节),最大是 15(60 字节)。
保留 (6位): 未使用,置 0。
控制标志 (6位 - 各1位):
URG: 紧急指针有效(很少用)。
ACK: 确认号有效(连接建立后几乎总是置位)。
PSH: 提示接收端应立即将数据交付应用层(Push,通常由应用层套接字选项设置,内核实现不一定严格遵守)。
RST: 重置连接(发生严重错误时)。
SYN: 同步序列号(用于建立连接)。
FIN: 发送方结束发送(用于关闭连接)。
窗口大小 (16位): 流量控制关键。 接收方通告的接收窗口大小(单位:字节),表示接收方当前还能接收多少字节的数据。这是接收方空闲缓冲区的动态大小。发送方发送的数据量不能超过对方通告的窗口大小。
校验和 (16位): 覆盖 TCP 头部、数据和伪头部(源 IP、目的 IP、协议、TCP 长度),用于错误检测(比 UDP 更严格)。
紧急指针 (16位): 当 URG=1 时有效,表示本报文段中紧急数据的末尾位置(相对于序列号的偏移)。
选项 (可变长): 用于扩展功能,常见的有:
MSS (Maximum Segment Size): 在 SYN 交换时通告,表示本方愿意接收的最大 TCP 段长度(不含 IP/TCP 头)。
WSOPT (Window Scale): 扩展窗口大小(原本 16 位最大 64KB,通过左移 0-14 位可扩展到最大 1GB)。
SACK (Selective ACK): 选择性确认,允许接收方告知发送方哪些非连续的数据块已收到,提高重传效率。
TSOPT (Timestamps): 时间戳,用于更精确的 RTT 测量和防止序列号回绕(PAWS - Protection Against Wrapped Sequence numbers)。
关键机制与实现细节:
连接管理 (三次握手):
SYN (Client -> Server): Client 发送 SYN=1, Seq=x (随机 ISN_c)。
SYN-ACK (Server -> Client): Server 发送 SYN=1, ACK=1, Seq=y (随机 ISN_s), Ack=x+1 (确认收到 Client 的 SYN)。
ACK (Client -> Server): Client 发送 ACK=1, Seq=x+1, Ack=y+1 (确认收到 Server 的 SYN-ACK)。
目的: 交换初始序列号 (ISN),协商参数(如 MSS, WS, SACK),确认双方都有发送和接收能力。此时双方分配资源(缓冲区、状态变量)建立连接状态。
可靠传输:
序列号与确认: 每个发送的字节都有唯一序列号。接收方通过发送 ACK(确认号 = 期望收到的下一个字节序号)来确认连续接收到的数据。累积确认:ACK N 表示所有小于 N 的字节都已收到。
超时重传 (Retransmission Timeout - RTO): 发送方为每个发送的段(或数据块)启动一个重传定时器。如果在 RTO 内未收到该数据的 ACK,则重发该段。RTO 是动态计算的,基于对往返时间 (RTT) 的持续测量(通常使用指数加权移动平均算法平滑 RTT 样本)。
快速重传 (Fast Retransmit): 如果发送方连续收到 3 个重复的 ACK (DupACK)(例如 ACK 50, ACK 50, ACK 50),则认为该 ACK 后面期望的数据段(例如 Seq=50)很可能丢失,会立即重传该段,而不等待超时。这大大减少了丢失恢复时间。
选择确认 (SACK - 如果协商使用): 接收方通过 SACK 选项告知发送方它收到了哪些非连续的数据块(如 “收到了 50-99 和 150-199”)。发送方可以只重传真正丢失的部分(如 100-149),而不是重传整个窗口,提高效率。
流量控制 (滑动窗口):
接收窗口 (rwnd): 接收方在 TCP 头部的 窗口大小 字段通告其当前剩余接收缓冲区空间(单位:字节)。这是接收方施加的限制。
发送窗口 (swnd): 发送方维护的状态变量,表示它当前可以发送的数据量。swnd = min(拥塞窗口 (cwnd), rwnd)。发送方只能发送 swnd 范围内且已发送但未确认的数据之后的数据。
滑动: 当发送方收到新的 ACK(确认了部分已发送数据),发送窗口向前“滑动”,允许发送新的数据(直到 swnd 限制)。
零窗口: 如果接收方缓冲区满,它会通告 窗口大小=0。发送方停止发送数据(启动一个持久定时器,定时发送探测段询问窗口是否恢复)。
拥塞控制 (核心算法):
慢启动 (Slow Start): 连接初始或超时重传后启动。cwnd 从一个很小的值(如 1 MSS 或 2/4/10 MSS)开始。每收到一个 新的 ACK(即确认了新数据),cwnd 就增加 1 MSS (cwnd += MSS)。这导致 cwnd 呈指数增长(1, 2, 4, 8…)。目的是快速探测可用带宽。
拥塞避免 (Congestion Avoidance): 当 cwnd 增长到慢启动阈值 (ssthresh) 时,进入拥塞避免阶段。此时每收到一个 新的 ACK,cwnd 增加约 1 MSS / RTT (cwnd += MSS * (MSS / cwnd)),即线性增长。目的是接近但不突破网络的拥塞点。
拥塞检测与响应:
超时 (RTO 过期): 视为严重拥塞。ssthresh = max(cwnd / 2, 2 * MSS), cwnd = 1 MSS,然后重启慢启动。
收到 3 个 DupACK (快速重传触发): 视为轻度拥塞。ssthresh = max(cwnd / 2, 2 * MSS), cwnd = ssthresh + 3 * MSS(有时直接设 cwnd = ssthresh),然后进入快速恢复。
快速恢复 (Fast Recovery - 常与快速重传结合): 在快速重传后进入。每收到一个 DupACK,cwnd 增加 1 MSS(补偿网络中减少的数据包)。当收到一个确认新数据的 ACK(非 DupACK)时,退出快速恢复,将 cwnd 设置为 ssthresh,进入拥塞避免阶段。
其他算法变种: TCP Tahoe (原始,超时/DupACK都进慢启动), TCP Reno (引入快速恢复), TCP NewReno (改进快速恢复处理多个丢失), TCP BBR (Google 提出的基于带宽和 RTT 探测的算法,更主动避免排队延迟) 等。
连接终止 (四次挥手):
FIN (主动关闭方A -> B): A 发送 FIN=1, Seq=u (已发送数据的最后一个字节+1)。
ACK (B -> A): B 发送 ACK=1, Ack=u+1。此时 B 进入 CLOSE_WAIT 状态,A 进入 FIN_WAIT_1 状态。B 的应用可能还有数据要发送给 A。
FIN (B -> A): B 处理完数据,准备关闭。发送 FIN=1, Seq=v (B发送数据的最后一个字节+1), ACK=1, Ack=u+1 (再次确认A的FIN)。
ACK (A -> B): A 收到 B 的 FIN,发送 ACK=1, Seq=u+1, Ack=v+1。A 进入 TIME_WAIT 状态 (2 * MSL - Maximum Segment Lifetime,通常 60-120秒),然后关闭。B 收到 ACK 后立即关闭。
目的: 确保双方数据都发送完毕,双方都确认对方要关闭,可靠地释放连接资源。TIME_WAIT 状态是为了让网络中可能残留的旧连接数据段彻底消失,并确保最后的 ACK 丢失时 B 重发的 FIN 能被响应。
状态机:
TCP 连接的生命周期由复杂的状态机驱动(如 CLOSED, LISTEN, SYN_SENT, SYN_RCVD, ESTABLISHED, FIN_WAIT_1, FIN_WAIT_2, CLOSE_WAIT, LAST_ACK, TIME_WAIT, CLOSING)。内核为每个连接维护这些状态和相关的变量(序列号、确认号、窗口、拥塞控制参数、定时器等)。
缓冲区管理:
内核为每个 TCP 套接字维护发送缓冲区和接收缓冲区。应用层写入的数据放入发送缓冲区,由 TCP 协议栈按序、受窗口和拥塞控制限制发送出去。接收到的数据放入接收缓冲区,按序交付给应用层读取。缓冲区大小影响性能(特别是窗口大小)。
优点:
可靠: 数据保证按序、无差错、不丢失、不重复地到达。
流量控制: 防止接收方被淹没。
拥塞控制: 防止网络拥塞崩溃,尝试公平共享带宽。
连接导向: 提供逻辑连接,便于管理长期会话。
缺点:
开销大: 头部大,连接管理、确认、重传、流量控制、拥塞控制都需要额外开销。
速度相对慢: 握手、确认、拥塞控制算法都会引入延迟。
复杂度高: 实现和理解都更复杂。
队头阻塞 (Head-of-Line Blocking): 在字节流模型中,如果序列号靠前的某个段丢失,即使后面的段已经到达,接收方应用也无法读取后面的数据,必须等待丢失段重传成功。这对于要求低延迟的应用(如实时音视频)不友好。
TCP 和 UDP 的选择总结
选择 UDP 当:
应用能容忍或处理一定丢包/乱序(如实时音视频、在线游戏状态更新、流媒体)。
速度至关重要,延迟必须极低。
需要广播或多播。
应用层协议非常轻量(如 DNS 查询)。
你需要或想要在应用层实现自己的可靠/控制机制(如 QUIC, RTP/RTCP, DTN)。
选择 TCP 当:
数据必须可靠、按序交付(如文件传输、网页加载、电子邮件、远程登录)。
应用不能处理底层网络的不可靠性。
连接是长期、双向交互的。
网络路径可能存在拥塞,需要公平共享带宽。
你不想在应用层重新发明可靠传输的轮子。
状态同步和帧同步
状态同步 vs 帧同步?它们的区别、优缺点和典型应用场景?
状态同步 (State Synchronization):
原理: 服务器是游戏世界的权威。服务器计算所有重要的游戏逻辑和状态变化,然后将变化后的状态(如位置、血量、分数)广播给所有客户端。客户端主要职责是渲染从服务器接收到的状态。
优点: 反作弊能力强(逻辑在服务器)、网络带宽相对可控(只同步变化的状态)、客户端实现相对简单(渲染为主)、更容易处理复杂逻辑和物理。
缺点: 对服务器性能要求高、存在网络延迟导致客户端看到的不是最新状态(需要预测和插值)、开发调试可能更复杂。
场景: MMORPG、MOBA、FPS(大部分现代FPS使用混合模式)、卡牌游戏、回合制策略等。Mirror、Photon主要支持状态同步。
帧同步 (Lockstep / Deterministic Lockstep):
原理: 所有客户端运行完全相同的游戏逻辑代码。服务器(或其中一个客户端作为主机)只负责收集所有客户端的输入指令,并在每一帧(或固定时间步)将这些指令广播给所有客户端。所有客户端收到所有输入后,使用相同的初始状态和相同的逻辑代码进行计算,从而保证最终状态一致。
优点: 带宽消耗极低(只同步输入)、客户端体验一致(无服务器延迟影响本地操作)、回放和观战实现简单(只需记录输入)。
缺点: 要求逻辑代码完全确定性(浮点数计算、随机数、物理等都可能破坏确定性)、反作弊困难(客户端有完整逻辑)、”卡顿”效应(一个客户端延迟会拖慢所有人)、初始状态同步和断线重连复杂。
场景: RTS(如星际争霸)、格斗游戏、部分休闲多人游戏。需要专门框架或高度定制的实现。
客户端权威 vs 服务器权威?
服务器权威 (Server-Authoritative):
服务器是游戏世界的”唯一真相来源”。
客户端发送操作请求(如”移动”、”攻击”),服务器验证请求的合法性(防止作弊),执行逻辑,计算结果,然后将结果状态同步给所有客户端。
优点: 安全性高,能有效防止常见作弊(速度黑客、位置欺骗等)。
缺点: 引入了网络延迟,玩家的操作会有响应延迟感。
客户端权威 (Client-Authoritative):
客户端对自己的角色或某些对象拥有控制权,直接决定状态变化并广播给服务器和其他客户端。服务器可能只做简单的转发或非常有限的验证。
优点: 延迟感低,操作响应快。
缺点: 极易作弊,安全性极差。一个恶意客户端可以发送任意数据破坏游戏。
现实: 现代游戏几乎不会采用纯客户端权威。常见的做法是混合模式:服务器权威是基础,但为了降低延迟,对非关键、低风险的操作(如纯客户端特效、本地动画)或需要快速响应的操作(如移动),采用客户端预测 + 服务器校验和纠正(见下一点)。服务器拥有最终决定权。
什么是客户端预测?它解决了什么问题?如何实现?
问题: 在服务器权威模型下,玩家的操作(如按下”前进”键)需要先发送到服务器,服务器处理后再把新位置同步回来,这导致客户端看到自己角色的移动有延迟,体验差。
解决方案 - 客户端预测:
客户端在发送移动请求给服务器的同时,立即在本地根据输入移动角色(预测服务器会同意这个操作)。
服务器收到请求,验证后执行移动,计算出权威位置,再同步回该客户端。
客户端收到服务器的权威位置后,将自己的预测位置与权威位置进行比较。
如果位置一致或差异很小: 一切正常。
如果位置差异较大(预测错误): 客户端需要进行位置纠正。粗暴的方法是直接”瞬移“到服务器位置(体验差)。优雅的方法是插值或回溯,平滑地过渡到正确位置。
关键点: 预测只适用于可预测且非关键的操作(如移动)。攻击、购买等关键操作必须等待服务器确认。
什么是滞后补偿?尤其在FPS游戏中如何应用?
问题 (FPS): 玩家A在客户端看到玩家B在位置X,于是开枪射击。但由于网络延迟:
玩家A的射击指令需要时间传到服务器。
服务器收到时,玩家B可能已经被其他客户端更新移动到了位置Y(服务器权威位置)。
如果服务器直接用当前时间的位置Y来判断,会判定玩家A没打中(即使玩家A的屏幕上看起来打中了X),这非常挫败。
解决方案 - 滞后补偿:
当服务器收到玩家A的射击请求(包含射击时间戳和方向)时。
服务器会回溯/回滚游戏世界状态到玩家A开枪的那个时间点。
在那个”过去”的状态下,检查子弹射线是否击中了当时处于位置X的玩家B。
如果击中,则判定为命中,结算伤害,并同步结果。
核心思想: 服务器在验证射击时,不是用”现在”的状态,而是用射击发生时的历史状态来判断,从而保证玩家在低延迟客户端上看到的命中判定与服务器一致。这是FPS游戏网络同步的核心技术之一。实现复杂,需要服务器存储一段时间内的所有对象状态快照。
TCP vs UDP?为什么游戏通常偏爱UDP?如何在UDP上实现可靠性?
TCP:
面向连接、可靠(保证数据包顺序到达、不丢失)、有拥塞控制。
缺点: 建立连接耗时(三次握手)、延迟高且不稳定(重传机制、队头阻塞)、包头开销相对大。
UDP:
无连接、不可靠(不保证顺序、不保证到达)、无拥塞控制(应用层自己处理)、包头开销小。
优点: 延迟低、速度快、控制灵活。
游戏偏爱UDP: 实时游戏对低延迟要求极高。TCP的可靠性和拥塞控制机制带来的延迟波动是其致命伤。UDP的”不可靠”特性反而给了开发者按需实现所需可靠性级别的灵活性。
在UDP上实现可靠性 (类似TCP的部分特性):
序列号: 给每个包打上序号,用于检测丢包和乱序。
ACK/NACK: 接收方发送确认(ACK)收到哪些包,或通知发送方哪些包没收到(NACK)。
选择性重传: 只重传真正丢失的包,而不是像TCP那样重传丢失包之后的所有包。
应用层拥塞控制: 自己实现更适应游戏流量特性的拥塞控制算法(如BBR)。
可靠有序通道: 在UDP之上实现一个子通道,保证特定类型消息(如聊天、交易)的可靠有序传输。其他类型消息(如位置更新)可以用不可靠通道。
常见库: ENet, LiteNetLib, KCP (一个快速可靠的ARQ协议),以及游戏引擎内置方案(如Mirror的KCP传输层、Photon的底层协议)。
如何同步一个Unity游戏对象在网络上生成和销毁?
生成:
通常只有服务器有权生成网络对象。
在服务器代码中,使用NetworkServer.Spawn(gameObject)。
这个调用会:
在服务器上实例化对象。
为该对象分配唯一的网络ID (NetId)。
将生成消息和对象初始状态(包括所有NetworkBehaviour及其初始SyncVar值)发送给所有应该看到该对象的客户端。
客户端收到消息后,在自己的场景中实例化该对象,并应用初始状态。
销毁:
通常只有服务器有权销毁网络对象。
在服务器代码中,在要销毁的对象上调用NetworkServer.Destroy(gameObject)或Destroy(gameObject)(如果对象有NetworkIdentity,Destroy内部会调用NetworkServer.Destroy)。
这个调用会:
在服务器上销毁对象。
向所有拥有该对象的客户端发送销毁消息。
客户端收到消息后,在自己的场景中销毁对应的对象实例。
关键组件: NetworkIdentity必须挂载在任何需要网络生成/销毁的对象上。
如何处理玩家断线重连?
核心思想: 让玩家重新连接到之前的游戏会话,并尽可能恢复其状态。
实现步骤:
1.会话保持: 服务器需要维护游戏会话状态(房间状态、玩家数据、游戏对象状态)。即使客户端断开,服务器上的玩家对象和相关状态不能立即销毁,需等待一段时间(超时)。
2.玩家标识: 使用唯一的、持久的玩家标识(如AccountID, ConnectionID或自定义SessionID)关联客户端和服务器上的玩家实体。
3.断线检测: 网络层(如Transport)或心跳机制检测到连接断开。
4.状态保留 (Grace Period): 服务器标记该玩家连接为断开,但保留其玩家对象和状态一段时间(如30秒-2分钟)。在此期间,该玩家对象通常处于”断线”状态(如角色呆立不动)。
5.重连请求: 客户端尝试重新连接,并在连接时发送其玩家标识。
6.身份验证与关联: 服务器验证玩家标识,找到之前为该标识保留的玩家对象和游戏状态。
7.状态同步: 服务器将断线期间发生的关键状态变化(通过快照或增量更新)同步给重新连接的客户端。这包括:
玩家的角色状态(位置、血量、装备、技能CD)。
周围环境的重要变化(被摧毁的建筑、被拾取的关键物品)。
游戏全局状态(比分、阶段)。
8.恢复控制: 服务器将之前保留的玩家对象重新关联到新的客户端连接上,客户端恢复对角色/单位的控制。
9.超时处理: 如果超过保留时间玩家仍未重连,服务器则销毁其玩家对象和相关资源(如掉线、退出游戏处理)。
挑战: 高效地序列化和同步断线期间的变化状态;处理复杂状态的恢复(如技能链、持续效果);避免重连玩家获得不公平优势(如看到短暂隐身状态)。
设计一个简单的房间匹配系统(如1v1)需要考虑哪些方面?
功能模块:
大厅/匹配服务: 玩家列表、房间列表、创建房间、加入房间(按ID、按条件搜索)、快速匹配(MMR/Elo匹配)、邀请好友。
房间管理: 房间状态(等待中、游戏中)、玩家准备状态、队伍分配、聊天、房主权限(开始游戏、踢人)。
游戏服务器分配: 匹配成功后,需要分配或启动一个具体的游戏服务器实例来承载该房间的游戏逻辑。
状态持久化: 玩家信息、房间信息、匹配记录(可选)。
网络通信:
客户端 <-> 大厅/匹配服务器 (通常使用REST/WebSocket/TCP)。
大厅/匹配服务器 <-> 游戏服务器 (管理通信)。
匹配成功后,客户端 <-> 分配的游戏服务器 (使用游戏网络协议,如UDP/KCP)。
数据结构: 玩家信息对象、房间信息对象、匹配队列。
关键算法: 匹配算法(基于MMR、Elo、ping值、地域等)。
容错: 游戏服务器宕机处理、玩家在匹配中断线处理。
扩展性: 支持大量并发玩家和房间。
如何优化网络带宽?
减少发送频率: 提高状态同步阈值、降低NetworkTransform/SyncVar的发送速率、对不重要对象使用更低的更新频率。
压缩数据:
字段压缩: 使用更小的数据类型(byte, short, float16/Half)、位域(BitField)打包多个布尔值、压缩浮点数精度(位置、旋转)。
序列化压缩: 使用高效的序列化库(Protobuf, MessagePack, FlatBuffers)替代默认的Unity序列化或JSON(体积小、序列化/反序列化快)。
网络压缩: 启用传输层的压缩(如KCP内置压缩、ENet压缩)。
只同步可见/相关数据: 基于距离、视锥体裁剪、兴趣管理(AOI - Area of Interest)只同步玩家附近或相关的对象状态。
差值同步: 只同步状态变化的差值(Delta),而不是完整状态(需要处理首次同步和丢包后的状态恢复)。
聚合消息: 将多个小消息积攒到一定数量或一定时间后打包成一个大消息发送(减少UDP包头开销和发送次数),注意平衡延迟。
使用不可靠通道: 对于可以容忍丢失的数据(如非关键的位置更新、特效触发),使用不可靠UDP发送。
精简协议: 设计精简的自定义网络协议,避免冗余字段。
对象池与重用: 复用网络消息对象以减少GC。
如何防止网络作弊 (Anti-Cheat)?
基石:服务器权威! 所有核心逻辑(伤害计算、物品拾取判定、胜负条件、移动验证)必须在服务器执行。客户端只能发送输入请求。
输入验证 (Input Validation): 服务器检查客户端发送的输入是否合理:
移动速度是否超过角色最大值?
技能是否在冷却中?
是否有足够资源(魔法值、金钱)执行操作?
射击方向/位置是否在物理上可能(防止自瞄/穿墙)?
状态验证 (State Validation): 服务器定期或随机抽查客户端报告的状态(如位置)是否与服务器计算的权威状态一致。差异过大则踢出或处罚。
作弊检测启发式:
异常高的操作频率(连点器)。
物理上不可能的运动轨迹(瞬移、穿墙)。
短时间内伤害/资源获取异常。
滞后补偿: 在服务器验证射击等操作时,必须使用操作发生时刻的历史状态进行判定,避免因延迟误判诚实玩家作弊。
加密与混淆:
网络通信加密 (DTLS, 自定义加密) 防止抓包分析/篡改。
客户端代码混淆、加壳,增加反编译和内存修改 (Cheat Engine) 难度(非绝对安全)。
不要信任客户端: 客户端发送的任何数据(尤其是分数、状态)都可能是伪造的。服务器必须重新计算或严格验证。
关键信息隐藏: 其他玩家的精确位置/血量等信息,客户端只在需要知道时才同步(AOI),且不要过早暴露(如服务器未确认的击杀不应显示)。
日志审计: 详细记录关键操作和状态变化,用于事后分析作弊模式。