Linux网络编程:TCP协议

目录

一、什么是TCP协议

二、TCP协议段格式

4位首部长度

确认应答(ACK)机制

32位序号和32位确认序号

16位窗口大小

6位标志位

16位紧急指针

三、TCP三次握手和四次挥手

三次握手

为什么是三次握手?

TCP的四次挥手

为什么是四次挥手?

四、超时重传机制

五、滑动窗口

快重传

六、流量控制

七、拥塞控制

八、延迟应答

九、捎带应答

十、面向字节流

TCP小结:


一、什么是TCP协议

TCP协议是传输层中非常重要的一个协议,它的全称是传输控制协议(Transmission Control Protocol),它主要是用来解决在传输层通信的过程中可靠性的问题。

二、TCP协议段格式

TCP协议的前5行一共20个字节,是TCP的标准长度,TCP协议的报头大部分情况下是标准长度20字节的,但当TCP协议带上选项行之后,报头长度就超过20个字节了,选项是可以选择带或不带的。

源/目的端口号:表示数据是从哪个进程来,到哪个进程去。老朋友了,我们就不介绍了。

4位首部长度

4位首部长度,它是用来表示TCP协议报头长度的,它只有4位来表示,所以取值范围是:

0101(等于5*4=20字节)~1111 ,也就是最大值是15 ,但是它的单位是4个字节,所以最大值是60个字节,也就是说TCP协议报头长度的最大值是60个字节 。如果TCP协议不带选项,那么4位首部长度就应该是0101,如果TCP协议带选项,那么选项的长度就应该是4位首部长度的值减去标准长度的值。

确认应答(ACK)机制

**如何判断我们发出去的消息有没有被对方成功接收呢?**只要得到了应答,就意味着我们发出去的消息被对方成功接收了。这就好比两个人A和B在桥的两端,A朝着B喊了一句话,如果B应答了A,那就说明A喊过去的话被B听到了,否则说明没有被B听到。

但是这又会存在另一个问题,A朝着B喊了一句之后B应答了,说明A的话B接收到了,但是B应答回去的话又要怎么判断A是否接收到了呢?那就必须要看A是否应答了B的话 。以此类推,每一次传话都需要对方的应答来判断自己的传话是否被对方接收成功,在长距离交互的时候,永远会有一条最新的信息是没有应答的。但是,只要我们发送的信息收到了应答,就代表我们的信息被对方成功接收了,所以世界上没有完全可靠的传输,TCP协议之所以是可靠传输,它的核心思想就是只要我们的核心信息先发出去,一定能收到对方的应答,这就代表我们的信息传输成功了。

这也叫做确定应答机制。

三次握手和四次挥手以及下面的32位序号就是建立在确认应答机制的基础上。

32位序号和32位确认序号

TCP是面向字节流的协议 ,TCP协议将每个字节的数据都进行了编号,即序列号 。其实TCP协议的发送缓冲区就是一块内存空间,内存空间可以看作一块连续的存储空间,我们可以把它看成一块char类型的数组,当我们将数据拷贝到发送缓冲区的时候,每一个元素的大小就是一个字节,每一个字节的数据都有数组下标作为编号,这个编号就是序列号。如图

32位序号用来标定一个报文的序号,它就是上面介绍的TCP为每个字节的数据进行的编号。而32位确认序号用来标定该序号之前的所有报文都已经全部接收成功了。

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

举个例子,现在客户端向服务器发送了1-200字节的数据段,那么此时该段的 序列号 是 1。

那么确认序号就是201,表示200号前都已收到,往后就从201号开始发,这样我们就完成了一次确认应答!

那么这里又出现了一个问题,如果我中间出现丢包怎么办?

假设我们这里丢的是150字节处的数据。那么对于服务端来说,他只收到了1-149,而你的32位序号是1,那我就给你返回150,客户端一看应答,不是我们想要的预期的201,那他马上就明白了,发生了丢包,马上就会补发,但是补发只补发150-200的数据,而不是整个重发!

因为TCP 是 "字节流",只会重发丢失的部分,而非整个段。 补发后,服务端将两次数据合并成1-200,再继续把确认序号改成201,至此,就保证了数据的可靠性!

为什么TCP协议要有序号和确认序号两组序号呢?一组序号不能解决应答问题吗?

因为TCP协议是全双工的,所谓的全双工指的就是一端在发消息的同时也可以接受消息。如果服务端接收到了客户端的消息,那它就要应答客户端,但是服务端同时又想给客户端发一条消息,想要应答就必须填充响应报文里的确认序号字段,想要发送消息就必须填充响应报文里的序号字段,TCP正是采用这样两组序号来保证两端通信时全双工的确认应答机制。

总结一下:TCP协议的可靠性是靠确认应答机制实现的,而TCP协议的确认应答机制是靠序号和确认序号来实现的,这一套机制保证了TCP传输的可靠性。

16位窗口大小

这张图我们在讲TCP套接字编程的时候就说过,TCP协议是有发送缓冲区和接收缓冲区的。我们在使用TCP套接字编程的时候,在用户层我们调用write函数接口时,并不是直接就将我们的数据通过这个接口写到对端主机上,而是经过了很多个缓冲区。write函数接口在用户层有它自己的缓冲区TCP协议也有自己的发送缓冲区 ,调用write函数接口将数据发送出去之后,数据先从write函数的缓冲区拷贝到TCP协议的发送缓冲区,在TCP协议的发送缓冲区中,数据什么时候发给对端,怎么发给对端,发送出错了怎么处理,要不要采用什么策略来提高发送效率,这些问题完全由操作系统内的TCP协议自己决定,我们用户层并不需要关心 ,这也是TCP叫做传输控制协议的原因。

但是如果发送端一次性发送非常多消息给接收端,接收端的接收缓冲区满了,后发送过来的数据无法被成功接收,只能被丢弃等待下一次重传,这样的话是不合理的,**所以在数据传输的时候,发送端必须知道接收端的接受能力,这里的接受能力就是用接收端的接收缓冲区剩余空间大小来衡量的。**发送端想要知道目前接收端的接收能力如何,就需要获取到接收端发送过来的应答,这个应答里包含了TCP响应报头,报头里有一个16位窗口大小字段,它保存的就是接收端的接收缓冲区当前剩余空间大小。

发送端每次在发送数据给接收端之前,都先看看目前接收端的窗口大小是多少,然后根据接收端的接收缓冲区剩余空间大小来发送对应大小的数据,这种控制发送数据大小的机制叫作流量控制机制。

6位标志位

TCP协议报文是有类别区分的,比如说我们是服务端,当我们接收到多个TCP报文的时候,不同的报文可能有不同的诉求,有可能是想跟我们建立连接的,有可能是想给我们发消息的,有可能是想跟我们断开连接的,所以我们必须要能够区分接收到的TCP报文属于什么类别。TCP协议中有6个标志位用来区分TCP报文的类别,它们分别是URG、ACK、PSH、RST、SYN、FIN

  1. SYN:用来标定该TCP报文是否是建立连接请求 ,接收端接收到TCP报文时,先会去看6个标记位,如果看到SYN标记位的值为1,则报文是建立连接请求的。也就是说,只要报文是建立连接的请求,SYN标记位需要被设置为1。
  2. FIN:如果该报文是一个断开连接的请求报文,FIN标记位需要被设置为1。
  3. ACK:这个叫做确认标记位,如果该标记位被设置为1,则代表该报文是对历史报文的确认。
  4. PSH:这个叫做数据推送标记位,用来提示接收端的应用程序立刻从TCP缓冲区中把数据读取出去。 举个例子,假设客户端发送数据给服务端,服务端的接收缓冲区中存放着的就是客户端发送过来的数据,我们可以调用read函数从接收缓冲区中将数据读取上来。当服务端的接收缓冲区没有数据时read函数底层会阻塞式地等待 ,直到接收缓冲区有数据了才会继续读取,这是因为read函数为我们提供了一个检测接收缓冲区是否有数据的功能。如果我们不使用read函数的这一功能,而是当接收缓冲区没有数据时或者数据还没到达我们规定的大小时,我们就不读取,一旦条件满足时,操作系统会提醒我们赶紧读取。这就需要使用到PSH标记位,当条件满足时,接收缓冲区会收到一条PSH标记位被设置为1的TCP报文,操作系统会通知上层应用赶紧读取缓冲区内的数据。
  5. PSH 就像快递的配送,你朋友给你寄了一个快递,但是他在快递单上勾选了「加急派送,立即通知收件人」。所以,并不需要快递员按部就班等待分拣一堆货物后再送去驿站等地方。当PSH=1的时候,快递员不会放到驿站,而是直接送到你家门口,按门铃让你立刻签收。
  6. URG:这个叫做紧急指针标记位 。报文在发送的时候虽然是按序发送出去的,但是接收端接收到报文却可能是乱序的,接收到的乱序报文是不可靠的,所以我们需要让我们接收到的报文与发送时的报文顺序一致,此时就需要在接收端先按照报文的32位序号字段进行排序,排成有序的之后再进行报文的解包分用等操作。这样可以保证接收到的报文是按顺序的,但是如果有些数据的优先级比较高,希望被优先处理,但它的顺序又比较靠后,就可以将URG标记位设置为1代表该报文内有数据需要被紧急处理。
  7. RST:这个叫做连接重置标记位,一旦TCP报文里的RST标记位被设置为1,就代表需要关闭TCP连接,重新建立TCP连接。 TCP是面向连接的,在TCP协议通信之前必须先经过三次握手来建立连接。三次握手成功之后,服务端就为客户端的连接维护数据结构,客户端也认为连接建立成功了。但是如果第三次握手,客户端的确认应答在通信过程中丢失了,服务端没有收到客户端的确认应答,所以服务端认为连接还没有建立成功,也就不会为该连接维护对应的数据结构,而客户端并不知道服务端有没有成功接收该应答,它以为服务端成功接收了,也就认为连接建立成功了,所以就开始向服务端发消息了。服务端一看这不对呀,我们两个连接还没建立好你就发消息过来了,所以服务端赶紧给客户端发了一个TCP报文,该报文中的RST标记位被设置为1,代表着让客户端重新与服务端建立TCP连接。但是,RST是强制重置连接的!类似于强制关机,是出现错误的时候用的,关闭时不需要走正常挥手的过程!

16位紧急指针

正如我们上面所说的URG标记位,而指针我们知道是指向地址,所以,16位紧急指针就是用来指向需要加急的数据的!

如果报文的URG标记位被设置为1,则代表该报文中有数据需要被紧急处理,那么报文中的16位紧急指针字段也会被填充,这里填充的是偏移量 ,拿着这个偏移量到数据字段中就能找到对应的那一个字节的数据,该数据就是需要被紧急处理的数据。需要注意的是:紧急指针只能指向紧急数据的末尾的下一个字节。

举个简单的例子:当前TCP报文序列号100、URG=1且紧急指针偏移量为10时,紧急数据是数据段开头的100~109字节,紧急指针就指向第110字节,

三、TCP三次握手和四次挥手

三次握手

  1. 第一次握手:首先是客户端给服务端发送一个TCP报文,该报文中的SYN标记位被设置为1,表示客户端向服务端发起建立连接的请求。
  2. 第二次握手:然后是服务端给客户端发送一个TCP报文,该报文中的SYN标记位和ACK标记位都被设置为1,表示服务端收到了客户端发来的建立连接请求,并且服务端同意与客户端建立连接。
  3. 第三次握手:最后客户端给服务端发送一个TCP报文,该报文中的ACK标记位被设置为1,表示客户端的确认应答,确认该连接建立完毕,自此三次握手完成。
  • 第一次握手时,只要客户端发送出去了带有SYN标记位的报文,客户端的状态就变成了SYN_SENT,叫同步发送状态。
  • 第二次握手时,只要服务端收到了客户端发送过来的报文并且给客户端发出去了带有SYN+ACK标记位的确认应答报文,服务端的状态就变成了SYN_RCVD,叫同步接收状态。
  • 第三次握手时,客户端只要接收到了服务端发送过来的带有SYN+ACK标记位的确认应答报文,并且客户端给服务端发出去了带有ACK标记位的确认应答报文,则客户端的状态就变成了ESTABLISHED,叫连接状态。
  • 最后,如果服务端成功接收到了客户端第三次握手时发送过来的带有ACK标记位的确认应答报文,那么服务端的状态也变成了ESTABLISHED连接状态。

注意:上图TCP三次握手中的箭头是斜着画的,不是横着画的,原因就是该图还包含了时间这一元素,也就是说三次握手每一步都是需要时间的。

作为一个TCP服务器,当它有大量的客户请求的时候,就会与这些客户建立大量的TCP连接 ,服务端的操作系统必须维护这些建立好的连接,维护的方式就是先描述再组织 。其实不只是服务端要维护好这些连接,客户端也需要。一旦连接建立成功,连接的双方必须要为维护这些连接而创建对应的数据结构。

而维护这些链接需要成本,所以,一些场景上,我们更喜欢使用UDP!

为什么是三次握手?

  • 一次握手的时候,客户端仅给服务端发送SYN报文就建立了链接,服务端一个SYN就会为其维护一个链接,如果遇到恶意客户端,那么服务端就容易收到SYN洪水攻击,白白损耗资源,占满空间导致服务器无法运行。
  • 二次握手也一样,但是二次握手是服务端发 SYN+ACK 就建连,如果是恶意客户端,也可以向一次握手一样,发送大量SYN,而对于服务端的ACK报文视而不见,但是服务端依然维持着大量链接,空间也很容易就被占满。

三次握手的好处 就在于,只有客户端完成最后一次确认,服务端才会正式为连接分配完整资源 ;如果是单主机发起 SYN 洪水攻击,它每发一个 SYN,都得对应完成第三次握手才能让服务端耗资源,相当于客户端自己也要跟着耗同等资源 ,单主机根本扛不住这种 "互相拉下水" 的消耗。

但这招只防得住单主机攻击,要是攻击者用多台主机同时发 SYN,每台主机只发不确认,服务端还是会被大量半连接占满资源 ------ 只不过三次握手已经把 SYN 洪水攻击的门槛大大提高,再也不是一台主机就能轻松搞垮服务器了。

还有另外一个角度的原因是,TCP是全双工的协议,两端既可以发送消息也可以接收消息,第一次握手和第二次握手实现的是客户端的发送和接收消息,第二次握手和第三次握手实现的是服务端发送和接收消息,三次握手以最小的成本验证了TCP的全双工。

TCP的四次挥手

在断开连接的时候,首先是先断开连接的一方发起断开连接请求,我们假设想要断开连接的是客户端,四次挥手过程如下:

  • 第一次挥手:客户端先给服务端发送一个TCP报文,该报文中的FIN标记位被设置为1,表示客户端向服务端发起断开连接的请求。
  • 第二次挥手:服务端收到客户端的断开连接请求后,给客户端发送回一个TCP报文,该报文中的ACK标记位被设置为1,表示服务端确认收到客户端的断开连接请求。
  • 第三次挥手:服务端给客户端发送一个TCP报文,该报文中的FIN标记位被设置为1,表示服务端向客户端发起断开连接的请求。
  • 第四次挥手:客户端收到服务端的断开连接请求后,给服务端发送回一个TCP报文,该报文中的ACK标记位被设置为1,表示客户端确认收到服务端的断开连接请求,自此,双方成功断开连接。

为什么是四次挥手?

  • 之所以需要四次挥手,是因为如果只有两次挥手,双方都只给对方发送包含FIN标记位的报文就可以断开连接了,但是这样不能保证双方都能成功收到断开连接请求的报文 ,所以四次挥手中还有确认应答的报文,是为了保证双方都能成功收到断开连接请求的报文。
  • 三次挥手也是不可以的,如果三次挥手,第一次挥手时客户端发送FIN报文给服务端,第二次挥手服务端发送断开连接请求的报文给客户端,将第二次挥手可以看做第一次挥手的应答,第三次挥手只需要客户端再向服务端发送应答即可。这样看似可以,但其实是不合理的,因为有可能服务端此时此刻并不想和客户端断开连接,客户端首先发起断开连接请求之后,客户端收到服务端的应答之后客户端就不能向服务端发送数据了,但是有可能此时服务端还想向客户端发送数据所以如果是三次挥手的话,必须要求两端同时断开连接,这是不合理的,违反全双工的设计

理想的断连应该是:一方先关自己的发送通道,等对方发完剩余数据,再关对方的发送通道。 总而言之,两次挥手是一定不可以的,三次挥手是可以的,但是仅限于特殊情况 ,该特殊情况是客户端和服务端同时都想断开连接,就可以在客户端先发送过来断开连接请求报文后,服务端再发送一条报文该报文中FIN标记位和ACK标记位同时设置为1(捎带应答机制), 表示服务端的应答且断开连接请求。四次挥手在任何情况下都是适用的。

当客户端先发起断开连接的请求时,服务端确认了以后,会进入CLOSE_WAIT状态 ,也叫作半关闭状态,该状态下服务端没有完全关闭,只是客户端单方面关闭了连接,服务端并没有发起断开连接的请求,有可能服务端仍然想给客户端发消息,所以在服务端半关闭状态这段时间内,服务端依旧可以给客户端发送消息。

当服务端也发起断开连接申请之后服务端状态由CLOSE_WAIT变成了LAST_ACK ,客户端在确认应答之后状态变成了TIME_WAIT 状态,也就是说,客户端在确认了服务端也要关闭连接之后,客户端并不会立马退出关闭,而是进入TIME_WAIT 状态,原因是客户端不确定第四次挥手的ACK确认应答服务端是否成功接收。如果第四次挥手的应答服务端没有收到,服务端会在特定的时间间隔进行超时重传,重新发送FIN请求,所以TIME_WAIT状态下的客户端会在特定时间段内等待,如果该时间段内服务端没有超时重传FIN请求,客户端就认为第四次挥手的ACK应答已经被服务端成功接受了,那么客户端就可以放心地退出关闭了。

总结来说,B->A, 发送fin报文, 说明B没有数据发送了,A->B 发送ACK,收到以后, B->A的通道关闭

四、超时重传机制

如果主机A向主机B发数据,但该数据在发送的途中丢失了,主机B并没有接收到该数据,也就没有给主机A发送回确认应答的报文,但主机A并不知道自己的报文有没有丢失,主机B甚至都不知道主机A给自己发了数据。所以主机A在数据发出之后的特定时间内,如果还没有收到主机B的确认应答,则认为该数据已经在途中丢包了,主机A就会重新发送该数据,这就叫做超时重传机制。

这里有一个需要注意的地方就是,主机A在发送出数据给主机B之前,必须要先将该数据在主机A本地保存起来,否则万一数据丢包了,主机A没有保存数据,就没办法重传了。

除此之外,还有另一种情况,主机A发给主机B的数据,主机B接收成功了,但是主机B给主机A发回的应答报文丢包了。但站在主机A的角度,这和主机A发送的数据丢包了是同一回事,因为主机A压根就不知道到底是数据丢包了还是应答报文丢包了。那么主机A同样会在特定的时间间隔后进行超时重传,将数据再发送给主机B。

但是这种情况下,如果主机A多次超时重传给主机B,主机B的应答报文都丢弃了,主机A继续重传,那么主机B就会有多分重复的数据,如果任由主机B将这些重复的数据递交给上一层,那么传输就不具有可靠性了。所以TCP还会对接收到的数据进行去重,是根据报文的序号字段来去重的,如果接收到的报文中出现了重复的序号,则后出现的报文会被直接丢弃。

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

我们知道网络的传输速度是动态变化的,比如同一个校园网内,当很多人在使用校园网上网时,网络的传输速度肯定变慢,当很少人使用校园网上网时,网络的传输速度就会变快。既然数据发送给对端的时间是动态变化的,那么超时重传的时间间隔也不能是恒定的。在Linux操作系统中(BSD、Unix和Windows也是如此),超时时间以500ms为一个单位进行控制,每次判断超时重传的时间都是500ms的整数倍。

**比如说,第一次超时重传的时间是500ms,如果第一次重传之后还没有应答,那么第二次超时重传的时间是2*500ms,如果仍得不到回答,下一次超时重传的时间就是4*500ms,以此类推,它是以指数形式增长的。**当累计到一定的重传次数之后,如果依旧没有应答,TCP会认为网络或者对端主机出现问题,从而强制关闭连接。

五、滑动窗口

我们之前讨论的确认应答机制,接收端对发送端每一个发送过来的报文,都要给一个ACK确认应答,发送端要等收到了ACK确认应答才继续发下一条报文,这种一发一收的方式有一个比较大的缺点,那就是效率特别低,尤其是在数据往返的时间较长的时候。(比如在国内给国外朋友发消息)

既然这种一发一收的方式效率很低,那么TCP采用的是一次性发送一批数据,接收端一次性接收到一批数据也响应回一批对应的确认应答,这样就能大大地提高效率(其实就是将多段的等待时间重叠在一起了)。

在发送端的发送缓冲区中,一共可以将缓冲区分成三部分:数据已经发送并且已经收到接收端确认应答的区域、数据已经发送但还没有收到确认应答的区域、数据还未发送的区域。

我们上面也提到了,TCP报文中有个16位窗口大小,指的就是无需等待确认应答而可以继续发送数据的最大值。比如窗口大小是4000个字节,那么我们就可以一次发送一批数据给接收端,这一批数据最大值就是4000个字节。滑动窗口指的就是发送缓冲区中可以在没有收到确认应答的情况下,直接发送的区域。

比如窗口大小是4000,那么滑动窗口的大小最大值就应该是4000,滑动窗口左边的区域属于数据已经发送并且收到确认应答的区域,滑动窗口右边属于数据还未发送的区域,而滑动窗口内的数据就是这一批中需要发送的数据,当这一批数据发送出去之后,滑动窗口的区域就属于数据已经发送但还没有收到确认应答的区域。

当数据已经发送但还没收到确认应答的区域内的数据被应答时,滑动窗口就会向右移动。举个例子,滑动窗口一次性发送4000个字节的数据给主机B,1001~2000段的数据收到了主机B的确认应答之后,滑动窗口向右移动一个单位。

其实发送缓冲区我们可以看作一个char类型的大数组,滑动窗口就是数组上的一块区域,维护滑动窗口只需要维护该区域的起始坐标和结束坐标即可,并且滑动窗口的坐标移动并不用担心越界问题,因为TCP的发送缓冲区是被设计成环状结构的。

快重传

那么,当滑动窗口发送的数据发生了丢包怎么办?

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

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

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

例如上图中的例子,当1001~2000这一段报文丢失的时候,主机B会重复发送1001的确认应答,意在告诉主机A说该段数据出现了丢包,赶快给我重传。即使主机A继续发送了很多其它段的数据给主机B,主机B响应回来的依旧是1001的应答。

当主机A连续三次收到了同样一个1001的确认应答之后,主机A会对1001~2000这段数据进行重新发送。

当主机B接收到主机A重新发送的1001~2000数据时,主机B就不再重发返回1001的确认应答了,而是返回7001的确认应答,因为主机A后续发送的报文主机B其实早就已经收到了,没有给出确认应答是因为1001的报文丢包了。这种机制就叫高速重发机制,也叫作快重传。

这样不需要等待超时就能触发重传,效率更高。

六、流量控制

接收端的处理数据速度是有限的,如果发送端的发送速度过快,就会导致接收端的接收缓冲区被占满,这个时候如果发送端继续发送,就会造成丢包,继而引起丢包重传等一系列的连锁反应。因此,TCP支持了根据接收端的处理能力,来决定发送端的发送速度,这种机制叫作流量控制(Flow Control)。

在发送端和接收端握手协商的阶段,接收端会将自己的接收缓冲区大小放入TCP首部的窗口大小字段中,通过ACK来通知发送端。窗口大小字段越大,说明网络的吞吐量越大。接收端一旦发现自己的接收缓冲区快满了,就会将窗口大小设置成一个更小的值通知发送端,发送端对应的也要减慢自己的发送速度。

如果接收端的接收缓冲区满了,接收端就会将窗口大小字段设置为0,这时发送端就不再向接收端发送数据了,但是发送端会定期发送一个窗口探测数据段, 让接收端把该时刻的窗口大小告诉发送端,当窗口大小不为0的时候,发送端又可以继续发送了。

七、拥塞控制

我们上面介绍的超时重传机制、流量控制等等,都是站在接收端的角度考虑问题的。如果我们发送一批数据,出现了少数的丢包情况,我们会认为这是发送端或者接收端的问题,发送端只需要在特定时间段后进行超时重传即可。

但是如果我们发送一批数据,发现有大量的数据出现了丢包情况,这时就不再是接收端或者发送端的问题了,而是网络的问题。有可能是该时间段网络拥挤,我们的数据根本就发送不出去。这个时候我们就不能进行超时重传了,原因是如果网络拥挤,一定不只是我们一台主机出现网络拥挤,一定是一群主机都出现了网络拥挤。如果此时这一群主机都进行超时重传,原本拥挤的网络就会突然又多了一批重传的数据,只会加重网络的压力。所以如果是网络出现了问题导致的大量丢包,发送端不能进行超时重传。

上述的网络拥塞问题本质是在网络中已经存在了很多待转发的数据,网络处理不过来了,所以会出现网络拥挤,新到的报文无法立即被转发,可能还在各个路由器上排着队,但发送端可能已经超时了就认为这些报文丢失了。所以要解决网络拥塞问题不是发送端重传数据,而是减少数据发送,让网络先处理待转发的数据,让网络缓一缓,当网络拥塞问题恢复了,发送端再进行数据的重新发送。

所以TCP有拥塞控制,当网络出现拥塞问题时,TCP引入慢启动机制,先发少量的数据去探探路,摸清当前网络的拥堵状态,再决定按照多大的速度传输数据。

这里需要引入拥塞窗口的概念,拥塞窗口没有体现在TCP报头当中,它相当于TCP连接当中的一个子属性字段 ,拥塞窗口是在慢启动时不断探测网络状态得出来的窗口大小,该窗口的含义是指在窗口发送数据量以内不会拥塞,超过了窗口发送数据量可能会导致拥塞。

当出现网络拥塞时,发送端开始慢慢发送数据去探测网络情况,第一次发送探测情况的时候,定义拥塞窗口的大小为1,当后续每次收到一个ACK应答的时候,拥塞窗口都加1。

所以TCP有了拥塞控制之后,再将前面的知识整合起来就是:

滑动窗口的大小=min(接收端的窗口大小,拥塞窗口大小)。

下图是TCP拥塞控制时拥塞窗口的变化情况,当网络进入拥塞状态,TCP开始慢启动时,前期是通过指数型增长来增大拥塞窗口的大小,不断探测网络状况。这里使用指数型增长是为了让前期慢慢增长,后期爆炸式增长。 但是TCP不能让拥塞窗口的大小爆炸式的增长,所以TCP会设置一个慢启动阈值,当拥塞窗口的大小小于这个阈值时按指数规律增长;当拥塞窗口的大小大于这个阈值时,按线性方式增长。

如图:当又一次到达网络拥塞状态时,TCP又必须重新开始慢启动发送数据了,并且这一次会重新计算阈值,此时的阈值为上一次拥塞窗口最大值的一半,比如上图中拥塞窗口到了24就进入了网络拥塞状态,所以新的阈值应该是12,同时拥塞窗口的值重新置为1,意味着TCP重新开始新一轮的慢启动,慢慢探测网络状态。我们把这一整套慢启动、调整阈值的算法叫做拥塞控制算法。

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

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

TCP拥塞控制这样的过程,就好像热恋的感觉:

TCP中:试探→加速→遇阻→冷静→再出发

热恋中 :小心翼翼靠近→热情升温→遇到矛盾→冷静调整→感情更稳

八、延迟应答

如果我们发送端想要尽可能多地发送数据,就需要发送端的滑动窗口大小尽可能地大,滑动窗口的大小是由接收端的窗口大小影响的,所以就需要接收端的接收缓冲区有尽可能多的剩余空间,这就需要接收端的上层尽可能快地从接收缓冲区中取走数据。

那么我们就会遇到这样的情况:

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

那么毫无疑问,500K和1M的发送策略是不同的,1M的窗口大小无疑是可以提高发送效率的。

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

延迟应答虽然可以提高发送效率,但是这个延迟的时间的设置也需要考虑。如果延迟时间设置得短了,达不到提高效率的目的,如果时间长了,有可能会让发送端以外数据丢包进而引发一连串的超时重传机制。所以TCP的延迟应答有两种策略:

  • 根据数量进行限制:每隔N个包就进行确认,这个N的值一般取2
  • 通过时间进行限制:超过最大延迟时间就应答一次,这个最大延迟时间与操作系统有关,但是这个最大延迟时间一定是小于超时重传的时间

九、捎带应答

发送端给接收端发送一条数据之后,接收端接收到了就会给发送端发送一条ACK确认应答,如果此时接收端也有消息想发给发送端,它就可以在ACK确认应答的报文上携带该数据,这样的策略就叫作捎带应答机制。其实接收端给发送端的确认应答报文本质上就是ACK标记位被设置为1并且填充好了确认序号的TCP报文,该报文是可以将数据段也填上的,再将对应的序号字段填上之后就可以发过去给发送端**,这本质就是一条发送数据的报文,只不过捎带了对发送端发过来的数据的确认应答而已。**

十、面向字节流

当我们创建一个TCP的套接字时,操作系统同时会为我们在内核中创建一个发送缓冲区和接收缓冲区。

对于发送端,我们在上层调用write函数接口通过TCP套接字发送数据时,实质并不是write函数接口将数据发送出去的,它只是帮我们将需要发送的数据拷贝到了TCP的发送缓冲区中。**如果我们发送的数据字节数太长,会被拆分成多个TCP的数据报再发出,这个拆分工作是TCP自己完成的,我们上层用户并不关心。**举个例子,比如上层用户调用write函数想要发送1M大小的数据,但是接收端的接收缓冲区目前只能接受500K大小的数据,所以TCP会将这1M的数据拆分开来,不会一次性发过去。如果发送的字节数太短,这些数据就会在缓冲区中先等待,等到缓冲区的长度差不多了,或者在其它合适的时机再发送出去。

对于接收端,数据也是从网卡驱动程序达到TCP的接收缓冲区。然后我们在上层调用read函数接口就可以将接收缓冲区中的数据拷贝到read函数的缓冲区中。

上述就是TCP缓冲区的发送流程,那么如何理解TCP的面向字节流呢?

在发送端写入的时候,上层调用write函数将数据拷贝到TCP的发送缓冲区中,假如要拷贝100个字节到发送缓冲区中,我们可以调用一次write函数拷贝100个字节到缓冲区中,也可以调用一百次write函数每次拷贝1个字节到缓冲区中。最终这100个字节在缓冲区中,如果字节数太大了,TCP会拆分成多个TCP的数据报再发出,如果字节数太小了,TCP会让这些数据在缓冲区等待一会再发送**,这些都是TCP层面关心的事情,上层用户并不需要关心** ,上层用户只需要调用完write函数将数据拷贝到缓冲区即可,这个就叫写入的时候与写入的格式毫不相关,写入时是面向字节流的。

在接收端读取的时候,TCP的接收缓冲区会收到若干个字节的数据,我们上层调用read函数从接收缓冲区中读取数据,可以一次读取一个字节,可以一次读取两个字节,可以一次读取十个字节或者一次一百个字节,读取的时候也是与格式毫不相关的,这就叫做读取时是面向字节流的。

在网络通信基于TCP协议通信时,上层我们调用write函数怎么写入,调用read函数怎么读取,二者都是毫不相关的,这就叫做面向字节流。总结一下,TCP的面向字节流,本质是为上层应用屏蔽了网络传输的细节------ 上层只需要关注 "要传什么数据",不用管 "数据怎么拆、怎么发、怎么拼"。

正是因为TCP打破了传统的报文格式限制,所以,在读取的时候会出现数据粘包的问题。

TCP是面向字节流的,在TCP看来所有数据都是以字节为单位的数据流,TCP不关心数据报文的边界,你上层怎么读取一次读一个字节还是一次读一百个字节,TCP不关心,但是应用层必须关心因为它要保证读取上来的报文是完整报文,不能多也不能少,只有这样才能对报文信息进行解析。

所以粘包问题的本质,就是应用层无法从连续的字节流中,准确区分出一个完整的报文在哪里开始、在哪里结束。

如图,很可能read上来也是两个数据包粘在一起的,那么如何避免粘包?

我们的核心思想就是:明确两个包之间的边界!

  • 对于定长的包,保证每次都按固定大小读取即可;
  • 对于变长的包,可以在包头的位置,约定一个包总长度的字段,从而就知道了包的结束位置;
  • 对于变长的包,还可以在包和包之间使用明确的分隔符(应用层协议,是程序猿自己来定的,只要保证分隔符不和正文冲突即可);

TCP小结:

本文介绍了TCP中非常多的机制,这些机制其实主要就是为了维护TCP的连接可靠性和提高TCP通信效率的。

其中,体现维护可靠性 的是:校验和、序号(按序到达)、确认应答、超时重传、连接管理(三次握手和四次挥手)、流量控制、拥塞控制。

体现在提高通信效率 的是:滑动窗口、快速重传、延迟应答、捎带应答。

相关推荐
代码的奴隶(艾伦·耶格尔)20 小时前
Nginx
java·服务器·nginx
头发还没掉光光20 小时前
HTTP协议从基础到实战全解析
linux·服务器·网络·c++·网络协议·http
液态不合群20 小时前
Nginx多服务静态资源路径冲突解决方案
运维·nginx
小白同学_C20 小时前
Lab2-system calls && MIT6.1810操作系统工程【持续更新】
linux·c/c++·操作系统os
物理与数学20 小时前
linux内核 struct super_block
linux·linux内核
Getgit20 小时前
Linux 下查看 DNS 配置信息的常用命令详解
linux·运维·服务器·面试·maven
数通工程师21 小时前
企业级硬件防火墙基础配置实战:从初始化到规则上线全流程
运维·网络·网络协议·tcp/ip·华为
zhangrelay21 小时前
Linux(ubuntu)如何锁定cpu频率工作在最低能耗模式下
linux·笔记·学习
_OP_CHEN21 小时前
【Linux系统编程】(二十)揭秘 Linux 文件描述符:从底层原理到实战应用,一篇吃透 fd 本质!
linux·后端·操作系统·c/c++·重定向·文件描述符·linux文件
岁岁种桃花儿21 小时前
详解kubectl get replicaset命令及与kubectl get pods的核心区别
运维·nginx·容器·kubernetes·k8s