UC头条:24 张图总结 TCP 基础知识,看完我飘了。
点击加载图片
TCP是一种面向连接的单播协议,在TCP中,并不存在多播、广播的这种行为,因为TCP报文段中能明确发送方和接受方的IP地址。
在发送数据前,相互通信的双方(即发送方和接受方)需要建立一条连接,在发送数据后,通信双方需要断开连接,这就是TCP连接的建立和终止。
TCP连接的建立和终止
如果你看过我之前写的关于网络层的一篇文章,你应该知道TCP的基本元素有四个:即发送方的IP地址、发送方的端口号、接收方的IP地址、接收方的端口号。而每一方的IP+端口号都可以看作是一个套接字,套接字能够被唯一标示。套接字就相当于是门,出了这个门,就要进行数据传输了。
TCP的连接建立->终止总共分为三个阶段
点击加载图片
下面我们所讨论的重点也是集中在这三个层面。
下图是一个非常典型的TCP连接的建立和关闭过程,其中不包括数据传输的部分。
TCP建立连接-三次握手
点击加载图片
服务端进程准备好接收来自外部的TCP连接,一般情况下是调用bind、listen、socket三个函数完成。这种打开方式被认为是被动打开(passiveopen)。然后服务端进程处于LISTEN状态,等待客户端连接请求。
客户端通过connect发起主动打开(activeopen),向服务器发出连接请求,请求中首部同步位SYN=1,同时选择一个初始序号sequence,简写seq=x。SYN报文段不允许携带数据,只消耗一个序号。此时,客户端进入SYN-SEND状态。
服务器收到客户端连接后,,需要确认客户端的报文段。在确认报文段中,把SYN和ACK位都置为1。确认号是ack=x+1,同时也为自己选择一个初始序号seq=y。这个报文段也不能携带数据,但同样要消耗掉一个序号。此时,TCP服务器进入SYN-RECEIVED(同步收到)状态。
客户端在收到服务器发出的响应后,还需要给出确认连接。确认连接中的ACK置为1,序号为seq=x+1,确认号为ack=y+1。TCP规定,这个报文段可以携带数据也可以不携带数据,如果不携带数据,那么下一个数据报文段的序号仍是seq=x+1。这时,客户端进入ESTABLISHED(已连接)状态
服务器收到客户的确认后,也进入ESTABLISHED状态。
这是一个典型的三次握手过程,通过上面3个报文段就能够完成一个TCP连接的建立。三次握手的的目的不仅仅在于让通信双方知晓正在建立一个连接,也在于利用数据包中的选项字段来交换一些特殊信息,交换初始序列号。
一般首个发送SYN报文的一方被认为是主动打开一个连接,而这一方通常也被称为客户端。而SYN的接收方通常被称为服务端,它用于接收这个SYN,并发送下面的SYN,因此这种打开方式是被动打开。
TCP建立一个连接需要三个报文段,释放一个连接却需要四个报文段。
TCP断开连接-四次挥手
数据传输结束后,通信的双方可以释放连接。数据传输结束后的客户端主机和服务端主机都处于ESTABLISHED状态,然后进入释放连接的过程。
点击加载图片
TCP断开连接需要历经的过程如下
客户端应用程序发出释放连接的报文段,并停止发送数据,主动关闭TCP连接。客户端主机发送释放连接的报文段,报文段中首部FIN位置为1,不包含数据,序列号位seq=u,此时客户端主机进入FIN-WAIT-1(终止等待1)阶段。
服务器主机接受到客户端发出的报文段后,即发出确认应答报文,确认应答报文中ACK=1,生成自己的序号位seq=v,ack=u+1,然后服务器主机就进入CLOSE-WAIT(关闭等待)状态。
客户端主机收到服务端主机的确认应答后,即进入FIN-WAIT-2(终止等待2)的状态。等待客户端发出连接释放的报文段。
这时服务端主机会发出断开连接的报文段,报文段中ACK=1,序列号seq=v,ack=u+1,在发送完断开请求的报文后,服务端主机就进入了LAST-ACK(最后确认)的阶段。
客户端收到服务端的断开连接请求后,客户端需要作出响应,客户端发出断开连接的报文段,在报文段中,ACK=1,序列号seq=u+1,因为客户端从连接开始断开后就没有再发送数据,ack=v+1,然后进入到TIME-WAIT(时间等待)状态,请注意,这个时候TCP连接还没有释放。必须经过时间等待的设置,也就是2MSL后,客户端才会进入CLOSED状态,时间MSL叫做最长报文段寿命(MaximumSegmentLifetime)。
服务端主要收到了客户端的断开连接确认后,就会进入CLOSED状态。因为服务端结束TCP连接时间要比客户端早,而整个连接断开过程需要发送四个报文段,因此释放连接的过程也被称为四次挥手。
TCP连接的任意一方都可以发起关闭操作,只不过通常情况下发起关闭连接操作一般都是客户端。然而,一些服务器比如Web服务器在对请求作出相应后也会发起关闭连接的操作。TCP协议规定通过发送一个FIN报文来发起关闭操作。
所以综上所述,建立一个TCP连接需要三个报文段,而关闭一个TCP连接需要四个报文段。TCP协议还支持一种半开启(half-open)状态,虽然这种情况并不多见。
TCP半开启
TCP连接处于半开启的这种状态是因为连接的一方关闭或者终止了这个TCP连接却没有通知另一方,也就是说两个人正在微信聊天,cxuan你下线了你不告诉我,我还在跟你侃八卦呢。此时就认为这条连接处于半开启状态。这种情况发生在通信中的一方处于主机崩溃的情况下,你xxx的,我电脑死机了我咋告诉你?只要处于半连接状态的一方不传输数据的话,那么是无法检测出来对方主机已经下线的。
另外一种处于半开启状态的原因是通信的一方关闭了主机电源而不是正常关机。这种情况下会导致服务器上有很多半开启的TCP连接。
TCP半关闭
既然TCP支持半开启操作,那么我们可以设想TCP也支持半关闭操作。同样的,TCP半关闭也并不常见。TCP的半关闭操作是指仅仅关闭数据流的一个传输方向。两个半关闭操作合在一起就能够关闭整个连接。在一般情况下,通信双方会通过应用程序互相发送FIN报文段来结束连接,但是在TCP半关闭的情况下,应用程序会表明自己的想法:“我已经完成了数据的发送发送,并发送了一个FIN报文段给对方,但是我依然希望接收来自对方的数据直到它发送一个FIN报文段给我”。下面是一个TCP半关闭的示意图。
点击加载图片
解释一下这个过程:
首先客户端主机和服务器主机一直在进行数据传输,一段时间后,客户端发起了FIN报文,要求主动断开连接,服务器收到FIN后,回应ACK,由于此时发起半关闭的一方也就是客户端仍然希望服务器发送数据,所以服务器会继续发送数据,一段时间后服务器发送另外一条FIN报文,在客户端收到FIN报文回应ACK给服务器后,断开连接。
TCP的半关闭操作中,连接的一个方向被关闭,而另一个方向仍在传输数据直到它被关闭为止。只不过很少有应用程序使用这一特性。
同时打开和同时关闭
还有一种比较非常规的操作,这就是两个应用程序同时主动打开连接。虽然这种情况看起来不太可能,但是在特定的安排下却是有可能发生的。我们主要讲述这个过程。
通信双方在接收到来自对方的SYN之前会首先发送一个SYN,这个场景还要求通信双方都知道对方的IP地址+端口号。
下面是同时打开的例子
点击加载图片
如上图所示,通信双方都在收到对方报文前主动发送了SYN报文,都在收到彼此的报文后回复了一个ACK报文。
一个同时打开过程需要交换四个报文段,比普通的三次握手增加了一个,由于同时打开没有客户端和服务器一说,所以这里我用了通信双方来称呼。
像同时打开一样,同时关闭也是通信双方同时提出主动关闭请求,发送FIN报文,下图显示了一个同时关闭的过程。
点击加载图片
同时关闭过程中需要交换和正常关闭相同数量的报文段,只不过同时关闭不像四次挥手那样顺序进行,而是交叉进行的。
聊一聊初始序列号
也许是我上面图示或者文字描述的不专业,初始序列号它是有专业术语表示的,初始序列号的英文名称是Initialsequencenumbers(ISN),所以我们上面表示的seq=v,其实就表示的ISN。
在发送SYN之前,通信双方会选择一个初始序列号。初始序列号是随机生成的,每一个TCP连接都会有一个不同的初始序列号。RFC文档指出初始序列号是一个32位的计数器,每4us(微秒)+1。因为每个TCP连接都是一个不同的实例,这么安排的目的就是为了防止出现序列号重叠的情况。
当一个TCP连接建立的过程中,只有正确的TCP四元组和正确的序列号才会被对方接收。这也反应了TCP报文段容易被伪造的脆弱性,因为只要我伪造了一个相同的四元组和初始序列号就能够伪造TCP连接,从而打断TCP的正常连接,所以抵御这种攻击的一种方式就是使用初始序列号,另外一种方法就是加密序列号。
TCP状态转换
我们上面聊到了三次握手和四次挥手,提到了一些关于TCP连接之间的状态转换,那么下面我就从头开始和你好好梳理一下这些状态之间的转换。
首先第一步,刚开始时服务器和客户端都处于CLOSED状态,这时需要判断是主动打开还是被动打开,如果是主动打开,那么客户端向服务器发送SYN报文,此时客户端处于SYN-SEND状态,SYN-SEND表示发送连接请求后等待匹配的连接请求,服务器被动打开会处于LISTEN状态,用于监听SYN报文。如果客户端调用了close方法或者经过一段时间没有操作,就会重新变为CLOSED状态,这一步转换图如下
点击加载图片
这里有个疑问,为什么处于LISTEN状态下的客户端还会发送SYN变为SYN_SENT状态呢?
知乎看到了车小胖大佬的回答,这种情况可能出现在FTP中,LISTEN->SYN_SENT是因为这个连接可能是由于服务器端的应用有数据发送给客户端所触发的,客户端被动接受连接,连接建立后,开始传输文件。也就是说,处于LISTEN状态的服务器也是有可能发送SYN报文的,只不过这种情况非常少见。
处于SYN_SEND状态的服务器会接收SYN并发送SYN和ACK转换成为SYN_RCVD状态,同样的,处于LISTEN状态的客户端也会接收SYN并发送SYN和ACK转换为SYN_RCVD状态。如果处于SYN_RCVD状态的客户端收到RST就会变为LISTEN状态。
点击加载图片
这两张图一起看会比较好一些。
这里需要解释下什么是RST
这里有一种情况是当主机收到TCP报文段后,其IP和端口号不匹配的情况。假设客户端主机发送一个请求,而服务器主机经过IP和端口号的判断后发现不是给这个服务器的,那么服务器就会发出一个RST特殊报文段给客户端。
点击加载图片
因此,当服务端发送一个RST特殊报文段给客户端的时候,它就会告诉客户端没有匹配的套接字连接,请不要再继续发送了。
RST:(Resettheconnection)用于复位因某种原因引起出现的错误连接,也用来拒绝非法数据和请求。如果接收到RST位时候,通常发生了某些错误。
上面没有识别正确的IP端口是一种导致RST出现的情况,除此之外,RST还可能由于请求超时、取消一个已存在的连接等出现。
位于SYN_RCVD的服务器会接收ACK报文,SYN_SEND的客户端会接收SYN和ACK报文,并发送ACK报文,由此,客户端和服务器之间的连接就建立了。
点击加载图片
这里还要注意一点,同时打开的状态我在上面没有刻意表示出来,实际上,在同时打开的情况下,它的状态变化是这样的。
点击加载图片
为什么会是这样呢?因为你想,在同时打开的情况下,两端主机都发起SYN报文,而主动发起SYN的主机会处于SYN-SEND状态,发送完成后,会等待接收SYN和ACK,在双方主机都发送了SYN+ACK后,双方都处于SYN-RECEIVED(SYN-RCVD)状态,然后等待SYN+ACK的报文到达后,双方就会处于ESTABLISHED状态,开始传输数据。
好了,到现在为止,我给你叙述了一下TCP连接建立过程中的状态转换,现在你可以泡一壶茶喝点水,等着数据传输了。
好了,现在水喝够了,这时候数据也传输完成了,数据传输完成后,这条TCP连接就可以断开了。
现在我们把时钟往前拨一下,调整到服务端处于SYN_RCVD状态的时刻,因为刚收到了SYN包并发送了SYN+ACK包,此时服务端很开心,但是这时,服务端应用进程关闭了,然后应用进程发了一个FIN包,就会让服务器从SYN_RCVD->FIN_WAIT_1状态。
点击加载图片
然后把时钟调到现在,客户端和服务器现在已经传输完数据了,此时客户端发送了一条FIN报文希望断开连接,此时客户端也会变为FIN_WAIT_1状态,对于服务器来说,它接收到了FIN报文段并回复了ACK报文,就会从ESTABLISHED->CLOSE_WAIT状态。
点击加载图片
位于CLOSE_WAIT状态的服务端会发送FIN报文,然后把自己置于LAST_ACK状态。处于FIN_WAIT_1的客户端接收ACK消息就会变为FIN_WAIT_2状态。
这里需要先解释一下CLOSING这个状态,FIN_WAIT_1->CLOSING的转换比较特殊
CLOSING这种状态比较特殊,实际情况中应该是很少见,属于一种比较罕见的例外状态。正常情况下,当你发送FIN报文后,按理来说是应该先收到(或同时收到)对方的ACK报文,再收到对方的FIN报文。但是CLOSING状态表示你发送FIN报文后,并没有收到对方的ACK报文,反而却也收到了对方的FIN报文。
什么情况下会出现此种情况呢?其实细想一下,也不难得出结论:那就是如果双方在同时关闭一个链接的话,那么就出现了同时发送FIN报文的情况,也即会出现CLOSING状态,表示双方都正在关闭连接。
FIN_WAIT_2状态的客户端接收服务端主机发送的FIN+ACK消息,并发送ACK响应后,会变为TIME_WAIT状态。处于CLOSE_WAIT的客户端发送FIN会处于LAST_ACK状态。
这里不少图和博客虽然在图上画的是FIN+ACK报文后才会处于LAST_ACK状态,但是描述的时候,一般通常只对于FIN进行描述。也就是说CLOSE_WAIT发送FIN才会处于LAST_ACK状态。
点击加载图片
所以这里FIN_WAIT_1->TIME_WAIT的状态也就是接收FIN和ACK并发送ACK之后,客户端处于的状态。
然后位于CLOSINIG状态的客户端这时候还有ACK接收的话,会继续处于TIME_WAIT状态,可以看到,TIME_WAIT状态相当于是客户端在关闭前的最后一个状态,它是一种主动关闭的状态;而LAST_ACK是服务端在关闭前的最后一个状态,它是一种被动打开的状态。
上面有几个状态比较特殊,这里我们向西解释下。
TIME_WAIT状态
通信双方建立TCP连接后,主动关闭连接的一方就会进入TIME_WAIT状态。TIME_WAIT状态也称为2MSL的等待状态。在这个状态下,TCP将会等待最大段生存期(MaximumSegmentLifetime,MSL)时间的两倍。
这里需要解释下MSL
MSL是TCP段期望的最大生存时间,也就是在网络中存在的最长时间。这个时间是有限制的,因为我们知道TCP是依靠IP数据段来进行传输的,IP数据报中有TTL和跳数的字段,这两个字段决定了IP的生存时间,一般情况下,TCP的最大生存时间是2分钟,不过这个数值是可以修改的,根据不同操作系统可以修改此值。
基于此,我们来探讨TIME_WAIT的状态。
当TCP执行一个主动关闭并发送最终的ACK时,TIME_WAIT应该以2*最大生存时间存在,这样就能够让TCP重新发送最终的ACK以避免出现丢失的情况。重新发送最终的ACK并不是因为TCP重传了ACK,而是因为通信另一方重传了FIN,客户端经常回发送FIN,因为它需要ACK的响应才能够关闭连接,如果生存时间超过了2MSL的话,客户端就会发送RST,使服务端出错。
TCP超时和重传
没有永远不出错误的通信,这句话表明着不管外部条件多么完备,永远都会有出错的可能。所以,在TCP的正常通信过程中,也会出现错误,这种错误可能是由于数据包丢失引起的,也可能是由于数据包重复引起的,甚至可能是由于数据包失序引起的。
TCP的通信过程中,会由TCP的接收端返回一系列的确认信息来判断是否出现错误,一旦出现丢包等情况,TCP就会启动重传操作,重传尚未确认的数据。
TCP的重传有两种方式,一种是基于时间,一种是基于确认信息,一般通过确认信息要比通过时间更加高效。
所以从这点就可以看出,TCP的确认和重传,都是基于数据包是否被确认为前提的。
TCP在发送数据时会设置一个定时器,如果在定时器指定的时间内未收到确认信息,那么就会触发相应的超时或者基于计时器的重传操作,计时器超时通常被称为重传超时(RTO)。
但是有另外一种不会引起延迟的方式,这就是快速重传。
TCP在每次重传一次报文后,其重传时间都会加倍,这种'间隔时间加倍'被称为二进制指数补偿(binaryexponentialbackoff)。等到间隔时间加倍到15.5min后,客户端会显示
Connectionclosedbyforeignhost.
TCP拥有两个阈值来决定如何重传一个报文段,这两个阈值被定义在RFC[RCF1122]中,第一个阈值是R1,它表示愿意尝试重传的次数,阈值R2表示TCP应该放弃连接的时间。R1和R2应至少设为三次重传和100秒放弃TCP连接。
这里需要注意下,对连接建立报文SYN来说,它的R2至少应该设置为3分钟,但是在不同的系统中,R1和R2值的设置方式也不同。
在Linux系统中,R1和R2的值可以通过应用程序来设置,或者是修改net.ipv4.tcp_retries1和net.ipv4.tcp_retries2的值来设置。变量值就是重传次数。
tcp_retries2的默认值是15,这个充实次数的耗时大约是13-30分钟,这只是一个大概值,最终耗时时间还要取决于RTO,也就是重传超时时间。tcp_retries1的默认值是3。
对于SYN段来说,net.ipv4.tcp_syn_retries和net.ipv4.tcp_synack_retries这两个值限制了SYN的重传次数,默认是5,大约是180秒。
Windows操作系统下也有R1和R2变量,它们的值被定义在下方的注册表中
HKLM\System\CurrentControlSet\Services\Tcpip\Parameters
HKLM\System\CurrentControlSet\Services\Tcpip6\Parameters
其中有一个非常重要的变量就是TcpMaxDataRetransmissions,这个TcpMaxDataRetransmissions对应Linux中的tcp_retries2变量,默认值是5。这个值的意思表示的是TCP在现有连接上未确认数据段的次数。
快速重传
我们上面提到了快速重传,实际上快速重传机制是基于接收端的反馈信息来触发的,它并不受重传计时器的影响。所以与超时重传相比,快速重传能够有效的修复丢包情况。当TCP连接的过程中接收端出现乱序的报文(比如2-4-3)到达时,TCP需要立刻生成确认消息,这种确认消息也被称为重复ACK。
当失序报文到达时,重复ACK要做到立刻返回,不允许延迟发送,此举的目的是要告诉发送方某段报文失序到达了,希望发送方指出失序报文段的序列号。
还有一种情况也会导致重复ACK发给发送方,那就是当前报文段的后续报文发送至接收端,由此可以判断当前发送方的报文段丢失或者延迟到达。因为这两种情况导致的后果都是接收方没有收到报文,但是我们却无法判断到底是报文段丢失还是报文段没有送达。因此TCP发送端会等待一定数目的重复ACK被接受来决定数据是否丢失并触发快速重传。一般这个判断的数量是3,这段文字表述可能无法清晰理解,我们举个例子。
点击加载图片
如上图所示,报文段1成功接收并被确认为ACK2,接收端的期待序号为2,当报文段2丢失后,报文段3。失序到达,但是与接收端的期望不匹配,所以接收端会重复发送冗余ACK2。
这样,在超时重传定时器到期之前,接收收到连续三个相同的ACK后,发送端就知道哪个报文段丢失了,于是发送方会重发这个丢失的报文段,这样就不用等待重传定时器的到期,大大提高了效率。
SACK
在标准的TCP确认机制中,如果发送方发送了0-10000序号之间的数据,但是接收方只接收到了0-1000,3000-10000之间的数据,而1000-3000之间的数据没有到达接收端,此时发送方会重传1000-10000之间的数据,实际上这是没有必要的,因为3000后面的数据已经被接收了。但是发送方无法感知这种情况的存在。
如何避免或者说解决这种问题呢?
为了优化这种情况,我们有必要让客户端知道更多的消息,在TCP报文段中,有一个SACK选项字段,这个字段是一种**选择性确认(selectiveacknowledgment)**机制,这个机制能告诉TCP客户端,用我们的俗语来解释就是:“我这里最多允许接收1000之后的报文段,但是我却收到了3000-10000的报文段,请给我1000-3000之间的报文段”。
但是,这个选择性确认机制的是否开启还受一个字段的影响,这个字段就是SACK允许选项字段,通信双方在SYN段或者SYN+ACK段中添加SACK允许选项字段来通知对端主机是否支持SACK,如果双方都支持的话,后续在SYN段中就可以使用SACK选项了。
这里需要注意下:SACK选项字段只能出现在SYN段中。
伪超时和重传
在某些情况下,即使没有出现报文段的丢失也可能会引发报文重传。这种重传行为被称为伪重传(spuriousretransmission),这种重传是没有必要的,造成这种情况的因素可能是由于伪超时(spurioustimeout),伪超时的意思就是过早的判定超时发生。造成伪超时的因素有很多,比如报文段失序到达,报文段重复,ACK丢失等情况。
点击加载图片
检测和处理伪超时的方法有很多,这些方法统称为检测算法和响应算法。检测算法用于判断是否出现了超时现象或出现了计时器的重传现象。一旦出现了超时或者重传的情况,就会执行响应算法撤销或者减轻超时带来的影响,下面是几种算法,此篇文章暂不深入这些实现细节
重复SACK扩展-DSACK
Eifel检测算法
前移RTO恢复-F-RTO
Eifel响应算法
包失序和包重复
上面我们讨论的都是TCP如何处理丢包的问题,我们下面来讨论一下包失序和包重复的问题。
包失序
数据包的失序到达是互联网中极其容易出现的一种情况,由于IP层并不能保证数据包的有序性,每个数据包的发送都可能会选择当前情况传输速度最快的链路,所以很有可能出现发送了A->B->C的三个数据包,到达接收端的数据包顺序是C->A->B或者B->C->A等等。这就是包失序的一种现象。
在包传输中,主要分为两种链路:正向链路(SYN)和反向链路(ACK)
如果失序发生在正向链路,TCP是无法正确判断数据包是否丢失的,数据的丢失和失序都会导致接收端收到无序的数据包,造成数据之间的空缺。如果这种空缺不够大的话,这种情况影响不大;但是如果空缺比较大的话,可能会导致伪重传。
如果失序发生在反向链路,就会使TCP的窗口前移,然后收到重复而应该被丢弃的ACK,导致发送端出现不必要的流量突发,影响可用网络带宽。
回到我们上面讨论的快速重传,由于快速重传是根据重复ACK推断出现丢包而启动的,它不用等到重传计时器超时。由于TCP接收端会对接收到的失序报文立刻返回ACK,所以网络中任何一个失序到达的报文都可能会造成重复ACK。假设一旦收到ACK,就会启动快速重传机制,当ACK数量激增,就会导致大量不必要的重传发生,所以快速重传应该达到重复阈值(dupthresh)再触发。但是在互联网中,严重的失序并不常见,因此dupthresh的值可以设置的尽量小,一般来说3就能处理绝大部分情况。
包重复
包重复也是互联网中出现很少的一种情况,它指的是在网络传输过程中,包可能会出现传输多次的情况,当重传生成时,TCP可能会出现混淆。
包的重复可以使接收端生成一系列的重复ACK,这种情况可以使用SACK协商来解决。
TCP数据流和窗口管理
我们在40张图带你搞懂TCP和UDP这篇文章中知道了可以使用滑动窗口来实现流量控制,也就是说,客户端和服务器可以相互提供数据流信息的交换,数据流的相关信息主要包括报文段序列号、ACK号和窗口大小。
点击加载图片
图中的两个箭头表示数据流方向,数据流方向也就是TCP报文段的传输方向。可以看到,每个TCP报文段中都包括了序列号、ACK和窗口信息,可能还会有用户数据。TCP报文段中的窗口大小表示接收端还能够接收的缓存空间的大小,以字节为单位。这个窗口大小是一种动态的,因为无时无刻都会有报文段的接收和消失,这种动态调整的窗口大小我们称之为滑动窗口,下面我们就来具体认识一下滑动窗口。
滑动窗口
TCP连接的每一端都可以发送数据,但是数据的发送不是没有限制的,实际上,TCP连接的两端都各自维护了一个发送窗口结构(sendwindowstructure)和接收窗口结构(receivewindowstructure),这两个窗口结构就是数据发送的限制。
发送方窗口
下图是一个发送方窗口的示例。
点击加载图片
在这幅图中,涉及滑动窗口的四种概念:
已经发送并确认的报文段:发送给接收方后,接收方回回复ACK来对报文段进行响应,图中标注绿色的报文段就是已经经过接收方确认的报文段。
已经发送但是还没确认的报文段:图中绿色区域是经过接收方确认的报文段,而浅蓝色这段区域指的是已经发送但是还未经过接收方确认的报文段。
等待发送的报文段:图中深蓝色区域是等待发送的报文段,它属于发送窗口结构的一部分,也就是说,发送窗口结构其实是由已发送未确认+等待发送的报文段构成。
窗口滑动时才能发送的报文段:如果图中的[4,9]这个集合内的报文段发送完毕后,整个滑动窗口会向右移动,图中橙色区域就是窗口右移时才能发送的报文段。
滑动窗口也是有边界的,这个边界是Leftedge和Rightedge,Leftedge是窗口的左边界,Rightedge是窗口的右边界。
当Leftedge向右移动而Rightedge不变时,这个窗口可能处于close关闭状态。随着已发送的数据逐渐被确认从而导致窗口变小时,就会发生这种情况。
点击加载图片
当Rightedge向右移动时,窗口会处于open打开状态,允许发送更多的数据。当接收端进程读取缓冲区数据,从而使缓冲区接收更多数据时,就会处于这种状态。
点击加载图片
还可能会发生Rightedge向左移动的情况,会导致发送并确认的报文段变小,这种情况被称为糊涂窗口综合症,这种情况是我们不愿意看到的。出现糊涂窗口综合症时,通信双方用于交换的数据段大小会变小,而网络固定的开销却没有变化,每个报文段中有用数据相对于头部信息的比例较小,导致传输效率非常低。
这就相当于之前你明明有能力花一天时间写完一个复杂的页面,现在你花了一天的时间却改了一个标题的bug,大材小用。
每个TCP报文段都包含ACK号和窗口通告信息,所以每当收到响应时,TCP接收方都会根据这两个参数调整窗口结构。
TCP滑动窗口的Leftedge永远不可能向左移动,因为发送并确认的报文段永远不可能被取消,就像这世界上没有后悔药一样。这条边缘是由另一段发送的ACK号控制的。当ACK标号使窗口向右移动但是窗口大小没有改变时,则称该窗口向前滑动。
如果ACK的编号增加但是窗口通告信息随着其他ACK的到达却变小了,此时Leftedge会接近Rightedge。当Leftedge和Rightedge重合时,此时发送方不会再传输任何数据,这种情况被称为零窗口。此时TCP发送方会发起窗口探测,等待合适的时机再发送数据。
接收方窗口
接收方也维护了一个窗口结构,这个窗口要比发送方的简单很多。这个窗口记录了已经接收并确认的数据,以及它能够接收的最大序列号。接收方的窗口结构不会存储重复的报文段和ACK,同时接收方的窗口也不会记录不应该收到的报文段和ACK。下面是TCP接收方的窗口结构。
点击加载图片
与发送端的窗口一样,接收方窗口结构也维护了一个Leftedge和Rightedge。位于Leftedge左边的被称为已经接收并确认的报文段,位于Rightedge右边的被称为不能接收的报文段。
对于接收端来说,到达序列号小于Leftefge的被认为是已经重复的数据,需要丢弃。超过Rightedge的被认为超出处理范围。只有当到达的报文段等于Leftedge时,数据才不会被丢弃,窗口才能够向前滑动。
接收方窗口结构也会存在零窗口的情况,如果某个应用进程消耗数据很慢,而TCP发送方却发送了大量的数据给接收方,会造成TCP缓冲区溢出,通告发送方不要再发送数据了,但是应用进程却以非常慢的速度消耗缓冲区的数据(比如1字节),就会告诉接收端只能发送一个字节的数据,这个过程慢慢持续,造成网络开销大,效率很低。
我们上面提到了窗口存在Leftedge=Rightedge的情况,此时被称为零窗口,下面我们就来具体研究一下零窗口。
零窗口
TCP是通过接收端的窗口通告信息来实现流量控制的。通告窗口告诉了TCP,接收端能够接收的数据量。当接收方的窗口变为0时,可以有效的阻止发送端继续发送数据。当接收端重新获得可用空间时,它会给发送端传输一个窗口更新告知自己能够接收数据了。窗口更新一般是纯ACK,即不带任何数据。但是纯ACK不能保证一定会到达发送端,于是需要有相关的措施能够处理这种丢包。
如果纯ACK丢失的话,通信双方就会一直处于等待状态,发送方心想拉垮的接收端怎么还让我发送数据!接收端心想天杀的发送方怎么还不发数据!为了防止这种情况,发送方会采用一个持续计时器来间歇性的查询接收方,看看其窗口是否已经增长。持续计时器会触发窗口探测,强制要求接收方返回带有更新窗口的ACK。
窗口探测包含一个字节的数据,采用的是TCP丢失重传的方式。当TCP持续计时器超时后,就会触发窗口探测的发送。一个字节的数据能否被接收端接收,还要取决于其缓冲区的大小。
拥塞控制
有了TCP的窗口控制后,使计算机网络中两个主机之间不再是以单个数据段的形式发送了,而是能够连续发送大量的数据包。然而,大量数据包同时也伴随着其他问题,比如网络负载、网络拥堵等问题。TCP为了防止这类问题的出现,使用了拥塞控制机制,拥塞控制机制会在面临网络拥塞时遏制发送方的数据发送。
拥塞控制主要有两种方法
端到端的拥塞控制:因为网络层没有为运输层拥塞控制提供显示支持。所以即使网络中存在拥塞情况,端系统也要通过对网络行为的观察来推断。TCP就是使用了端到端的拥塞控制方式。IP层不会向端系统提供有关网络拥塞的反馈信息。那么TCP如何推断网络拥塞呢?如果超时或者三次冗余确认就被认为是网络拥塞,TCP会减小窗口的大小,或者增加往返时延来避免。
网络辅助的拥塞控制:在网络辅助的拥塞控制中,路由器会向发送方提供关于网络中拥塞状态的反馈。这种反馈信息就是一个比特信息,它指示链路中的拥塞情况。
下图描述了这两种拥塞控制方式
点击加载图片
TCP拥塞控制
如果你看到这里,那我就暂定认为你了解了TCP实现可靠性的基础了,那就是使用序号和确认号。除此之外,另外一个实现TCP可靠性基础的就是TCP的拥塞控制。如果说
TCP所采用的方法是让每一个发送方根据所感知到的网络的拥塞程度来限制发出报文段的速率,如果TCP发送方感知到没有什么拥塞,则TCP发送方会增加发送速率;如果发送方感知沿着路径有阻塞,那么发送方就会降低发送速率。
但是这种方法有三个问题
TCP发送方如何限制它向其他连接发送报文段的速率呢?
一个TCP发送方是如何感知到网络拥塞的呢?
当发送方感知到端到端的拥塞时,采用何种算法来改变其发送速率呢?
我们先来探讨一下第一个问题,TCP发送方如何限制它向其他连接发送报文段的速率呢?
我们知道TCP是由接收缓存、发送缓存和变量(LastByteRead,rwnd,等)组成。发送方的TCP拥塞控制机制会跟踪一个变量,即拥塞窗口(congestionwindow)的变量,拥塞窗口表示为cwnd,用于限制TCP在接收到ACK之前可以发送到网络的数据量。而接收窗口(rwnd)是一个用于告诉接收方能够接受的数据量。
一般来说,发送方未确认的数据量不得超过cwnd和rwnd的最小值,也就是
LastByteSent-LastByteAcked<=min(cwnd,rwnd)
由于每个数据包的往返时间是RTT,我们假设接收端有足够的缓存空间用于接收数据,我们就不用考虑rwnd了,只专注于cwnd,那么,该发送方的发送速率大概是cwnd/RTT字节/秒。通过调节cwnd,发送方因此能调整它向连接发送数据的速率。
一个TCP发送方是如何感知到网络拥塞的呢?
这个我们上面讨论过,是TCP根据超时或者3个冗余ACK来感知的。
当发送方感知到端到端的拥塞时,采用何种算法来改变其发送速率呢?
这个问题比较复杂,且容我娓娓道来,一般来说,TCP会遵循下面这几种指导性原则
如果在报文段发送过程中丢失,那就意味着网络拥堵,此时需要适当降低TCP发送方的速率。
一个确认报文段指示发送方正在向接收方传递报文段,因此,当对先前未确认报文段的确认到达时,能够增加发送方的速率。为啥呢?因为未确认的报文段到达接收方也就表示着网络不拥堵,能够顺利到达,因此发送方拥塞窗口长度会变大,所以发送速率会变快
带宽探测,带宽探测说的是TCP可以通过调节传输速率来增加/减小ACK到达的次数,如果出现丢包事件,就会减小传输速率。因此,为了探测拥塞开始出现的频率,TCP发送方应该增加它的传输速率。然后慢慢使传输速率降低,进而再次开始探测,看看拥塞开始速率是否发生了变化。
在了解完TCP拥塞控制后,下面我们就该聊一下TCP的拥塞控制算法(TCPcongestioncontrolalgorithm)了。TCP拥塞控制算法主要包含三个部分:慢启动、拥塞避免、快速恢复,下面我们依次来看一下
慢启动
当一条TCP开始建立连接时,cwnd的值就会初始化为一个MSS的较小值。这就使得初始发送速率大概是MSS/RTT字节/秒,比如要传输1000字节的数据,RTT为200ms,那么得到的初始发送速率大概是40kb/s。实际情况下可用带宽要比这个MSS/RTT大得多,因此TCP想要找到最佳的发送速率,可以通过慢启动(slow-start)的方式,在慢启动的方式中,cwnd的值会初始化为1个MSS,并且每次传输报文确认后就会增加一个MSS,cwnd的值会变为2个MSS,这两个报文段都传输成功后每个报文段+1,会变为4个MSS,依此类推,每成功一次cwnd的值就会翻倍。如下图所示
点击加载图片
发送速率不可能会一直增长,增长总有结束的时候,那么何时结束呢?慢启动通常会使用下面这几种方式结束发送速率的增长。
如果在慢启动的发送过程出现丢包的情况,那么TCP会将发送方的cwnd设置为1并重新开始慢启动的过程,此时会引入一个ssthresh(慢启动阈值)的概念,它的初始值就是产生丢包的cwnd的值/2,即当检测到拥塞时,ssthresh的值就是窗口值的一半。
第二种方式是直接和ssthresh的值相关联,因为当检测到拥塞时,ssthresh的值就是窗口值的一半,那么当cwnd>ssthresh时,每次翻番都可能会出现丢包,所以最好的方式就是cwnd的值=ssthresh,这样TCP就会转为拥塞控制模式,结束慢启动。
慢启动结束的最后一种方式就是如果检测到3个冗余ACK,TCP就会执行一种快速重传并进入恢复状态。
拥塞避免
当TCP进入拥塞控制状态后,cwnd的值就等于拥塞时值的一半,也就是ssthresh的值。所以,无法每次报文段到达后都将cwnd的值再翻倍。而是采用了一种相对保守的方式,每次传输完成后只将cwnd的值增加一个MSS,比如收到了10个报文段的确认,但是cwnd的值只增加一个MSS。这是一种线性增长模式,它也会有增长逾值,它的增长逾值和慢启动一样,如果出现丢包,那么cwnd的值就是一个MSS,ssthresh的值就等于cwnd的一半;或者是收到3个冗余的ACK响应也能停止MSS增长。如果TCP将cwnd的值减半后,仍然会收到3个冗余ACK,那么就会将ssthresh的值记录为cwnd值的一半,进入快速恢复状态。
快速恢复
在快速恢复中,对于使TCP进入快速恢复状态缺失的报文段,对于每个收到的冗余ACK,cwnd的值都会增加一个MSS。当对丢失报文段的一个ACK到达时,TCP在降低cwnd后进入拥塞避免状态。如果在拥塞控制状态后出现超时,那么就会迁移到慢启动状态,cwnd的值被设置为1个MSS,ssthresh的值设置为cwnd的一半。