TCP报文格式与核心机制
TCP与UDP都是传输层的重要协议,TCP的特性包括有连接、可靠传输、面向字节流、全双工。
TCP的报文格式如下:
一、报文格式
1.源端口号、目的端口号
源端口和目的端口是五元组中重要的两个性质,源端口即数据是从哪里来的,目的端口即数据要发送到哪去。
一般情况下,端口号都是两个字节,即16个bit位,可以表示0-65535的数字,通常使用1024-65535之间的数字作为端口号。
在编写代码时,服务器的端口号通常是程序员自定义的,但客户端的端口号是由操作系统自己生成的。
2.首部长度
首部长度即TCP报头(除去载荷)部分的长度。以四个字节为一个单位,所以TCP报头的最大为60个字节。
3.保留位
由于UDP报文的长度为固定的64KB,于是就导致当数据量过大时,UDP就装不下了。TCP在此基础上吸取教训,设计了保留位。即这些空间现在不用,但先保留这么大的空间,以防后面需要使用。
4.标志位
TCP标志位是TCP报头最核心的部分,即URG、ACK、PSH、RST、SYN、FIN,与TCP的可靠传输有着密切联系。在后面的TCP核心机制中会详细介绍。
5.选项
选项是可有可无的,但选项的大小必须为4的倍数。正是由于选项的存在,就导致TCP报头的大小是不确定的,也就使得首部长度的存在变得合理。由于TCP报头最大为60字节,并且报头的固定空间为20字节,于是选项最大为40字节。
6.校验和
与UDP一样,是检验数据在发送的过程中是否变化。
但与UDP不同的是,UDP对发生变化的数据报是直接丢弃的,但TCP会出触发重新发送的操作,在后面的核心机制中会详细介绍。
二、核心机制
1.确认应答
1)当A给B发消息时,由于A并不知道B是否已经收到了消息,于是就需要B给A发送一个确认收到的消息,这样A就知道B收到了他发的短信。TCP也是一样,当两台设备进行通信时,发送方发送数据后,接收方在接收到数据后就需要给发送方发送一个应答报文(acknowledge,ack),这里的ack就是TCP报头中的标志位,当ack为1时,就代表返回的报文是应答报文。图示如下:
2)在进行网络通信时,有时会出现后发先至的情况(由于网络数据会经过路由器和交换机等网络设备,但数据的传输并不一定会经过同一条道路,对于不同的道路,有的设备拥堵,有的设备畅通,就导致数据在路上消耗的时间是不一样的,也就导致到达接收方的顺序是不一样的),即A给B发了若干条消息,虽然B给A回应的消息是按照A的消息顺序进行回应的,但由于存在后发先至的情况,B的回应消息到达A时并不是按照B发送的顺序到达的,如下图所示:
由图可知,B的回应消息到达A时顺序发生了改变,就导致A无法将发送消息与回应消息进行对应。
对于这种情况,TCP给出了解决方法,即给传输的数据进行编号。
对于编号,就涉及到TCP报头中的序号和确认序号。确认序号只存在于应答报文中,当ack为1时,就说明该报文为应答报文。并且只对TCP在和部分进行编号,TCP报头不参与编号。
由于TCP是面向字节流的,于是编号是对每个字节进行编号的,并且编号是连续递增的。TCP报头中的序号就是这段载荷的第一个字节的编号,确认序号就是对回应的那条消息的载荷的最后一个字节编号加1。对于A消息,B是A的应答报文,如果A的载荷的第一个字节编号为1,最后一个字节编号为1000,那么A报文的序号就是1,B报文的确认序号就是1001。那么下一次A再发送数据时,报文序号就会从1001开始继续编号。
引入编号后,接收方就可以根据报文序号进行排序。在内存或操作系统的内核中,会存在接收缓冲区,接收的数据会先进入接收缓冲区,将数据编号从小到大排序完成后,就可以调用方法读取数据,这时读取的数据就是有序的了。
2.超时重传
当A与B进行通信时,A给B发送消息可能会出现丢包情况(当路由器/交换机工作繁忙时,当路由器/交换机的转发数据超过了其能承受的上限,就会将最新进来的数据丢弃),即A给B发送的消息B没收到,或者B发给A的确认应答(以下统称ack)A没收到。这种情况下就会触发超时重传操作。
当发送方发送数据后,在一定的时间阈值T内没有收到接收方发送的ack,就会延长T到T1并进行重传,当T1时间后还没有收到ack就会再一次延长T1到T2进行重传。由于丢包的概率是很小的,当超时次数达到一定程度/超时时间达到一定程度,就不会继续重传了,而是会放弃这次传输,并且此时的网络已经存在严重故障了。
有上面知道,引发超时重传的原因可能有两个,即A给B发送的消息B没收到,或者B发给A的ack A没收到。如果是前面一种情况,就相当于B只收到了一次数据,这时合理的;但如果是后面一种情况,B就会收到两个一样的数据,并且这两份数据载荷的序号是一样的,这就不合理了。于是就有了下面的操作。
当B接收到数据后,数据会先存放在接收缓冲区,TCP会在接收缓冲区内部进行去重操作。若接收的数据在缓冲区中已经存在了,就会将这个数据丢弃,若不存在就会直接放进缓冲区中。
超时重传和确认应答是TCP能够进行可靠传输所依赖的两个最核心的机制。
3.连接管理
连接管理分为确认连接和断开连接。
1)建立连接
所谓连接,是一种抽象的概念。建立连接的过程就是通信双方保存对端信息(IP、端口号等)的过程。TCP建立连接是通过三次握手完成的。
当客户端想要与服务器进行通信时,就会给服务器发送同步报文段(sync,即在TCP报头中的标志位syn为1时,下面简称syn),当服务器接收到syn后,就会先给客户端发送ack,表示已经收到了客户端的信息,然后再发送syn给客户端,当客户端收到服务器的syn后,就会给服务器发送ack,经过这四次发送,就可以让服务器与客户端建立连接了。图示如下:
但我们说的是三次握手,上面进行了四次发送,是否可以将其中的两次进行合并呢?答案是可以的。当服务器接收到客户端的syn后,由于syn与ack都是由操作系统内核进行返回,就会将后面的ack与syn进行合并发送,即在报头中的ack与syn同时为1。这样可以提高传输的效率。图示如下:
CLOSED:假象的,不存在的状态,表示设备还没有连接的状态;
LISTEN:表示服务器可以接收到客户端发来的syn;
SYN_SEND:发送了syn,正常情况下这种状态留存时间很短,是看不到的;
SYN_RCVD:接收到syn,正常情况下这种状态留存时间很短,是看不到的;
ESTABLISHED:客户端与服务器连接建立完成,可以发送业务数据。
三次握手的作用:
①确保通信线路是畅通的。
②检验双方的接收、发送能力是否正常。
在客户端向服务器发送了syn后,服务器返回了ack和syn,表示客户端的发送能力与服务器的接收能力是正常的;
客户端接收到服务器的syn与ack后返回了ack,就说明客户端的接收能力与服务器的发送能力是正常的。
③协商关键信息。
通常协商的是报头序号是从几开始。一般情况下TCP报头序号不是从0开始的。
在客户端与服务器第一次建立连接完成后开始发送若干组数据,若其中有一组数据传输较慢,在第一次连接断开后还没有发送到,并且此时客户端与服务器建立了第二次连接,在第二次连接中也发送了若干组数据,在发送过程中,第一次连接的那组数据才被服务器接收到。这时,服务器对这组数据的处理方式应该是丢弃,但服务器是如何知道这组数据是第一次发送的呢?
这时在客户端与服务器建立连接的过程中,就可以协商报头序号从几开始。若第一次从10000开始,那么第二次就从60000开始。由于在传输过程中报头序号是递增的,于是当发现接收到的数据比这次连接协商的报头序号小时,就说明这个数据来晚了,需要被丢弃。
注意:为什么是三次握手,不是两次、四次呢?
若是两次,由作用②可知,当服务器给客户端您发送syn与ack后,就代表服务器的发送能力与客户端的接受能力是正常的,但此时服务器不知道客户端的接受能力是正常的,于是就需要客户端再向服务器发送syn,就可以让服务器知道客户端的接受能力没有问题。
若是四次,就是将服务器发给客户端的syn与ack分开发送,但会降低传输效率。
2)断开连接
断开连接的过程就是各自将对端的信息删除。TCP断开连接是通过四次挥手完成的。
在建立连接中,通常是客户端发起建立连接的请求,但是在断开连接中,客户端与服务器都有可能发起断开连接的请求。
当A与B想要断开连接时,A就会先给B发送结束报文(TCP报头标志位FIN为1,下面简称FIN),B收到FIN后,就会先给A发送ACK,再发送FIN,当A收到B发送的FIN后,就会给B发送ACK,经过这四次通讯后,A与B就断开了连接。图示如下:
与三次握手类似的,B发给A的ACK与FIN能否合并呢?
答案是有可能能,也有可能不能。
能是因为TCP的另一个核心机制:延时应答,后面会介绍;
不能是因为ACK与FIN发送的时机是不一样的。ACK通常是由操作系统内核返回,只要B收到了A发过来的FIN,就会马上回复一个ACK。但FIN的发送是则是代码中调用close方法或进程结束,这两个时间的触发时机通常不是一起的。
核心状态:
CLOSE_WAIT:当A给B发送FIN后,由于B的代码要执行到close方法还有一段时间,这也就会有一个时间段。但通常情况下,这个时间段应该要非常短,即代码感知到A发送了FIN后就应该要尽快执行close方法。若在开发中看到CLOSE_WAIT状态存在时间过长或存在大量CLOSE_WAIT状态,就说明代码中可能存在BUG。
TIME_WAIT:在A与B进行四次挥手的过程中,也有可能会出现丢包情况,由以下几种丢包情况,
①A发给B的FIN丢包了,即B没有收到FIN,也就不会给A发送ACK,那么A在一定时间内没有收到ACK就会触发超时重传操作,即重新发送FIN;
②B发给A的ACK丢包了,即A没有收到ACK,那么A就会默认第一个FIN丢包了,一定时间之后也会触发超时重传操作,即重新发送FIN;
③B发给A的FIN丢包了,即A没有收到FIN,那么A也就不会给B发送ACK,当B在一定时间内没有收到ACK,就会认为FIN丢包了,也会触发超时重传操作,即重新发送FIN;
④A发给B的ACK丢包了,此时A已经将B的信息删除了,那么B没有收到ACK后也会认为FIN丢包了,那么此时如果B再重新发送FIN给A后,A也就无法收到FIN,导致B一直收不到ACK。对于这种情况,就需要引入TIME_WAIT,即当A收到B发的FIN后,不会马上把B的信息删除掉,而是会等待一段时间,即 2 * MSL(网络上任何两个结点传输过程中消耗的最大时间),通常是分钟级别的。如果在这段时间内A又收到了B发送的FIN,就会重新发送ACK。当等待这么长时间之后,A才会将B的删除掉。但是B由于没有收到A发过来的ACK,依然会保存A的信息,但A与B是无法进行通信的了。
4.滑动窗口
算法中的滑动窗口就是从这里演变出来的。
由于TCP是基于可靠性传输的,那么就会损失一部分传输效率。引入滑动窗口就是为了降低这部分损失。但其效率一眼比不上UDP。
当A给B发送数据时,如果是按照发一个数据包等一个ACK的模式,就会使得等待的时间过长(图左),从而导致效率低下。那么就可以一次发若干组数据,等到接收到第一组数据的ACK后再发送一组数据,接收到第二组数据的ACK后再发送一组数据(图右),这样就可以将等待多组ACK的时间压缩成等待一组ACK的时间。图示如下:
在A与B传输过程中,当A接收到1001后,就会将5001-6000的数据发送出去,就相当于窗口向右移动了一位,当A接收到2001后,就会将6001-7000的数据发送出去,图示如下:
当B先给A发送了1001,后发送了2001,但2001比1001先到达A,那么A就会认为1-1000与1001-2000的数据都已经被B接收到了,于是就会将窗口向右移动两位。
但是在发送过程中,也有可能会出现丢包情况,这里的丢包分为下面两种情况:
①B给A发送的ACK丢包了,这是没有问题的。确认序号的含义是小于确认序号的数据都已经收到了。若先发送的ACK丢包了,只要能收到后发送的ACK,那么就代表之前的数据都已经收到了;
②A给B发送的若干组数据中有一组数据丢失了。当1001-2000这一组数据丢包后,B就会不断的给A发送1001,而不会因为接收到了后面的数据而发送后面的ACK。当A发现B总是在返回同一个ACK时,就会将1001-2000进行重新发送,当B接收到这组数据后,就会直接返回5001的ACK,就代表前面的数据都已经收到了,A可以从5001开始重新发送数据了。这就是在滑动窗口中特有的重传操作:快速重传。图示如下:
当发生丢包后,是不会影响B读取A发送数据的顺序的,即A是按什么顺序发送的,B就是按什么顺序读取的。因为在接收缓冲区中给1001-2000的数据预留了一定的空间,当B接收到这组数据后,就会放在预留的空间上。图示如下:
注意:
超时重传与快速重传是不矛盾的。
超时重传是当数据量不大时的操作,没有必要构成滑动窗口;
快速重传是当数据量很大时的操作,需要构成滑动窗口。
5.流量控制
当发送方发送数据时,接收方就会将数据暂时存在接收缓冲区中,然后在接收缓冲区中进行读取数据。那么接收缓冲区就相当于蓄水池,发送方发送数据就相当于进水,接收方读取数据就相当于放水。当进水速度过快而出水速度过慢,就会出现水越来越多的情况,最后蓄水池装不下导致水溢出。这种情况就相当于发送方发送数据过快而接收方读取数据过慢,导致接收缓冲区被数据占满,那么新的数据就会出现丢包情况,这时就需要对发送方发送数据的速度进行控制。
在TCP报头中,窗口大小这个属性就代表了接收缓冲区的剩余大小是多少。在选项中,也有窗口拓展因子这一特殊属性。
当A给B发送数据时,B在给A返回ACK的同时也会返回接收缓冲区剩余的大小。这个剩余大小就会填入到窗口大小中。当A接收到B返回的窗口大小后,就会将窗口大小与窗口拓展因子进行计算,即窗口大小<<窗口拓展因子,得出来的数值就是滑动窗口中的窗口大小,A之后就会按照这样大小的窗口进行数据发送。当B返回给A的窗口大小为0时,就代表接收缓冲区已经装满了,再进行发送就会丢包了。于是A就不再发送业务数据,但会间断地发送窗口探测的数据包,目的只是为了让B发送ACK以及当前接收缓冲区的剩余空间。当接收缓冲区剩余空间不为0时,A就会继续发送业务数据。
6.拥塞控制
流量控制是依据接收方的接受能力来控制发送方的发送速度,拥塞控制是根据通信链路的转发能力来控制发送方的发送速度。
当两台设备进行通信时,数据会经过若干个路由器或交换机,这些机器的转发能力是有上限的,当发送方发送速度过快时,就会出现丢包情况。
当我们想知道一个通信链路的转发上限时,就可以采取做实验的方式。
我们现让A以速度v来发送数据,若此时B没有出现丢包情况,就增大v,当B出现了丢包情况时,就马上减小v(比一开始的速度大),然后继续增大v,经过几次后,就基本可以确定这条通信链路的转发上限了。图示如下:
一开始是慢启动,即发送方以较小的速度发送数据,然后发送速度呈指数增长, 当速度达到初始的阈值(ssthress)时,就让速度线性增长,当B出现丢包后,接下来有两种做法:
新:将发送速度减小到新的阈值,然后再让其线性增长,循环往复;
旧:将发送速度减小到一开始的速度,在让其呈指数增长,但这次增长的没有第一次的快,循环往复;
经过多轮试验后,就可以找到适合的窗口大小了。
当我们确定通信链路的转发上限后,与流量控制的窗口大小进行比较,取较小值作为A的窗口大小。
7.延时应答
在一般情况下,当接收方收到数据后,就会马上返回一个ACK,但是通过延时应答可能会提高效率。
若不使用延时应答,那么当发送方发送数据后,就会将数据存放在接收缓冲区中,如果这时接收方马上就返回一个ACK,也就会返回当前情况下的窗口大小(接收缓冲区的剩余容量)。如果接收方延缓ACK的发送,那么在延缓的这段时间内,接收方就会读取接收缓冲区中的数据,使得接收缓冲区中的数据减少,那么其剩余容量就会增大,那么等到接收方再返回ACK时,就会返回一个较大的窗口大小,那么当发送方收到ACK后,就会使用一个较大的窗口发送数据,这时就会提高通信的效率。
但是在延缓情况下,可能发送方依然在发送数据,就会导致接收缓冲区中的剩余容量进一步减少,从而会返回一个更小的窗口大小,导致发送方使用更小的窗口发送数据。在这种情况下,就会降低通信的效率。
于是引入延时应答机制,并不会一定地提高通信效率,而是有可能会提高通信效率。
延时应答也会受到两个因素的限制,即数量限制与时间限制。
数量限制:每隔N个包应答一次;
时间限制:超过最大延迟时间就应答一次。
在数据报较多时,就会使用第一种;当数据报较少时,就会使用第二种。
8.捎带应答
发送方在发送请求报文后,接收方接受到业务数据就会返回一个ACK,之后再返回一个响应报文。
由于响应报文只是在报头将ACK设为1、窗口大小设为接收缓冲区剩余容量、选择合适的确认序号等,与载荷无关,但响应报文只是在载荷中加入数据,于是可以将ACK与响应报文结合起来一起发送,这就是捎带应答,即在返回业务数据地同时将上次的ACK顺带发送过去。
引入捎带应答后,也可以提高TCP的传输效率。
9.面向字节流
TCP的一个传输特性就是面向字节流。但这也存在一个问题,就是粘包问题。由于TCP在读取数据时是按照字节进行读取的,就不好区分每组数据之间的界限,就可能会将下一组数据读到这一组数据中。
当A给B发送数据时,连续发送aaa、bbb、ccc这三组数据,那么当B进行第一次读取时,就有可能会读到以下几种情况:aa、aaa、aaab等,在这几组数据中,只有aaa是正确的。那么应该如何防止这种情况发生呢?
1)可以定义好每组数据之间的分隔符,若规定组与组之间以 \n 进行分割,那么A传输过来的数据就是aaa\nbbb\nccc\n,当B在读取时,在读到第一个 \n 时,就代表第一组数据已经读取完成,接下来就是第二次读取;
2)在每组数据之前加上包的长度,若A发送给B的数据为aa、bbb、cccc,那么就将每一组的长度放在最前面从一并发送,即2aa3bbb4cccc,那么当B在进行读取时,就会先读到2,然后才会读两个a,这样第一次读取就已经结束。第二次读取时,就会先读到3,然会就会向后读3个b,到这里第二次读取就已经结束。
这两种情况在HTTP中也有所体现,在GET方法中,没有正文,就会以空行结束;在POST方法中,由于存在正文,就会在响应头中的属性Content-Length中标明正文的长度。
对于UDP而言,就不存在粘包问题。因为UDP是面向数据报进行传送的,每次读取就会按照一个数据报进行读取。
10.异常情况的处理
在TCP通信过程中,会出现以下几种异常情况:
1)某个进程崩溃了
在这种情况下,与进程主动退出没有本质区别,都会回收文件描述符表的每个资源,调用close方法,然后四次挥手断开连接。虽然进程已经没了,但是通信双方的信息依然保存着,并且四次挥手是在操作系统内核中完成的,那么即使进程崩溃,四次挥手依然能完成。
2)主机关机
通常情况下,主机关机与进程结束差不多,都会进行四次握手,但会出现四次握手还没有结束主机A就已经不工作了。在这种情况下,当B重新发送多次FIN后都没有收到ACK,就会认为此时网络出现严重故障,那么B就会将A的信息删除掉,即B主动释放连接。
3)主机掉电
主机掉电与主机关机不同,主机掉电是直接拔掉电源,对于没有备用电池的电脑,拔掉电池后会使主机直接不工作。
当接收方发生主机掉电:即发送方发送的任何数据都没有ACK,这时发送方就会发送复位报文(即重新建立连接,报头的标志位RST为1,下面简称RST),同样的,发送后也不会又ACK返回,这时发送方就会直接释放TCP连接;
当发送方发生主机掉电:即接收方不会接收到任何数据,这时接收方就会向发送方发送“心跳包”,即周期性的数据报,不携带载荷,只是为了触发ACK,但依然没有ACK返回,这时接收方就会发送RST,同样的也不会有ACK返回,此时接收方就会释放TCP连接。(TCP保活机制)
4)网线断开
接收方网线断开:与接收方主机掉电一样;
发送方网线断开:与发送方主机掉电一样。
三、剩余属性
在经过上面的介绍后,还剩下一个属性、两个标志位没有介绍。
1、紧急指针、URG
在正常情况下,TCP的读取顺序是按照接收顺序读取的,但紧急指针相当于“插队”,即先从紧急指针的需要进行读取,同时将URG标志位置为1.
2、PSH
催促标志位,当这一位为1时,接收方就会尽快将这个数据读取到应用程序中。