跳转到内容

03 · 计算机网络:TCP 与 UDP——游戏视角

定位:游戏岗的网络题有一半在问 TCP/UDP 的原理,另一半在问 “你们游戏为什么选 UDP”——后者是游戏客户端/服务器岗的分水岭题,答好了直接把话题引到 KCP/帧同步这些你熟悉的领域。本章把 TCP 的连接、可靠、拥塞三块 + UDP + 粘包一次讲透。 学完标准:三次握手/四次挥手每一步的状态和“为什么”都能说;流量控制 vs 拥塞控制分得清;能从队头阻塞角度论证“实时战斗为什么不用 TCP”;能接着讲 NAT 怎么打洞、哪些包值得在 UDP 上重传,以及帧同步和状态同步怎么选。


应用层 游戏协议、HTTP 传输层 TCP / UDP,靠端口 网络层 IP,主机到主机 链路层 以太网 / Wi-Fi
TCP/IP 四层 干什么 代表协议/设备 游戏里的对应
应用层 业务语义 HTTP/DNS/自定义游戏协议 登录协议、帧同步指令、聊天
传输层 端到端(进程到进程) TCP/UDP(端口号在这里) 战斗连接 vs 登录/支付
网络层 主机到主机(全球寻址) IP/ICMP(路由器) 玩家到机房
链路层 相邻节点传帧 以太网/WiFi(交换机、MAC) 最后一公里

面试金句:“TCP/UDP 只管把你送到对方’端口’,IP 只管送到对方’主机’——层次各管一段,出错各层各补。” 为什么分层:解耦(换 WiFi 不影响应用)、复用(HTTP 和游戏协议共用同一条 IP 路)、定位问题(丢包在哪层查)。

三次握手(每步状态要背):

客户端 服务端 SYN · seq = x CLOSED → SYN_SENT SYN+ACK · seq = y, ack = x+1 LISTEN → SYN_RCVD ACK · ack = y+1 可捎带数据,客户端已建立 服务端收到这份 ACK,双方 ESTABLISHED
  • 为什么不是两次? 两个理由:①防止历史重复 SYN 建立无效连接——旧 SYN 迟到,两次握手下服务端直接建立、单方面浪费资源;三次让客户端可以用第三次 ACK 拒掉(收到乱序 ack 就 RST)。②确认双方收发能力都正常:两次无法证明“服务端发的包客户端能收到”。
  • 第三次 ACK 丢了怎么办? 服务端重传 SYN+ACK;此时客户端已 ESTABLISHED,若它发数据,数据包自带 ack 也能让服务端完成建立。
  • 能带数据吗? 前两次不行,第三次 ACK 可以捎带数据(TCP Fast Open 就是利用它)。

四次挥手:

主动方 被动方 FIN 主动方进入 FIN_WAIT_1 ACK 被动方 CLOSE_WAIT,主动方 FIN_WAIT_2 FIN 被动方把数据发完,进入 LAST_ACK 最后的 ACK 主动方 TIME_WAIT,被动方 CLOSED TIME_WAIT 等 2MSL,只在主动关闭的一方
  • 为什么四次而握手只要三次? 挥手时被动方的 ACK 和 FIN 不能合并——收到 FIN 只说明对方不发了,自己可能还有数据没发完,得先 ACK、等应用层把数据发完再单独 FIN。握手时服务端没这负担,SYN+ACK 一口气发。
  • TIME_WAIT 为什么要等 2MSL(约 1~4 分钟)? ①保证最后一个 ACK 丢了还能收到对方重传的 FIN 并再回 ACK(让对方正常关闭);②让本连接的旧报文在网络里自然死亡,避免污染下一个相同四元组的新连接。
  • TIME_WAIT 过多怎么办?(服务器高频短连接时堆积)SO_REUSEADDR 允许复用、开 tcp_tw_reuse(时间戳保证下安全)、用长连接池从根上减少挥手次数。

面试金句:“握手三次是’确认双方都能收发’,挥手四次是’关发送和关接收要分开谈’;TIME_WAIT 等的是’网络里的遗孤报文死透’。”

3. TCP 的可靠性:序列号 + 确认 + 重传

Section titled “3. TCP 的可靠性:序列号 + 确认 + 重传”
  • 序列号 seq / 确认号 ack:每个字节编号;接收方回“我收到了前 ack-1 个字节”。解决丢、重复、乱序三件事(重复靠 seq 去重,乱序靠 seq 重排或丢弃重传)。
  • 超时重传 RTO:发出去没 ack 就重发;RTO 自适应(RTT 加权平均 + 波动余量)。
  • 快速重传:收到 3 个重复 ack 立刻重传对应段,不等超时——比超时触发快得多。
  • SACK:选择性确认,告知收到的乱序区间,发送方只补洞。

流量控制 vs 拥塞控制(最易混,必考):

流量控制 拥塞控制
保护谁 接收方(别把对方缓冲撑爆) 网络(别把链路压死)
依据 接收方通告的滑动窗口 rwnd 发送方自己探测的 cwnd
机制 窗口随接收方余量伸缩(零窗口→坚持定时器探活) 慢启动→拥塞避免→快重传→快恢复

实际发送窗口 = min(rwnd, cwnd)。

发送窗口 = min(rwnd, cwnd) 已确认 ack 左边 飞行中 已发未确认 可发送 还在窗口里 窗口外 等右沿推进 右沿 = 已确认序号 + min(rwnd, cwnd) rwnd 是对方余量,cwnd 是自己探测的网络

拥塞控制四阶段(背曲线):

  1. 慢启动:cwnd 从 1 MSS 起,每 RTT 翻倍(指数涨,“慢”只是相对直冲);
  2. 拥塞避免:到阈值 ssthresh 后改为每 RTT +1(线性爬);
  3. 快重传:3 个重复 ack 立即重传丢失段;
  4. 快恢复:配合快重传——ssthresh = cwnd/2,cwnd 从新 ssthresh 起线性增长(不回到 1 重新慢启动);若是超时重传(说明网络真崩了)则 cwnd=1 重新慢启动。

UDP 本体:无连接、不保证到达/顺序、尽力而为;头部 8 字节(TCP 20 起步);一对多广播/组播可用的就是它;没有拥塞控制=发多快你说了算(也是一把双刃剑)。

为什么实时战斗不用 TCP(按这条逻辑链答,满分):

  1. TCP 的可靠是有序的可靠——序号 5 丢了,6、7 到了也得在缓冲区等 5 重传,上层一毫秒都读不到:这叫 队头阻塞(HOL blocking);
  2. 实时游戏里“过时的包”没有价值——上一帧的位置丢了,正确做法是直接用最新帧,不是补发旧帧;
  3. TCP 拥塞控制会在丢包时主动降速(无线网络误丢也降)——延迟和抖动直接失控;还有 Nagle 算法攒小包、延迟 ack 攒确认,都在加延迟;
  4. UDP 头 8 字节 + 无连接 + 可控节奏——把“要不要重传、要不要顺序”还给应用层按业务定制。

所以游戏怎么组网(体现工程视野):

  • 实时同步(位置/技能/帧同步指令):UDP + 应用层可靠性——收最新的、丢就丢了;关键事件(击杀/结算)在应用层做重传确认。
  • 可靠有序(登录、匹配、聊天、支付):单独走 TCP——这些不在乎 100ms,在乎绝对可靠。
  • KCP 思路(加分项):在 UDP 上做“可配置的 TCP”——ARQ(选择重传)+ 快速重传(1 个重复 ack 就触发,不等 3 个)+ 非退让流控(牺牲公平换延迟)+ 无序收包可选。一句话:TCP 求公平,KCP 求快,快在很多参数你可以自己调大(更频繁的重传=更低的延迟=更多的带宽浪费)。

讲完为什么用 UDP,下一句通常是:玩家都在路由器后面,怎么连上?包丢了,关键技能怎么办?

NAT 为什么挡住 UDP:家用路由把内网 192.168.x.x:端口 换成公网 IP:端口。外面的人不知道内网地址;没有人先发包,映射就不存在,对方的包会被丢掉。TCP 有连接,NAT 好跟踪;UDP 无连接,映射还带超时,几十秒没流量就拆掉。

打洞(UDP hole punching),P2P 或帧同步主机常用这三步:

  1. 双方都先连一台有公网 IP 的协调服(STUN / 大厅),各自的 NAT 因此留下“内网 ↔ 公网”映射;
  2. 协调服把双方的公网 IP 和端口互相告诉对方;
  3. 双方同时向对方的公网地址发 UDP。各自的 NAT 看到这是自己先发出去的会话的回包,于是放行。这条映射就是洞。

要主动说出限制:对称 NAT 对每个目标换一个端口,打洞经常失败,退路是 TURN 中继(数据绕协调服转发,能通,但多一跳延迟)。现代做法会提 ICE:先试直连,失败再中继。答到“协调服交换地址 + 同时发包 + 失败走中继”就是满分骨架。映射有超时,所以要心跳保活。

可靠 UDP 的最小帧(应用层自己做,不要把 TCP 搬上来):

字段 作用
sequence 本包序号。收方永远保留最新序号,旧包直接丢(位置同步)
ack + ack_bits “我收到到哪了”,外加最近 32 个包的位图。一次确认盖住一小段,比逐包 ACK 省
时间戳 算 RTT,做插值
payload 业务。单包留在 MTU 内(≤1472),自己拆,不靠 IP 分片

两条策略分开讲,不要混在一起:

  • 状态流(位置、朝向):不可靠,只取最新。丢了就丢,下一帧盖掉。
  • 事件流(开火、技能、结算):可靠。没出现在 ack_bits 里就重传,但重传次数按玩法调。这就是 KCP 相对 TCP 的差别:TCP 为了公平会退让,游戏为了延迟可以更勤地重传,用带宽换卡顿。

抖动缓冲(jitter buffer):包的到达间隔忽大忽小。渲染和逻辑不要来一包用一包,而是先积几十毫秒,按时间戳匀速取。缓冲越大越顺,延迟也越高。竞技游戏把这个数压到还能接受的最低。

帧同步和状态同步(UDP 题的下一问):网易、腾讯的客户端面几乎都会接这一句。前面的可靠 UDP 是传输策略,这一问是玩法层怎么用它。

状态同步 服务器跑权威逻辑 网上传的是结果 适合 MMO、射击 断线重连拿快照 帧同步 各端跑同一份逻辑 网上传的是输入 适合 MOBA、格斗 断线通常要补帧

确定性三件套,少一件就会各端结果不同:固定逻辑帧(不要用渲染的 deltaTime 推逻辑)、同一套随机数、不要依赖遍历顺序不稳定的容器。浮点在不同 CPU 上也可能分叉,严肃的帧同步会锁指令或改用定点数。一句话:状态同步传的是结果,帧同步传的是输入。

SYN Flood(握手题的安全追问):攻击者只发 SYN,不回第三次 ACK,服务端的半连接队列被占满,正常人排不进去。防御是 SYN Cookie:不存半连接状态,把状态编码进自己发出的序号,第三次 ACK 回来再校验重建。

面试金句:“UDP 解决的是不要队头阻塞;NAT 打洞解决的是双方都没有公网地址;可靠 UDP 解决的是哪些包值得重传。三件事是三层,不要用 TCP 一把梭。”

6. 粘包 / 拆包(TCP 特有,游戏协议设计题)

Section titled “6. 粘包 / 拆包(TCP 特有,游戏协议设计题)”

为什么“包”会粘:TCP 是字节流,没有消息边界——它只保证字节按序到达,根本不知道你的“一条消息”是什么。Nagle/延迟确认还会把小段合并,recv 一次可能读到 半条 + 一条 + 半条。

四种解法(游戏主流是第 3 种):

方案 做法 用在哪
定长消息 每条固定 N 字节,不够补齐 简单但浪费,少见
分隔符 特殊字符结尾(\r\n) 文本协议(Redis、HTTP 头)
长度前缀(TLV) 头部 len + type,先读头再读 len 字节体 游戏标准做法
应用层约定 靠上层状态机切 复杂协议
// 游戏网络层经典套路:环形缓冲 + 状态机
while (有数据) {
if (缓冲不足 头部长度) break; // 等半个头
读头 { uint16 len; uint16 msg_id; };
if (缓冲不足 len) break; // 等半个体
dispatch(msg_id, 取出 len 字节);
}

追问“UDP 有粘包吗?”——没有。UDP 是数据报,recvfrom 一次就是对方一次 sendto 的完整内容;但代价是单包有大小上限(建议 ≤ MTU 1472 字节避免 IP 分片——分片丢一片整包作废,实时游戏宁可拆小包)。

7. 高频面试题 Q&A(合上书能讲)

Section titled “7. 高频面试题 Q&A(合上书能讲)”

Q1:TCP 和 UDP 的区别? 连接(有/无)、可靠性(确认重传有序/尽力而为)、头部(20+/8 字节)、流量+拥塞控制(有/无)、通信形态(一对一/支持广播组播)、速度与开销(慢重/快轻)。选型一句话:要可靠有序走 TCP,要低延迟可控走 UDP,游戏里通常两路并存。

Q2:三次握手为什么不能是两次? ①防历史 SYN:旧 SYN 迟到,两次握手下服务端直接建成无效连接,三次握手的客户端可以拒绝;②两次无法确认“服务端的包客户端能收到”,收发能力没验证完。

Q3:四次挥手为什么四次?TIME_WAIT 为什么等 2MSL? 被动方收到 FIN 只代表对方不发了,自己可能还有数据——ACK 先回,数据发完再 FIN,所以 ACK 和 FIN 分开。TIME_WAIT 两个目的:①最后一个 ACK 丢了还能应答对方重传的 FIN;②让旧报文自然死亡,不污染同四元组的新连接。

Q4:流量控制和拥塞控制的区别? 流量控制保护接收方(按对方通告的 rwnd 伸缩发送窗口);拥塞控制保护网络(自己探测 cwnd:慢启动指数、拥塞避免线性、3 重复 ack 快重传、快恢复减半不归一)。实际窗口取两者最小值。

Q5:TCP 怎么保证可靠? 序列号+确认号(丢/重/乱的治理)、自适应超时重传、快速重传(3 重复 ack)、校验和、滑动窗口流控、拥塞控制、连接管理。本质是“编号+确认+重传”三板斧加上一堆优化。

Q6:为什么游戏实时同步用 UDP? 核心是 TCP 的有序可靠造成队头阻塞:丢一个包,后面到的也得等重传,上层延迟直接抖动;而实时游戏里旧数据(上上帧位置)没有补发的价值,要的是“永远读最新”。加上 TCP 拥塞控制遇丢包降速、Nagle/延迟 ack 攒包,延迟不可控。UDP 让应用层自己定策略:常规状态走“只收最新”,关键事件应用层确认重传,可靠有序的(登录聊天)另开 TCP。

Q7:什么是粘包?怎么解决? TCP 是无边界字节流,recv 可能一次读到多条/半条消息。解法:定长、分隔符、长度前缀(游戏主流:头=长度+类型)、应用层状态机。UDP 无此问题(数据报保边界),但有单包尺寸建议(≤MTU 防分片)。

Q8:什么是队头阻塞? 前一环没就绪,后面排队的全被堵:TCP 层=序号低的丢包堵住后面已到的数据;HTTP/1.1 层=前一个响应没回完堵住同连接的下一个请求(HTTP/2 在 TCP 上仍有 TCP 层 HOL,QUIC 彻底解)。

Q9:RTO 和快重传的区别? RTO 是超时兜底(等一个自适应的超时时间,慢);快重传靠 3 个重复 ack 提前触发(收方连收 3 个后继段说明中间那个丢了),快得多——代价是只能定位“第一个洞”。

Q10:TCP keep-alive 和应用层心跳的区别? TCP keepalive 默认 2 小时才探测且只探测连接不探测业务,只适合清死链;游戏要的是秒级“玩家还活着吗+RTT 测量”,必须应用层心跳(顺带校时、带宽探测)。

Q11:一个 UDP 包最大能发多大? 理论上 64KB,但超过 MTU(以太网约 1500 字节,减头剩 ~1472)会 IP 分片——任何一片丢失整包重发,实时游戏应自己拆小包,不靠 IP 分片。

Q12:玩家都在 NAT 后面,UDP 怎么连上? 双方先连公网协调服,NAT 上留下映射;协调服交换双方的公网 IP 和端口;双方同时向对方发包,NAT 把回包当成自己发出会话的响应而放行,这就是打洞。对称 NAT 经常失败,就退到 TURN 中继,多一跳延迟。完整方案是 ICE:先尝试直连,不行再中继。映射有超时,要用心跳保活。

Q13:可靠 UDP 和 TCP 有什么区别?帧里放什么? TCP 是有序可靠,再加拥塞退让,丢一个包后面全部等待。可靠 UDP 把策略拆开:位置类只保留最新序号,旧包丢弃;技能和结算用序号加 ack 位图做选择重传,重传可以比 TCP 更勤,用带宽换延迟。帧里至少有 sequence、ack、ack_bits、时间戳和 payload,并且自己把包控制在 MTU 内。抖动用一小段缓冲,按时间戳匀速消费。

Q14:帧同步和状态同步怎么选? 状态同步是服务器算结果、广播实体状态,客户端插值,适合 MMO 和射击,重连拿快照,带宽随实体涨。帧同步是各端跑同一份确定性逻辑、网上只传操作,适合 MOBA 和格斗,带宽小,但浮点、随机数、遍历顺序都不能分叉,断线往往要追帧。一句话:状态同步传结果,帧同步传输入。

8. 本章自测(10 题,限时 20 分钟,先自己答再看答案)

Section titled “8. 本章自测(10 题,限时 20 分钟,先自己答再看答案)”

1. 三次握手中每一步双方的 TCP 状态?

2. 第三次握手的 ACK 丢了会怎样?

3. 挥手为什么比握手多一次?

4. TIME_WAIT 停留在谁那边?

我的原答:等多久?两个目的?

5. 拥塞控制四阶段,各自 cwnd 怎么变?

6. 快重传的触发条件?为什么不等到超时?

7. 从队头阻塞出发,说清"实时战斗为什么不用 TCP"。

8. KCP 在 UDP 上加了什么?为什么比 TCP 快?

9. 粘包的四字解法?

我的原答:游戏主流是哪种、头里放什么?

10. 为什么游戏要应用层心跳而不是依赖 TCP keepalive?

📖 答案(先自己答完再展开)
  1. 客户端 SYN_SENT→ESTABLISHED;服务端 LISTEN→SYN_RCVD→ESTABLISHED(收到第三次 ACK)。
  2. 服务端重传 SYN+ACK;客户端若已发数据,数据包自带 ack 也能让服务端进入 ESTABLISHED。
  3. 被动方收到 FIN 只知对方不发,自己可能还有数据——先 ACK,发完再 FIN;两次动作不能合并,握手时 SYN+ACK 能合并所以只需三次。
  4. 主动关闭方;2MSL;①应答对方重传的 FIN 保证其正常关闭 ②旧报文死透不污染新连接。
  5. 慢启动:每 RTT 翻倍;到 ssthresh 后拥塞避免:每 RTT +1;3 重复 ack→快重传立即补发;快恢复:ssthresh 和 cwnd 减半后线性涨(超时则 cwnd=1 重新慢启动)。
  6. 收到 3 个重复 ack;说明后继段已到、中间那个大概率丢了——比等超时(一个 RTT 量级起步)快得多。
  7. TCP 有序可靠→丢一包后面全堵(队头阻塞);实时数据过时就无用,要“读最新”而非“补旧”;TCP 拥塞控制遇丢降速 + Nagle/延迟 ack 加延迟;UDP 无连接、头小、节奏可控,可靠性按业务在应用层定制(常规状态不重传、关键事件确认)。
  8. ARQ 选择重传 + 1 个重复 ack 即快重传 + 非退让流控 + 可选无序接收;快在重传更激进、不等保守超时、牺牲带宽换延迟——参数可按业务调。
  9. 定长/分隔符/长度前缀/应用层状态机;主流=长度前缀:头放 len+msg_id,先读头再按 len 收体。
  10. TCP keepalive 默认 2 小时、只探连接不探业务;游戏要秒级活性判断,心跳顺带测 RTT、校时、估算带宽。

9. 进阶追问(答不上来就回来复习)

Section titled “9. 进阶追问(答不上来就回来复习)”
  • SYN Flood 是什么?怎么防?见本章 §5(半连接队列被打满;SYN Cookie 不存半连接状态)
  • QUIC/HTTP3 怎么解决 TCP 队头阻塞?(应用层 UDP 上做多流,流间独立无序交付;版本对比见 04)
  • 窗口缩放、时间戳选项各干嘛?(大带宽长延迟网络的窗口扩容;精确 RTT + 防旧序号混入)
  • 游戏网络线程收到 UDP 包后的完整路径?见本章 §5 和 §6,加上本项目 02:epoll 唤醒 → recvfrom → 环形缓冲 → 长度前缀切包 → 按序号取最新或按 ack 位图重传 → 消息分发到逻辑队列(与 09 章的任务队列接上)