当前位置: 首页 > news >正文

【计算机网络-传输层】传输层协议-TCP核心机制与可靠性保障

  📚 博主的专栏

🐧 Linux   |   🖥️ C++   |   📊 数据结构  | 💡C++ 算法 | 🅒 C 语言  | 🌐 计算机网络

上篇文章:传输层协议-UDP

下篇文章: 网络层

我们的讲解顺序是:通过前面的学习,理解能理解的TCP字段、学习TCP的策略,并且理解报头字段、学习其他的可靠性策略

文章摘要

本文全面剖析TCP协议的核心机制,深入探讨其如何保障网络通信的可靠性。从协议段格式、确认应答、超时重传、连接管理(三次握手与四次挥手)等基础原理出发,详解流量控制、滑动窗口、拥塞控制等性能优化策略。同时解析粘包问题、延迟应答、捎带应答等实际场景中的关键技术,并通过对比TCP与UDP的差异,阐明两者的适用场景。文章结合代码示例与网络状态分析,揭示TCP异常处理与资源管理的底层逻辑,为理解高效可靠的数据传输提供系统性视角。无论是网络初学者还是开发者,均可从中获得理论与实践的深度洞见。

目录

TCP 协议段格式

确认应答(ACK)机制

如何保证服务器端到客户端的可靠性?

真正的网络发送:

TCP 将每个字节的数据都进行了编号. 即为序列号

为什么需要有两种序号:序号和确认序号

捎带应答:TCP中提高效率的重要机制

6 位标志位:

如何理解序号?

超时重传机制

序号可以提供确认应答机制、能够去重、还能够保证按序交付。来保证可靠性

那么,如果超时,超时的时间如何确定?

连接管理机制

建立连接:三次握手、由一方主动发起,过程是由双方OS自主完成的

​编辑建立连接的本质就是在赌,赌最后一个发送的ACK对方(接收方)一定收到了

如果最后发送的ACK丢失了,会出现什么结果?

四次挥手:最小的通信成本,建立了断开连接的共识

TCP 状态转换的一个汇总图:

建立连接,为什么要三次握手(重要)

再谈四次挥手

验证TCP四次挥手的两种状态:

理解CLOSE_WAIT状态:

文件描述符泄漏现象:

理解 TIME_WAIT 状态

setsockopt

流量控制

PSH标记位

URG标记位、紧急指针是否有效

滑动窗口

问题1

问题2

如何理解滑动窗口?

最左侧报文丢失:解决办法

滑动窗口问题解答

实际上滑动窗口 = min(应答窗口, 拥塞窗口)

拥塞控制

"慢启动" 只是指初使时慢, 但是增长速度非常快.

后线性是为了精细化探测出最新拥塞窗口的值、线性探测

延迟应答

捎带应答

第三次握手可以捎带应答 

***面向字节流***重要***粘包问题***

面向字节流

粘包问题:

那么如何避免粘包问题呢? 归根结底就是一句话, 明确两个包之间的边界

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

TCP异常情况

TCP 小结

可靠性与提高性能

基于 TCP 应用层协议

TCP/UDP 对比


学习每一个协议时都要理解的两个问题:

1.如何解包(如何分装)

2.如何分用(如何从上层向下层交付)

1.首先,宏观上:需要知道报文从应用层向下层交付的时候,交付的数据部分,而在TCP(TCP的上一层就是应用层)中,会将数据给拷贝到缓冲区里,从缓冲区拿出数据,会自动添加报头(添加sk_buff结构)后再发送。 

2.对方收到报文前,TCP这里会处理报文,将报头分离开,提取出有效载荷放入接收缓冲区,让用户读取。

TCP 协议段格式

TCP 全称为 "传输控制协议(Transmission Control Protocol"). 人如其名, 要对数据的传输进行一个详细的控制;

如何解包: 

1.读取前20个字节的固定首部

2.提取4位首部长度(单位是4字节)(范围:[0, 15] -> [0, 60]

以上就是,先理解能理解的报文字段,接下来,我们边学习TCP的策略,并且理解报头字段

确认应答(ACK)机制

在发送消息和接收消息的时候,总有最新的消息没有应答。

例如:

对于客户端:客户端给服务器端发送消息后,并不能确认自己的消息成功被服务器端接收到,但,只要服务器给客户端响应回来,就能确认服务器端收到了客户端历史发送的消息,如果没有收到应答,就认为报文丢失。

要保证从左到右的可靠性:就要保证我发的历史的消息数据(而不是最新的),收到了应答,就能保证历史数据100%被对方收到了。

再次理解可靠性:

如果只考虑客户端给服务器端发消息(单向),客户端发一次消息,服务器端做一次应答,正常通信时,客户端不需要对服务器端的应答做应答。只保证自己发的历史消息,服务器做了应答,客户端就再发下一次的数据消息。

因此可靠性实际上并不是指客户端所发送的消息100%被服务器端所收到,这是理想情况。

所谓的可靠性,是指客户端给服务器端所发送的消息,发送方需要知道接收方是否收到。

如何保证服务器端到客户端的可靠性?

同上。服务器端给客户端发送消息,也需要收到客户端的应答,才能保证发送的历史消息被收到。没收到应答,就确认未收到,就重发。

结论:

可靠:重点是保证对发送的数据的进行可靠,应答是否可靠不考虑。只要收到应答,就能保证历史数据是可靠的。

双方都采用确认应答机制,就能保证两个朝向上数据通信的可靠性。

真正的网络发送:

是双方的OS(TCP协议)自动做的,所谓的发消息,是发送方的TCP协议自动发的,由对方的TCP协议来自动接受做ACK应答的。

TCP通信模式:(两种)

1.在不做说明的时候,我们一般是指一个朝向的通信,例如现在所说的就是客户端(发送端)向服务器端(接收端)发送消息。两个朝向的通信,也就是发送端和接收端一直做身份互换,客户端给服务器端发,服务器端作为发送端又给客户端发。

2.实际上,正常情况下,做的串行发送,是在发送第一个报文后收到应答,再发下一个,这样的效率低。(第二种通信方式)(滑动窗口),发送端一次发送多条报文给接收端,因为TCP要做确认应答,理论上,每一批发送的消息,接收端都需要做分批的应答,因为处理时间很快,这样可以做到发消息和收应答的时间发生重叠,可以提高发送效率。而如果收到的一批应答并不是发送的个数,这就不知道应答的是那一条消息,因此对应答需要做唯一性标识,引入序号和确认序号。

TCP 将每个字节的数据都进行了编号. 即为序列号

理解序号:

收到1001的确认应答:则表示前1000个报文数据接收方都收到了,下次要接受的是从1001开始的数据。

注意:我们的发送和响应的都是TCP报文,无论他是什么数字,要么只有报头,要么含有数据带报头的报文。发送的指的不是1000、2000这些数字,这些是32位序号,真实的发送和响应的是要么只有报头,要么含有数据带报头的报文、总之,都含有报头。这里的数据1000、2000等本质就是给32位序号填值。所谓的应答就是裸的报头,被设置了32位序号(确认应答)没在特定时间收到应答,会进行超时重传。如果没收到3001,而是收到了 4001的确认应答,能表示接收方收到了4001之前的所有报文,能支持应答的少量丢失。

为什么需要有两种序号:序号和确认序号

因为TCP是全双工的,在客户端给服务器端发送消息的时候,服务器端也有可能给客户端发送消息,并且也有可能做出对接收到消息的响应。发的数据是报头带数据,做出的响应是只含报头。

捎带应答:TCP中提高效率的重要机制

在TCP当中,为了提高效率:如果服务器端既要对接收到消息做应答,也需要发送消息,这时候服务器端有可能会将应答和要发送的数据的两个不同的报文合并成一个报文。做应答只需要设置32位确认序号,发送消息则是设置32位序号,带上数据字段。因此服务器需要同时使用序号和确认序号两种。

客户端如何确认所接收到的消息,是确认应答还是发送过来的消息,甚至是同时具有应答和发送过来的消息的报文。引入TCP报头中的6个标志位。

注意:我们需要意识到,实际情况中,是多个客户端向服务器端发送消息,未来服务器端回收到多种类型的报文,有可能客户端所发送的请求时请求连接、断开连接、确认报文、正常数据、确认+数据(捎带应答)。这就说明,服务器作为TCP协议的接收方时,TCP协议要有处理不同类型报文的能力,即:TCP的报文是有不同的类型的。

6 位标志位:

URG: 紧急指针是否有效

ACK: 确认号是否有效

PSH: 提示接收端应用程序立刻从 TCP 缓冲区把数据读走

RST: 对方要求重新建立连接; 我们把携带 RST 标识的称为复位报文段

SYN: 请求建立连接; 我们把携带 SYN 标识的称为同步报文段

FIN: 通知对方, 本端要关闭了, 我们称携带 FIN 标识的为结束报文段

从之前文章的学习中,我们可以知道无论是UDP报头、还是TCP报头,本质上都是结构体: 

typedef struct _tcp_hdr  
{  unsigned short src_port;    //源端口号  unsigned short dst_port;    //目的端口号  unsigned int seq_no;        //序列号  unsigned int ack_no;        //确认号  #if LITTLE_ENDIAN  unsigned char reserved_1:4; //保留6位中的4位首部长度  unsigned char thl:4;        //tcp头部长度  unsigned char flag:6;       //6位标志  unsigned char reseverd_2:2; //保留6位中的2位  #else  unsigned char thl:4;        //tcp头部长度  unsigned char reserved_1:4; //保留6位中的4位首部长度  unsigned char reseverd_2:2; //保留6位中的2位  unsigned char flag:6;       //6位标志   #endif  unsigned short wnd_size;    //16位窗口大小  unsigned short chk_sum;     //16位TCP检验和  unsigned short urgt_p;      //16为紧急指针  
}tcp_hdr; 

如果发的是含有应答的报文,就需要将ACK置为1,就要关注确认序号、如果是捎带应答,就也要关注携带的数据。

如何理解序号?

TCP 将每个字节的数据都进行了编号. 即为序列号.

将数据从应用层以字节流的方式拷贝到发送缓冲区,就相当于把数据放在数组当中,在发送缓冲区中的数据,天然的就具有了序号,约定从第一个字节,开始发,发送100个字节,那么发送序号就是1~100,101~200。序号就是该char endbuffer[65535]缓冲区的数组下标。

而真实情况会更复杂,因为刚开始的发送序号可能是随机的。

每一个 ACK 都带有对应的确认序列号, 意思是告诉发送者, 我已经收到了哪些数据; 下一次你从哪里开始发。

超时重传机制

  • 主机 A 发送数据给 B 之后, 可能因为网络拥堵等原因, 数据无法到达主机 B;
  • 如果主机 A 在一个特定时间间隔内没有收到 B 发来的确认应答, 就会进行重发;

我收到ACK,对方收到数据(客观事实);我没有收到ACK,对方没有收到数据(规定);

为什么是规定,这是因为 主机 A 未收到 B 发来的确认应答, 也可能是因为 ACK 丢失了;

因此主机 B 会收到很多重复数据. 那么 TCP 协议需要能够识别出那些包是重复的包, 并且把重复的丢弃掉。

这时候我们可以利用前面提到的序列号, 就可以很容易做到去重的效果。

按序到达

对于接受方,收到的报文的顺序,一定是发送时的顺序吗,不一定,因此,需要按序号排列报文再向上层交付。

序号可以提供确认应答机制、能够去重、还能够保证按序交付。来保证可靠性

那么,如果超时,超时的时间如何确定?

  • 最理想的情况下, 找到一个最小的时间, 保证 "确认应答一定能在这个时间内返回".
  • 但是这个时间的长短, 随着网络环境的不同, 是有差异的.
  • 如果超时时间设的太长, 会影响整体的重传效率;
  • 如果超时时间设的太短, 有可能会频繁发送重复的包;

TCP 为了保证无论在任何环境下都能比较高性能的通信, 因此会动态计算这个最大超时时间.

  • Linux 中(BSD Unix 和 Windows 也是如此), 超时以 500ms 为一个单位进行控制, 每次判定超时重发的超时时间都是 500ms 的整数倍.
  • 如果重发一次之后, 仍然得不到应答, 等待 2*500ms 后再进行重传.
  • 如果仍然得不到应答, 等待 4*500ms 进行重传. 依次类推, 以指数形式递增.
  • 累计到一定的重传次数, TCP 认为网络或者对端主机出现异常, 强制关闭连接.

连接管理机制

在正常情况下, TCP 要经过三次握手建立连接, 四次挥手断开连接

建立连接:三次握手、由一方主动发起,过程是由双方OS自主完成的

1.listen状态:随时等待连接

2.connect状态:要求客户端构建一个TCP报文(报头),将SYN(同步标志位)置1,代表,发送连接建立的请求。服务器会应答(只含报头)SYN设置为1,ACK表示确认收到请求。

3.紧接着,客户端会在发送一个TCP报文,将ACK置1,表示对服务器发送的ACK做应答。

应用层调connect系统调用,严格意义上来讲:只是发起了三次握手,我们可以理解为connect就是指向服务器发送SYN,发起握手,从此往后,三次握手的过程由双方的OS自动协商。

三次握手完成,accept会返回新的连接套接字描述符,accept不参与三次握手的过程。

套接字状态发生变化:

服务器从CLOSED状态开启后处于LISTEN状态,在发送端客户端处于CLOSED状态到开启发送一个SYN的时候,发送方的套接字会变成SYN_SENT(同步发送),服务器收到SYN标志位,服务器就会变成SYN_RCVD状态,同时会发送应答,一旦客户端收到ACK,就会发送ACK,并且处于ESTABUSHED状态(保证不了这个ACK是否发送给了服务器),客户端最后发送的ACK没有应答,但是我们规定,一旦客户端最后发出ACK就代表,客户端的三次握手完成了,客户端就认为自己就建立好了连接。服务器端认为建立好连接,是在收到ACK,处于ESTABUSHED状态。

因此,客户端和服务端双方确认好连接,会有一个短暂的时间差。

建立连接的本质就是在赌,赌最后一个发送的ACK对方(接收方)一定收到了

TCP保证可靠性能保证:建立连接一定会成功吗,不能保证、但是不用担心。

如果最后发送的ACK丢失了,会出现什么结果?

就会超时,就会补发,当发送端认为已经建立好连接,就会马上发送数据DATA,当服务器端收到DATA,会查看该报文中的源端口与目的端口,确认是否与该客户端建立好了连接(是否收到了最后一次的ACK),服务器就会给客户端做应答。如果确认没有收到最后一次的ACK,也就是没有建立好连接,应答的报头中就会将RST(连接重置、要求重新建立连接)置为1。客户端收到就会重新和服务器端再次三次握手(收到该标志位的主机,要对异常连接释放,重新建立连接)。

四次挥手:最小的通信成本,建立了断开连接的共识

一旦双方通信结束,需要关闭连接。双方都不和对方通信了,并且也知道对方也不和我通信了。

客户端会将报头的标志位FIN置1,在服务器收到FIN后,就会相应ACK,表示两次握手完成,服务器端完成服务后才会和客户端断开连接,就会给客户端发送FIN,当客户端收到FIN后就会给服务器发送ACK的响应,客户端关闭连接,服务端收到ACK后,关闭连接,完成4次握手。

断开连接是要获取到双方的确认、同意。所以需要四次挥手。

前两次挥手:

客户端和服务器端断开连接本质是:客户端要给服务器发送的数据发送完毕(应用层将数据发送完,并且调用了close)。

中间状态:服务器端继续给客户端发送消息,客户端必须继续接收消息,并且给出应答ACK。OS维护正常的可靠性的ACK,客户端还需要保证。

后两次挥手:

服务器端和客户端断开连接的本质是,服务器端要给客户端发送的数据发送完了。

调用close是关闭读写端全部关闭。只有客户端close、服务器端才会close。

shutdown(属于套接字中的系统调用接口、可以指定关闭读、关闭写、同时关闭读写)

TCP 状态转换的一个汇总图:

• 较粗的虚线表示服务端的状态变化情况;

• 较粗的实线表示客户端的状态变化情况;

• CLOSED 是一个假想的起始点, 不是真实状态

因此为什么在我们刚关闭服务器端,又再次用相同端口开启服务器端的时候,会开启失败呢?

进程已经退了,但是连接还在,这个连接在和浏览器在进行四次挥手。连接的端口号被占用,因此开启失败。

建立连接,为什么要三次握手(重要)

因为将会有多个客户端和服务器建立连接,并且还会有正在赶来建立连接的客户端。

在OS上有多种状态的连接,因此在OS上需要对连接进行管理。先描述、再组织。

所谓的连接本质也是内核数据结构的对象。建立好连接,就需要创建好一个内核数据结构对象。建立连接就需要在服务器端,malloc内核数据结构的空间,套接字信息,谁建立的、什么时候、源IP,目的IP,建立好的连接要有对应的缓冲区,还要维护好状态。

一个连接既要有内存空间,也要花时间初始化连接。维护连接、是有成本的(时间+空间)。3次握手,一旦成功,就是在双方维护对应的连接结构体。四次挥手结束,连接才会释放。

一个客户端给服务器发送大量的SYN称作SYN洪水。并且不做任何处理。会导致,服务器出现问题。为什么三次握手就可以呢?三次握手也存在SYN洪水供给问题,但是细节就是最后一次报文一定是客户端给服务器发的。所以要建立好连接,需要客户端先3次握手完成(有其他策略保证握手时有SYN洪水问题)。但1、2次有明显的问题,出现问题容易被利用。

因此,为什么要有三次握手、是三次握手?重要

1.验证全双工、验证双方网络的连通性。网络状态是双方进行通信的前提条件。最小的次数验证了客户端能发消息,能收消息,也验证了服务器端既能收也能发。

2.建立双方通信的共识意愿。由于服务器一般无条件同意客户端的连接请求,三次握手本质上就是四次握手,是四次握手将中间的ACK、SYN进行捎带应答了,因此称作三次握手

再谈四次挥手

深度理解下:

如果能做到:在服务器端收到客户端发来的FIN时也恰好想要关闭连接,此时可以将ACK和FIN捎带应答成一次吗,可以将四次挥手称之为三次挥手?如果恰好遇到这种情况,是可以的、这是其中的特殊情况。但是由于客户端给服务器发消息,服务器不一定将数据发完。而建立连接,客户端给服务器请求连接,服务器是一定要同意的,因此合并在一起毫无问题,四次握手也没有问题。

验证TCP四次挥手的两种状态:

理解CLOSE_WAIT状态:

以之前写过的 TCP 服务器为例, 我们稍加修改,可以看前两篇博客编写HTTP服务器

服务器不关闭文件描述符,他的状态就一直处于CLOSE_WAIT的状态

运行程序,并且,使用telnet命令与服务器建立一个连接,再使用netstat -nlap查看所有的网络服务:可以观察到,服务器状态为ESTABLISHD、telnet也就是我们的客户端状态为ESTABLISHD,我们查到了两个连接(因为这是在本主机上建立的)。

关闭telnet连接

此时的服务器就处于CLOSE_WAIT状态

做这个实验需要量多一些的客户端,当多个客户端断开连接后,并且服务端也断开连接,我们再看网络状态:(使用的端口号是8889),这是服务器在等待客户端回复ACK,当收到之后就变成CLOSED状态。

文件描述符泄漏现象:

如果我们的服务器卡顿,查一下是不是存在大量的CLOSE_WAIT

理解 TIME_WAIT 状态

主动开始断开连接,自己最终要处于TIME_WAIT的状态,三次握手一般是滞后于进程退出的,因为进程退出了,连接还会被OS维护。这里又体现了双方的TCP连接是由双方的OS自主管理完成的(通信细节)。

主动断开连接的一方,会在第四次挥手完成,等待一定的时长:2*MSL

现在做一个测试,首先启动 httpserver,然后启动 client,然后用 Ctrl-C 使 httpserver 终止,结果是:

此时再退出客户端,四次挥手完成,从服务器到客户端的一方存在TIME_WAIT状态

做一个测试,首先启动 httpserver,然后启动 client,然后用 Ctrl-C 使 httpserver 终止,再马上打开httpserver:

状态也是TIME_WAIT的状态,直到大概过了30~60s(系统配置)之后,才能再次开启统一端口的服务器。

引入一个系统调用接口:

setsockopt

setsockopt 是用于设置套接字选项的系统调用函数,允许开发者对套接字的行为进行细粒度控制。它在网络编程中非常关键,常用于优化性能、处理异常或调整协议细节。以下是详细讲解:

#include <sys/socket.h>int setsockopt(int sockfd,         // 套接字描述符int level,          // 选项的协议层(如 SOL_SOCKET、IPPROTO_TCP)int optname,        // 选项名称(如 SO_REUSEADDR)const void *optval, // 指向选项值的指针socklen_t optlen    // 选项值的长度
);

返回值:

  • 成功返回 0,失败返回 -1 并设置 errno

关键参数说明

1. 协议层(level

  • SOL_SOCKET:通用套接字层选项。

  • IPPROTO_TCP:TCP 协议层选项。

  • IPPROTO_IP:IP 协议层选项(如多播、广播配置)。

2. 选项名称(optname

  • 常见选项:

    • SO_REUSEADDR:允许地址重用(解决 bind 地址占用问题)。

    • SO_KEEPALIVE:启用 TCP 心跳检测(自动探测连接是否存活)。

    • SO_RCVBUF/SO_SNDBUF:调整接收/发送缓冲区大小。

    • SO_LINGER:控制 close() 关闭时的行为(立即关闭或等待数据发送)。

    • TCP_NODELAY:禁用 Nagle 算法(减少小数据包的延迟)。

更新套接字类中代码:

再次实施之前的操作,就能一直重启服务器了

 通过指令查MSL时间长度 

pupu@VM-8-15-ubuntu:~/computer-network/class_67/http$ cat /proc/sys/net/ipv4/tcp_fin_timeout 
60

想一想, 为什么是 TIME_WAIT 的时间是 2MSL?

• MSL 是 TCP 报文的最大生存时间, 因此 TIME_WAIT 持续存在 2MSL 的话

• 就能保证在两个传输方向上的尚未被接收或迟到的报文段都已经消失(否则服务器立刻重启, 可能会收到来自上一个进程的迟到的数据, 但是这种数据很可能是错误的);

• 同时也是在理论上保证最后一个报文可靠到达(假设最后一个 ACK 丢失, 那么服务器会再重发一个 FIN. 这时虽然客户端的进程不在了, 但是 TCP 连接还在, 仍然可以重发 LAST_ACK);

流量控制

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

因此 TCP 支持根据接收端的处理能力(接受缓冲区中剩余空间的大小 、根据16位窗口大小(填写的都是自己的)、填的都是自己的接收能力), 来决定发送端的发送速度.

这个机制就叫做流量控制(Flow Control);

接收端如何把窗口大小告诉发送端呢? 回忆我们的 TCP 首部中, 有一个 16 位窗口字段,就是存放了窗口大小信息;

注意细节:我们需要知道,在三次握手期间(不仅建立连接,也能协商通信数据),双方就已经协商交换过了双方的接收能力

• 接收端将自己可以接收的缓冲区大小放入 TCP 首部中的 "窗口大小" 字段, 通过 ACK 端通知发送端;

• 窗口大小字段越大, 说明网络的吞吐量越高;

• 接收端一旦发现自己的缓冲区快满了, 就会将窗口大小设置成一个更小的值通知给发送端;

• 发送端接收到这个窗口之后, 就会减慢自己的发送速度;

• 如果接收端缓冲区满了, 就会将窗口置为 0; 这时发送方 不再发送数据, 但是需要定期发送一个窗口探测数据段, 使接收端把窗口大小告诉发送端.

那么问题来了, 16 位数字最大表示 65535, 那么 TCP 窗口最大就是 65535 字节么?

实际上, TCP 首部 40 字节选项中还包含了一个窗口扩大因子 M, 实际窗口大小是 窗口

字段的值左移 M 位;

PSH标记位

如果我们发的数据想让对方尽快处理、交付(交给上层),就可以设置PSH(PUSH)。尽快将缓冲区腾出空位。

URG标记位、紧急指针是否有效

只要URG标记位为0,16位紧急指针就无意义,一旦设置为1,16位紧急指针才有意义

因此16位紧急指针是什么?标识数据部分,哪部分是紧急数据,紧急数据相对于报文的数据部分的偏移量。在工作场景,实际当中凡事具有指向、标识功能的都可以称之为指针。C语言中指地址。

TCP中紧急数据只有一个字节、紧急指针一般很少用,对TCP通信做管理,规定一些状态数字(1字节就够了)(0:正常、1:暂停、2:取消)

在之前讲过的recv、send系统调用接口函数中都有一个flag标志位,可以设置为:MSG_OOB、携带紧急指针。

out-of-band:带外数据、很少使用

滑动窗口

问题1

1.流量控制:在发送方如何根据对方的接收能力,发送数据?

2.超时重传:超时时间以内,已经发送的报文不能被丢弃,而是要保存起来,保存在哪里?

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

既然这样一发一收的方式性能较低, 那么我们一次发送多条数据, 就可以大大的提高性能(其实是将多个段的等待时间重叠在一起了)、前面讲过:

发送方:规定一个概念,滑动窗口,在滑动窗口以内的数据,可以直接发送,暂时不用收到应答。

滑动窗口的本质:发送缓冲区当中的一部分。

• 窗口大小指的是无需等待确认应答而可以继续发送数据的最大值. 上图的窗口大小就是 4000 个字节(四个段).

• 发送前四个段的时候, 不需要等待任何 ACK, 直接发送;

• 收到第一个 ACK 后, 滑动窗口向后移动, 继续发送第五个段的数据; 依次类推;

• 操作系统内核为了维护这个滑动窗口, 需要开辟 发送缓冲区 来记录当前还有哪些数据没有应答; 只有确认应答过的数据, 才能从缓冲区删掉;

• 窗口越大, 则网络的吞吐率就越高;

在滑动窗口以内的数据:暂时可以不用应答,可以直接发送

在滑动窗口左边的数据:已发送,已确认

在滑动窗口右侧的数据:待发送

问题2

1.滑动窗口只能向右滑动吗?是的,能不能向左滑动?不能

2.滑动窗口是一直不变的吗?可以变大、变小吗?滑动窗口是能够变化的(流量控制)

3.滑动窗口可以为0吗?可以

如何理解滑动窗口?

滑动窗口的本质就是发送窗口的下标

可以为滑动窗口建立一个模型,由两个指针指向滑动窗口的边界

滑动窗口的模型

窗口向右移动,也就是让win_start++、win_end++,改变滑动窗口的大小,也就是让滑动窗口的左右边界指针谁++的更快(例如,窗口增大:win_start增长的慢、win_end增长的快、极端情况也就是win_start不变。win_end一直++)、滑动窗口为0,则是两个指针指向同一个位置。

滑动窗口是如何更新的?

应答里面一定有一个确认序号、因此win_start = ack_seq 、win_end = win_start +win(win是对方的接收能力)。

那么如果出现了丢包, 如何进行重传?        这里分两种情况讨论

数据包已经抵达, ACK 被丢了

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

情况二: 数据包就直接丢了

• 当某一段报文段丢失之后, 发送端会一直收到 1001 这样的 ACK, 就像是在提醒发送端 "我想要的是 1001" 一样;(win_start不会更新,仍然指向1001)

• 如果发送端主机连续三次收到了同样一个 "1001" 这样的应答, 就会将对应的数据 1001 - 2000 重新发送;

• 这个时候接收端收到了 1001 之后, 再次返回的 ACK 就是 7001 了(因为 2001 - 7000)接收端其实之前就已经收到了, 被放到了接收端操作系统内核的接收缓冲区中;

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

也有可能收到两个同样的“1001”应答,这个时候就会由超时重传来接管,这可能是因为,剩余的报文不满足三个了,发回来的应答不足三个。

以上的情况就是最左侧报文丢失:

最左侧报文丢失:解决办法

1.确认序号规定的约束,滑动窗口左侧不动

2.快重传&&超时重传,对最左侧报文进行补发

中间报文丢失?

如果是2001~3001的报文丢失了,填2001确认序号、win_start更新到2001。这时候就变成新窗口的最左侧报文丢失的问题。

最右侧报文丢失?

如果是4001~5001的报文丢失了,填4001确认序号、win_start更新到4001。这时候就变成新窗口的最左侧报文丢失的问题。

当上层接收了数据,就会更新接收能力,win_end更新。

滑动窗口问题解答

1.流量控制:在发送方如何根据对方的接收能力,发送数据?

对方的接收能力为0,那么滑动窗口为0,5000,5000以内的数据都可以由TCP直接发。

流量控制就是通过滑动窗口实现的。

2.超时重传:超时时间以内,已经发送的报文不能被丢弃,而是要保存起来,保存在哪里?

保存在滑动窗口中,丢包问题都会被转化成最左侧报文丢失。

滑动窗口一直向右滑动,会越界吗?

不会、将发送缓冲区想象成环形队列(本质物理上发送缓冲区也是一个队列),就不会出现溢出越界问题。

是否需要清除已发送已确认的数据?

不用,代表的就是废弃数据,因为是环状队列,之后滑动窗口再滑动的时候,数据拷贝下来的时候,会将这些废弃数据覆盖。将滑动窗口移到了最左侧就相当于丢弃报文、因为这些报文已经被发送被确认。

实际上滑动窗口 = min(应答窗口, 拥塞窗口)

拥塞控制

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

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

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

• 此处引入一个概念称为拥塞窗口

因为网络的状况是浮动的,因此拥塞窗口的大小,也必然是浮动的,主机应该怎么才能得知,拥塞窗口的接近大小应该是多大?必须经过多轮尝试,才能知道。

发送开始的时候, 定义拥塞窗口大小为 1;

每次收到一个 ACK 应答, 拥塞窗口加 1;

• 每次发送数据包的时候, 将拥塞窗口和接收端主机反馈的窗口大小做比较, 取较小的值作为实际发送的窗口; 滑动窗口 = min(应答窗口,拥塞窗口)

"慢启动" 只是指初使时慢, 但是增长速度非常快.

像上面这样的拥塞窗口增长速度, 是指数级别的.

前期慢,可以慢慢减少网络发送,让网络恢复,网络恢复,我们的通信过程也要恢复起来,中后期增长就快。因此指数增长

• 为了不增长的那么快, 因此不能使拥塞窗口单纯的加倍.

• 此处引入一个叫做慢启动(2^n)的阈值

• 当拥塞窗口超过这个阈值的时候, 不再按照指数方式增长, 而是按照线性方式增长

后线性是为了精细化探测出最新拥塞窗口的值、线性探测

• 当 TCP 开始启动的时候, 慢启动阈值等于窗口最大值;

• 在每次超时重发的时候, 慢启动阈值会变成原来拥塞窗口的一半, 同时拥塞窗口置回 1;

少量的丢包, 我们仅仅是触发超时重传; 大量的丢包, 我们就认为网络拥塞;

当 TCP 通信开始后, 网络吞吐量会逐渐上升; 随着网络发生拥堵, 吞吐量会立刻下降;

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

拥塞窗口的值是有上限的,根据OS决定

延迟应答

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

• 假设接收端缓冲区为 1M. 一次收到了 500K 的数据; 如果立刻应答, 返回的窗口就是 500K;

• 但实际上可能处理端处理的速度很快, 10ms 之内就把 500K 数据从缓冲区消费掉了;

• 在这种情况下, 接收端处理还远没有达到自己的极限, 即使窗口再放大一些, 也能处理过来;

• 如果接收端稍微等一会再应答, 比如等待 200ms 再应答, 那么这个时候返回的窗口大小就是 1M;

一定要记得, 窗口越大, 网络吞吐量就越大, 传输效率就越高. 我们的目标是在保证网络不拥塞的情况下尽量提高传输效率;
那么所有的包都可以延迟应答么? 肯定也不是;

• 数量限制: 每隔 N 个包就应答一次;

• 时间限制: 超过最大延迟时间就应答一次;

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

捎带应答

在延迟应答的基础上, 我们发现, 很多情况下, 客户端服务器在应用层也是 "一发一收"的. 意味着客户端给服务器说了 "How are you", 服务器也会给客户端回一个 "Fine, thank you";

那么这个时候 ACK 就可以搭顺风车, 和服务器回应的 "Fine, thank you" 一起回给客户端

第三次握手可以捎带应答 

注意:三次握手完成才意味着连接建立成功,连接建立成功才能开始通信。而对于客户端来说,由于第二次握手收到了服务器发来的肯定,第二次握手成功后,客户端已经认为连接建立成功,因此在第三次握手的时候客户端 可以携带数据

***面向字节流***重要***粘包问题***

面向字节流

• 调用 write 时, 数据会先写入发送缓冲区中;

• 如果发送的字节数太长, 会被拆分成多个 TCP 的数据包发出;

• 如果发送的字节数太短, 就会先在缓冲区里等待, 等到缓冲区长度差不多了, 或者其他合适的时机发送出去;

• 接收数据的时候, 数据也是从网卡驱动程序到达内核的接收缓冲区;

• 然后应用程序可以调用 read 从接收缓冲区拿数据;

• 另一方面, TCP 的一个连接, 既有发送缓冲区, 也有接收缓冲区, 那么对于这一个连接, 既可以读数据, 也可以写数据. 这个概念叫做 全双工

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

• 写 100 个字节数据时, 可以调用一次 write 写 100 个字节, 也可以调用 100 次write, 每次写一个字节;

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

粘包问题:

可以看http从0开始实现的博客

• 首先要明确, 粘包问题中的 "包" , 是指的应用层的数据包.

• 在 TCP 的协议头中, 没有如同 UDP 一样的 "报文长度(首部长度)" 这样的字段, 但是有一个序号这样的字段.

• 站在传输层的角度, TCP 是一个一个报文过来的. 按照序号排好序放在缓冲区中.

• 站在应用层的角度, 看到的只是一串连续的字节数据.

• 那么应用程序看到了这么一连串的字节数据, 就不知道从哪个部分开始到哪个部分, 是一个完整的应用层数据包.

那么如何避免粘包问题呢? 归根结底就是一句话, 明确两个包之间的边界

• 对于定长的包, 保证每次都按固定大小读取即可; 例如上面的 Request 结构, 是固定大小的, 那么就从缓冲区从头开始按 sizeof(Request)依次读取即可;

• 对于变长的包, 可以在包头的位置, 约定一个包总长度的字段, 从而就知道了包的结束位置;

• 对于变长的包, 还可以在包和包之间使用明确的分隔符(应用层协议, 是由我们自己来定的, 只要保证分隔符不和正文冲突即可);

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

• 对于 UDP, 如果还没有上层交付数据, UDP 的报文长度仍然在. 同时, UDP 是一个一个把数据交付给应用层. 就有很明确的数据边界.

• 站在应用层的站在应用层的角度, 使用 UDP 的时候, 要么收到完整的 UDP 报文, 要么不收. 不会出现"半个"的情况

数据库软件:Redis、MySQL会存在许多序列化和反序列化,也需要有格式存取,这也就是协议。

网络通信和本地文件流,没有区别。

TCP异常情况

进程终止: 进程终止会释放文件描述符, 仍然可以发送 FIN. 和正常关闭没有什么区别.

机器重启: 和进程终止的情况相同.OS会做,其他进程都结束了OS才会退出。都会四次挥手

机器掉电/网线断开: 接收端认为连接还在, 一旦接收端有写入操作, 接收端发现连接已经不在了, 就会进行 reset. 即使没有写入操作, TCP 自己也内置了一个保活定时器(一般不用或用的很少), 会定期询问对方是否还在. 如果对方不在, 也会把连接释放.

另外, 应用层的某些协议, 也有一些这样的检测机制. 例如 HTTP 长连接中, 也会定期检测对方的状态. 例如 QQ, 在 QQ 断线之后, 也会定期尝试重新连接.

TCP 小结

可靠性与提高性能

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

可靠性:

• 校验和

• 序列号(按序到达)

• 确认应答

• 超时重发

• 连接管理

• 流量控制

• 拥塞控制

提高性能:

• 滑动窗口

• 快速重传

• 延迟应答

• 捎带应答

其他:

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

基于 TCP 应用层协议

• HTTP

• HTTPS

• SSH

• Telnet

• FTP

• SMTP

当然, 也包括你自己写 TCP 程序时自定义的应用层协议;

TCP/UDP 对比

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

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

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

归根结底, TCP 和 UDP 都是程序员的工具, 什么时机用, 具体怎么用, 还是要根据具体的需求场景去判定.

 结语:

       随着这篇博客接近尾声,我衷心希望我所分享的内容能为你带来一些启发和帮助。学习和理解的过程往往充满挑战,但正是这些挑战让我们不断成长和进步。我在准备这篇文章时,也深刻体会到了学习与分享的乐趣。    

         在此,我要特别感谢每一位阅读到这里的你。是你的关注和支持,给予了我持续写作和分享的动力。我深知,无论我在某个领域有多少见解,都离不开大家的鼓励与指正。因此,如果你在阅读过程中有任何疑问、建议或是发现了文章中的不足之处,都欢迎你慷慨赐教。

        你的每一条反馈都是我前进路上的宝贵财富。同时,我也非常期待能够得到你的点赞、收藏,关注这将是对我莫大的支持和鼓励。当然,我更期待的是能够持续为你带来有价值的内容

相关文章:

  • ai break down 带有#和t=的路由
  • 《探索React Native社交应用中WebRTC实现低延迟音视频通话的奥秘》
  • 从 Qwen-3 发布看 AI 服务器选型新方向:硬件配置与成本优化策略
  • 大数据狙击金融欺诈——技术如何守护交易安全?
  • 成龙电影中的三菱汽车
  • VUE2课程计划表练习
  • LeetCode 3342.到达最后一个房间的最少时间 II:dijkstra算法(和I一样)
  • Linux 系统无法启动的排查与修复方案
  • C#黑魔法:鸭子类型(Duck Typing)
  • 实现strStr
  • python中,什么是协程?
  • 分享一款开源的图片去重软件 ImageContrastTools,基于Electron和hash算法
  • 蓝桥杯青少 图形化编程(Scratch)编程题每日一练——小猫的城堡
  • 机器学习-数据集划分和特征工程
  • Git clone时出现SSL certificate problem unable to get local issuer certificate
  • 2025-05-10-FFmepg库裁切有水印的视频
  • 系统思考:短期困境与长期收益
  • 嵌入式开发学习日志Day17
  • 设计模式-策略模式(Strategy Pattern)
  • VBA -- 学习Day4
  • 中国象棋协会坚决支持司法机关依法打击涉象棋行业的违法行为
  • 一企业采购国产化肥冒充“挪威化肥”:7人被抓获
  • 国家主席习近平同普京总统共见记者
  • 汪明荃,今生不负你
  • 习近平《在庆祝中华全国总工会成立100周年暨全国劳动模范和先进工作者表彰大会上的讲话》单行本出版
  • 驱逐行动再加码?特朗普或向利比亚和卢旺达遣送非法移民