TCP/IP 协议族、IP 与 TCP 协议详解
TCP/IP 协议族、IP 与 TCP 协议详解
时间:2026/05/04
关键词:TCP/IP 分层、以太网、ARP、IP、ICMP、TCP、三次握手、四次挥手、滑动窗口、拥塞控制
核心目标:把写服务端必须理解的网络协议基础串起来,知道内核 socket API 背后大致发生了什么。
1. 为什么服务端程序员要懂协议
写高性能服务器时,很多问题表面是代码问题,本质是协议语义问题:
- 为什么 TCP 会粘包
- 为什么
recv()返回 0 是关闭 - 为什么连接会有
TIME_WAIT - 为什么小包延迟突然变高
- 为什么丢包后吞吐掉得很厉害
- 为什么抓包看到重传、乱序、RST
如果只会调 socket API,不理解 TCP/IP 的基本机制,排障时很容易靠猜。
2. TCP/IP 协议族分层
常见理解可以分成四层:
| 层次 | 典型协议 | 解决的问题 |
|---|---|---|
| 应用层 | HTTP、DNS、SMTP、自定义 RPC | 应用语义 |
| 传输层 | TCP、UDP | 端到端传输 |
| 网络层 | IP、ICMP | 跨网络寻址和路由 |
| 链路层 | Ethernet、ARP | 同一链路内传输 |
一次 HTTP 请求大致会被逐层封装:
1 | HTTP message |
接收端再反向解封装。
服务端程序通常直接接触的是应用层和 socket API,但问题经常发生在传输层或网络层。
3. 以太网、MAC 与 ARP
在同一个局域网内,真正投递帧需要 MAC 地址。
IP 地址解决:
- 目标主机在哪个网络
MAC 地址解决:
- 这一跳具体发给哪个网卡
ARP 的作用是:
已知目标 IPv4 地址,询问它对应的 MAC 地址。
典型过程:
1 | 主机 A: 谁是 192.168.1.10? |
如果目标不在同一网段,主机通常不会 ARP 目标主机,而是 ARP 默认网关的 MAC,然后把包交给网关继续转发。
4. IP 协议解决什么
IP 层的核心职责是:
- 给包标记源 IP 和目的 IP
- 根据路由表决定下一跳
- 尽力把包送到目的地
IP 不保证:
- 一定送达
- 按序到达
- 不重复
- 不丢包
这些可靠性能力主要由 TCP 或应用层协议提供。
5. IPv4 头部里服务端最该关注的字段
常见字段:
| 字段 | 作用 |
|---|---|
version |
IP 版本,IPv4 是 4 |
IHL |
IP 头部长度 |
total length |
整个 IP 包长度 |
identification |
分片重组标识 |
flags/fragment offset |
分片控制 |
TTL |
生存时间,每过一跳减一 |
protocol |
上层协议,如 TCP=6、UDP=17 |
header checksum |
IP 头部校验 |
src/dst address |
源/目的 IP |
服务端排障时常见关注:
- TTL 异常:路径或中间设备变化
- 分片:大包和 MTU 问题
- protocol:确认包到底是 TCP 还是 UDP/ICMP
- src/dst:NAT、反向代理、容器网络下特别容易混淆
6. IP 分片为什么要尽量避免
如果 IP 包大于链路 MTU,可能发生分片。
分片的问题:
- 任意一个分片丢失,整个 IP 包都无法重组
- 中间网络设备可能丢弃分片
- 重组消耗接收端资源
- 排障复杂度上升
对 UDP 特别明显,因为一个 UDP 报文如果被 IP 分片,任一分片丢失就等于整个 UDP 报文丢失。
工程建议:
- UDP 单包尽量控制在保守 MTU 内
- TCP 大数据交给内核分段,不要自己按 IP 分片思路设计
- 需要时关注 PMTU、MSS 和抓包里的 fragmentation
7. ICMP 的作用
ICMP 常用于网络层错误和诊断。
常见用途:
ping:基于 Echo Request / Echo Reply- 目标不可达
- TTL 超时,
traceroute会利用这一点 - 路径 MTU 发现相关反馈
服务端排障时,如果 TCP 连接失败,不一定只有 TCP 包值得看。
某些网络问题会通过 ICMP 返回,例如目标不可达、需要分片但 DF 位被设置等。
8. TCP 是可靠字节流
TCP 提供的是:
- 面向连接
- 可靠传输
- 按序交付
- 去重
- 流量控制
- 拥塞控制
- 字节流语义
最容易被忽略的是最后一点:
TCP 不是消息协议,而是字节流协议。
所以应用层必须自己定义消息边界:
- 定长
- 分隔符
- 长度前缀
- TLV
9. TCP 头部里的关键字段
| 字段 | 作用 |
|---|---|
| 源端口 / 目的端口 | 标识应用进程 |
序号 seq |
当前报文段数据的起始序号 |
确认号 ack |
期望收到的下一个序号 |
| 标志位 | SYN/ACK/FIN/RST/PSH/URG 等 |
| 窗口 | 接收方通告的可接收空间 |
| 校验和 | 校验 TCP 头和数据 |
| 选项 | MSS、窗口扩大、时间戳、SACK 等 |
抓包时重点看:
SYN是否出去- 是否收到
SYN,ACK - 是否有重复 ACK
- 是否有重传
- 谁先发
FIN或RST
10. 三次握手
TCP 建连大致是:
1 | client -> server: SYN, seq=x |
三次握手的意义:
- 双方确认彼此收发能力
- 交换初始序号
- 协商 TCP 选项,如 MSS、窗口扩大、SACK、时间戳
服务端代码里的 listen() 和 accept() 与这件事有关:
- 内核完成握手后,把连接放入已完成连接队列
- 应用调用
accept()取出连接 fd
如果应用 accept() 不及时,连接队列可能积压,客户端会表现为连接慢或失败。
11. 四次挥手与半关闭
TCP 关闭是双向的。
一方不再发送数据,可以发 FIN,但仍然可以继续接收。
典型流程:
1 | A -> B: FIN |
这就是为什么 socket 有:
1 | shutdown(fd, SHUT_WR); |
它表示:
- 我不再写了
- 但我还可以读对端剩余数据
很多应用协议不依赖 TCP 半关闭,而是在协议层明确消息结束。
但理解半关闭有助于看懂 FIN_WAIT、CLOSE_WAIT 等状态。
12. TCP 状态与服务端排障
常见状态:
| 状态 | 含义 |
|---|---|
LISTEN |
服务端正在监听 |
SYN_SENT |
客户端已发 SYN |
SYN_RECV |
服务端收到 SYN 并回了 SYN+ACK |
ESTABLISHED |
连接已建立 |
FIN_WAIT1/2 |
主动关闭方等待关闭完成 |
CLOSE_WAIT |
被动关闭方收到 FIN,但应用还没 close |
TIME_WAIT |
主动关闭方等待旧包自然消失 |
LAST_ACK |
被动关闭方发出 FIN,等待 ACK |
排障经验:
- 大量
CLOSE_WAIT:通常是应用收到 EOF 后没有及时close - 大量
TIME_WAIT:通常是本端主动关闭很多短连接 - 大量
SYN_RECV:可能是握手压力、队列问题或 SYN flood - 大量
ESTABLISHED但无吞吐:可能是应用阻塞、对端慢读或协议层卡住
13. 为什么会有 TIME_WAIT
主动关闭方进入 TIME_WAIT,主要是为了:
- 确保最后一个 ACK 有机会重传
- 避免旧连接的延迟报文污染后续相同四元组连接
它不是“系统出问题”的直接证据。
真正要看的是:
- 数量是否异常
- 是否耗尽本地端口
- 是否影响新连接
- 主动关闭策略是否合理
不要一看到 TIME_WAIT 就盲目调内核参数。
14. 滑动窗口与流量控制
TCP 用接收窗口告诉对方:
我还能接收多少数据。
发送方不能无限发送,必须受对端窗口限制。
如果接收方应用层不及时读数据,内核接收缓冲变满,就会通告小窗口甚至零窗口。
服务端里的对应问题:
- 应用不读,连接会反压到对端
- 对端不读,你的发送缓冲会堆积
- 输出缓冲无限增长会把服务端内存拖垮
所以高性能服务端必须有:
- 输入缓冲上限
- 输出缓冲上限
- 慢连接关闭策略
- 超时和背压机制
15. 拥塞控制的直觉
流量控制关心接收方还能不能收。
拥塞控制关心网络还能不能承受。
TCP 会根据丢包、延迟、ACK 等信号调整发送速率。
常见现象:
- 丢包后吞吐下降
- RTT 变大时发送变慢
- 重传增多时尾延迟上升
应用层能做的不是替 TCP 改拥塞控制,而是:
- 减少无意义重传和超时
- 控制请求大小
- 避免小包风暴
- 用连接复用减少握手开销
16. Nagle、延迟 ACK 与小包延迟
Nagle 算法会尝试合并小包,减少网络上的 tiny packets。
延迟 ACK 则可能让接收方稍等一下再确认。
两者叠加时,小请求/小响应协议可能出现额外延迟。
如果业务是低延迟小包交互,可以考虑:
1 | setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &yes, sizeof(yes)); |
但不要把 TCP_NODELAY 当成万能开关。
更重要的是应用层设计:
- 能合并就合并
- 能批量就批量
- 明确请求响应边界
17. 从访问 Web 服务器看完整链路
一次访问 http://example.com/ 大致经历:
- DNS 解析域名到 IP
- 客户端选择本地临时端口
- TCP 三次握手
- 发送 HTTP 请求字节流
- 服务端解析请求
- 服务端返回 HTTP 响应
- 双方按协议决定 keep-alive 或关闭
抓包时可以按这条线看:
1 | DNS -> SYN -> SYN/ACK -> ACK -> HTTP request -> HTTP response -> FIN/RST |
如果某一段缺失,就能定位问题大概在哪层。
18. 一页总结
这篇最值得记住的是:
- TCP/IP 分层能帮你定位问题在哪一层
- IP 只尽力投递,不保证可靠
- TCP 提供可靠、按序、去重的字节流
- TCP 不保留消息边界,应用层必须拆包
- 三次握手建立连接,四次挥手关闭两个方向
TIME_WAIT和CLOSE_WAIT是排障时非常重要的信号- 滑动窗口、拥塞控制、小包策略都会影响服务端延迟和吞吐
如果只记一句:
服务端代码里的 socket API 是入口,真正的行为要放回 TCP/IP 协议栈里理解。
19. 参考资料
Linux man-pages: tcp
https://man7.org/linux/man-pages/man7/tcp.7.htmlLinux man-pages: ip
https://man7.org/linux/man-pages/man7/ip.7.htmltcpdump manual
https://www.tcpdump.org/manpages/tcpdump.1.html