终于来到了TCP的滑动窗口,本来计算第二节就讲滑动窗口,但是每次都是计划赶不上变化,一推再推,所以来到了第5节,不过也不晚,趁早搞懂TCP/IP还是很有用的。

5.1 延时ACK

TCP的一个重点知识点延时ACK,但是还是从交互式通信讲起。

5.1.1 交互式通信

"交互式"TCP连接是指,该连接需要在客户端和服务器之间传输用户输入信息,如按键操作、短消息、操作杆或鼠标的动作等。这些包都是比较小的包,如果没一个很小的包都要组成一个报文来传输,那么传输协议需要耗费很高的代价,因为每个交换分组中包含的有效负载字节较少。反之,报文段较大怎会引入更大的延时。对延迟敏感类应用(如在线游戏,协同工具等)造成负面影响。因此需要找到一个折中的方法、

交互式通信其实比较简单,就是客户端获取用户输入信息,然后将其传给服务器。服务器对命令进行解析并生成相应返回给客户端。有点像一请求一应答模式。

但是让我也奇怪的是,每个交互按键通常都会生成一个单独的数据包。也就是说,每个按键都是独立传输的(每次一个字符而非每次一行,说实话,我不是看到TCP详解中说,我也很纳闷,这么会这样设计)。

像我们平时使用的ssh和服务器通信的话,服务器需要对输入字符做出回显。所以这个过程应该是这样子的:
TCP/IP协议(五、tcp滑动窗口)_接收端
如果将数据字节确认和数据字节回显合并的话,会不会高效一点,这种方法就是我们下面说的延时确认。

5.1.2 延时确认

在许多情况下,TCP并不对每个到来的数据包都返回ACK,利用TCP的累积ACK字段(这个我也不是很清楚,清楚的可以在下面留言)就能实现这个功能。累积确认可以允许TCP延迟一段时间发送ACK,以便将ACK和相同方向上需要传的数据结合发送。TCP实现ACK延时的时延应小于500ms。实践中时延最大取200ms.

采用延时ACK的方法会减少ACK传输的数目,可以一定程度地减轻网络负载。

用法:
通常TCP在某些情况下使用延时ACK的方法,但时延不会很长。
在大数据包传输中只要采用的是拥塞控制和延时ACK。
在小数据包传输中,如交互式应用,需要采用另外的算法。(就是下面说的Nagle算法)

5.1.3 Nagle算法

从前面的讲解可以看出,ssh连接中,通常单次击键就会引发数据流的传输,这些小包的数据量不到,但是需要带上整个TCP/ip头,所以会造成很高的网络传输代价。所以John提出了一种简单有效的解决方法,现在成其为Nagle算法。下面讲解一下这个算法的含义;

  • 当一个TCP连接中有在传数据(即那些已发送但还为经确认的数据),小的报文段(长度小于SMSS)就不能被发送,直到所有的再传数据都收到ACK
  • 在收到ACK之后,TCP需要收集这些小数据,讲起整合到一个报文段中发送。

举个栗子:
首先观察禁用Nagle算法的情况:
TCP/IP协议(五、tcp滑动窗口)_滑动窗口_02
这一个图是从tcp/ip详解中截取出来的,我们先看下面,F1键我是输入了3个字符,在禁用Nagle算法的时候,是每一个字符单独发送,所以我们看到的是3个单独的包,在服务器回复ack的时候,出现了丢包,客户端没有接受到ack2,服务器会重传一个ack2.客户端就接收到后,就回复了ack6,说明6之前的包都接收好了,这个在滑动窗口有说过。

然后第二个按键也是3个字符,这样就不说了,都是一样的。

通过这个例子我们明显感觉到,小包太多了,这样很浪费网络资源,所以就提出了Nagle算法。

下面我们来看看开启了Nagle算法之后的效果:
TCP/IP协议(五、tcp滑动窗口)_接收端_03
这就是开启Nagle算法的过程,是不是中间没有小包的存在了,通信也简洁了。

但是这样子会带来一个问题:
有没有发现每次发送一个请求报文之后,回复的ACK都是200ms左右,这就是延时ACK决定的,服务器收到了一个请求报文,会开启一个定时器,等待200ms,如果这时候再次接受到客户端的请求,会两个ack做为一个返回,但是犹豫客户端使用的是Nagle算法,他会一直等待ACK才会再次发送请求,这里就会形成一个死锁,不过这个死锁不是一直的,直到延时ack超时之后,就会给客户端发送ack了,客户端就会可以再次发送了,这也是看到图发现一个请求都是等待200ms左右才有回应。

总结:
从前面的例子可以看出,在有些情况下并不适用Nagle算法。

典型的包括那些要求时延尽量小的应用,如远程控制中鼠标或按键操作需要及时送达以得到快键的反馈。
还有多人网络游戏,人物的动作需要及时地传送以确保不影响游戏进程。

禁用Nagle算法也简单,可以设置TCP_NODELAY选项即可。

5.2 滑动窗口

TCP是传输控制协议,socket:流式套接字,顺序。
我们应用层都是用socket来接收的,那我们接收的数据怎么保证顺序性呢?
我们要知道网络中传输各种情况都有可能发生,前一个包选择了一个路由路径,后一个包就可能选择另外的一个路由,可能会导致后面的包比前面的包提前到大。

所以我们接下来说的TCP协议栈中使用滑动窗口这种机制来确保接收数据的顺序性。

5.2.1 发送窗口

TCP连接的每一端斗可收发数据。连接的收发数据量是通过一组窗口结构来维护的。每个TCP活动连接的两端都维护一个发送窗口结构和接收窗口结构。

接下来先看看发送窗口的结构:
TCP/IP协议(五、tcp滑动窗口)_滑动窗口_04
发送窗口有四个部分,第一个部分已发送并已经确认,就是接收端已经回复了ACK,第二个部分已经发送但未经确认,这一部分可能在传输中,也可能已经被接收了,但是由于没有到延时ack的时间,所以还没确认,第三部分即将发送,将要发送的数据,其中第二部分和第三部分是由接收端通告的窗口称为提供窗口,是接收到3字节之后,回复ack中,顺带了一个6字节大小的窗口,在之前TCP初识的这篇,有介绍过窗口的字段。(因为窗口字节是4字节,最大为65535,所以在选项字节中有一个窗口因子,是扩展了窗口大小的)。最后一部分,是不能发送的,接收端就是依靠这个窗口来控制发送端的速率,就是控制只能发送多少字节,后面的不能发送,等到接收到已经发送但未确定的数据,并返回ACK之后,发送端的窗口才继续往右移。所以这个叫做滑动窗口。

注意:TCP不支持窗口右边界左移。

每个TCP报文段都包含ACK号和窗口通告信息,TCP发送端可以据此调节窗口结构。窗口左边界不能左移,因为它控制的是已确认的ACK号,具有累积性,不能返回。当得到的ACK号增大而窗口大小保持不变时(通常如此)。我们就说窗口向前“滑动”。若随着ACK号增大窗口却减小,则左右边界距离减小。当左右边界相等时,称之为**“零窗口”**。此时发送发送端不能再发送新数据。这种情况下,TCP发送端开始探测(probe)对方窗口,伺机增大提供窗口。

5.2.2 接收窗口

接收端也维护了一个窗口结构,但比发送端窗口简单。
TCP/IP协议(五、tcp滑动窗口)_接收端_05
接收窗口包含了3个部分,第一部分已接收并确认的数据,第二部分,接收后将会保存的数据,该窗口可以保证其接收数据的正确性,特别是,避免存储重复的已接收和确认数据,也可以避免存储不应该接收的数据(超过发送方右窗口边界的数据)、第三部分就是不能接收的数据。

注意:由于TCP的累积ACK结构,只有当到达数据序列号等于左边界时,数据才不会被丢弃,窗口才能向前滑动。对选择确认TCP来说,使用SACK选项,窗口内的其他报文段也可以被接收确认,但只有在接收到等于左边界的序列号数据时,窗口才能前移。

5.2.3 零窗口与TCP持续计时器

我们在前面了解到,TCP是通过接收端的通告窗口来实现流量控制的。通告窗口指示了接收端可接收的数据量。当窗口值变为0时,可以有效的阻止发送端继续发送,直到窗口大小恢复为非零值。

当接收端重新获得可用空间时,会给发送端传输一个窗口更新,告知发送端可以继续发送数据了,但是这样的窗口更新只是一个纯ACK,没有包含数据,所以如果出现了丢失,就不会发送重传。这样子的话,发送和接收两端都处于等待状态,会形成一个死锁。

为防止这种死锁发生,发送端会采用一个持续计时器间歇性的查询接收端,看其窗口是否已经增长。持续计时器会触发窗口探测的传输,强制要求接受端返回ACK(其中包含窗口信息),随后以指数时间间隔发送。

5.2.4 糊涂窗口综合征

看书看的比较糊涂,所以百度了一下,有一篇写的还可以:TCP-IP详解:糊涂窗口综合症(Silly Window syndrome)

这里就抄抄一下原因:
这个问题可以归结为小包的问题,就是由于发送端和接收端上的处理不一致,导致网络上产生很多的小包,之前也介绍过避免网络上产生过多小包的措施,比如Nagle算法。在滑动窗口机制下,如果发送端和接收端速率很不一致,也会产生这种比较犯傻的状态:发送方发送的数据,只要一个大大的头部,携带数据很少。

对于接收端来讲,如果接收很慢,一次接收1个字节或者几个字节,这个时候接收端 缓冲区很快就会被填满,然后窗口通告为0字节,这个时候发送端停止发送,应用程序收上去1个字节后,发出窗口通告为1字节,发送方收到通告之后,发出1个字节的数据,这样周而复始,传输效率会非常低。

同时如果发送端程序一次发送一个字节,虽然窗口足够大,但是发送仍是一个字节一个字节的传输,效率很低

解决方案:
接收端:

  • 不应该通告小的窗口值。(在窗口可增至一个全长的报文段MSS或者接收端缓存空间的一遍之前,不能通告比当前窗口更大的窗口)。
  • 延时ACK,和累积ACK。

发送端:

  • 不应该发送小的报文,而且需由nagle算法控制何时发送。

为了避免SWS问题,满足下面的条件才能传输报文段。

  • 全长(发送MSS字节)的报文段可以发送
  • 数据长度≥接收端通告过的最大窗口值的一半,可以发送
  • 某一个ACK不是目前期盼的(需要重发)
  • 禁用Nagle算法

现在的linux内核中的TCP算法是支持窗口自动调优的,以后有空再分析。