TCP 是一种面向链接的单播
协议,在 TCP 中,并不存在多播、广播的这种行为,由于 TCP 报文段中能明确发送方和接受方的 IP 地址。程序员
在发送数据前,相互通讯的双方(即发送方和接受方)须要创建一条链接
,在发送数据后,通讯双方须要断开链接,这就是 TCP 链接的创建和终止。算法
若是你看过我以前写的关于网络层的一篇文章,你应该知道 TCP 的基本元素有四个:即发送方的 IP 地址、发送方的端口号、接收方的 IP 地址、接收方的端口号。而每一方的 IP + 端口号均可以看做是一个套接字
,套接字可以被惟一标示。套接字就至关因而门,出了这个门,就要进行数据传输了。shell
TCP 的链接创建 -> 终止总共分为三个阶段缓存
下面咱们所讨论的重点也是集中在这三个层面。服务器
下图是一个很是典型的 TCP 链接的创建和关闭过程,其中不包括数据传输的部分。微信
被动打开(passive open)
。而后服务端进程处于 LISTEN
状态,等待客户端链接请求。connect
发起主动打开(active open)
,向服务器发出链接请求,请求中首部同步位 SYN = 1,同时选择一个初始序号 sequence ,简写 seq = x。SYN 报文段不容许携带数据,只消耗一个序号。此时,客户端进入 SYN-SEND
状态。SYN-RECEIVED(同步收到)
状态。ESTABLISHED (已链接)
状态ESTABLISHED
状态。这是一个典型的三次握手过程,经过上面 3 个报文段就可以完成一个 TCP 链接的创建。三次握手的的目的不只仅在于让通讯双方知晓正在创建一个链接,也在于利用数据包中的选项字段来交换一些特殊信息,交换初始序列号。网络
通常首个发送 SYN 报文的一方被认为是主动打开一个链接,而这一方一般也被称为客户端
。而 SYN 的接收方一般被称为服务端
,它用于接收这个 SYN,并发送下面的 SYN,所以这种打开方式是被动打开。
TCP 创建一个链接须要三个报文段,释放一个链接却须要四个报文段。并发
数据传输结束后,通讯的双方能够释放链接。数据传输结束后的客户端主机和服务端主机都处于 ESTABLISHED 状态,而后进入释放链接的过程。socket
TCP 断开链接须要历经的过程以下tcp
FIN-WAIT-1(终止等待 1)
阶段。CLOSE-WAIT(关闭等待)
状态。FIN-WAIT-2(终止等待2)
的状态。等待客户端发出链接释放的报文段。LAST-ACK(最后确认)
的阶段。TIME-WAIT(时间等待)
状态,请注意,这个时候 TCP 链接尚未释放。必须通过时间等待的设置,也就是 2MSL
后,客户端才会进入 CLOSED
状态,时间 MSL 叫作最长报文段寿命(Maximum Segment Lifetime)
。TCP 链接的任意一方均可以发起关闭操做,只不过一般状况下发起关闭链接操做通常都是客户端。然而,一些服务器好比 Web 服务器在对请求做出相应后也会发起关闭链接的操做。TCP 协议规定经过发送一个 FIN 报文来发起关闭操做。
因此综上所述,创建一个 TCP 链接须要三个报文段,而关闭一个 TCP 链接须要四个报文段。TCP 协议还支持一种半开启(half-open)
状态,虽然这种状况并很少见。
TCP 链接处于半开启的这种状态是由于链接的一方关闭或者终止了这个 TCP 链接却没有通知另外一方,也就是说两我的正在微信聊天,cxuan 你下线了你不告诉我,我还在跟你侃八卦呢。此时就认为这条链接处于半开启
状态。这种状况发生在通讯中的一方处于主机崩溃的状况下,你 xxx 的,我电脑死机了我咋告诉你?只要处于半链接状态的一方不传输数据的话,那么是没法检测出来对方主机已经下线的。
另一种处于半开启状态的缘由是通讯的一方关闭了主机电源 而不是正常关机。这种状况下会致使服务器上有不少半开启的 TCP 链接。
既然 TCP 支持半开启操做,那么咱们能够设想 TCP 也支持半关闭操做。一样的,TCP 半关闭也并不常见。TCP 的半关闭操做是指仅仅关闭数据流的一个传输方向。两个半关闭操做合在一块儿就可以关闭整个链接。在通常状况下,通讯双方会经过应用程序互相发送 FIN 报文段来结束链接,可是在 TCP 半关闭的状况下,应用程序会代表本身的想法:"我已经完成了数据的发送发送,并发送了一个 FIN 报文段给对方,可是我依然但愿接收来自对方的数据直到它发送一个 FIN 报文段给我"。 下面是一个 TCP 半关闭的示意图。
解释一下这个过程:
首先客户端主机和服务器主机一直在进行数据传输,一段时间后,客户端发起了 FIN 报文,要求主动断开链接,服务器收到 FIN 后,回应 ACK ,因为此时发起半关闭的一方也就是客户端仍然但愿服务器发送数据,因此服务器会继续发送数据,一段时间后服务器发送另一条 FIN 报文,在客户端收到 FIN 报文回应 ACK 给服务器后,断开链接。
TCP 的半关闭操做中,链接的一个方向被关闭,而另外一个方向仍在传输数据直到它被关闭为止。只不过不多有应用程序使用这一特性。
还有一种比较很是规的操做,这就是两个应用程序同时主动打开链接。虽然这种状况看起来不太可能,可是在特定的安排下倒是有可能发生的。咱们主要讲述这个过程。
通讯双方在接收到来自对方的 SYN 以前会首先发送一个 SYN,这个场景还要求通讯双方都知道对方的 IP 地址 + 端口号。
下面是同时打开的例子
如上图所示,通讯双方都在收到对方报文前主动发送了 SYN 报文,都在收到彼此的报文后回复了一个 ACK 报文。
一个同时打开过程须要交换四个报文段,比普通的三次握手增长了一个,因为同时打开没有客户端和服务器一说,因此这里我用了通讯双方来称呼。
像同时打开同样,同时关闭也是通讯双方同时提出主动关闭请求,发送 FIN 报文,下图显示了一个同时关闭的过程。
同时关闭过程当中须要交换和正常关闭相同数量的报文段,只不过同时关闭不像四次挥手那样顺序进行,而是交叉进行的。
也许是我上面图示或者文字描述的不专业,初始序列号它是有专业术语表示的,初始序列号的英文名称是Initial sequence numbers (ISN),因此咱们上面表示的 seq = v,其实就表示的 ISN。
在发送 SYN 以前,通讯双方会选择一个初始序列号。初始序列号是随机生成的,每个 TCP 链接都会有一个不一样的初始序列号。RFC 文档指出初始序列号是一个 32 位的计数器,每 4 us(微秒) + 1。由于每一个 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:(Reset the connection)用于复位因某种缘由引发出现的错误链接,也用来拒绝非法数据和请求。若是接收到 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 是服务端在关闭前的最后一个状态,它是一种被动打开的状态。
上面有几个状态比较特殊,这里咱们向西解释下。
通讯双方创建 TCP 链接后,主动关闭链接的一方就会进入 TIME_WAIT 状态。TIME_WAIT 状态也称为 2MSL
的等待状态。在这个状态下,TCP 将会等待最大段生存期(Maximum Segment Lifetime, 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 在发送数据时会设置一个定时器,若是在定时器指定的时间内未收到确认信息,那么就会触发相应的超时或者基于计时器的重传操做,计时器超时一般被称为重传超时(RTO)。
可是有另一种不会引发延迟的方式,这就是快速重传。
TCP 在每次重传一次报文后,其重传时间都会加倍
,这种"间隔时间加倍"被称为二进制指数补偿(binary exponential backoff) 。等到间隔时间加倍到 15.5 min 后,客户端会显示
Connection closed by foreign host.
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 成功接收并被确认为 ACK 2,接收端的期待序号为 2,当报文段 2 丢失后,报文段 3。失序到达,可是与接收端的指望不匹配,因此接收端会重复发送冗余 ACK 2。
这样,在超时重传定时器到期以前,接收收到连续三个相同的 ACK 后,发送端就知道哪一个报文段丢失了,因而发送方会重发这个丢失的报文段,这样就不用等待重传定时器的到期,大大提升了效率。
在标准的 TCP 确认机制中,若是发送方发送了 0 - 10000 序号之间的数据,可是接收方只接收到了 0 -1000, 3000 - 10000 之间的数据,而 1000 - 3000 之间的数据没有到达接收端,此时发送方会重传 1000 - 10000 之间的数据,实际上这是没有必要的,由于 3000 后面的数据已经被接收了。可是发送方没法感知这种状况的存在。
如何避免或者说解决这种问题呢?
为了优化这种状况,咱们有必要让客户端知道更多的消息,在 TCP 报文段中,有一个 SACK 选项字段,这个字段是一种选择性确认(selective acknowledgment)机制,这个机制能告诉 TCP 客户端,用咱们的俗语来解释就是:“我这里最多容许接收 1000 以后的报文段,可是我却收到了 3000 - 10000 的报文段,请给我 1000 - 3000 之间的报文段”。
可是,这个选择性确认机制的是否开启还受一个字段的影响,这个字段就是 SACK 容许选项字段,通讯双方在 SYN 段或者 SYN + ACK 段中添加 SACK 容许选项字段来通知对端主机是否支持 SACK,若是双方都支持的话,后续在 SYN 段中就可使用 SACK 选项了。
这里须要注意下:SACK 选项字段只能出如今 SYN 段中。
在某些状况下,即便没有出现报文段的丢失也可能会引起报文重传。这种重传行为被称为 伪重传(spurious retransmission) ,这种重传是没有必要的,形成这种状况的因素多是因为伪超时(spurious timeout),伪超时的意思就是过早的断定超时发生。形成伪超时的因素有不少,好比报文段失序到达,报文段重复,ACK 丢失等状况。
检测和处理伪超时的方法有不少,这些方法统称为检测
算法和响应
算法。检测算法用于判断是否出现了超时现象或出现了计时器的重传现象。一旦出现了超时或者重传的状况,就会执行响应算法撤销或者减轻超时带来的影响,下面是几种算法,此篇文章暂不深刻这些实现细节
上面咱们讨论的都是 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 协商来解决。
咱们在 40 张图带你搞懂 TCP 和 UDP 这篇文章中知道了可使用滑动窗口来实现流量控制,也就是说,客户端和服务器能够相互提供数据流信息的交换,数据流的相关信息主要包括报文段序列号、ACK 号和窗口大小。
图中的两个箭头表示数据流方向,数据流方向也就是 TCP 报文段的传输方向。能够看到,每一个 TCP 报文段中都包括了序列号、ACK 和窗口信息,可能还会有用户数据。TCP 报文段中的窗口大小表示接收端还可以接收的缓存空间的大小,以字节为单位。这个窗口大小是一种动态的,由于无时无刻都会有报文段的接收和消失,这种动态调整的窗口大小咱们称之为滑动窗口
,下面咱们就来具体认识一下滑动窗口。
TCP 链接的每一端均可以发送数据,可是数据的发送不是没有限制的,实际上,TCP 链接的两端都各自维护了一个发送窗口结构 (send window structure) 和 接收窗口结构 (receive window structure),这两个窗口结构就是数据发送的限制。
下图是一个发送方窗口的示例。
在这幅图中,涉及滑动窗口的四种概念:
滑动窗口也是有边界的,这个边界是 Left edge
和 Right edge
,Left edge 是窗口的左边界,Right edge 是窗口的右边界。
当 Left edge 向右移动而 Right edge 不变时,这个窗口可能处于 close
关闭状态。随着已发送的数据逐渐被确认从而致使窗口变小时,就会发生这种状况。
当 Right edge 向右移动时,窗口会处于 open
打开状态,容许发送更多的数据。当接收端进程读取缓冲区数据,从而使缓冲区接收更多数据时,就会处于这种状态。
还可能会发生 Right edge 向左移动的状况,会致使发送并确认的报文段变小,这种状况被称为糊涂窗口综合症,这种状况是咱们不肯意看到的。出现糊涂窗口综合症时,通讯双方用于交换的数据段大小会变小,而网络固定的开销却没有变化,每一个报文段中有用数据相对于头部信息的比例较小,致使传输效率很是低。
这就至关于以前你明明有能力花一天时间写完一个复杂的页面,如今你花了一天的时间却改了一个标题的 bug,大材小用。
每一个 TCP 报文段都包含ACK 号和窗口通告信息,因此每当收到响应时,TCP 接收方都会根据这两个参数调整窗口结构。
TCP 滑动窗口的 Left edge 永远不可能向左移动,由于发送并确认的报文段永远不可能被取消,就像这世界上没有后悔药同样。这条边缘是由另外一段发送的 ACK 号控制的。当 ACK 标号使窗口向右移动可是窗口大小没有改变时,则称该窗口向前滑动。
若是 ACK 的编号增长可是窗口通告信息随着其余 ACK 的到达却变小了,此时 Left edge 会接近 Right edge。当 Left edge 和 Right edge 重合时,此时发送方不会再传输任何数据,这种状况被称为零窗口
。此时 TCP 发送方会发起窗口探测
,等待合适的时机再发送数据。
接收方也维护了一个窗口结构,这个窗口要比发送方的简单不少。这个窗口记录了已经接收并确认的数据,以及它可以接收的最大序列号。接收方的窗口结构不会存储重复的报文段和 ACK,同时接收方的窗口也不会记录不该该收到的报文段和 ACK。下面是 TCP 接收方的窗口结构。
与发送端的窗口同样,接收方窗口结构也维护了一个 Left edge 和 Right edge。位于 Left edge 左边的被称为已经接收并确认的报文段,位于 Right edge 右边的被称为不能接收的报文段。
对于接收端来讲,到达序列号小于 Left efge 的被认为是已经重复的数据,须要丢弃。超过 Right edge 的被认为超出处理范围。只有当到达的报文段等于 Left edge 时,数据才不会被丢弃,窗口才可以向前滑动。
接收方窗口结构也会存在零窗口的状况,若是某个应用进程消耗数据很慢,而 TCP 发送方却发送了大量的数据给接收方,会形成 TCP 缓冲区溢出,通告发送方不要再发送数据了,可是应用进程却以很是慢的速度消耗缓冲区的数据(好比 1 字节),就会告诉接收端只能发送一个字节的数据,这个过程慢慢持续,形成网络开销大,效率很低。
咱们上面提到了窗口存在 Left edge = Right edge 的状况,此时被称为零窗口,下面咱们就来具体研究一下零窗口。
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 是由接收缓存、发送缓存和变量(LastByteRead, rwnd,等)
组成。发送方的 TCP 拥塞控制机制会跟踪一个变量,即 拥塞窗口(congestion window)
的变量,拥塞窗口表示为 cwnd
,用于限制 TCP 在接收到 ACK 以前能够发送到网络的数据量。而接收窗口(rwnd)
是一个用于告诉接收方可以接受的数据量。
通常来讲,发送方未确认的数据量不得超过 cwnd 和 rwnd 的最小值,也就是
LastByteSent - LastByteAcked <= min(cwnd,rwnd)
因为每一个数据包的往返时间是 RTT,咱们假设接收端有足够的缓存空间用于接收数据,咱们就不用考虑 rwnd 了,只专一于 cwnd,那么,该发送方的发送速率大概是 cwnd/RTT 字节/秒
。经过调节 cwnd,发送方所以能调整它向链接发送数据的速率。
一个 TCP 发送方是如何感知到网络拥塞的呢?
这个咱们上面讨论过,是 TCP 根据超时或者 3 个冗余 ACK 来感知的。
当发送方感知到端到端的拥塞时,采用何种算法来改变其发送速率呢 ?
这个问题比较复杂,且容我娓娓道来,通常来讲,TCP 会遵循下面这几种指导性原则
带宽探测
,带宽探测说的是 TCP 能够经过调节传输速率来增长/减少 ACK 到达的次数,若是出现丢包事件,就会减少传输速率。所以,为了探测拥塞开始出现的频率, TCP 发送方应该增长它的传输速率。而后慢慢使传输速率下降,进而再次开始探测,看看拥塞开始速率是否发生了变化。在了解完 TCP 拥塞控制后,下面咱们就该聊一下 TCP 的 拥塞控制算法(TCP congestion control algorithm)
了。TCP 拥塞控制算法主要包含三个部分:慢启动、拥塞避免、快速恢复,下面咱们依次来看一下
当一条 TCP 开始创建链接时,cwnd 的值就会初始化为一个 MSS 的较小值。这就使得初始发送速率大概是 MSS/RTT 字节/秒
,好比要传输 1000 字节的数据,RTT 为 200 ms ,那么获得的初始发送速率大概是 40 kb/s 。实际状况下可用带宽要比这个 MSS/RTT 大得多,所以 TCP 想要找到最佳的发送速率,能够经过 慢启动(slow-start)
的方式,在慢启动的方式中,cwnd 的值会初始化为 1 个 MSS,而且每次传输报文确认后就会增长一个 MSS,cwnd 的值会变为 2 个 MSS,这两个报文段都传输成功后每一个报文段 + 1,会变为 4 个 MSS,依此类推,每成功一次 cwnd 的值就会翻倍。以下图所示
发送速率不可能会一直增加,增加总有结束的时候,那么什么时候结束呢?慢启动一般会使用下面这几种方式结束发送速率的增加。
ssthresh(慢启动阈值)
的概念,它的初始值就是产生丢包的 cwnd 的值 / 2,即当检测到拥塞时,ssthresh 的值就是窗口值的一半。当 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 的一半。
我本身肝了六本 PDF,全网传播超过10w+ ,微信搜索「程序员cxuan」关注公众号后,在后台回复 cxuan ,领取所有 PDF,这些 PDF 以下