网络通信的奥秘:TCP与UDP详解(三)
TCP数据包详解
假设你写的纸条(TCP 数据包)长这样:
| 内容类型 | 具体内容(类比 TCP 字段) | 占多少字节 |
|---|---|---|
| 首部(备注) | 源端口(67890) | 2 |
| 目的端口(443) | 2 | |
| 序号(1000) | 4 | |
| 确认序号(2001) | 4 | |
| 首部长度(6)+ 保留(0)+ 控制位 | 2 | |
| 窗口大小(8192) | 2 | |
| 检验和(0x1234) | 2 | |
| 紧急指针(0) | 2 | |
| 选项(MSS=1460) | 4 | |
| 数据(正文) | 晚上一起吃火锅 | 20 |
- 这里的 “首部长度” 字段会填 “6”(注意:4 位首部长度的单位是 “4 字节”,所以 6×4=24 字节)—— 意思是 “整个备注(首部)一共 20 字节”。
- 服务器收到后,就会跳过前 24 字节,从第 25 字节开始读,这才是 “晚上一起吃火锅” 这个核心数据,不会把 “源端口 67890”“序号 1000” 这些备注当成正文读。
为啥不能只算 “端口号 + 序号 + 确认序号”?
因为 TCP 首部可能有 “选项字段”(比如上面的 MSS=1460),这个字段不是每次都有,长度也不固定(可以是 0 字节,也可以是 40 字节)。如果只算 “端口号 + 序号 + 确认序号”(固定 10 字节),一旦加了选项(比如加 4 字节 MSS),首部总长度就变成 14 字节,服务器还按 “跳过 10 字节” 读,就会把选项的 4 字节当成数据的一部分,读成 “1460 晚上一起吃火锅”,完全乱了。
而 “首部长度” 字段算的是 “整个首部的总长度”(不管有没有选项,都按实际长度算),就能确保服务器永远找对数据的起始位置 —— 这就是它的关键作用。
首部总长度 = 必须有的 20 字节 + 实际加了的选项字节数,没加选项就只算 20 字节.
TCP 预留字段和选项字段的核心差异:
1. 预留字段(6 位):“固定死的小角落”,只能应付 “未来极小的改动”
- 特点:长度固定(6 位,连 1 字节都不到),且 “必须永远留空”(现在全为 0,未来也只能填非常简单的标记)。
- 为啥不能用来替代选项?就像你笔记本最后留的 6 页空白,只能写点 “勾、叉” 这种简单符号 —— 如果未来需要加一个 “协商最大传输速度”(需要多个字节),6 位根本不够用,强行写会挤掉其他内容(比如覆盖掉序号字段)。TCP 设计时就明确:预留字段是 “应对极端情况的应急空间”,比如未来发现某个致命漏洞,需要加一个 1 位的标记(比如 “是否加密”),就可以用这里的 1 位,其他 5 位继续留空。但它绝对承担不了 “灵活扩展功能” 的任务。
2. 选项字段:“可长可短的便利贴”,专门应付 “多样化的扩展需求”
- 特点:长度不固定(可以是 0 字节,也可以到 40 字节),且 “按需添加”(比如只在建立连接时协商 “最大分段大小”,平时传输数据时不加)。
- 为啥必须有选项字段?因为 TCP 需要支持的 “扩展功能” 太多样了,而且很多功能 “不是每次传输都需要”:
- 比如 “MSS(最大分段大小)”:只在三次握手时协商一次(告诉对方 “我一次最多能接 1460 字节”),之后传输数据就不用带了 —— 如果用固定预留字段,总不能每次传数据都带着这个协商结果吧?太浪费空间。
- 比如 “时间戳”:有些场景需要(比如计算网络延迟),有些场景不需要(比如简单的文字聊天),用选项字段可以 “需要时加上,不需要时去掉”,灵活控制首部长度。
- 比如 “窗口扩大因子”:当接收方缓存很大(超过 65535 字节),需要扩展窗口大小时才用 —— 这种 “偶尔才需要的功能”,如果占用固定预留字段,就是对空间的浪费。
3. 为啥不直接 “加长 TCP 首部” 或 “加多预留字段”?
加长首部 =“永远带着不必要的行李”:TCP 首部的默认长度是 20 字节(无选项时),如果为了扩展功能,强行把首部加长到 40 字节(比如多加 20 字节预留),那么每次传数据都要多传 20 字节的 “空内容”—— 就像你出门永远带着一沓用不上的便利贴,徒增负担(网络传输中,多余的字节会浪费带宽,降低效率)。而选项字段是 “按需添加”,平时传输数据时可以不加,首部保持 20 字节的紧凑 size,高效!
加多预留字段 =“预测未来”,根本做不到:TCP 设计时(1981 年),谁也想不到几十年后会有 “大数据传输、高并发通信” 这些需求。如果当时盲目加多预留字段(比如加 100 位),很可能 99% 的位永远用不上,纯浪费;而如果预留少了,未来又不够用。选项字段的 “动态长度” 完美解决了这个问题 —— 未来有新需求,就定义一个新的选项(比如 2000 年需要 “时间戳防回绕”,就加一个时间戳选项),不用改 TCP 的基本结构,兼容性极强。
总结:预留字段是 “应急小仓库”,选项字段是 “灵活工具箱”
- 预留字段:固定 6 位,像口袋里的 “备用硬币”,只能应付小额支付(简单标记),平时用不上,关键时刻救急。
- 选项字段:长度可变,像随身带的 “多功能工具刀”,需要拧螺丝就装螺丝刀头,需要剪线就装剪刀头(按需扩展功能),不用时可以收起来,不占地方。
二者分工不同:预留字段应付 “极小、极特殊的未来改动”,选项字段负责 “多样化、可按需开启的扩展功能”—— 这正是 TCP 协议能从 1981 年用到现在,还能适应各种网络场景的关键设计之一。
TCP 是 “拆包保证有序、确认保证不丢、单连接保证串行” 的可靠传输
- 拆包 + 序号:再长的消息也能拼回去;
- 确认 + 重传:再差的网络也能保证不丢;
- 单连接串行:再多的消息也能按顺序交付。
三次握手:

超时重传:

- 主机A发送数据给B之后,可能因为⽹络拥堵等原因,数据⽆法到达主机B;
- 如果主机A在⼀个特定时间间隔内没有收到B发来的确认应答,就会进⾏重发;
- 但是,主机A未收到B发来的确认应答,也可能是因为ACK丢失了;

因此主机B会收到很多重复数据.那么TCP协议需要能够识别出那些包是重复的包,并且把重复的丢弃掉. 这时候我们可以利⽤前⾯提到的序列号,就可以很容易做到去重的效果. 那么,如果超时的时间如何确定?
- 最理想的情况下,找到⼀个最⼩的时间,保证"确认应答⼀定能在这个时间内返回".
- 但是这个时间的⻓短,随着⽹络环境的不同,是有差异的.
- 如果超时时间设的太⻓,会影响整体的重传效率;
- 如果超时时间设的太短,有可能会频繁发送重复的包; TCP为了保证⽆论在任何环境下都能⽐较⾼性能的通信,因此会动态计算这个最⼤超时时间.
- Linux中(BSDUnix和Windows也是如此),超时以500ms为⼀个单位进⾏控制,每次判定超时重发的 超时时间都是500ms的整数倍.
- 如果重发⼀次之后,仍然得不到应答,等待2*500ms后再进⾏重传.
- 如果仍然得不到应答,等待4*500ms进⾏重传.依次类推,以指数形式递增.
- 累计到⼀定的重传次数,TCP认为⽹络或者对端主机出现异常,强制关闭连接.
连接管理
在正常情况下,TCP要经过三次握⼿建⽴连接,四次挥⼿断开连接
建⽴连接的意义:
- 投⽯问路,确认当前通信路径是否畅通.
- 协商参数,通信双⽅共同确认⼀些通信中的必备参数数值.
滑动窗⼝

窗⼝⼤⼩指的是⽆需等待确认应答⽽可以继续发送数据的最⼤值.上图的窗⼝⼤⼩就是4000个字节 (四个段).
- 发送前四个段的时候,不需要等待任何ACK,直接发送;
- 收到第⼀个ACK后,滑动窗⼝向后移动,继续发送第五个段的数据;依次类推;
- 操作系统内核为了维护这个滑动窗⼝,需要开辟发送缓冲区来记录当前还有哪些数据没有应答;只有确认应答过的数据,才能从缓冲区删掉;
- 窗⼝越⼤,则⽹络的吞吐率就越⾼;

如果出现了丢包,如何进⾏重传?这⾥分两种情况讨论

数据包直接丢了:

阻塞窗口的确认与重传逻辑
当某一段数据(如 1001~2000)丢失时:
- 接收方会持续发送 “期望下一个是 1001” 的确认,直到收到该段数据。
- 发送方收到3 次重复确认后会快速重传该段;如果没收到重复确认,就会等超时时间,超时后也会重传。若多次重传仍失败,才会认为连接中断。
窗口的滑动与后续传输
在等待 1001~2000 重传的过程中,滑动窗口会越过阻塞的段,继续传输后续未阻塞的窗口数据(如 4001 及之后的字节)。这一设计让 TCP 在保证可靠性的同时,最大限度地利用了网络带宽,避免因个别段丢失导致整体传输停滞。
这种机制被称为"⾼速重发控制"(也叫"快重传").
流量控制
接收端处理数据的速度是有限的.如果发送端发的太快,导致接收端的缓冲区被打满,这个时候如果发送端继续发送,就会造成丢包,继⽽引起丢包重传等等⼀系列连锁反应.
因此TCP⽀持根据接收端的处理能⼒,来决定发送端的发送速度.这个机制就叫做流量控制
- 接收端将⾃⼰可以接收的缓冲区⼤⼩放⼊TCP⾸部中的"窗⼝⼤⼩"字段,通过ACK端通知发送端;
- 窗⼝⼤⼩字段越⼤,说明⽹络的吞吐量越⾼;
- 接收端⼀旦发现⾃⼰的缓冲区快满了,就会将窗⼝⼤⼩设置成⼀个更⼩的值通知给发送端;
- 发送端接受到这个窗⼝之后,就会减慢⾃⼰的发送速度;
- 如果接收端缓冲区满了,就会将窗⼝置为0;这时发送⽅不再发送数据,但是需要定期发送⼀个窗⼝探 测数据段,使接收端把窗⼝⼤⼩告诉发送端
拥塞窗⼝
- 发送开始的时候,定义拥塞窗⼝⼤⼩为1;
- 每次收到⼀个ACK应答,拥塞窗⼝加1;
- 每次发送数据包的时候,将拥塞窗⼝和接收端主机反馈的窗⼝⼤⼩做⽐较,取较⼩的值作为实际发送的窗⼝; 像上⾯这样的拥塞窗⼝增⻓速度,是指数级别的."慢启动"只是指初使时慢,但是增⻓速度⾮常快.
- 为了不增⻓的那么快,因此不能使拥塞窗⼝单纯的加倍.
- 此处引⼊⼀个叫做慢启动的阈值
- 当拥塞窗⼝超过这个阈值的时候,不再按照指数⽅式增⻓,⽽是按照线性⽅式增⻓
- 当TCP开始启动的时候,慢启动阈值等于窗⼝最⼤值;
- 在每次超时重发的时候,慢启动阈值会变成原来的⼀半,同时拥塞窗⼝置回1;

拥塞控制,归根结底是TCP协议想尽可能快的把数据传输给对⽅,但是⼜要避免给⽹络造成太⼤压⼒的折中⽅案.
粘包问题
一、先明确:什么是粘包?
TCP 是 “面向字节流” 的协议,它会把应用程序发送的数据当成连续的字节流来处理,而不是按 “个”(比如每次 send 的数据包)来划分边界。当发送方连续发送多组数据,或接收方读取不及时时,接收方可能会一次性收到多组数据的拼接内容,这种 “多组数据被合并接收” 的现象,就是粘包。
举个例子:
- 发送方分 2 次 send:
“hello”和“world” - 接收方可能 1 次 recv 就收到:
“helloworld”(这就是粘包),而不是预期的 2 次分别收到。
二、粘包的 2 个核心原因
粘包的根源是 TCP 的 “流特性”+“缓冲区机制”,具体分发送方和接收方两类原因:
1. 发送方导致的粘包:Nagle 算法与缓冲区合并
TCP 默认开启Nagle 算法,其目的是减少网络小数据包数量(避免网络拥堵),核心逻辑是:
- 发送方会先把应用程序发送的数据存入发送缓冲区;
- 如果缓冲区中的数据量较小,不会立即发送,而是等缓冲区满了、或等收到前一个数据包的确认后,再将缓冲区中的多组小数据 “合并成一个大数据包” 发送;
- 这就导致发送方的多组小数据被 “粘” 成一个包发出去,接收方自然会粘包。
2. 接收方导致的粘包:接收缓冲区与读取时机
接收方也有接收缓冲区,数据到达后会先存入缓冲区,再由应用程序调用 recv 读取。如果:
- 接收方的应用程序读取速度太慢,导致缓冲区中堆积了发送方连续发来的多组数据;
- 当应用程序终于调用 recv 时,会一次性把缓冲区中堆积的所有数据读走,从而造成粘包。
三、哪些场景下会遇到粘包?
不是所有 TCP 通信都会粘包,以下 2 类场景最容易出现:
| 场景类型 | 典型例子 | 粘包原因 |
|---|---|---|
| 发送方连续发小数据 | 聊天软件连续发 2 条短消息(各 10 字节) | 发送方 Nagle 算法合并小数据包 |
| 接收方读取不及时 | 接收方在处理前 1 组数据时,发送方又发了 3 组 | 接收方缓冲区堆积多组数据 |
反之,如果发送方每次发大数据包(比如 10KB),或接收方即时读取(发 1 组读 1 组),粘包概率会极低。
四、解决粘包的 3 种核心方案
解决粘包的本质是:给 TCP 的字节流 “划分边界”,让接收方知道 “哪部分是一组数据”。核心方案有 3 种,实际开发中最常用前 2 种:
1. 方案 1:固定数据包长度(最简单)
- 约定:发送方每次发送的数据长度固定(比如每次都发 100 字节);
- 接收方:每次 recv 固定长度(100 字节),即使数据不足 100 字节,也按 100 字节读取(不足部分可补 0);
- 优点:实现简单,无需额外处理;
- 缺点:灵活性差 —— 如果数据实际长度远小于固定长度(比如只发 10 字节却要占 100 字节),会浪费带宽。
示例:发送方每次 send 前,把数据补到 100 字节(比如“hello”补 95 个 0);接收方每次 recv (100),再去掉补的 0,得到原始数据。
2. 方案 2:添加 “消息头”(最常用)
- 约定:每个数据包分为 “消息头” 和 “消息体” 两部分;
- 消息头:固定长度(比如 4 字节),用来存储 “消息体的实际长度”;
- 接收方步骤:
- 先 recv 固定长度的消息头(4 字节),解析出消息体的长度(比如解析出是 20 字节);
- 再 recv 对应长度的消息体(20 字节),即可准确拿到一组完整数据;
- 优点:灵活,不浪费带宽,适合任何长度的数据;
- 缺点:需要自定义数据包格式,实现稍复杂。
示例:要发“helloworld”(10 字节),先构造消息头000A(4 字节,16 进制表示 10),再拼接消息体“helloworld”,最终发送“000Ahelloworld”;接收方先读 4 字节头,知道消息体是 10 字节,再读 10 字节即可。
3. 方案 3:使用 “分隔符”(适合文本数据)
- 约定:在每组数据的末尾添加一个特殊的 “分隔符”(比如
\r\n、|,但要确保分隔符不会出现在数据本身中); - 接收方:持续读取字节流,直到遇到约定的分隔符,就认为从上次分隔符到当前分隔符之间的内容是一组完整数据;
- 优点:实现简单,适合文本传输(比如 HTTP 协议的
\r\n\r\n就是分隔符); - 缺点:如果数据本身包含分隔符(比如传输的文本里有
\r\n),会导致误判,需要额外处理(比如转义)。
示例:发送方每次发数据后加\r\n,比如发“hello\r\n”和“world\r\n”;接收方读数据时,遇到\r\n就分割,分别得到“hello”和“world”。
总结
粘包不是 TCP 的 “bug”,而是它 “面向字节流”+“缓冲区优化” 的必然结果。实际开发中,“固定头 + 消息体” 是兼容性最好、最常用的解决方案,几乎能应对所有场景。
TCP⼩结
为什么TCP这么复杂?因为要保证可靠性,同时⼜尽可能的提⾼性能.
可靠性:
- 校验和
- 序列号(按序到达)
- 确认应答
- 超时重发
- 连接管理
- 流量控制
- 拥塞控制
提⾼性能:
- 滑动窗⼝
- 快速重传
- 延迟应答
- 捎带应答
其他:
定时器(超时重传定时器,保活定时器,TIME_WAIT定时器等)
TCP/UDP对⽐
我们说了TCP是可靠连接,那么是不是TCP⼀定就优于UDP呢?TCP和UDP之间的优点和缺点,不能简单, 绝对的进⾏⽐较
- TCP⽤于可靠传输的情况,应⽤于⽂件传输,重要状态更新等场景;
- UDP⽤于对⾼速传输和实时性要求较⾼的通信领域,例如,早期的QQ,视频传输等.
另外UDP可以⽤于⼴播; 归根结底,TCP和UDP都是程序员的⼯具,什么时机⽤,具体怎么⽤,还是要根据具体的需求场景去判定.
⽤UDP实现可靠传输(经典⾯试题)
核心设计目标
让 UDP 具备 TCP 的核心可靠特性:
- 数据不丢失(丢失重传)
- 数据有序到达(序号与重排)
- 流量控制(避免接收方缓冲区溢出)
- 拥塞控制(避免网络拥塞)
关键实现机制
1. 序号与确认机制(ACK)
序号(Sequence Number):为每个 UDP 数据包分配唯一序号(按发送顺序递增),确保接收方识别数据顺序和丢失情况。
确认(ACK):接收方收到数据后,向发送方返回 ACK 包,告知已成功接收的最大序号(或具体序号)。
超时重传:发送方若在超时时间内未收到 ACK,重传对应数据包。
例:发送方发送包 1、2、3,若接收方只收到 1 和 3,会返回 ACK=1(表示已收到 1,等待 2),发送方超时后重传 2。
2. 滑动窗口机制
- 类似 TCP 的滑动窗口,限制未确认数据包的最大数量,实现流量控制:
- 发送窗口:发送方可连续发送的未确认数据包上限(由接收方的接收窗口大小决定)。
- 接收窗口:接收方缓冲区的剩余容量,通过 ACK 告知发送方,避免发送方发送过快导致接收方溢出。
3. 超时重传与重传策略
- 超时时间(RTO):动态计算(类似 TCP 的 RTT 估算),根据网络延迟调整,避免过早或过晚重传。
- 快速重传:当接收方收到乱序包(如预期包 2,却收到 3),连续发送 3 次对包 2 的 ACK,触发发送方立即重传(无需等待超时)。
4. 拥塞控制
- 模拟 TCP 的拥塞控制算法,避免大量数据包导致网络拥塞:
- 慢启动:初始阶段,发送窗口按指数增长。
- 拥塞避免:窗口达到阈值后,按线性增长。
- 拥塞发生:超时或收到 3 次重复 ACK 时,减小窗口(如重置阈值为当前窗口一半,窗口设为 1)。
5. 数据校验与去重
- 校验和:在 UDP 数据报中添加校验和,接收方验证数据完整性,丢弃损坏的包。
- 去重:接收方通过序号记录已接收的包,若收到重复序号(如重传包),直接丢弃并返回对应 ACK。
实现流程概要
- 连接建立:类似 TCP 的三次握手(可选),交换初始序号、窗口大小等参数,确认双方通信准备就绪。
- 数据发送:
- 发送方按序号封装数据,在窗口范围内连续发送。
- 维护未确认包列表,启动超时计时器。
- 数据接收:
- 接收方按序号缓存数据,若有序则提交给应用层,否则暂存乱序包。
- 向发送方返回 ACK(包含已接收最大有序序号和接收窗口大小)。
- 重传处理:
- 发送方收到重复 ACK 或超时,重传对应数据包。
- 拥塞控制:根据网络状态动态调整发送窗口。
- 连接关闭:类似 TCP 的四次挥手(可选),确保双方数据传输完毕。
优缺点
- 优点:相比 TCP 更灵活,可根据场景定制(如简化拥塞控制、调整超时时间),适合低延迟场景(如游戏、实时音视频)。
- 缺点:实现复杂(需手动处理所有可靠机制),通用性不如 TCP,且可能因设计不当导致性能问题(如重传策略不合理)。
经典应用
- 实时游戏:自定义可靠 UDP 协议,在保证关键数据(如玩家操作)可靠的同时,允许非关键数据(如场景细节)丢失,降低延迟。
- QUIC 协议:基于 UDP 实现,集成了可靠传输、TLS 加密等特性,性能优于 TCP。
通过上述机制,UDP 可在应用层模拟 TCP 的可靠性,同时保留 UDP 低延迟的优势。
