深入理解TCP
1.TCP基础概念了解
1.1简介
TCP(Transmission Control Protocol)是一种计算机网络协议,用于在网络上可靠地传输数据。它确保数据的完整性、顺序性和可靠性,通过建立连接、数据分段、错误检测和恢复机制,TCP使数据能够在不同计算机之间可靠地传输,被广泛用于互联网和局域网通信。数据的"可靠性"是非常重要的,尤其在网络的这种长距离数据运输中,数据难免会发生各种问题,我们的tcp就是为了解决这些问题而孕育而生的。
1.2udp与tcp的比较
TCP提供可靠的数据传输,适用于需要确保数据完整性和顺序性的应用,而UDP提供快速的数据传输,适用于对实时性要求较高的应用。(比如说直播)
1.3TCP的协议格式
TCP报头当中各个字段的含义如下:
-
源/目的端口号:表示数据是从哪个进程来 /发送到对端主机上的哪个进程
tips:内核中用哈希的方式维护了端口号与进程ID之间的映射关系, 因此传输层可以通过端口号快速找到其对应的进程ID 进而找到对应的应用层进程。
-
32位序号/32位确认序号:分别代表TCP报文当中每个字节数据的编号以及对对方的确认 (是TCP保证可靠性的重要字段)
-
4位TCP报头长度:表示该TCP报头的长度
-
6位保留字段:TCP报头中暂时未使用的6个比特位
-
16位窗口大小:保证TCP可靠性机制和效率提升机制的重要字段
-
16位检验和:由发送端填充, 采用CRC校验 ,接收端校验不通过, 则认为接收到的数据有问题 (检验包含TCP首部+TCP数据部分)
-
16位紧急指针:标识紧急数据在报文中的偏移量 ,需要配合标志字段当中的URG字段统一使用
-
选项字段:TCP报头当中允许携带额外的选项字段 ,最多40字节
TCP报头当中的6位标志位:
- URG:紧急指针是否有效
- ACK:确认序号是否有效
- PSH:提示接收端应用程序立刻将TCP接收缓冲区当中的数据读走
- RST:表示要求对方重新建立连接 我们把携带RST标识的报文称为复位报文段
- SYN:表示请求与对方建立连接 我们把携带SYN标识的报文称为同步报文段
- FIN:通知对方 本端要关闭了 我们把携带FIN标识的报文称为结束报文段
1.4报头与有效载荷的分离方法
TCP从底层获取到一个报文后,虽然TCP不知道报头的具体长度 ,但报文的前20个字节是TCP的基本报头。
其中的4位首部长度记录了tcp的包头的总长度。
下面是整个流程:
- 当TCP获取到一个报文后 首先读取报文的前20个字节 并从中提取出4位的首部长度 此时便获得了TCP报头的大小size
- 如果size的字节大于20字节 则再次从报文中读取size - 20的数据 这些数据是选项大小
- 读取完报头和选项之后剩下的就是有效载荷了
如果TCP报头当中不携带选项字段, 那么TCP报头的长度就是20字节 ,此时报头当中的4位首部长度的初始化值实际就为 5 (实际上size长度的换算是拿5乘以4这个固定值来换算的)(由于它占4位,所以它的取值范围是0到15)
1.5确认应答机制(ACK)
我们之前介绍过tcp的格式中有两个序号:序号与确认序号
其实这两序号解决的核心问题是:确认通讯双方彼此收到了对方的信息
可以想象这么一个情景:
当一台主机给另一台主机发送数据时,怎么确定对面受到该数据呢?当然是另一台主机回复"我已经受到了"这样的信号,可这另一台主机怎么确定"我已经受到了"这个数据对面收到了呢?当然是第一次发送数据的一方再给另一台主机发送类似"我收到了这条信息"类似的信号,那么这将会导致一个无穷无尽的循坏。
当然我们只要保证双方通信时发送的每一个核心数据都有对应的响应就可以了, 而对于一些无关紧要的数据(比如响应数据), 我们没有必要保证它的可靠性。
这种策略在我们的TCP中叫做确认应答机制。
1.6 32位序号和32位确认序号
上面我们只讲解了与这两序号相关的机制,却没有讲解其具体相关内容,这里我们进行补充。
实际上为了保证我们的通讯效率,一方向另一方连续发送多个报文数据,并且需要保证每次发送都有对应的响应信息。不过往往由于在发送报文的时候路径选择的不同,所以报文到达的时间不一定相同, 也就是说先发的报文不一定会先到,因此便会诞生一个问题:如何确保报文的先后到达顺序?(换句话说如和让对面看到你想让他先看到的信息?)
所以TCP将发送出去的每个字节数据都进行了编号, 这个编号叫做序列号。
-
比如现在发送端要发送3000字节的数据 如果发送端每次发送1000字节 那么就需要用三个TCP报文来发送这3000字节的数据
-
此时这三个TCP报文当中的32位序号填的就是发送数据中首个字节的序列号 因此分别填的是1、1001和2001
接收端在收到了这三个TCP报文后 ,就可以根据TCP报头当中的32位序列号对这三个报文进行顺序重排(该动作在传输层进行)。 重排后将其放到TCP的接收缓冲区当中 ,此时接收端这里报文的顺序就和发送端发送报文的顺序是一样的了。
TCP中的三十二位确认序号是告诉对方, 我当前已经收到了哪些数据 ,你的数据下一次应该从哪里开始发。
- 告诉对方你前面1~1000的数据我已经全部收到了
- 下次发送数据从1001开始发
报文丢失的特殊情况 |
---|
如上图的这种情况:
主机A发送了三个数据给主机B ,发送的三个序号分别是1 1001 2001。
如果这三个报文在传输的过程中序号1001~2001的数据发生了丢包,在主机B将这些报文进行排序的时候会发现中间缺失了一段, 此时主机B会向主机A发送1001的确认序号,此时主机A就会明白自己的数据可能是发生丢包了 ,之后会重新发送从1001序号开始的数据。
1.7窗口大小
当然在了解这一机制的前,我们先来介绍一下tcp的缓冲区。
tcp拥有接收缓冲区和发送缓冲区:
TCP的传输是面向字节流的,我们可以将TCP的发送缓冲区和接收缓冲区都想象成一个数组。
当我们将数据从应用层拷贝到TCP的发送缓冲区当中时,这些按字节为单位的数据就具有了一个天然的序号但是和真正数组不同的是这个下标是从1开始的。
其中报头的序号其实就是发送的若干字节的数据的首个字节数据对应的下标, 而接收端发送的确认序号其实就是该段若干数据的下一个字节对应的下标。
当然了这里得提一嘴,我们之前在写socket的编程代码时候用的类似read和write函数,在应用层调用write函数的时候, 实际上就是向TCP的发送缓冲区中写入数据, TCP协议会等待合适的时机发送。与此同时当TCP接收从网络中发送过来数据的时候并不是直接发送到应用层 ,而是拷贝(注意别和c++的拷贝构造混淆了,我们这边说的拷贝单纯就是复制粘贴的意思)到接收缓冲区中, 等待应用层使用read函数来读取。
这里我们不妨将tcp的这两个缓冲区和前文提到的udp的一个缓冲区做对比:
为什么两个协议都要有接收缓冲区呢?因为应用层梳处理数据的速度是有限的,为了保证没来得及处理的数据不被直接丢弃所以说我们要设立接收缓冲区。
那为什么tcp要创立发送缓冲区呢?那是因为方便保证我们数据发送的安全性,比如出现了丢包问题后,就可以重新把短暂保存在发送缓冲区的数据在发一遍。
好,在有了这些前置知识的基础上我们进入正题--------窗口大小的用处
我们的缓冲区是有大小的,如果对方发送缓冲区发送数据的速度大于我方接收缓冲区接收数据的速度那么我们的数据就会溢出就会造成丢包废弃数据的情况,窗口大小就是帮助我们解决这一问题的。
TCP使用这十六位的窗口大小来表示自身接受缓冲区的大小,此时我们的发送端就可以通过该十六位窗口大小来判断自己应该发送多少数据,控制自己发送数据的速率。
1.8六个标志位
tcp为了保证通信的可靠性,其报头目的多种多样,因此就造成了多种不同目的的体现(经过后文的讲解会一一体现出来),为了表示这样的多种目的,标志位产生了。
从最上面的图可以看出标志位总共占了6个字节,表示6在不同的目的,1就表示有这种目的,0就没有。
SYN |
---|
- 报文当中的SYN被设置为1 表明该报文是一个连接建立的请求报文
- 只有在连接建立阶段 SYN才被设置 正常通信时SYN不会被设置
ACK |
---|
- 报文当中的ACK被设置为1 表明该报文可以对收到的报文进行确认
- 一般来说 除了第一次发送请求连接的报文不需要设置ACK之外其余所有的报文都需要设置ACK 因为向对方发送的数据的时候也可以确认对方之前发送的一些数据
FIN |
---|
- 报文当中的FIN被设置为1 表明该报文是一个连接断开的请求报文
- 只有在断开连接阶段 FIN才被设置 正常通信时FIN不会被设置
URG |
---|
- 当URG标志位被设置为1时 需要通过TCP报头当中的16位紧急指针 来找到紧急数据 否则一般情况下不需要关注TCP报头当中的16位紧急指针
什么是十六位紧急指针呢?
它其实就是紧急数据 在报文中的偏移量,也就是说当紧急指针为0时紧急数据就在最前面, 紧急指针越大偏移量越大 ,紧急数据就越在后面。
紧急数据的大小有多少呢?
紧急数据的大小一般只有一个字节 ,至于为什么设置一个字节这里就不过多讨论了。
PSH |
---|
- 报文中的PSH被设置为1就是在告知对方该数据要尽快的交付给上层
RST |
---|
- 报文中的RST被设置为1 表示希望重新建立连接
- 在通信双方在连接未建立好的情况下 一方向另一方发数据 此时另一方发送的响应报文当中的RST标志位就会被置1 表示要求对方重新建立连接
- 如果连接的过程中出现了任何的异常也会要求重新建立连接
1.9超时重传机制
TCP(传输控制协议)使用超时重传机制来确保数据的可靠传输,这个机制是为了处理网络中可能出现的丢包或数据包延迟到达的情况。
在向对方主机发送出一个数据之后,TCP就会设置一个类似于"闹钟" 的机制,如果在闹钟响之前收到应答,这个闹钟就会关闭;如果在闹钟响了之后还没有收到应答,就会触发超时重传机制 ,重发数据。
- 发送方发送数据包到接收方。
- 接收方收到数据包后,发送确认(ACK)给发送方,表示数据包已经成功接收。
- 发送方启动一个定时器,用于跟踪数据包的传输时间。
- 如果在定时器超时之前,发送方收到确认,那么数据包被成功接收,发送方停止等待。
- 如果定时器超时后,发送方没有收到确认,它会认为数据包丢失,然后会重传该数据包。
超时重传的等待时间 |
---|
我们超时重传的时间不能设置的太长或者太短:
- 如果我们超时重传的时间设置的过长会导致数据丢失之后对面长时间得不到对应的数据,进而影响整体重传的效率。
- 如果我们超时重传的时间设置的过短会导致数据没有丢失还是会触发超时重传机制,此时对面就会收到大量的重复报文,甚至太短的话整个网络都可能会崩溃。
2.连接管理机制
之前我们在写socket套接字编程时就感受到这种一一连接机制了,实际上设立这些复杂的机制也是为了用于确保可靠的连接的建立、维护和关闭。
连接是TCP协议的基础,有了连接才能保证可靠性。但是一台机器上可能会有大量的连接 ,所以说操作系统必须要对这些连接进行管理。
那么应该如何进行管理呢?
根据系统管理的第一法则 :先描述 ,再组织。所以说在Linux中一定会有一个这样子描述连接的结构体,该结构体中有需要管理连接用的各项属性,每次创建一个连接在系统看来就是定义了一个结构体。
描述完毕之后就是组织了,系统中创建了结构体之后会将它们用双链表的形式连接起来以方便管理 。此时操作系统对于连接的管理在实际上就变为了对于双链表的增删改查。
2.1连接管理机制里的重点------三次握手
TCP协议在通信之前要建立连接,我们将这个过程称为三次握手
-
第一步(客户端到服务器):客户端向服务器发送一个SYN(同步)标志的数据包,表明客户端想要建立连接。
-
第二步(服务器到客户端):服务器收到客户端的SYN后,回复一个带有SYN和ACK(确认)标志的数据包。这表示服务器同意建立连接,并确认客户端的SYN。
-
第三步(客户端到服务器):客户端收到服务器的响应后,发送一个带有ACK标志的数据包,表示客户端也确认连接建立。
当然这里有个面试经常考的问题,那就是为什么只建立三次握手?
实际上我们建立连接的初衷就是保证客户端和服务器之间的连接可以安全可靠的建立起来。
假设只有一次握手,如果客户端发一个SYN,服务器就创建结构体将这个连接管理起来,那么钥匙客户端不断发送SYN岂不是乱套了?我们将这种攻击称为SYN洪水攻击。
假设只有两次握手,客户端发送SYN到服务器,但由于某些原因,这个SYN在网络中滞留了很长时间。然后客户端认为连接已建立,开始发送数据。最后,这个延迟的SYN到达服务器,但服务器已经关闭了连接。这时,服务器会发送一个RST(复位)标志给客户端,以拒绝连接请求。但客户端此时可能会认为连接已建立,导致数据的混乱。
当然参考我们的超市重传机制,我们无法保证第三次客户端发送给服务端的ack,服务端有没有收到,
那么当最后一次握手失败的时候会发生什么情况呢?
由于客户端已经发出了一个报文,并且这个报文在被服务端收到并应答之后服务端再次发送一个报文给客户端 ,客户端收到了这个报文 。那么此时站在客户端的视角它就会认为: 网络是畅通的 ,对方能与我正常通信 。于是客户端就会维护起一个连接;但是此时服务器缺没有收到客户端的回复, 于是服务器就认为自己不能和对方正常通信 ,服务器就不会维护起连接。
而对于服务器来说它维护的连接肯定是要比客户端多得多的 ,所以说客户端即使多维护一个无用连接其实也还能接受。
2.2连接管理机制里的重点------四次挥手
TCP协议在通信之后要断开连接 我们很形象的将断开连接的过程称为四次挥手
-
首先客户端向服务器发送的报文中FIN位被设置为1 表示请求和服务器断开连接
-
服务器收到客户端断开连接的请求之后响应
-
服务器向客户端发送的报文中FIN位被设置为1 表示请求和客户端断开连接
-
客户端收到服务器断开连接的请求之后响应
经过这四次挥手过程,双方完成了连接的关闭,确保没有未处理的数据在网络中滞留,并且双方都知道连接已经终止。四次挥手过程是TCP连接的正常关闭方式,它使连接的关闭变得可靠和有序,这其实也支持了我们的全双工通信的实现。(我们平时讲的全双工和半双工是一种通信特性,我们的udp和tcp是一种协议,两者并没有直接联系)
tips:实际我们这个4次挥手过程就是我们套接字编程中双方调用close函数发生的。
2.3几个握手,挥手时重要状态的理解
2.31CLOSE_WAIT状态
如下是进行四次挥手时客户端与服务端在不同时间节点所处不同状态的图片:
- 首先在第一次挥手发出前双方都处于ESTABLISHED状态
- 客户端想要和服务器断开连接主动向服务器发送断开连接请求 发送FIN位被设置的报文 此时状态变化为FIN_WAIT1
- 服务器在收到客户端断开连接的请求之后对其进行响应 此时服务器的状态就会变为ClOSE_WAIT
- 当服务器发送完所有的数据也想要断开连接的时候会发送FIN报文 此时服务器的状态会变为LASE_ACK
- 客户端收到服务器发来的第三次挥手后 会向服务器发送最后一个响应报文 此时客户端进入TIME_WAIT状态
- 当服务器收到客户端的一个响应报文的时候 服务器就会彻底的关闭连接变为CLOSED状态
- 客户端在等待2MSL(Maximum Segment Lifetime 报文最大生存时间)的时间之后也会变为CLOSED状态
自此之后双方才断开连接。
当然这里我们重点要谈的还是我们的CLOSE_WAIT状态。
那么为什么要有这种状态呢?
在TCP连接中,客户端和服务器可能在不同的时间点准备关闭连接。CLOSE_WAIT状态允许服务器告知客户端它已经完成了数据传输,但客户端可能仍有数据需要传输或其他操作需要完成。如果服务器突然关闭连接,而客户端仍有待发送的数据,这些数据可能会丢失,CLOSE_WAIT状态允许服务器等待客户端确认数据的接收,从而避免数据的丢失。
2.32TIME_WAIT状态
为什么在进行完这个四次挥手之后客户端会先进入TIME_WAIT状态而不是直接关闭连接呢?
实际上,在此状态下,该端口会保留一段时间是为了确保对方接收到连接终止的通知,同时防止旧的重复数据包干扰新的连接。
解决TIME_WAIT状态引起的bind失败的方法 |
---|
四次挥手的时候主动发起挥手一方在四次挥手结束的时候会进入TIME_WAIT状态一段时间。
如果服务器在有客户端的情况下主动退出了,就相当于是服务器先进行了四次挥手 ,那么服务器就将要进入TIME_WAIT一段时间。
如果我们此时想要重新启动服务器绑定原本的端口号,我们会发现我们绑定失败了。
这是因为在TIME_WAIT期间这个连接并没有被完全释放 ,这也就意味着我们的端口是被占用着的 。
此时服务器想要继续绑定该端口号就只能等待TIME_WAIT时间结束。
我们服务器崩溃时最重要的就是让服务器重新启动并且是绑定原来的端口号 。(因为一般一些服务的端口号都是固定的 如果你修改了端口号的话在网络中别人可能就不知道你了)如果想要让服务器崩溃后在TIME_WAIT期间也能立马重新启动, 需要让服务器在调用socket函数创建套接字后 ,继续调用setsockopt函数设置端口复用 。这也是编写服务器代码时的推荐做法。
c
int setsockopt(int sockfd, int level, int option_name, const void *option_value, socklen_t option_len);
参数说明:
sockfd:指定要设置选项的套接字的文件描述符。
level:指定选项的协议层次。通常,这是一个整数,表示选项所属的协议族,例如SOL_SOCKET表示套接字选项,IPPROTO_TCP表示TCP选项,等等。
option_name:指定要设置的选项名称,这通常是一个常量,用于标识选项的具体含义。例如,SO_REUSEADDR表示允许地址重用。
option_value:一个指向包含选项值的缓冲区的指针。选项值的具体数据结构和内容取决于选项名称。
option_len:指定option_value缓冲区的长度。
返回值:
如果函数成功设置选项,返回0。
如果出现错误,返回-1,并设置errno以指示错误的类型。
我们这里要设置的就是监听套接字 ,将监听套接字在套接字层设置端口复用选项SO_REUSEADDR ,该选项设置为非零值表示开启端口复用。
c
int opt = 1;
setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
2.33理解listen的第二个参数
当然在对这块内容讲解之前我们先来讲解一下三次挥手时候的状态变化。
最开始的时候客户端和服务器都处于CLOSED的状态
之后服务器为了能接收到客户端发送连接申请,让自己处于LISTEN状态(其实就是我们以前讲的调用listen函数)
( 当connect返回的时候要么是三次握手成功了,要么是三次握手失败了)
客户端开始发送SYN连接申请 自己的状态变为SYN_SENT状态
服务器收到客户端发送的SYN连接申请之后会给客户端响应一个SYN+ACK的响应报文,并且状态变为SYN_RCVD
客户端收到服务器的回复之后便会认为网络畅通 ,自己能和客户端正常通信, 于是状态变为ESTABLISHED并且像服务器发送响应报文。
服务器收到响应报文之后便认为三次握手完成 ,连接建立成功于是状态也会变为ESTABLISHED。
在SYN_RCVD
状态,服务器可以检查客户端发来的连接请求中包含的参数,如最大窗口大小、最大段大小等。服务器可以根据这些参数来协商连接的参数,以便双方在建立连接后能够以最有效的方式进行数据传输。
SYN_RCVD
状态也可以用于处理错误和异常情况,如超时、连接拒绝等。在这种情况下,服务器可以向客户端发送适当的响应,以便客户端能够了解连接的状态和原因。
好那么对此有个基础了解后我们进入正文:
我们在调用listen函数的时候需要输入两个参数 :第一个参数是我们要设置为监听状态的套接字 ,第二个参数是一个数字, 它表示服务器可以建立的最大连接数。
我们在进行TCP连接管理的时候会用到两个连接队列:
-
全连接队列(accept队列): 全连接队列用于保存处于ESTABLISHED状态 ,但没有被上层调用accept取走的连接。(被accept之后全连接队列会减少)
-
半连接队列 :半连接队列用于保存处于SYN_SENT和SYN_RCVD状态的连接, 也就是还未完成三次握手的连接。
我们在这里谈及Listen第二个参数的原因是 :在linux操作系统中 ,全连接队列的长度就等于Listen的第二个参数+1。 所以说我们服务器的Listen参数设置为2 ,此时服务器的全连接队列长度就为3 ,也就是说此时能够建立ESTABLISHED状态的连接就为3个 。(意思是本来就已经有了一个连接队列,你现在listen设置成2也就是1+2)
而当全连接队列满了之后服务器此时就不能够建立ESTABLISH连接了, 此时如果客户端请求建立连接那么服务器不会进行SYN+ACK响应 ,而是会将该连接放在半连接队列中。 状态变为SYN_RECV 在一段事件后如果三次握手还没有成功则会自动释放连接。
此外如果半连接队列也满了 ,服务器会自动拒绝所有连接请求。
当然连接队列不宜设计过长。
全连接队列的长度由两个值决定:
- 用户层调用listen时传入的第二个参数backlog
- 系统变量net.core.somaxconn 默认值为128
事实上我们全连接队列的长度就等于这两个队列长度的较小值+1
我们一般将全连接队列设置成5。
这就好比海底捞在外面放排队的椅子机制一样,椅子多了队伍长了反正估计也吃不到了,为什么还要排队?
3.流量控制
TCP通过接收端的接收数据能力来决定发送端的发送速度 这种机制叫做流量控制
3.1基本概念的补充
我们上文在讲16位窗口大小是只是浅浅的谈了下其作用,并没有细究其工作原理,那么这边我们来系统的给大家总结一下。
( 实际窗口大小的数值计算方法:在TCP报头的选项中有一个窗口扩大因子M ,实际窗口大小是窗口字段的值左移M位得到的。)
窗口大小字段越大, 说明网络的吞吐量越高, 说明发送方可以发送数据的速度越快。
如果说接收端发现自己的接收缓冲区快满了 ,那么在发送下次报文的时候就会减小滑动窗口的大小 ,此时发送端的发送速度就会变慢。
实际上,在双方三次握手的阶段其实就通过报文交换了各自的一些数据, 其中就包括了告知对方自己的接收能力。
所以说在双方正式开始通信的时候就知道了双方的数据接收能力 ,在第一次发送数据的时候不会出现数据溢出问题。
3.2滑动窗口
为了彻底弄懂窗口机制,我们不得不谈的一个话题就是滑动窗口。
我们之前也提到了,为了保证信息传递的效率,我们可以一次性的多向对端发送数据,但是我们需要考虑对端的接收能力,所以为了能很好的处理这里的关系,我们引进了滑动窗口这一机制。
我们将发送缓冲区的数据分为三部分:
我们一般把中间蓝色的那一段叫做滑动窗口 ,当然也有人将这三部分整体称为滑动窗口 ,其中蓝色的部分叫做滑动窗口大小。
滑动窗口的描述是:发送方不用等待ACK所能一次性发送数据的最大量
滑动窗口的大小等于对方窗口大小和自身拥塞窗口的较小值,自身拥塞窗口是跟网络有关的,因为我们发送数据不光要考虑对方的接收状况还要考虑网络状况。
我们在前面讲过TCP发送的数据如果超过一段时间没有被应答就会触发超时重传机制, 也就是说我们TCP必须要保存我们缓冲区中的数据一段时间,事实上我们要在发送缓冲区中保存的代码就是滑动窗口里面的数据。
根据我们的ack机制滑动窗口一定会右移吗?(拿上面那张图举例)
假设我们一开始的滑动窗口大小就是4000字节,对面接收了1001~2000的数据之后给我们ACK了一个2001, 但是此时接收端滑动窗口的大小变为3000了,那么对于我们发送端来说会将1001~2000的数据放到第一段, 此时滑动窗口不会进行右移了。
滑动窗口中的数据一定都会被对方收到吗?
不一定,如上这种情况就是我们在上文讲的特殊丢包问题。
3.3快重传 VS 超时重传
- 快重传光看名字也就知道了一定要比超时重传快 ,快重传是接收三次相同确认序号的ACK立马重传, 而超时重传则是设定计时器在计时器时间到之后才会进行重传。
- 虽然快重传机制能够快速判定数据包的丢失但是它并不能完全取代超时重传机制, 因为报文丢失后三次ACK可能因为网络原因没有发送到发送端主机上从而只能进行超时重传。
- 所以说快重传对于TCP来说是一种效率上的提升, 而超时重传是所有重传机制的保底策略。
4.拥塞控制
虽然丢包是不可避免的,但如果双方在进行通信时进行大量丢包,网络大面积崩溃,这是时候我们就认定这是网络问题,网络出现问题一定是网络中大部分主机共同作用的结果。
- 如果网络中的主机在同一时间发送给网络大量的数据 ,那么就有可能导致网络中的某些关键节点的路由器下就可能排了很长的报文 ,最终会导致报文无法在ddl之内抵达对面的主机。
当网络出现堵塞的时候双方主机虽然不能提出有效的解决方案但是它们能够做到不加重网络的负担,就比如每个人主机少发点数据。我们的拥塞控制就是处理这列问题的。
拥塞机制中的重要核心------慢启动机制 |
---|
-
初始拥塞窗口:在TCP连接建立时,发送方开始具有一个称为"初始拥塞窗口"的拥塞窗口大小。这个窗口通常较小,以防止在连接建立初期发送过多的数据包。
-
指数增长:在慢启动阶段(网络奔溃之后),发送方会以指数方式增加其拥塞窗口的大小。每当发送方成功接收到一个确认(ACK)时,它将拥塞窗口大小翻倍。这意味着每一个往返时间(Round-Trip Time,RTT),拥塞窗口大小都会翻倍。这种增长速度是指数级的,因此称为慢启动。
-
网络容量探测:慢启动的目标是尽量接近网络的容量,同时不引发拥塞。通过以指数方式增加拥塞窗口大小,发送方可以快速确定网络的容量。如果网络具有较大的可用带宽,拥塞窗口将迅速增加,从而充分利用网络资源。
-
拥塞检测:慢启动过程中,发送方会继续发送数据,直到检测到网络容量不足或出现数据包丢失。一旦发生拥塞信号,发送方会停止慢启动,转入拥塞避免阶段。拥塞信号可以是丢包、延迟增加或其他拥塞指示。
-
防止过度拥塞:慢启动的指数增长机制是为了避免过度拥塞。如果在慢启动过程中出现拥塞信号,拥塞窗口将重新设置为较小的初始值,然后发送方将进入拥塞避免阶段。这有助于确保发送方不会在拥塞情况下发送太多数据包,从而导致网络性能下降。
5.延时应答
延时应答机制(Delayed Acknowledgment)是一种TCP通信中的机制,用于减少不必要的确认报文的发送,以提高网络效率。TCP协议通常使用累积确认,即一次确认多个连续的数据包,而延时应答机制是一种优化策略,使接收方在特定条件下延迟发送确认。
- 等待一段时间:接收方在接收到数据包后不立即发送确认,而是等待一小段时间(通常是200毫秒左右)。这个等待时间被称为"延时确认定时器"。
- 处理累积确认:接收方会检查接收到的数据包序列,如果它收到的数据包是按顺序到达的,它将等待一段时间,看是否有更多的数据包到达,以便一并确认。只有在延时确认定时器到期或者一段时间内没有更多数据包到达时,接收方才会发送确认。
- 减少确认报文的数量:通过延时应答机制,接收方可以减少发送确认报文的数量。这对于网络效率和带宽利用很有益处,因为减少了确认报文的数量,从而减少了网络上的流量。
6.捎带应答
捎带应答利用了确认报文的机制,以允许接收方在发送确认报文时附带一些额外的数据。这些额外的数据可以是之前未确认的数据,因此,它们"捎带"在确认报文中一起发送。这意味着接收方可以在确认数据的同时,有效地发送一些未确认的数据,从而充分利用网络带宽,降低通信时延。
比如说主机A此时给主机B发送了一条消息 ,此时主机B收到之后刚好也要向主机A发送一条消息 ,那么此时主机B的ACK就不必使用单独的报文发送而是可以搭上主机B要发送消息的这个报文的顺风车。那么此时主机B既发送了消息又应答了主机A的消息,这就叫做捎带应答。
7.粘包问题
我们首先要明确 粘包的包是什么----- 它指的是应用层的数据包。
在TCP协议的报头中是没有和UDP协议报头中报文长度这一字段的
站在传输层的视角TCP报文是一个个的排好序放在接收缓冲区中的
站在应用层的视角缓冲区就就是由多个字节组成的字节流
那么应用层看到了这一串字节流之后就会认为这是一个完整的数据包,它不知道应该把这个包从哪里到哪里分开 这就是粘包问题。
如何解决粘包问题 |
---|
- 对于定长的包来说 我们每次读取固定大小长度的数据就可以了
- 对于非定长的包来说 我们可以在应用层的报头上增加一个长度字段 通过这个字段我们就能知道该数据包的结束位置从而防止粘包问题了 比如HTTP报头当中就包含Content-Length属性 表示正文的长度
- 对于非定长的包来说 我们还可以在每个包的结尾设置一个明确的分隔符 这个工作也是由应用层解决的
8.一些异常的情况
进程终止
当我们在正常通信的时候 如果进程崩溃了 那么建立好的连接会怎么办呢?
我们在信号这一章节讲过 进程崩溃的本质实际上就是操作系统给进行发送了信号从而杀死了进程
进程被杀死的时候它会关闭底层所有的文件描述符和释放资源 也就是说底层它仍然会调用close函数进行四次挥手和正常关闭进行没有什么两样
关机
如果我们在正常通信的时候关机了 那么建立好的连接会怎么办呢?
其实这种情况和第一种情况类似 因为如果我们关机了 操作系统也会杀死所有的进程释放所有的进程资源包括文件描述符 所以说在我们关机的情况下也会正常的进行四次挥手
突然断电
如果我们在正常通信的情况下突然断电了 那么建立好的连接会怎么办呢?
当我们的客户端突然掉线之后实际上我们的服务器是无法得知客户端掉线了的 所以说在短期内服务器仍然会维护这个连接 但是时间一长 服务器发现自己发送的数据对方都没有应答之后就会强制关闭连接 这和TCP的保活策略有关
- 服务器会定期询问客户端的情况 如果服务器的多次询问都没有收到ACK那么此时服务器会强制关闭连接
- 客户端也会定期向服务器报平安 如果服务器长期没有收到客户端的消息 服务器也会强制关闭连接
其中服务器定期询问客户端的存在状态的做法 叫做基于保活定时器的一种心跳机制 是由TCP实现的
此外 应用层的某些协议 也有一些类似的检测机制 例如基于长连接的HTTP 也会定期检测对方的存在状态
9.一些小总结
其实在整个TCP协议中,都没有涉及到数据的具体发送 ,而是提供了一套的理论支持, 因为传输层的作用就是保证数据传输的可靠性。
在整个网络传输的过程中 ,其实由TCP做决策 IP+MAC做执行, 它们的最终目的就是将数据可靠高效的传输到对端 ,至于到达对端之后对方要怎么使用那是应用层的事情了。
所以说我们应用层决定通信的意义 ,传输层即以下决定通信的方式。
补充内容:常见的基于TCP的应用层协议如下:
-
HTTP协议(超文本传输协议)
-
SSH协议(安全外壳协议)
-
FTP协议 (文件传输协议)
-
SMTP协议 (电子邮件传输协议)
-
TELNET协议 (远程终端协议)
接 这和TCP的保活策略有关
-
服务器会定期询问客户端的情况 如果服务器的多次询问都没有收到ACK那么此时服务器会强制关闭连接
-
客户端也会定期向服务器报平安 如果服务器长期没有收到客户端的消息 服务器也会强制关闭连接
其中服务器定期询问客户端的存在状态的做法 叫做基于保活定时器的一种心跳机制 是由TCP实现的
此外 应用层的某些协议 也有一些类似的检测机制 例如基于长连接的HTTP 也会定期检测对方的存在状态
9.一些小总结
其实在整个TCP协议中,都没有涉及到数据的具体发送 ,而是提供了一套的理论支持, 因为传输层的作用就是保证数据传输的可靠性。
在整个网络传输的过程中 ,其实由TCP做决策 IP+MAC做执行, 它们的最终目的就是将数据可靠高效的传输到对端 ,至于到达对端之后对方要怎么使用那是应用层的事情了。
所以说我们应用层决定通信的意义 ,传输层即以下决定通信的方式。
补充内容:常见的基于TCP的应用层协议如下:
- HTTP协议(超文本传输协议)
- SSH协议(安全外壳协议)
- FTP协议 (文件传输协议)
- SMTP协议 (电子邮件传输协议)
- TELNET协议 (远程终端协议)