目录
[UDP 和 TCP 协议原理详解](#UDP 和 TCP 协议原理详解)
[一、UDP 协议](#一、UDP 协议)
[1. 通信识别](#1. 通信识别)
[2. UDP 报头结构](#2. UDP 报头结构)
[3. 报头分离与分用](#3. 报头分离与分用)
[4. UDP 特点](#4. UDP 特点)
[5. 缓冲区](#5. 缓冲区)
[6. 报文大小限制](#6. 报文大小限制)
[1. 读取与调度](#1. 读取与调度)
[2. 报文管理结构:struct sk_buff](#2. 报文管理结构:struct sk_buff)
[三、TCP 协议](#三、TCP 协议)
[1. 交付与分离](#1. 交付与分离)
[2. TCP 报头结构](#2. TCP 报头结构)
[3. 首部长度](#3. 首部长度)
[4. 报文大小与边界](#4. 报文大小与边界)
[5. 可靠性:应答机制](#5. 可靠性:应答机制)
[6. 应答规则](#6. 应答规则)
[7. 捎带应答](#7. 捎带应答)
[8. 流量控制](#8. 流量控制)
[9. 6 个标志位](#9. 6 个标志位)
[四、TCP 核心机制](#四、TCP 核心机制)
[1. 序列号](#1. 序列号)
[2. 重传机制](#2. 重传机制)
[3. 连接管理](#3. 连接管理)
[4. 调用过程](#4. 调用过程)
[5. 为什么要三次握手?](#5. 为什么要三次握手?)
[6. 为什么要四次挥手?](#6. 为什么要四次挥手?)
[7. 文件描述符泄漏](#7. 文件描述符泄漏)
[8. TIME_WAIT 状态](#8. TIME_WAIT 状态)
[1. 窗口构成](#1. 窗口构成)
[2. 丢包问题](#2. 丢包问题)
[3. 重传区分](#3. 重传区分)
[4. 流量控制](#4. 流量控制)
[5. 拥塞控制](#5. 拥塞控制)
[6. 延迟应答](#6. 延迟应答)
[六、TCP 异常处理](#六、TCP 异常处理)
[1. 进程终止](#1. 进程终止)
[2. 机器关机](#2. 机器关机)
[3. 网线断开](#3. 网线断开)
[TCP 可靠性机制](#TCP 可靠性机制)
[TCP 效率机制](#TCP 效率机制)
UDP 和 TCP 协议原理详解
一、UDP 协议
1. 通信识别
TCP/IP 协议中使用五元组来唯一标识一个通信:
-
源端口号
-
源 IP 地址
-
目标端口号
-
目标 IP 地址
-
协议号(TCP 或 UDP)
应用场景:如果客户端有 2 个画面(如视频聊天+文件传输),不需要分开发送,可以用不同的端口号来区分不同的请求。
2. UDP 报头结构
UDP 报头固定为 8 字节,包含 4 个字段,各占 16 位(2 字节):
-
源端口
-
目的端口
-
UDP 长度(整个 UDP 数据报的长度,包括报头和数据)
-
UDP 校验和
端口号设计为 16 位,是为了与内核协议栈中的端口表示保持一致。
3. 报头分离与分用
-
分离 :由于 UDP 报头是定长(8 字节),可以轻松地将报头和有效载荷分离
-
分用:利用目标端口号,内核将数据交给对应端口的应用程序
面向数据报:
-
UDP 保持消息边界,每个报文都是独立的
-
发送端 sendto 一次,接收端 recvfrom 一次,一一对应
-
操作系统可以直接发送结构体对象(但要考虑字节序和对齐问题)
4. UDP 特点
| 特点 | 说明 |
|---|---|
| 无连接 | 不需要建立连接,直接发送数据 |
| 不可靠 | 不保证数据到达,不保证顺序 |
| 面向数据报 | 保持消息边界,一个报文一个整体 |
例如:一个报文有 100 字节,UDP 会直接发送 100 字节,不会拆分成 10 次 10 字节发送。
5. 缓冲区
发送缓冲区:
-
UDP 没有真正意义上的发送缓冲区
-
数据发送完成后就丢弃,无法重传
-
这也是 UDP 效率高、无法处理丢包的原因
接收缓冲区:
-
UDP 有接收缓冲区
-
但不保证和发送时的数据顺序一致
-
如果缓冲区满了,新到的数据会被直接丢弃
6. 报文大小限制
UDP 报文长度字段占 16 位,因此 UDP 数据报的最大长度为 2^16 = 64 KB(包括 8 字节报头)。
二、报文在内核中的管理
1. 读取与调度
应用层正在解析报文时,操作系统可以同时从网络读取新的报文。因为:
-
接收消息和进程调度都是由时钟中断驱动的
-
操作系统会不断进行调度,让这两个动作交替进行
2. 报文管理结构:struct sk_buff
内核使用 sk_buff 结构体管理网络报文,包含 4 个关键指针:
head ──> [ 以太网头 │ IP头 │ TCP头 │ 数据 ] <── end
data ────────────────────────^
tail ──────────────────────────────^
-
head:指向缓冲区的开始
-
data:指向当前协议层的有效载荷开始位置
-
tail:指向当前协议层的有效载荷结束位置
-
end:指向缓冲区的结束
封装过程(发送):
-
每增加一层协议,
data指针向上移动,留出空间给新的报头 -
新的协议报头被填充到
data之前的位置
解包过程(接收):
- 每去掉一层协议,
data指针向下移动,跳过已处理的报头
三、TCP 协议
1. 交付与分离
-
交付:使用源端口和目标端口,将数据交给对应的应用程序
-
分离 :TCP 报头是定长(20 字节基本报头 + 可选选项),可以分离
2. TCP 报头结构

TCP 报头基本长度 20 字节,选项部分长度可变。
关键字段:
| 字段 | 长度 | 说明 |
|---|---|---|
| 源端口 | 16位 | 发送方端口 |
| 目的端口 | 16位 | 接收方端口 |
| 序列号 | 32位 | 数据的第一个字节的序号 |
| 确认号 | 32位 | 期望收到的下一个字节的序号 |
| 首部长度 | 4位 | 表示 TCP 报头长度(单位:4 字节) |
| 保留位 | 6位 | 保留将来使用 |
| 标志位 | 6位 | URG、ACK、PSH、RST、SYN、FIN |
| 窗口大小 | 16位 | 接收方剩余缓冲区大小,用于流量控制 |
| 校验和 | 16位 | 校验整个 TCP 报文段 |
| 紧急指针 | 16位 | 当 URG=1 时有效,指向紧急数据位置 |
| 选项 | 可变 | 如 MSS、窗口扩展因子、SACK 等 |
3. 首部长度
首部长度字段占 4 位,最大值是 15,但单位是 4 字节 ,因此最大可表示 15 × 4 = 60 字节:
-
基本报头 20 字节
-
选项最多 40 字节
-
取值范围:[20, 60] 字节
4. 报文大小与边界
TCP 是面向字节流的协议:
-
报文可能被拆分或组合
-
没有固定的报文大小
-
报文是否完整需要上层协议来判断
边界问题:
-
报文和下一个报头不会粘在一起 (由
sk_buff的指针区分) -
但多个报文的数据可能粘在一起(TCP 粘包问题),需要上层协议(如自定义协议头)来分离
5. 可靠性:应答机制
TCP 的可靠性由**确认应答(ACK)**保障。
示例:
-
服务器发送序号:200
-
客户端应答序号:201(表示 200 号字节已收到,期待 201)
注意:
-
在收到 201 应答之前,服务器无法确保 200 已收到
-
应答只对历史的消息完整性负责,不对最新消息负责
6. 应答规则
基本规则:
-
确认序号 = 发送序号 + 1
-
收到某个序号的应答,表示该序号之前的所有数据都已收到
-
下一次发送时,使用新的确认序号作为起始
特点:
-
允许少量序号丢失
-
若 200 没收到应答,但紧接着收到了 301 的应答,则判定 200 已收到(虽然它可能确实丢了)
-
乱序问题:TCP 不保证数据按顺序到达,但保证按顺序交付给上层
7. 捎带应答
为什么应答要使用新编号?因为可以实现捎带应答:
-
可以在应答的同时,顺便返回自己的消息
-
应答放在报头中,消息放在报文中,互不冲突
类比:聊天时的回答 + 反问
8. 流量控制
TCP 使用 16 位窗口大小字段实现流量控制:
-
该字段存储自己接收缓冲区的剩余大小
-
发送方根据这个值调整发送速度
-
目的:让发送速度更合理(太快减速,太慢加速),提升效率,减少丢包重传
9. 6 个标志位
为什么需要标志位?因为 TCP 收到的报文有不同的类型,需要不同的处理方式。
| 标志 | 名称 | 作用 |
|---|---|---|
| SYN | 同步标志 | 连接建立时使用,表示请求同步序列号 |
| ACK | 确认标志 | 表示确认号字段有效 |
| FIN | 结束标志 | 表示发送方数据已发送完毕,请求断开连接 |
| PSH | 推送标志 | 催促接收方尽快将数据交给应用层 |
| RST | 重置标志 | 重置连接,用于异常情况 |
| URG | 紧急标志 | 表示紧急指针字段有效 |
三次握手
通信前要先确认连接:
-
客户端 → 服务端 :
SYN=1,seq=x(询问) -
服务端 → 客户端 :
SYN=1, ACK=1,seq=y,ack=x+1(应答+询问) -
客户端 → 服务端 :
ACK=1,seq=x+1,ack=y+1(应答)
特点:
-
前两次握手只能发送带有标志位的报头,不能携带数据
-
但可以携带窗口大小等选项,协商接收能力
四次挥手
断开连接:
-
客户端 → 服务端 :
FIN=1(询问断开) -
服务端 → 客户端 :
ACK=1(应答,表示收到断开请求) -
服务端 → 客户端 :
FIN=1(服务端数据发完,询问断开) -
客户端 → 服务端 :
ACK=1(应答确认)
PSH(推送)
当对方缓冲区满了,带 PSH 标志的报文可以催促客户端快点读取,腾出空间。
RST(重置)
三次握手不一定成功。如何确定成功?
-
客户端视角:收到第 2 次握手,说明自己可以发、也可以收
-
服务端视角:收到第 3 次握手,说明自己可以收(第 1 次)、也可以发(第 3 次确认了第 2 次)
异常情况:
-
如果第 1、2 次丢了:双方都认为错误,重新来过
-
如果第 3 次丢了:客户端认为没问题,服务端认为有问题
-
此时客户端会无脑给服务端发消息,服务端就要发送
RST信号告诉客户端要重来
此外,通信过程中任何问题都可以发送 RST 重置连接。
URG(紧急指针)
-
与 16 位紧急指针一起使用
-
当
URG=1时,紧急指针才有效 -
紧急数据可以插队优先读取(如卡顿时询问原因)
-
紧急数据大小为 1 个字节,相当于状态码
四、TCP 核心机制
1. 序列号
TCP 对每个字节都进行了编号,可以理解为一个 char 数组,数组下标就是序列号。
每个 ACK 回答都包含确认序号,告诉发送者:
-
已经收到哪些数据
-
下一次从哪个序号开始发送
2. 重传机制
发送方没收到应答,可能有两种情况:
-
数据包丢了
-
应答包丢了
无论哪种原因,发送方都会等待一个时间间隔,如果收不到应答,就执行超时重传。
时间间隔:由 TCP 动态计算,通常是 500ms 的整数倍。
重复报文处理:
-
如果是应答丢了,接收方会收到 2 个同样的数据包
-
接收方可以根据序列号识别并丢弃重复包
序列号的作用:
-
确认应答
-
按序到达
-
确认丢弃
3. 连接管理
服务器可能有多个客户端连接,每个连接状态不同,需要管理。
管理结构:struct link 或类似的结构体。
注意:建立连接是有成本的(内存、时间)。
4. 调用过程
connect() // 执行三次握手(操作系统内核完成)
accept() // 不参与握手,只将已建立的连接从队列中取出
write/read() // 只对缓冲区操作
关键理解:
-
三次握手在操作系统内核中自动完成
-
即使没有调用
accept(),连接也能建立成功(只是没有被应用程序取走) -
这种设计实现了解耦,支持生产者消费者模型,可以应对忙闲不均
5. 为什么要三次握手?
-
验证双方全双工通信能力的最小次数:
-
一次握手:只能证明客户端能发
-
二次握手:证明服务端能收能发,但无法证明客户端能收
-
三次握手:证明双方都能收能发
-
-
本质上是 4 次,只是服务端通常将应答和自己的 SYN 请求合并在一个报文中(捎带),优化为 3 次
-
最小成本确认双方通信意愿
6. 为什么要四次挥手?
四次挥手是最小成本确认双方断开请求的方式:
-
A 确认数据已发完 → B 应答收到
-
B 确认数据已发完 → A 应答收到
半关闭:
-
可能出现客户端数据发完、服务端数据没发完的情况
-
此时只执行了前两次挥手(客户端发 FIN,服务端 ACK)
-
客户端关闭读端,但写端不能关闭,要继续接收消息
半关闭实现 :调用 shutdown() 接口,变为只读不写的半双工状态。
3 次 vs 4 次:
-
建立连接时,服务端对客户端服务,收到连接申请一定会同时发送自己的连接申请,因此 4 次可以优化为 3 次
-
断开连接时,服务端和客户端基本不会同时退出,较少能优化为 3 次
-
本质上都是 4 次,只是因为服务性质不同,表现出的次数不同
7. 文件描述符泄漏
假设客户端主动退出:
-
客户端:发出 ACK 应答完成四次挥手
-
服务端:接收到 ACK 应答完成四次挥手
问题:两个过程不同步。客户端发出 ACK 后需要等待一段时间才能完全退出。
查看连接状态:
netstat -natp | grep 8081

-
客户端:
FIN_WAIT2状态 -
服务端:
CLOSE_WAIT状态
查看文件描述符:
ls /proc/[进程号]/fd -l

可以看到 3、4 号文件描述符仍然被占用,造成文件描述符泄漏(文件描述符数量有限)。
8. TIME_WAIT 状态
先退出的一方会进入 TIME_WAIT 状态,等待 2 个 MSL(报文最大生存时间)再完全退出。
MSL(Maximum Segment Lifetime):报文在网络中的最大存活时间,通常为 30 秒、1 分钟或 2 分钟。
为什么需要 TIME_WAIT?
考虑一个场景:
-
客户端报文 A 卡在网络中
-
客户端超时重传,发送了 A',并收到了应答
-
双方都认为通信正常完成
-
客户端要退出
-
如果没有等待时间,下次连接握手时,服务端可能会莫名收到 A,导致新连接混乱
等待的作用:
-
让历史残存报文在网络中自然消亡
-
防止影响新连接
端口复用:
-
处于
TIME_WAIT状态的端口不能立即重用 -
紧急情况下可以调用
setsockopt(SO_REUSEADDR)复用端口 -
此时如果收到序号不匹配的历史报文,可以丢弃,防止危害
五、滑动窗口控制
1. 窗口构成
将发送缓冲区看作一个 char 数组:
[ 已确认 ][ 已发送待确认 ][ 可发送 ][ 不可发送 ]
↑ ↑ ↑
left window end
-
left 指针:区分是否收到应答,左边是已确认的,右边是待确认的
-
窗口大小:可以发送但未确认的数据量,由对方报头的窗口大小字段决定
-
end 指针:区分能否发送,右边是还不能发送的数据
如果对方窗口大小为 0,表示对方当前无法接收数据。
2. 丢包问题
假设 1000~2000 的数据没收到应答:
情况 1:收到了 3001 应答
-
说明只是应答丢了,数据没丢
-
left 指针可以直接移动到 3000 位置
情况 2:一直收到 1001 应答(收到 3 次)
-
说明数据包丢了
-
触发快重传机制
-
重传丢失的数据,直到收到 2001 应答,left 指针右移
由于确认信息是连续的,中间丢失的报头会迟到,变为最左边的确认。
3. 重传区分
| 重传类型 | 触发条件 | 作用 |
|---|---|---|
| 快重传 | 收到 3 次相同的 ACK | 提高效率,快速恢复 |
| 超时重传 | 长时间收不到任何 ACK | 兜底机制,处理网络问题 |
注意:TCP 发送的数据不能立即删除,要存储在滑动窗口中,直到收到确认。实际上,发出报文后就会开始计时,准备超时重传。
4. 流量控制
-
第一、二次握手不能带数据,第三次可以携带
-
第一次发送时,通过前两次握手报头中的窗口大小字段,可以知道对方的接收能力
-
窗口大小虽然只有 16 位,但可以使用窗口扩展因子选项来增大
5. 拥塞控制
问题:丢 2 个包和丢 998 个包,原因不同:
-
丢 2 个:可能只是个别包出错
-
丢 998 个:大概率是网络拥塞
处理原则:
-
大面积丢包 → 判定为网络拥塞
-
不能立即重传所有丢包(会加重拥塞)
-
对于使用相同协议的主机,拥塞时的策略应该相同
慢启动策略
发送方维护一个拥塞窗口:
-
初始阶段:发送数据量按指数增长(1, 2, 4, 8...)
-
达到阈值(ssthresh)后:改为线性增长
-
目的:探测网络承载能力,避免一开始就发送大量数据造成拥塞
拥塞窗口
实际发送速度由两个因素共同决定:
实际发送窗口 = min(对方窗口,拥塞窗口)
-
对方窗口:接收方的接收能力
-
拥塞窗口:网络的承载能力
拥塞窗口的变化:
-
网络好时:窗口不断增大(但受限于带宽,不会无限增大)
-
网络不好时:窗口迅速减小,然后重新慢启动探测
理解:
-
网络好时,拥塞窗口的增大像"吹牛",因为无法证明它过大,此时实际限制是对方窗口
-
网络不好时,拥塞窗口就知道"牛吹过了",迅速压下来,此时它才对发送速度有实际影响
6. 延迟应答
问题:64KB 缓冲区接收了 20KB,立即应答的话窗口值只能报 44KB。
优化:稍等片刻,应用层可能消费了一些数据,窗口值可能变为 54KB,这样发送方可以多发一些数据,提升效率。
策略:
-
隔 N 个包应答一次(通常 2 个包)
-
超过最大延迟时间(通常 200ms)应答一次
因此,应答次数可能少于发送次数,但由于滑动窗口的存在,不影响可靠性。
六、TCP 异常处理
1. 进程终止
和正常关闭差不多:
-
进程退出时,文件描述符自动关闭(引用计数变为 0)
-
触发四次挥手
2. 机器关机
-
关机会先停止进程,因此和进程终止类似
-
会正常执行四次挥手
3. 网线断开
-
没有机会执行四次挥手(不可达)
-
对方发送数据时会发现网络不可达,最终关闭连接
-
保活机制:TCP 会定时发送探测报文,检查连接是否存活
- 如果长时间无响应,就释放连接
七、总结
TCP 可靠性机制
| 机制 | 作用 |
|---|---|
| 校验和 | 检测数据是否损坏 |
| 序列号 | 按序交付、去重 |
| 确认应答 | 确认数据已收到 |
| 超时重传 | 处理丢包 |
| 连接管理 | 三次握手、四次挥手 |
| 流量控制 | 防止接收方被淹没 |
| 拥塞控制 | 防止网络被淹没 |
TCP 效率机制
| 机制 | 作用 |
|---|---|
| 滑动窗口 | 批量发送,提高吞吐量 |
| 快重传 | 快速恢复丢包 |
| 延迟应答 | 提高窗口利用率 |
| 捎带应答 | 减少报文数量 |