【Linux】 TCP进阶详解:字节流、粘包问题、异常情况与UDP完整对比2

📌 相关专栏

很高兴你点开这篇文章✨

这里会持续更新我喜欢的内容,关注我,一起慢慢变好呀

👍 点赞 ⭐ 收藏 💬 评论


前言

在上篇文章中,我们掌握了TCP基础结构与可靠传输的核心原理。本篇作为TCP协议进阶收尾笔记,聚焦TCP容易踩坑、考试面试高频难点。

💡:本文重点讲解TCP面向字节流的特性、网络开发最常见的粘包问题及解决思路、TCP各类异常断开场景,最后全面总结TCP核心特点,并与UDP协议进行全方位对比。帮你补齐TCP所有知识盲区,彻底学完学懂TCP传输协议。

🐶 🐾 ✨ 🐾 🐶


4. TCP保证可靠性的机制(续)

4. 流量控制

接收端处理数据的速度是有限的. 如果发送端发的太快, 导致接收端的缓冲区被打满, 这个时候如果发送端继续发送, 就会造成丢包, 继而引起丢包重传等等⼀系列连锁反应

🐾 因此TCP支持根据接收端的处理能力, 来决定发送端的发送速度. 这个机制就叫做流量控制(FlowControl);

  • 接收端将自⼰可以接收的缓冲区剩余空间大小放入 TCP ⾸部中的 "窗口大小" 字段, 通过ACK端通知发送端;
  • 窗口大小字段越大, 说明网络的吞吐量越⾼;
  • 接收端⼀旦发现自⼰的缓冲区快满了, 就会将窗口大小设置成⼀个更小的值通知给发送端;
  • 发端接受到这个窗口之后, 就会减慢自⼰的发送速度;
  • 如果接收端缓冲区满了, 就会将窗口置为0; 这时发送方不再发送数据, 但是需要定期发送⼀个窗口探测数据段, 使接收端把窗口大小告诉发送端

🐶 🐾 ✨ 🐾 🐶


5. 滑动窗口

刚才我们讨论了确认应答策略, 对每⼀个发送的数据段, 都要给⼀个ACK确认应答. 收到ACK后再发送下⼀个数据段. 这样做有⼀个比较大的缺点, 就是性能较差. 尤其是数据往返的时间较长的时候


🐾 那我们一次多发几条数据,是不是就能解决一发一收带来的性能较低问题了!(其实就是将多个段的等待时间重叠在一起了)

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

🐶 🐾 ✨ 🐾 🐶


🐾 QS1:发送报文后, 收到 ACK 应答前,在这期间处于超时重传的等待时间。在等待时间以内,发送方不能将已经发送的报文丢弃,而是要保存起来。 问题是,这些数据应该保存在哪?

  • 答:TCP将部分数据在发送的时候直接保留在发送缓冲区了,然后只要通过某种方式进行区域划分即可,所以引入了滑动窗口(本质上就是双指针

🐾 将发送缓冲区中的数据分成三部分

  1. 已经发送 & 已经收到应答的数据。
  2. 已经发送 & 还没收到应答的数据 (滑动窗口)。
  3. 待发送的数据

注意滑动窗口的大小不是固定的,会根据接收端的数据接收能力来调整滑动窗口的大小

  • 接收端的数据接收能力强,滑动窗口就大;接收端的数据接收能力弱,滑动窗口就小

  • 动窗口的实际大小不止依靠接收端的窗口大小,还要根据之后的拥塞控制中的拥塞窗口来判断


🐾 但是,出现了丢包的情况,该如何重传呢?这里分两种情况讨论!

情况1: 数据包已经抵达,ACK丢了

这种情况下, 部分ACK丢了并不要紧, 因为可以通过后续的ACK进行确认


情况2: 数据包直接丢了

  • 当某⼀段报⽂段丢失之后, 发送端会⼀直收到 1001 这样的ACK, 就像是在提醒发送端 "我想要的是1001" ⼀样;
  • 如果发送端主机连续三次收到了同样⼀个 "1001" 这样的应答, 就会将对应的数据 1001 - 2000 重新发送;
  • 这个时候接收端收到了 1001 之后, 再次返回的ACK就是7001了(因为2001 - 7000)接收端其实之前就已经收到了, 被放到了接收端操作系统内核的接收缓冲区中;

这种机制被称为 "高速重发控制"(也叫 "快重传").


5.1 滑动窗口通过双指针实现

TCP 的缓冲区本质上就是个字符数组,而 TCP 报文中的序号就是这个字符数组的下标

即:动窗口本质上就是通过两个指针 (数组下标) 来进行维护

  • win_start 标识滑动 窗口的起始下标
  • win_end 标识滑动窗口就的结尾下标

当发送端收到接收端返回的的应答报文(ACK)时,假设ACK为 N,窗口大小为M(接收端返回的 TCP 报文中的 16 位窗口大小)。此时 win_start 更新为 N,将 win_end 更新为 win_start + M(N+M)

  • 当滑动窗口为零时, win_start == win_end
  • 当滑动窗口扩大时, win_start固定, win_end++
  • 当滑动窗口缩小时,win_end固定,让 win_start++

注意:由于ACK只会越来越大,所以滑动窗口只会向右移动,不会向左


6. 拥塞控制

6.1 使用拥堵控制解决网络拥堵

虽然TCP有了滑动窗口这个大杀器, 能够⾼效可靠的发送大量的数据. 但是如果在刚开始阶段就发送大量的数据, 仍然可能引发问题.

因为网络上有很多的计算机, 可能当前的网络状态就已经比较拥堵. 在不清楚当前网络状态下, 贸然发送大量的数据, 是很有可能引起雪上加霜的.

TCP引入 慢启动 机制, 先发少量的数据, 探探路, 摸清当前的网络拥堵状态, 再决定按照多大的速度传输数据;

  • 此处引入⼀个概念称为拥塞窗口
  • 发送开始的时候, 定义拥塞窗口大小为1;
  • 每次收到⼀个ACK应答, 拥塞窗口加1;
  • 每次发送数据包的时候, 将拥塞窗口和接收端主机反馈的窗口大小做比较, 取较小的值作为实际发送的窗口;

6.2 慢启动阈值

像上面这样的拥塞窗口增长速度, 是指数级别的. "慢启动" 只是指初始时慢, 但是增长速度非常快

  • 为了不增长的那么快, 因此不能使拥塞窗口单纯的加倍.
  • 此处引入⼀个叫做慢启动的阈值
  • 当拥塞窗超过这个阈值的时候, 不再按照指数方式增长, 而是按照线性方式增长


  • 当TCP开始启动的时候, 慢启动阈值等于窗口最大值;
  • 在每次超时重发的时候, 慢启动阈值会变成上一次发生网络拥塞时拥塞窗口的一半(24/2=12), 同时拥塞窗口置回1;

🐾 为何这么设计?

  • 指数增长(慢开始):解决"冷启动"问题,一开始不知道网络带宽,快速试探
  • 乘法减小 ssthresh :将新的 ssthresh 设置为上一次发生网络拥堵时拥堵窗口的一半,也就是 24/2=12 (图中标注的"新的ssthresh值12");
  • 线性增长(拥塞避免):防止过快把网络"撑爆",给其他连接留空间;

🐾 理解:

  • 少量丢包,触发超时重传

  • 大量丢包,触发网络拥塞

  • 当TCP通信开始后, 网络吞吐量会逐渐上升,即指数增长

  • 随着网络发⽣拥堵, 吞吐量会立刻下降,即线性增长

拥塞控制归根结底是TCP协议想尽可能把数据传输给对方,但是又要避免给网络造成太大压力的折中方案


7. 延迟应答

如果接收数据的主机立刻返回ACK应答, 这时候返回的窗口可能比较小;

🐾 为什么呢?

  • 假设接收端缓冲区为1M ,⼀次收到了500K的数据; 如果立刻应答, 返回的窗口就是500K;
  • 但实际上可能处理端处理的速度很快, 10ms之内就把500K数据从缓冲区消费掉了,此时它的缓冲区又恢复到了1M;
  • 在这种情况下, 接收端处理还远没有达到自⼰的极限, 即使窗口再放大⼀些, 也能处理过来;
  • 如果接收端稍微等⼀会再应答, 比如等待200ms再应答, 那么这个时候返回的窗口大小就是1M;

一定要记得,窗口越大,网络吞吐量越大,传输效率就越高。我们的目标就是保证网络不拥塞的情况下尽量提高传输效率!

🐾 那么所有的包都可以延迟应答么? 肯定也不是;

  • 数量限制: 每隔N个包就应答⼀次;
  • 时间限制: 超过最大延迟时间就应答⼀次;

具体的数量和超时时间, 依操作系统不同也有差异;
⼀般 N 取 2, 超时时间取200ms;


8. 捎带应答

在延迟应答的基础上, 我们发现, 很多情况下, 客户端服务器在应用层也是 "⼀发⼀收" 的.

bash 复制代码
客户端A                               服务端B
data1-------------------------------->接收并处理data1
收到服务端的应答ACK1 + data2<------------回应ACK1给客户端,并捎带上data2

🐶 🐾 ✨ 🐾 🐶


5. 面向字节流

创建⼀个TCP的socket, 同时在内核中创建⼀个 发送缓冲区 和⼀个 接收缓冲区;

  • 调用write时, 数据会先写入发送缓冲区中;
  • 如果发送的字节数太长, 会被拆分成多个TCP的数据包发出;
  • 如果发送的字节数太短, 就会先在缓冲区⾥等待, 等到缓冲区长度差不多了, 或者其他合适的时机发送出去;
  • 接收数据的时候, 数据也是从网卡驱动程序到达内核的接收缓冲区;
  • 然后应用程序可以调用read从接收缓冲区拿数据;
    另⼀方⾯, TCP的⼀个连接, 既有发送缓冲区, 也有接收缓冲区, 那么对于这⼀个连接, 既可以读数据, 也可以写数据. 这个概念叫做 全双⼯

由于缓冲区的存在, TCP程序的读和写不需要一 一 匹配, 例如:

  • 写100个字节数据时, 可以调用⼀次write写100个字节, 也可以调用100次write, 每次写⼀个字节;
  • 读100个字节数据时, 也完全不需要考虑写的时候是怎么写的, 既可以⼀次read 100个字节, 也可以⼀次read⼀个字节, 重复100次

🐶 🐾 ✨ 🐾 🐶


6. 粘包问题

6.1 什么是粘包问题?

粘包问题是指,在TCP网络传输中,发送方发送的几条独立数据,被接收方一次全收下了(粘在一起),导致无法区分数据边界。

举例:

  • 你上网买了几种糕点,商家为了省个包装费,把几种糕点全装在一块了,几种糕点粘在了一起,你收到货后,分不清哪种糕点是哪种了。

6.2 站在TCP的角度看粘包问题

都知道TCP是面向字节流的,只认得字节,所以你发了多少个数据包,他都不认得,他只认得你发的数据包一共有多少个字节,他不知道数据包之间的边界是哪个

  • ⾸先要明确, 粘包问题中的 "包" , 是指的应用层的数据包.
  • 在TCP的协议头中, 没有如同UDP⼀样的 "报⽂长度" 这样的字段, 但是有⼀个序号这样的字段.
  • 站在传输层的⻆度, TCP是⼀个⼀个报⽂过来的. 按照序号排好序放在缓冲区中.
  • 站在应用层的⻆度, 看到的只是⼀串连续的字节数据.
  • 那么应用程序看到了这么⼀连串的字节数据, 就不知道从哪个部分开始到哪个部分, 是⼀个完整的应用层数据包.

6.3 那么如何避免粘包问题呢?

归根结底就是⼀句话, 明确两个包之间的边界.

  • 对于定长的包, 保证每次都按固定大小读取即可; 例如Request结构, 是固定大小的, 那么就从缓冲区从头开始按sizeof(Request)依次读取即可;
  • 对于变长的包, 可以在包头的位置, 约定⼀个包总长度的字段, 从而就知道了包的结束位置;
  • 对于变长的包, 还可以在包和包之间使用明确的分隔符(应用层协议, 是程序猿自⼰来定的, 只要保证分隔符不和正⽂冲突即可);

6.4思考: 对于UDP协议来说, 是否也存在 "粘包问题" 呢?

  • 对于UDP, 如果还没有上层交付数据, UDP的报⽂长度仍然在. 同时, UDP是⼀个⼀个把数据交付给应用层. 就有很明确的数据边界.
  • 站在应用层的站在应用层的⻆度, 使用UDP的时候, 要么收到完整的UDP报⽂, 要么不收. 不会出现"半个"的情况.

🐶 🐾 ✨ 🐾 🐶


7. TCP异常情况

  • 进程终⽌: 进程终⽌会释放⽂件描述符, 仍然可以发送FIN. 和正常关闭没有什么区别.
  • 机器重启: 和进程终⽌的情况相同.
  • 机器掉电/网线断开: 接收端认为连接还在, ⼀旦接收端有写入操作, 接收端发现连接已经不在了, 就会进行reset. 即使没有写入操作, TCP自⼰也内置了⼀个保活定时器, 会定期询问对方是否还在. 如果对方不在, 也会把连接释放.
  • 另外, 应用层的某些协议, 也有⼀些这样的检测机制. 例如HTTP长连接中, 也会定期检测对方的状态. 例如QQ, 在QQ断线之后, 也会定期尝试重新连接

🐶 🐾 ✨ 🐾 🐶


8. 基于TCP应用层协议

协议 说明
HTTP 超文本传输协议
HTTPS 安全数据传输协议
SSH 安全外壳协议
TELNET 远程终端协议
FTP 文件传输协议
SMTP 电子邮件传输协议

🐶 🐾 ✨ 🐾 🐶


9. TCP小结

为什么TCP这么复杂? 因为要保证可靠性, 同时又尽可能的提高性能

可靠性

  • 校验和
  • 序列号(按序到达)
  • 确认应答
  • 超时重发
  • 连接管理
  • 流量控制
  • 拥塞控制

提⾼性能

  • 滑动窗口
  • 快速重传
  • 延迟应答
  • 捎带应答

其他

  • 定时器(超时重传定时器, 保活定时器, TIME_WAIT定时器等

10. TCP/UDP对比

我们说了TCP是 可靠连接, 那么是不是TCP⼀定就优于UDP呢? TCP和UDP之间的优点和缺点, 不能简单,绝对的进行比较

  • TCP用于可靠传输的情况, 应用于⽂件传输, 重要状态更新等场景;

  • UDP用于对⾼速传输和实时性要求较⾼的通信领域, 例如, 早期的QQ, 视频传输等. 另外UDP可以用于⼴播;

🐶 🐾 ✨ 🐾 🐶


谢谢你看到这里呀

如果喜欢这篇内容,点个关注,下次更新不迷路✨

👍 点赞 ⭐ 收藏 💬 评论

相关推荐
用户03284722207013 小时前
如何搭建本地yum源(上)
运维
A小辣椒2 天前
TShark:Wireshark CLI 功能
linux
A小辣椒2 天前
TShark:基础知识
linux
BingoGo2 天前
PHP 泛型之殇 泛型 RFC 提案被拒绝
后端·php
JaguarJack2 天前
PHP 泛型之殇 泛型 RFC 提案被拒绝
后端·php
AlfredZhao2 天前
OCI 明明分配了 200G 系统盘,为什么 df 只看到 30G?
linux·oci
AlfredZhao3 天前
vi 删除指定范围的行,不用再反复按 dd
linux·vi
用户3074596982073 天前
PHP 扩展——从入门到理解
php
用户9718356334663 天前
银河麒麟 KY10 申威(SW64) 安装 nginx-1.16.1-2.p01.ky10.sw_64.rpm 详细步骤
linux
猪脚踏浪3 天前
linux 拷贝文件或目录到指定的位置
linux