1、网络基础:TCP/IP 与 UDP 协议概述
说实话,做了这么多年 SDK 优化,我见过太多人一上来就扎进代码细节,结果网络层的基本功却没打牢。就像盖楼不打地基,后面全是坑。这一章我们就把 TCP/IP 和 UDP 这两个最核心的协议彻底讲透,你后面理解流量优化才会顺手。
TCP/IP 四层模型:你的数据包经历了什么?
先别觉得四层模型老掉牙。我入行那会儿也觉得这东西太理论,后来真正做 SDK 优化才发现,90% 的网络问题都能在这四层里找到答案。说白了,每一层都有它存在的理由,也都有它带来的开销。
- 应用层:你的代码就在这里跑。HTTP、DNS、WebSocket 都算。你只管发数据,下面的层帮你干活。
- 传输层:TCP 和 UDP 就在这儿。这一层决定了你的数据是否可靠、有没有握手、要不要重传。我习惯先看传输层,因为 SDK 优化的核心瓶颈通常在这。
- 网络层:IP 协议在这里干活。路由、寻址、分片。你想想看,一个数据包怎么从北京走到上海,全看这一层。
- 网络接口层:网卡、以太网、Wi-Fi。最底层的东西,负责把 0 和 1 变成电信号传出去。
记得有一次排查 SDK 延迟问题的根因,抓包后发现一个数据包在网络层被分片成 5 个小包,结果第二个小包丢了,整个数据包要重传——这就是应用层没用好 MTU 的后果。
IP 协议:路由器的寻址与逐跳转发
IP 协议,说白了就是快递公司。它不管你的包裹里面是什么,只负责从 A 点送到 B 点。但问题是——它不能保证包裹一定送到。你想想看,如果你的快递在半路丢了,IP 协议是不知道的,也不会帮你补寄。
这也叫“尽力而为”交付。我自己的项目中,曾经因为某个路由器丢包导致整个 SDK 的 RTT 暴涨,最后发现是 IP 层的路由表出了问题。
重要:IP 协议本身不重传、不确认、不排序。这些活必须交给传输层(TCP)或应用层自己处理。
TCP 协议:可靠传输的代价
TCP 为什么可靠?因为它做了很多额外的事。但每件事都需要开销。我有个朋友在游戏公司做 SDK,他们把所有通信都走 TCP,结果用户一多,服务器 CPU 跑到 90%。后来改成混用 UDP,CPU 直接降到 30%。所以你要知道 TCP 在背后做了什么。
连接管理
三次握手建立连接,四次挥手断开连接。每次握手都要交换报文,消耗一次 RTT。我建议对于短连接频繁的场景,一定要复用连接,不然握手开销就会吃掉性能。
可靠传输机制
- 序列号 & 确认:每个字节都有一个序号。接收方确认收到哪些字节,发送方就知道哪里丢了。
- 超时重传:发送方等一段时间没收到确认,就把数据重传。这个等待时间的设置很敏感,设太短造成拥塞,设太长增加延迟。
- 流量控制:接收方告诉发送方“我目前还能收多少”,这就是窗口大小。我见过一个 SDK 因为窗口设成 0 导致应用层误以为网络挂了。
拥塞控制
这大概是 TCP 里最复杂的部分。发送方通过“慢启动”和“拥塞避免”逐步试探网络负载。说白了,就是网络忙的时候就少发,网络空闲就多发。
避坑指南:我曾经遇到过 SDK 在弱网环境下丢包率达到 5%,TCP 的拥塞控制直接把发送窗口压到 1 个包,吞吐量几乎归零。如果你做实时音视频,千万别只用 TCP。
粘包与拆包
TCP 是流协议,没有消息边界。你的应用层发两条消息,TCP 可能一次性发出去,也可能分成好几次。我习惯在应用层加消息头(比如 4 字节长度字段)来处理。
// 发送方:先写长度,再写消息体
uint32_t len = htonl(payload_size);
send(sock, &len, 4, 0);
send(sock, payload, payload_size, 0);
// 接收方:先读 4 字节得到长度,再读完整消息
recv(sock, &len, 4, MSG_WAITALL);
payload_size = ntohl(len);
recv(sock, payload, payload_size, MSG_WAITALL);
UDP 协议:低延迟的“裸奔”
UDP 和 TCP 完全是两个极端。它没有握手、没有重传、没有流量控制。你发送方只管发,接收方能收多少算多少。说实话,UDP 的好处很明显——延迟低、开销小。但风险也大,丢了数据你都不知道。
为什么 SDK 优化里经常用到 UDP?因为很多场景(语音、视频、游戏位置同步)可以容忍少量丢包,但无法容忍高延迟。我建议你在以下场景优先考虑 UDP:
- 实时音视频通话
- 多人在线游戏位置同步
- DNS 查询(一次查询一个包)
- QUIC 协议(基于 UDP 的 HTTP/3)
小技巧:如果你的业务逻辑需要可靠性但又想用 UDP,可以在应用层自己实现轻量级确认和重传,这样比 TCP 更灵活。
核心对比:一图看懂 TCP vs UDP
| 特性 | TCP | UDP |
|---|---|---|
| 可靠性 | 可靠,有确认和重传 | 不可靠,无确认无重传 |
| 连接 | 面向连接,需三次握手 | 无连接,直接发 |
| 流量控制 | 支持 | 不支持 |
| 拥塞控制 | 支持,慢启动/拥塞避免 | 不支持 |
| 首部开销 | 20~60 字节 | 固定 8 字节 |
| 应用场景 | 文件传输、Web 浏览、邮件 | 音视频、游戏、IoT 传感器 |
嗯,这里要注意:表格里的首部开销只是传输层的。加上 IP 头(20 字节)和以太网头(14 字节),一个最小 TCP 包实际在网络上有 54 字节。如果你 SDK 里大量发送非常小的数据包,TCP 的首部开销占比会很大。
这个图展示了数据从你的代码发出后,每经过一层就被“裹”上一层头。到网络接口层时,原始的数据已经被层层包裹。每个头都占用带宽,这就是网络开销的来源之一。
切记:UDP 虽然轻量,但如果你在应用层不做任何可靠性处理,在高丢包环境下用户会直接感受到服务中断。我曾经有个 SDK 项目用 UDP 传传感器数据,结果 3% 的丢包导致 10% 的数据丢失,后来加了一层简单的确认机制才解决。
嗯,到这基础概念就聊完了。下一篇会把这些原理和 SDK 的实际开销联系起来,你就能知道自己写的代码到底在网络层面付出了什么代价。