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
2
3
4
HTTP message
-> TCP segment
-> IP packet
-> Ethernet frame

接收端再反向解封装。

服务端程序通常直接接触的是应用层和 socket API,但问题经常发生在传输层或网络层。


3. 以太网、MAC 与 ARP

在同一个局域网内,真正投递帧需要 MAC 地址。

IP 地址解决:

  • 目标主机在哪个网络

MAC 地址解决:

  • 这一跳具体发给哪个网卡

ARP 的作用是:

已知目标 IPv4 地址,询问它对应的 MAC 地址。

典型过程:

1
2
主机 A: 谁是 192.168.1.10?
主机 B: 我是 192.168.1.10,我的 MAC 是 xx:xx:xx

如果目标不在同一网段,主机通常不会 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
  • 是否有重传
  • 谁先发 FINRST

10. 三次握手

TCP 建连大致是:

1
2
3
client -> server: SYN, seq=x
server -> client: SYN+ACK, seq=y, ack=x+1
client -> server: ACK, ack=y+1

三次握手的意义:

  • 双方确认彼此收发能力
  • 交换初始序号
  • 协商 TCP 选项,如 MSS、窗口扩大、SACK、时间戳

服务端代码里的 listen()accept() 与这件事有关:

  • 内核完成握手后,把连接放入已完成连接队列
  • 应用调用 accept() 取出连接 fd

如果应用 accept() 不及时,连接队列可能积压,客户端会表现为连接慢或失败。


11. 四次挥手与半关闭

TCP 关闭是双向的。
一方不再发送数据,可以发 FIN,但仍然可以继续接收。

典型流程:

1
2
3
4
A -> B: FIN
B -> A: ACK
B -> A: FIN
A -> B: ACK

这就是为什么 socket 有:

1
shutdown(fd, SHUT_WR);

它表示:

  • 我不再写了
  • 但我还可以读对端剩余数据

很多应用协议不依赖 TCP 半关闭,而是在协议层明确消息结束。
但理解半关闭有助于看懂 FIN_WAITCLOSE_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/ 大致经历:

  1. DNS 解析域名到 IP
  2. 客户端选择本地临时端口
  3. TCP 三次握手
  4. 发送 HTTP 请求字节流
  5. 服务端解析请求
  6. 服务端返回 HTTP 响应
  7. 双方按协议决定 keep-alive 或关闭

抓包时可以按这条线看:

1
DNS -> SYN -> SYN/ACK -> ACK -> HTTP request -> HTTP response -> FIN/RST

如果某一段缺失,就能定位问题大概在哪层。


18. 一页总结

这篇最值得记住的是:

  1. TCP/IP 分层能帮你定位问题在哪一层
  2. IP 只尽力投递,不保证可靠
  3. TCP 提供可靠、按序、去重的字节流
  4. TCP 不保留消息边界,应用层必须拆包
  5. 三次握手建立连接,四次挥手关闭两个方向
  6. TIME_WAITCLOSE_WAIT 是排障时非常重要的信号
  7. 滑动窗口、拥塞控制、小包策略都会影响服务端延迟和吞吐

如果只记一句:

服务端代码里的 socket API 是入口,真正的行为要放回 TCP/IP 协议栈里理解。


19. 参考资料

  1. Linux man-pages: tcp
    https://man7.org/linux/man-pages/man7/tcp.7.html

  2. Linux man-pages: ip
    https://man7.org/linux/man-pages/man7/ip.7.html

  3. tcpdump manual
    https://www.tcpdump.org/manpages/tcpdump.1.html