发送⽅不能⽆脑的发数据给接收⽅,要考虑接收⽅处理能⼒。
如果⼀直⽆脑的发数据给对⽅,但对⽅处理不过来,那么就会导致触发᯿发机制,从⽽导致⽹络流量的⽆端的浪费。
为了解决这种现象发⽣,TCP 提供⼀种机制可以让「发送⽅」根据「接收⽅」的实际接收能⼒控制发送的数据量,这就是所谓的流量控制。
举例说明,首先假设:
- 客户端是接收⽅,服务端是发送⽅
- 假设接收窗⼝和发送窗⼝相同,都为 200
- 假设两个设备在整个传输过程中都保持相同的窗⼝⼤⼩,不受外界影响
根据上图的流量控制,说明下每个过程:
- 客户端向服务端发送请求数据报⽂。这⾥要说明下,本次例⼦是把服务端作为发送⽅,所以没有画出服务端的接收窗⼝。
- 服务端收到请求报⽂后,发送确认报⽂和 80 字节的数据,于是可⽤窗⼝ Usable 减少为 120 字节,同时SND.NXT 指针也向右偏移 80 字节后,指向 321,这意味着下次发送数据的时候,序列号是 321。
- 客户端收到 80 字节数据后,于是接收窗⼝往右移动 80 字节, RCV.NXT 也就指向 321,这意味着客户端期望的下⼀个报⽂的序列号是 321,接着发送确认报⽂给服务端。
- 服务端再次发送了 120 字节数据,于是可⽤窗⼝耗尽为 0,服务端⽆法再继续发送数据。
- 客户端收到 120 字节的数据后,于是接收窗⼝往右移动 120 字节, RCV.NXT 也就指向 441,接着发送确认报⽂给服务端。
- 服务端收到对 80 字节数据的确认报⽂后, SND.UNA 指针往右偏移后指向 321,于是可⽤窗⼝ Usable增⼤到 80。
- 服务端收到对 120 字节数据的确认报⽂后, SND.UNA 指针往右偏移后指向 441,于是可⽤窗⼝ Usable增⼤到 200。
- 服务端可以继续发送了,于是发送了 160 字节的数据后, SND.NXT 指向 601,于是可⽤窗⼝ Usable 减少到 40。
- 客户端收到 160 字节后,接收窗⼝往右移动了 160 字节, RCV.NXT 也就是指向了 601,接着发送确认报⽂给服务端。
- 服务端收到对 160 字节数据的确认报⽂后,发送窗⼝往右移动了 160 字节,于是 SND.UNA 指针偏移了160 后指向 601,可⽤窗⼝ Usable 也就增⼤⾄了 200。
一、操作系统缓冲区与滑动窗⼝的关系
前⾯的流量控制例⼦,我们假定了发送窗⼝和接收窗⼝是不变的,但是实际上,发送窗⼝和接收窗⼝中所存放的字节数,都是放在操作系统内存缓冲区中的,⽽操作系统的缓冲区,会被操作系统调整。
当应⽤进程没办法及时读取缓冲区的内容时,也会对我们的缓冲区造成影响。
1、那操作系统的缓冲区,是如何影响发送窗⼝和接收窗⼝的呢?
(1)例1
首先观察:当应⽤程序没有及时读取缓存时,发送窗⼝和接收窗⼝的变化。
考虑以下场景:
- 客户端作为发送⽅,服务端作为接收⽅,发送窗⼝和接收窗⼝初始⼤⼩为 360 ;
- 服务端⾮常的繁忙,当收到客户端的数据时,应⽤层不能及时读取数据。
根据上图的流量控制,说明下每个过程:
- 客户端发送 140 字节数据后,可⽤窗⼝变为 220 (360 - 140)。
- 服务端收到 140 字节数据,但是服务端⾮常繁忙,应⽤进程只读取了 40 个字节,还有 100 字节占⽤着缓冲区,于是接收窗⼝收缩到了 260 (360 - 100),最后发送确认信息时,将窗⼝⼤⼩通告给客户端。
- 客户端收到确认和窗⼝通告报⽂后,发送窗⼝减少为 260。
- 客户端发送 180 字节数据,此时可⽤窗⼝减少到 80。
- 服务端收到 180 字节数据,但是应⽤程序没有读取任何数据,这 180 字节直接就留在了缓冲区,于是接收窗⼝收缩到了80(260 - 180),并在发送确认信息时,通过窗⼝⼤⼩给客户端。
- 客户端收到确认和窗⼝通告报⽂后,发送窗⼝减少为 80。
- 客户端发送 80 字节数据后,可⽤窗⼝耗尽。
- 服务端收到 80 字节数据,但是应⽤程序依然没有读取任何数据,这 80 字节留在了缓冲区,于是接收窗⼝收缩到了 0,并在发送确认信息时,通过窗⼝⼤⼩给客户端。
- 客户端收到确认和窗⼝通告报⽂后,发送窗⼝减少为 0。
可⻅最后窗⼝都收缩为 0 了,也就是发⽣了窗⼝关闭。当发送⽅可⽤窗⼝变为 0 时,发送⽅实际上会定时发送窗⼝探测报⽂,以便知道接收⽅的窗⼝是否发⽣了改变。
(2)例2
当服务端系统资源⾮常紧张的时候,操⼼系统可能会直接减少了接收缓冲区⼤⼩,这时应⽤程序⼜⽆法及时读取缓存数据,那么这时候就有严᯿的事情发⽣了,会出现数据包丢失的现象。
说明下每个过程:
- 客户端发送 140 字节的数据,于是可⽤窗⼝减少到了 220。
- 服务端因为现在⾮常的繁忙,操作系统于是就把接收缓存减少了 120 字节,当收到 140 字节数据后,⼜因为应⽤程序没有读取任何数据,所以 140 字节留在了缓冲区中,于是接收窗⼝⼤⼩从 360 收缩成了 100,最后发送确认信息时,通告窗⼝大小给对⽅。
- 此时客户端因为还没有收到服务端的通告窗⼝报⽂,所以不知道此时接收窗⼝收缩成了 100,客户端只会看⾃⼰的可⽤窗⼝还有 220,所以客户端就发送了 180 字节数据,于是可⽤窗⼝减少到 40。
- 服务端收到了 180 字节数据时,发现数据⼤⼩超过了接收窗⼝的大小,于是就把数据包丢失了。
- 客户端收到第 2 步时,服务端发送的确认报⽂和通告窗⼝报⽂,尝试减少发送窗⼝到 100,把窗⼝的右端向左收缩了 80,此时可⽤窗⼝的⼤⼩就会出现诡异的负值。
所以,如果发⽣了先减少缓存,再收缩窗⼝,就会出现丢包的现象。
【解决方案】
为了防⽌这种情况发⽣,TCP 规定是不允许同时减少缓存⼜收缩窗⼝的,⽽是采⽤先收缩窗⼝,过段时间再减少缓存,这样就可以避免了丢包情况。
二、窗口关闭
TCP 通过让接收⽅指明希望从发送⽅接收的数据⼤⼩(窗⼝⼤⼩)来进⾏流量控制。
如果窗⼝⼤⼩为 0 时,就会阻⽌发送⽅给接收⽅传递数据,直到窗⼝变为⾮ 0 为⽌,这就是窗⼝关闭。
1、窗⼝关闭潜在的危险
接收⽅向发送⽅通告窗⼝⼤⼩时,是通过 ACK 报⽂来通告的。
那么,当发⽣窗⼝关闭时,接收⽅处理完数据后,会向发送⽅通告⼀个窗⼝⾮ 0 的 ACK 报⽂,如果这个通告窗⼝的 ACK 报⽂在⽹络中丢失了,那麻烦就⼤了。
这会导致发送⽅⼀直等待接收⽅的⾮ 0 窗⼝通知,接收⽅也⼀直等待发送⽅的数据,如不采取措施,这种相互等待的过程,会造成了死锁的现象。
【TCP 是如何解决窗⼝关闭时,潜在的死锁现象呢?】
为了解决这个问题,TCP 为每个连接设有⼀个持续定时器,只要 TCP 连接⼀⽅收到对⽅的零窗⼝通知,就启动持续计时器。
如果持续计时器超时,就会发送窗⼝探测 ( Window probe ) 报⽂,⽽对⽅在确认这个探测报⽂时,给出⾃⼰现在的接收窗⼝⼤⼩。
- 如果接收窗⼝仍然为 0,那么收到这个报⽂的⼀⽅就会重新启动持续计时器;
- 如果接收窗⼝不是 0,那么死锁的局⾯就可以被打破了。
窗⼝探测的次数⼀般为 3 次,每次⼤约 30-60 秒(不同的实现可能会不⼀样)。如果 3 次过后接收窗⼝还是 0 的话,有的 TCP 实现就会发 RST 报⽂来中断连接。
三、糊涂窗⼝综合症
如果接收⽅太忙了,来不及取⾛接收窗⼝⾥的数据,那么就会导致发送⽅的发送窗⼝越来越⼩。
到最后,如果接收⽅腾出⼏个字节并告诉发送⽅现在有⼏个字节的窗⼝,⽽发送⽅会义⽆反顾地发送这⼏个字节,这就是糊涂窗⼝综合症。
要知道,我们的 TCP + IP 头有 40 个字节,为了传输那⼏个字节的数据,要达上这么⼤的开销,这太不经济了。
就好像⼀个可以承载 50 ⼈的⼤巴⻋,每次来了⼀两个⼈,就直接发⻋。除⾮家⾥有矿的⼤巴司机,才敢这样玩,不然迟早破产。要解决这个问题也不难,⼤巴司机等乘客数ᰁ超过了 25 个,才认定可以发⻋。
【举例说明】,考虑以下场景:
接收⽅的窗⼝⼤⼩是 360 字节,但接收⽅由于某些原因陷⼊困境,假设接收⽅的应⽤层读取的能⼒如下:
- 接收⽅每接收 3 个字节,应⽤程序就只能从缓冲区中读取 1 个字节的数据;
- 在下⼀个发送⽅的 TCP 段到达之前,应⽤程序还从缓冲区中读取了 40 个额外的字节;
每个过程的窗⼝⼤⼩的变化,在图中都描述的很清楚了,可以发现窗⼝不断减少了,并且发送的数据都是⽐较⼩的了。
所以,糊涂窗⼝综合症的现象是可以发⽣在发送⽅和接收⽅:
- 接收⽅可以通告⼀个⼩的窗⼝
- 发送⽅可以发送⼩数据
于是,要解决糊涂窗⼝综合症,就解决上⾯两个问题就可以了。
- 让接收⽅不通告⼩窗⼝给发送⽅
- 让发送⽅避免发送⼩数据
1、【解决方案】怎么让接收⽅不通告⼩窗⼝呢?
接收⽅通常的策略如下:
当「窗⼝⼤⼩」⼩于 min( MSS,缓存空间/2 ) ,也就是⼩于 MSS 与 1/2 缓存⼤⼩中的最⼩值时,就会向发送⽅通告窗⼝为 0 ,也就阻⽌了发送⽅再发数据过来。
等到接收⽅处理了⼀些数据后,窗⼝⼤⼩ >= MSS,或者接收⽅缓存空间有⼀半可以使⽤,就可以把窗⼝打开让发送⽅发送数据过来。
2、【解决方案】怎么让发送⽅避免发送⼩数据呢?
发送⽅通常的策略:
使⽤ Nagle 算法,该算法的思路是延时处理,它满⾜以下两个条件中的⼀条才可以发送数据:
- 要等到窗⼝⼤⼩ >= MSS 或是 数据⼤⼩ >= MSS
- 收到之前发送数据的 ack 回包
只要没满⾜上⾯条件中的⼀条,发送⽅⼀直在囤积数据,直到满⾜上⾯的发送条件。
另外,Nagle 算法默认是打开的,如果对于⼀些需要⼩数据包交互的场景的程序,⽐如,telnet 或 ssh 这样的交互性⽐较强的程序,则需要关闭 Nagle 算法。
可以在 Socket 设置 TCP_NODELAY 选项来关闭这个算法(关闭 Nagle 算法没有全局参数,需要根据每个应⽤⾃⼰的特点来关闭)
setsockopt(sock_fd, IPPROTO_TCP, TCP_NODELAY, (char *)&value, sizeof(int));
整理自小林coding所著的《图解网络》,仅做学习用,侵删