摘自:《深入理解计算机网络》 王达著 机械工业出版社
相关知识链接
1. IPV4数据报头部格式
2. IPv4数据报的封装与解封装
3. IPv4数据报的分段与重组
4. TCP的主要特性
5. TCP的套接字
6. TCP端口
7. TCP连接状态转移
8. TCP传输的建立
9. TCP 传输链接的释放
10. TCP 的可靠传输
虽然“流量控制”和“拥塞控制”是两个不同的的概念,但是它们之间还是有些关联的。“流量控制”是基于通信双方的数据发送和接收速率匹配方面考虑的,其最终的目的是不要让数据发送得太快,以便接收端能够来得及接收,是一个链路两端的点对点行为。而“拥塞控制”则是基于网路中各段链路的带宽和中间设备数据处理能力方面而考虑的,不要是网络出现数据传输阻塞,也就是不要让发送端发送的数据大于接收端数据处理能力,是一个端对端的行为。但流量控制又可能对拥塞控制有所帮助,其实解决好 TCP 连接所经过的所有链路的流量控制问题,也就基本解决了拥塞问题了。本文介绍 TCP 的流量控制,下一篇博客会介绍拥塞控制。
TCP 的流量控制是采用滑动窗口协议来进行的。而在本章前面已经提到过,TCP 数据段是以字节为单位进行编号的,但由于一个数据段只有一个 TCP 头部,所以 TCP 是以数据段为单位进行传输的,接收端通过 TCP 头部来识别所接受的数据属于哪个数据段。一个数据段只要没有完全接受,接收端就不会认为已接受了该数据段,就像我们平时通过 QQ 等工具传输文件时,只要还有部分文件没有接收完,则我们的计算机就不会认为该文件已正确接收,即使有了文件名,也打不开已接收的部分。
在前面的博客已提到过,在通信双方主机上都分别有一个“发送窗口”和一个“接受窗口”。其窗口的大小又分为“物理窗口大小”和“可用窗口大小”两种。对于一台具体的主机来说,“物理窗口大小”值是固定的,要视对应主机所配置的缓存而定;而“可用窗口”大小值是可变的,一端会根据另一端的“接受窗口”大小(会在每个数据段的“窗口大小”字段中设置)调整本端的“发送窗口大小”,也就是说,TCP 的“可用窗口大小”实在不断变化的,而不是固定的,这也说明了TCP 每次发送的数据段数(即数据大小)可能都不一样。而可用“接受窗口”大小也会因向已发送确认的数据段数量和所接收到的不连续序号数据段数量(因为已接收到但不是连续序号的数据段是仍需要缓存在物理“接受窗口”中的)而在不断变化。下面以一个具体的例子来介绍 “ TCP 滑动窗口机制”。
上图是一个滑动窗口的示例,其中的序号为每个数据段的序号也就是对应数据段的第一个字节序号。现假设每个字段的大小为100字节(这仅仅是为了方便介绍,实际的窗口大小都在千个字节以上),物理“发送窗口”大小为500字节(这也是为了方便介绍,实际情况远远不止这个数)。
1) 首先假设现在发送端收到了接收端发来的一个确认数据段,“确认号”为 301,“窗口大小”为 500,表示可以连续发送 5 个数据段(起始序号为 301)。左边那个虚线“发送窗口”代表的就是后面发送的 301、401、501、601、701 这五个数据段,此时因为已达到了对方窗口大小值不能再发送了,需要停下来等待对端的确认。
2) 如果某一时间收到了一个“确认号”为 501 的确认数据段(“ACK”字段值为 1 的数据段,下同),表示接收端已正确接受了 301 和 401 这两个数据段,即从“发送端口”删除这两个数据段,窗口向前滑动 200 字节(也就是 301 和 401 这两个数据段的大小),移到了图中虚线的实线“发送窗口”位置,其中 501、601、和 701 这 3 个数据段是原来已发送但没有收到确认的缓存数据段,801 和 901 这两个数据段才是要等待发送的。
假设以上返回的确认字段中的“窗口大小”为 400 (这里起到了流量控制的作用),理论上说,发送端可以一次性连续发送4个数据段,但因为“发送窗口”中缓存了原来已经发送的 501、601 和 701 这三个数据段(300 字节),于是此次只能发送 100 字节,即 801 号数据段。
3) 如果此时收到了一个“确认号”为 801 的确认数据段,同时“窗口大小”字段值又为 500了(这里也起到了流量控制的作用),则发送端知道接收端已收到了 701 号及以前所有的数据段了,于是“发送窗口”中删除这些缓存的数据段,此时实际上缓存的数据段仅为 801 号数据段。但原来 901 号数据段已在“发送窗口”中,等待发送,此时“发送窗口”只需要继续向前移300字节,也就是三个数据段(此时假设数据段大小均为 100 字节的情况下),也就是上图所示的粗实线表示“发送窗口”,继续发送 901、1001、1101、1201 这 4 个数据段。
如果在数据传输过程中有一个或多个数据段丢失,则发送端接收不到对这些数据段的确认数据段,这是可以通过上篇博客介绍的超时重传来解决。现在要探讨的问题是,如果某个时间,对端发送的数据段显示“窗口大小”字段值为 0,这是发送端自然不能再发送数据了,只好等待对方发来一个“窗口大小”字段值不为 0 的数据段。可是如果对端发来的这个数据段在传输过程丢失了,那么这时对于发送端来说,一直在等待来自接收端“窗口大小”字段值非 0 的数据段,而接收端又在等待发送端发来新的数据,因为它自己不知道所发送的数据段在中途丢失了(当然这仅适用于 C/S(客户端/服务器) 模式,对于非 C/S 模式,也就没有发送端和接收端之分了,双方都可以发送,都可以使用超时重传机制)。
就像你与你的朋友约定,在条件满足时他通过快递方式给你寄一样东西,但你并不知道他具体寄什么,他寄的时候也没有告诉你(因为他只相信快递公司)。而恰好这件快递在途中丢失了,结果是你一直等待你朋友寄来你所需要的东西(你也不好意思催你朋友是否寄了),而你的朋友一直在等待你的收到的通知(总认为快递一般不会寄丢东西)。这样可能等了很久,你的朋友才想起了这件事,询问你是否收到了快递。
为了解决这个问题,TCP 中引入了一个称为“持续计时器”(persistence timer)的定时器。在 TCP 连接的一段收到对端的一个“窗口大小”字段值为 0 时,启动该定时器。在这个定时器到期后,收到这个“窗口大小”字段值为 0 的数据段一端会向对端发送一个非常小的探测数据段(一般仅携带 1 字节的数据),这时,对端在收到这个探测数据段后会返回一个确认数据段。如果在确认数据段中的“窗口大小”仍为 0,则发送端重启上面的“持续计时器”,否则结合确认数据段中的“窗口大小”字段值和当前可用“发送窗口”大小,发送相应字节的数据,打破了以上这种双方持续等待的局面。
我们在前面的博客提到过,TCP 数据段中只对“数据”部分的字节进行编号,原因是在对方传输层解封装后得到,并且所需要的只是“数据”部分内容。这样就可以很容易地知道接受的数据是否连续。否则,如果加上 TCP 数据报头,以及传输到网络层的 IP 报头,传输到数据链路层报头,数据部分的编号就不可能连续,况且不同数据段的报头大小都可能不太一样,因为还有“可选项”部分。
对于大型的数据传输,这些报头加起来也才几十字节,对于一个数据段动辄上千个字节来说,这些协议头部分显然是可以忽略不计的,也就是说,传输速率是非常高的。但是我们要考虑各方面的应用,如在一些交互式应用中,每次传输的数据部分可能仅一个或几个字节,如果为每个这样的数据传输一次,显然传输效率是很低的,因为使用几十个协议头而最终传输的有用信息才只有几个字节。这类情况还会在发送确认数据段时经常发生,如果仅是用来确认数据段,里面的数据量是非常小的,也正因为如此,所以通常是在捎带大量数据的数据段中把 ACK 字段置 1,同时起到确认的作用。
这时就涉及到了什么时候把这些数据组装成一个数据段进行传输的问题了。其中也涉及了两个非常重要的算法:Nagle 算法和 Clark 算法。
Nagle算法规定:如果数据每次以 1 字节的方式进入到发送端,则发送端只是发送第一个字节,然后其余字节先缓存起来,知道发送出去的那个字节被确认为止。然后,将所有的缓存起来的数据放在一个 TCP 数据段中发送出去,随后继续开始缓存后续字节,因为不会因 1 个字节而使用几十个协议头来携。试想一下,例如,一个用户的键盘输入很快,而网络很慢,这时就会把用户的许多次输入字符组装成一个数据段,然后一次发送出去,大大减少所占用网络带宽。Nagle 算法还规定,当到达的数据已达到发送窗口大小的一半或者已达到数据段的最大长度时,也要发送出去。
还有一种情况,发送端的 TCP 实体从应用层接收到大量的数据,等待发送,而此时接收端的应用却每次只需要 1 字节的数据,。发送端收到确认数据段后自然也就只能再发送 1 字节的数据(但其中的 TCP 和 IP 协议头至少是 40 字节),然后接收端再次发回针对这 1 字节数据的确认数据段。显然,这种传输方式的效率也是很低的。
为了解决这种问题,Clark 提出了一种解决方案,就是禁止接收端发送“窗口大小”字段值为 1 (也就是数据部分仅 1 字节)的数据段,即让接收端继续等待一段时间,使得接收端的“接收窗口”有足够的空间可以容纳一个最大数据段长度的数据,或者它的缓存空间一半已空时(取两者的最小值)才发送确认数据段。这是,“窗口大小”字段值肯定不是 1 了,这样发送端就可以一次发送更多的数据,从而提高传输效率。