网络协议学习——TCP协议

时间:2024-04-09 16:01:57

目录

​编辑

一,TCP协议报文

1,端口号

2,头部长度

3,32位序号

4,32位确认序号

5,六位保留字段

二,确认应答机制

确认应答机制

延迟应答

超时重传机制

超时重传的时间

三次握手

全连接队列

三次握手时的状态变化

四次挥手

流量控制

滑动窗口

三,拥塞控制机制

四,TCP的其他问题

1,面向字节流

2,粘包问题(不同的数据包粘在一起)

TCP异常问题


 

一,TCP协议报文

如上图便是TCP报文 的组成,很明显,TCP报文比UDP要复杂的多。这是因为TCP是可靠的。要做到可靠便要比UDP复杂的多。

1,端口号

TCP报文当中的端口号和UDP协议当中的端口号便标识了这个TCP报文的传输方和接收方。

2,头部长度

TCP协议当中的头部长度代表着TCP报头的大小,在多数情况下TCP报文的位数是4位。 所以TCP的长度范围是0~15是吗?但是在这个范围的数字有意义吗?答案是没有,因为没有单位。在这里要明确的是头部长度的单位是4个字节,所以头部长度能够表示的范围是0~60字节

报文和有效载荷的分离

报文和有效载荷该如何分离呢?回答如下:

 当接收到一个TCP报文时,先取前20个字节的内容。然后取出头部长度,头部长度的大小便是报文的大小,然后根据这个长度便能将报文和有效载荷分离开来。

3,32位序号

TCP协议的一大特点便是可靠。而无序(先出发不一定先到达)绝对是影响可靠的一个大问题。所以,在TCP协议的报文当中,32位序号便是确保报文能够被有序的发送的保证,因为在报文发送时这个字段能够将报文进行编号,然后服务端在收到这些报文时便能根据这些序号排序。

比如我要发送4000个字节的数据,并且要分四次发送,每一次发送1000个字节的数据:

在每一次向服务端发送数据时便要在报头的32位序号里面标记序号,其实我们也可以将这一整块数据据想象成一个数组,将32位序号便是每一段数据第一位的下标。并且这个序号还能够实现去重的作用,即客户端重复传输同一段数据时可以利用序号来查看是否是重复的数据,如果是便去掉。

4,32位确认序号

 32位确认序号是在ASK报文当中使用的一个字段。确认序号的作用是告诉发送方,在这个序号前的数据已经被服务端处理完毕,请客户端从确认序号开始发送数据。如下图:

能将序号和确认序号合为一个字段吗?

答案便是不能,这其实是很明显的问题,因为TCP协议是一个由大佬定制的协议如果能够合为一个字段的话那TCP协议当中肯定就不会变有这两个字段了。为什么不能呢?

1,因为TCP是全双工的,所以客户端或者服务端都能同时发消息和接收消息。所以就需要有一个序号告诉对方你发来的这个序号之前的数据我已处理,还要将这次要处理的数据的序号发给对方。

5,六位保留字段

六位保留字段的作用是标记好TCP报文的类型。如下便是这些字段的作用:

1,SYN:请求建立连接

SYN类型的报文通常在TCP通信建立连接时发送。用于建立连接。

2,ACK:确认号是否有效

ACK类型的报文相当于一个回应报文,通常由接收端发送给发送端,用于对发送端进行回应。并且发送ACK报文是TCP确认应答机制的一环。

3,FIN:断开连接

FIN类型的报文主要用于断开连接时使用。当我想要断开连接时便可以发送一个带有FIN字段的报文给对方用于断开连接请求。

4,PSH: 提示接收端立马将数据从TCP缓冲区读走

当接收端缓冲区将被打满时,发送端仍然要将数据发送给服务端时便可以使用这个类型的报文用于强制服务端读取TCP缓冲区内的数据。

5,RST:复位文段

1,当RST文段被置为1时意味着让对方重连。

2,当通信双方未建立好连接时如果一方向另一方发送数据,那这个时候接收方便会发送一个带有RST的响应报文给对方让对方重新建立连接。

3,当两方建立好连接后,但是一方因为一些异常原因断开了连接。这时也会发送RST报文要求重新连接。

6,URG&&紧急指针

 URG被设为1时标识紧急指针文段有用。紧急指针文段是一个16 位的数字,标识是的是紧急数据的偏移量。如果URG文段有效,那从这个偏移量开始后面的一个字节的数据会被服务器优先处理。

二,确认应答机制

 TCP最大的特点便是可靠,如何保证可靠性呢?在这方面TCP协议花了很多的功夫。

确认应答机制

在保证数据可靠性方面,TCP协议便使用了一个确认应答机制来确保数据的可靠性。过程便是在客户端发送一个请求后在服务端收到请求以后向发送端发送一个ACK确认报文。

这样便能够保证第一条数据的可靠性。当然,我们能够保证最后一条数据的可靠性吗?答案当然是不能的,所以TCP协议不一定是100%可靠的。

延迟应答

延迟应答的出现也是为了提高TCP的通信效率,这种方式提高效率的本质便是将接收端的缓冲区尽量的变大,从而在发送端接收到响应后将自己的滑动窗口变大,从而提高发送的数据量。

超时重传机制

当我们的请求在长时间得不到响应时,超时重传机制便会上线打怪,这个机制便会在发送端在一定的时间内没有得到ACK响应报文时重新将数据发送一遍。如果还是在一定的时间内收不到ACK响应时,就又会启动重传机制重新发送这段数据。

一般来说,超时重传机制启动的原因如下:

1,发生丢包。

2,因为网络阻塞原因导致数据发送时间过长。

丢包的情况有两种,1,发送端的数据被丢包  

这种情况下,确确实实是要将数据重新传输的。 

 2,接收端的应答被丢包

这种情况下重新传输数据便会造成重复数据的问题,所以32位序号便可以启动去重功能。 

超时重传的时间

 超时重传的时间是一个动态的时间,不是固定的时间。比如在第一次设定的超时时间为50ms。r如果超过了这个时间,那便要开始重传。第二次的等待时间便是50ms*2=100ms。如果这次还是超时的话,就再次重传这次的等待时间便是4*50ms。一直收不到就一直重传,直到超过一个限度时间还是收不到的话遍直接断开连接。

在Linux中,一般第一次的等待时间是500ms。此后每次的等待时间便是500*2^n(n代表重连的次数)。

三次握手

在TCP协议中,要想建立连接便要走三次握手的流程才能成功的建立连接。如图:

基于确认应答机制,我们的每一次请求其实都是要有ACK报文响应的。但是在建立连接的过程当中,只需要三次,因为服务端的SYN(建立连接)和ACK(响应)报文被合成了一次一起打包以捎带应答的方式发送给客户端。

为什么是三次握手?

在前面的文章内容当中,我们可以知道的点便是没有百分之百可靠的建立连接。最后一次的请求是不保证可靠性的。所以,三次握手时最后一次的ACK不一定能够保证服务器收到。所以,选择三次握手的原因更多的是因为三次握手的以下两个优点:

1,三次握手是验证双方建立通信成功的最小次数。

2,三次握手能够把建立连接失败的风险嫁接到客户端。  

 三次握手的过程中,只有第三次是bu'nenbunen敢保证可靠的。所以在客户端的请求被ACK后,客户端便会建立起连接。但是服务端的请求没有被ACK的话,那服务端便不会建立连接。从而减少链接资源的浪费,减小服务端瘫痪的风险。

全连接队列

在listen函数内部,第二个参数是一个数字。这个数字加一便表示全链接对列中元素的个数

一些认识:

1,全连接队列里面的连接在没有被accept时是要进行长时间维护的,所以listen的第二个参数便不能过大。

2,为了提高效率让服务器过长时间的停止工作,这个数字也不能太小。

三次握手时的状态变化

    • 1,开始时两方都处于closed状态

    • 2,服务端要能够接收客户端的连接请求,就要变为listen状态

    • 3,客户端向服务端发送那个请求变为SYN_SENT状态

    • 4,服务端接收到客户端发来的请求以后变为SYN_RECV状态。

    • 5,当客服端再次接收到服务端发来的ACK报文以后便可以变为ESTABLISHED状态。

    • 6,服务端发送回ASK报文以后服务端也变为ESTABLISHED状态。

四次挥手

四次挥手是TCP连接正常断开要走的一个标准流程。

  • 四次挥手的过程

    • 1,第一次挥手:客户端向服务器发送FIN字段被置为1的报文,表示要和对方断开连接。

    • 2,第二次挥手便是服务器端向服务端回应表示同意断开连接。

    • 3,第三次挥手是服务器向客户端发送FIN被置为1的报文,请求断开连接。

    • 4,第四次挥手便是客户端向服务端回应表示同意断开连接。

  • 为什么要四次挥手

    • TCP是全双工的,所以服务端和客户端之间有两个连接,所以我们在断开时也需要断开两条连接让两台主机变为closed状态。断开一条连接需要两次挥手,断开两条连接便要四次挥手。

    • 第二次挥手和第三次挥手不能连接在一起变为一次挥手,因为不能保证客户端要求关闭连接后服务器端也是要断开连接的,服务器端可能还有数据要发送给客户端。

  • 四次挥手的状态变化

    • 开始时两者都是ESTABLISHED状态,在客户端发出FIN为1的报文以后客户端便转为了FIN_WAIT_1的状态。

    • 服务端收到断开请求后发出回应报文,这个时候服务端的状态便转化为CLOSE_WAIT状态。

    • 服务端在接收到响应以后,便会转化为FIN_WAIT_2状态。

    • 如果服务端没有数据要发送给客户端了,那服务器便要发起断开请求。那这个时候服务器的状态便变为LAST_ACK,等待最后一次响应。

    • 客户端接收到服务器的请求后便开始响应,这个时候客户端的状态变为TIME_WAIT状态。

    • 服务端接收到客户端的响应后便会断开连接,状态变为CLOSED状态。

    • 客户端在等待两个MSL时间后便会关闭和服务端的另一条连接,变为CLOSED状态。

  • 两个半关闭状态
    • TIME_WAIT

      • TIME_WAIT状态是一个主动的半关闭状态,在这个状态下两方忍让能够进行通信。并且这个状态会持续2个MSL(Maximum Segment Lifetime)时间。

      • 为什么要是2个MSL时间

        • 一个MSL便是数据在网络当中存在的最长时间。

        • 最大可能地保证最后一次响应被客户端接收到,进而正常的四次挥手,正常的关闭两端的连接。

        • 最大可能地让数据在网络传输的过程中消散掉,避免对后面的通信造成干扰。两个MSL便能保证两端的数据都消散掉。

    • CLOSE_WAIT

      • CLOSE_WAIT状态是一个被动的半关闭状态,这个状态下两方还是可以进行通信,所以本质上两方的文件描述符其实还是没有关闭,这个时候如果一直没有调用close,那就会造成文件描述符资源的泄露。

可以通过以下命令来查看MSL时间的大小,通常在Linux下是60s,在Windows下是120s。

cat /proc/sys/net/ipv4/tcp_fin_timeout

流量控制

在TCP中,前面介绍的确认应答机制虽然可以确保数据发送时的可靠性。但是,如果在接收方接收时是因为缓冲区大小的限制而产生丢包行为的,那就得不偿失了。所以,在TCP协议当中引入了·一个流量控制的机制来限制两端数据发送的速度和大小。

如何得知对方缓冲区的大小呢?

在TCP报头当中,会有一个叫做滑动窗口的字段,接收双方便是通过对方报文的滑动窗口大小来确认缓冲区大小的。确认好缓冲区大小后便可以进行流量控制。

当缓冲区大小变为0后如何在之后重启通信?

当接收端的缓冲区腾出了一定的空间后会向发送端发送一个ACK报文,告知接收端可以继续发送请求。

滑动窗口

在TCP协议中,缓冲区会被分为如下四个部分:

在这之中蓝色的这部分缓冲区被称为滑动窗口。滑动窗口的大小便表示对方缓冲区的大小,网络吞吐量的大小。 

滑动窗口会不会整体向右移?

答案是不一定,因为发送方滑动窗口的大小是取决于接收端缓冲区大小的。如果接收端的缓冲区大小变小,那发送端的滑动窗口大小也会变小,从而只有左端向右移右端不移动的情况。

滑动窗口丢包问题

 1,丢失ACK响应

在接收端接收响应时,丢失了当前这个最新的ACK报文之前的ACK响应报文是被允许的。为什么呢?因为响应报文的确认信号的意义是:

保证确认序号之前的数据是可靠的。所以只要接收到了最新的报文便可以知道最后一个确认序号的大小。

2,丢失数据包

如果是在网络传输的过程当中丢失了数据包的话,那发送端必须重新传输数据包给接收端。这个时候便有两种传输方式。

快重传(提高TCP通信速度的机制)

当接收端连续三次都接收不到来自服务端的ACK响应时便会启动快重传,不再等待超时,立马开始进行数据包补发。

超时重传

快重传是一种有条件的重传,所以在补发数据包时还是需要有超时重传来给数据包补发进行兜底。即当我们的数据包丢失不是连续三个时,在超过一定的时间后才对数据进行补发。

三,拥塞控制机制

当在网络通信当中出现了大量的丢包问题时,可能问题不是出现在两台主机上而是出现在网络上。可能是网络柱塞或者网络异常。所以拥塞控制便出现了。

1,慢启动

当出现网络拥塞问题时,便会启动慢启动机制。在这里要插入一个概念——拥塞窗口。这个窗口在慢启动启动时的大小是1。所以出现拥塞情况时我们的发送端是可以发出少量的报文去探路的。当这些探路的报文的ACK响应被返回时,在发送端被接收后这个阻塞窗口便会扩大,并且是以指数级的方式扩大的。

所以慢启动的满其实是一开始慢,后面的速度还是很快的。

阈值

 为了避免网络再次阻塞,所以阻塞窗口大小的增长不能一直是指数级增长的。在超过一个阈值后阻塞窗口的增长便是线性增长的了。

这个阈值是多大?

在TCP通信刚启动时,这个阈值被设置为对方接收窗口的最大值。当再次网络阻塞时这个阈值就会减半。

注意:解决网络堵塞问题不是一两台主机就能办到的,所以这个阻塞机制是所有进行TCP通信的主机都要遵守的。当网络出现拥塞问题时,所有的主机都要减少向网络中发送数据包。

四,TCP的其他问题

1,面向字节流

面向字节流表示TCP协议在读取数据时是按照一个一个字节来读取的,不关心数据是否完整,这就叫做面向字节流。传输层的TCP协议不关心数据如何解释,只知道准确的读取数据。所以,在发送端发送一百个字节给接收端是,接收端读取数据的方式会有很多种,比如调用一次read一次性读取一百个字节数据或者调用一百次read一次读取一个字节。

至于读取到的数据如何解释,便是在应用层需要做的。

2,粘包问题(不同的数据包粘在一起)

 在TCP传输时,因为TCP的一些控制操作:

1,为了提高效率可能会把多个包合并在一起发送。

2,因为拥塞控制可能把一个包切分成多个包发送。

3,可能因为接收端的接收速度和发送端的发送速度不匹配 。

因为以上原因,在读取时便会造成粘包问题。当然,这个粘包问题其实是在应用层上的问题,不是传输层的问题。

如何解决?

解决粘包问题,一般有以下几个方法:

1,确定消息边界:每个消息用一个特殊字符来确定消息边界,根据这个消息边界来读取消息。

2,定长度:每个消息按照定长发送,读取端按照这个长度读取数据。

3,消息带一个长度字段:在每个消息的里面带一个长度字段,读取端按照这个长度来读取数据到应用层。

UDP有会粘包问题吗?

 UDP没有粘包问题,因为UDP是面向数据报的,所以在发送端每个数据都是整个整个发送在接收端是整个整个读取的,并且每个数据之间还有边界。

TCP异常问题

 1,进程被杀掉

正常的四次挥手后两端正常的断开连接。

2,关机

正常的四次挥手后两端正常的断开连接。

3,断电或者断网

这个时候客户端的或者服务器端任何一方断网了,接收端都会在一段时间以后关闭和对方建立的连接。因为,在TCP连接中有一个保活机制。

1,发送端会定时的向服务器端发送消息,报平安。

2,接收端也会定时的向发送端发消息进行询问。

当长时间没有得到应答时,没有接收到应答的一方便会关闭连接。