1. TCP 的交互数据流 - 以 Telnet、Rlogin 交互程序传输数据流为例
- TCP 通信量研究发现按照分组数量计算,TCP 报文段包含成块数据:若按交互数据,按分组数量计算,各占 50%,若按字节计算则比例为 9:1,因为成块数据的报文段基本上都是满长度(full-sized)的(通常为 512 字节的用户数据),而交互数据则小得多(上述研究表明 Telnet 和 Rlogin 分组中通常约 90%左右的用户数据小于 10 个字节)。
- TCP 延时确认、Nagle 算法减少小分组的数目
- TCP 延迟确认:TCP 延迟确认是传输控制协议的一些实现所使用的技术,以努力改善网络性能。实质上,可以将几个 ACK 响应组合在一起成为单个响应,从而减少协议开销。但是,在某些情况下,该技术可能会降低应用程序性能(见后文)。
- 交互式输入,通常每一个交互按键都会产生一个数据分组,通过延时 ACK,可以有效减少网络的负载
- 当 Rologin 键入 5 个字符
date\n
时的数据流,产生了多个数据包交互 - 通常 TCP 在接收到数据时并不立即发送 ACK;相反,它推迟发送,以便将 ACK 与需要沿该方向发送的数据一起发送(有时称这种现象为数据捎带 ACK)。绝大多数实现采用的时延为 200 ms,也就是说,TCP 将以最大200 ms 的时延等待是否有数据一起发送。
- TCP 使用了一个 200 ms 的定时器,实现定时发送
- 主机可以延迟发送 ACK 响应最多 500 毫秒,此外,对于全尺寸传入段的流,必须为每个第二段发送 ACK 响应。
- Nagle 算法:
- 在一个 Rlogin 连接上客户一般每次发送一个字节到服务器,这就产生了一些 41 字节长的分组:20 字节的 IP 首部、20 字节的 TCP 首部和 1 个字节的数据(小包问题),在局域网上,这些小分组(被称为微小分组(tinygram))通常不会引起麻烦,因为局域网一般不会出现拥塞,但在广域网上,这些小分组则会增加拥塞出现的可能。
- Nagle 算法要求一个 TCP 连接上最多只能有一个未被确认的未完成的小分组,在该分组的确认到达之前不能发送其他的小分组。
- TCP 收集这些少量的分组,并在确认到来时以一个分组的方式发出去。该算法的优越之处在于它是自适应的:确认到达得越快,数据也就发送得越快。而在希望减少微小分组数目的低速广域网上,则会发送更少的分组。
- 在局域网环境下两个主机之间发送数据时很少使用这个算法。
- 关闭 Nagle 算法:必须无时延地发送应用,比如 X 窗口系统服务器,Socket API 用户可以使用
TCP_NODELAY
选项来关闭 Nagle 算法。
- 每当 TCP 接收到一个超出期望序号的失序数据时,它总是发送一个确认序号为其期望序号的确认
- 交互数据总是以小于最大报文段长度的分组发送。在 Rlogin 中通常只有一个字节从客户发送到服务器。Te lnet 允许一次发送一行输入数据,但是目前大多数实现仍然发送一个字节
- 对于这些小的报文段,接收方使用经受时延的确认方法来判断确认是否可被推迟发送,以便与回送数据一起发送。这样通常会减少报文段的数目,尤其是对于需要回显用户输入字符的 Rlogin 会话。
- 在较慢的广域网环境中,通常使用 Nagle 算法来减少这些小报文段的数目。这个算法限制发送者任何时候只能有一个发送的小报文段未被确认。
- Nagle 算法与 TCP 延迟确认相互作用问题:延迟 ACK 引入的额外等待时间在与某些应用程序和配置交互时可能导致进一步的延迟
- 该算法与TCP 延迟确认会有不好的相互作用,例如当程序发送端进行两次连续的小段写再跟着读时,接收端接收到第一次写后因 TCP 延迟确认(接收端稍后有数据要发送),而等待第二次写后一并发送 ACK,发送端则因第二次写数据长度小于 MSS 而等待第一次写的 ACK,最终将导致两对端都进入等待直到 ACK 延迟超时。
- 因为这个原因,TCP 实现通常为应用程序提供一个禁用 Nagle 算法的接口(通常称为 TCP_NODELAY 选项),用户级解决方案是避免套接字上的写-写-读 序列。写-读-读 和 写-写-写都是没问题的。但 写-写-读 则是性能杀手。
- 所以,如果可以的话,缓冲对 TCP 的小段写,然后一次发送它们,在每次读之前使用标准的 UNIX I/O 包并冲刷写缓存通常能起作用。
- Nagle 算法描述:
|
|
2. TCP 的成块数据流
- 滑动窗口:TCP 基于滑动窗口协议进行流量控制,该协议允许发送方在停止并等待确认前可以连续发送多个分组,以加速数据的传输。
- 慢启动、成块数据流的吞吐量
- 网络 IP 数据包到达后的一些事项处理流程:
- 硬中断:当一个分组到达时,它首先被设备中断例程进行处理,网卡驱动将其放入放置到 IP 的输入队列中
- IP 将按同样顺序将它们交给 TCP 模块,当 TCP 处理报文段时,连接有可能被标记为产生一个经受时延的确认(还有数据要发,延迟 ACK 响应),当 TCP 有多个未完成的报文段需要确认,可以通过产生一个已接收序号的的 ACK 响应,同时清除该连接产生经受时延的确认标志
- 滑动窗口协议:
- 使用 TCP 的滑动窗口协议时,接收方不必确认每一个收到的分组。在 TCP 中,ACK 是累积的—它们表示接收方已经正确收到了一直到确认序号减 1 的所有字节。
- 网络传输的复杂性:发送方 TCP 的实现、接收方 TCP 的实现、接收进程读取数据(依赖于操作系统的调度)和网络的动态性(如以太网的冲突和退避等),没有一种单一的、正确的方法来交换给定数量的数据。
- 快的发送方和慢的接收方:当发送方发送很快,但接收方的 TCP 缓冲区满时候(因为应用程序还没有机会读取这些数据),则接收方会发送一个窗口大小为 0 的 ACK 响应包(win 0)来描述此类情况;当接收方应用程序从 TCP 缓冲区获取了所有的数据后,会再发一个更新的窗口 ACK 包给到发送方,表明自己可以开始接收了(此类情况称为窗口更新)
- 滑动窗口图示,窗口大小是与确认序号相对应的,发送方计算它的可用窗口,该窗口表明多少数据可以立即被发送,当接收方确认数据后,这个滑动窗口不时地向右移动。
- 三个术语来描述窗口左右边沿的运动:
- 窗口合拢:窗口左边沿向右边沿靠近,发生在数据被发送和确认时
- 窗口张开:窗口右边沿向右移动时将允许发送更多的数据,发生在另一端的接收进程读取已经确认的数据并释放了 TCP 的接收缓存,
- 窗口收缩:窗口右边沿向左移动时,RFC 强烈建议不要使用这种方式,但 TCP 必须能够在某一端产生这种情况时进行处理
- 接收方向发送方通告其窗口为 0(将使发送方停止),当窗口张开时,需要接收方发送另一个窗口非 0 的 ACK 来使发送方重新启动(后续通告窗口和滑动窗口可理解为同样概念)
- 如果接收到一个指示窗口左边沿向左移动的 ACK,则它被认为是一个重复 ACK,则网卡会丢弃该包,另外关于滑动窗口几点注意:
- MSS(maximum segment size)最大分段长度,在 TCP 连接建立阶段确定
- 发送方不必发送一个全窗口大小的数据
- 来自接收方的一个报文段确认数据并把窗口向右边滑动,这是因为窗口的大小是相对于确认序号的。
- 窗口的大小可以减小,但是窗口的右边沿却不能够向左移动
- 接收方在发送一个 ACK 前不必等待窗口被填满
- 窗口大小可修改:由接收方提供的窗口的大小通常可以由接收进程控制,这将影响 TCP 的性能。4.2BSD 默认设置发送和接收缓冲区的大小为 2048 个字节。在 4.3BSD 中双方被增加为 4096 个字节。如 Solaris 2.2、4.4BSD 和 AIX3.2 则使用更大的默认缓存大小,如 8192 或 16384 等。
- PUSH 标志:发送方使用该标志通知接收方将所收到的数据全部提交给接收进程,这里的数据包括与 PUSH 一起传送的数据以及接收方 TCP 已经为接收进程收到的其他数据(接收缓存区内的数据)。即当服务器的 TCP 接收到一个设置了 PUSH 标志的报文段时,它需要立即将这些数据递交给服务器进程而不能等待判断是否还会有额外的数据到达。伯克利的实现一般从不将接收到的数据推迟交付给应用程序(立即交付),因此它们忽略所接收的 PUSH 标志。
- 若发送方 TCP 知道它有 4 个可立即发送的报文段(比如接收方告知可以发送 4 个大小的报文段),可以只设置了最后一个报文段(13)的 PUSH 标志。
- 慢启动:
- 如果在发送方和接收方之间存在多个路由器和速率较慢的链路时,一些中间路由器必须缓存分组,并有可能耗尽存储器的空间,TCP 需要支持一种被称为“慢启动(slow start)”的算法。该算法通过观察到新分组进入网络的速率应该与另一端返回确认的速率相同而进行工作。
- 慢启动为发送方的 TCP 增加了另一个窗口:拥塞窗口(congestion window),记为 cwnd。
- 拥塞窗口:由发送者维护,拥塞窗口是一种阻止发送方和接收方之间的链路流量过载的手段,它是通过估计链路上的拥塞程度来计算的,是一个涵盖链路上通信设备的全局过程,其目的是防止过多的数据注入到网络中,这样可以使网络中的路由器或链路不致过载,同时也希望尽可能提高传输速率。
- 当发送方与另一个网络的主机建立 TCP 连接时,拥塞窗口被初始化为 1 个报文段(即另一端通告的报文段大小 - 通告窗口)。
- 每当发送方收到一个 ACK,拥塞窗口就增加一个报文段(cwnd 以字节为单位,但是慢启动以报文段大小为单位进行增加)。
- 发送方取拥塞窗口与通告窗口中的最小值作为发送上限。
- 拥塞窗口是发送方使用的流量控制,而通告窗口则是接收方使用的流量控制。
- 拥塞控制原则:只要网络没有出现拥塞,拥塞窗口(cwnd)就再增大一些,以便把更多的分组发送出去。但只要网络出现拥塞,拥塞窗口就减小一些,以减少注入到网络中的分组数。
- 拥塞控制方法:
- 慢开始( slow-start ):在不清楚网络负荷情况下,逐步适应链路网络,由小到大逐渐增大分组数量(按指数增加 1,2,4,8..),为了防止拥塞窗口 cwnd 增长过大引起网络拥塞,还需要设置一个慢开始门限 ssthresh 状态变量,超过阈值门,则采用线性增加
- 拥塞避免( congestion avoidance ):让拥塞窗口 cwnd 缓慢地增大,即每经过一个往返时间 RTT 就把发送方的拥塞窗口 cwnd 加 1,而不是指数增加,这样拥塞窗口 cwnd 按线性规律缓慢增长
- 快重传( fast retransmit )
- 快恢复( fast recovery )。
- 拥塞窗口与拥塞控制图示:
- 成块数据的吞吐量:
- 窗口大小、窗口流量控制(滑动长款)以及慢启动,都对传输成块数据的 TCP 连接的吞吐量有影响作用
- 通常发送一个分组的时间取决于两个因素:传播时延(由光的有限速率、传输设备的等待时间等引起)和一个取决于媒体速率(即媒体每秒可传输的比特数)的发送时延。
- 对于一个给定的两个接点之间的通路,传播时延一般是固定的,而发送时延则取决于分组的大小。在速率较慢的情况下发送时延起主要作用,而在千兆比特速率下传播时延则占主要地位。
- 发送方和接收方之间的管道(pipe)被填满。此时不论拥塞窗口和通告窗口是多少,它都不能再容纳更多的数据。每当接收方在某一个时间单位从网络上移去一个报文段,发送方就再发送一个报文段到网络上。但是不管有多少报文段填充了这个管道,返回路径上总是具有相同数目的 ACK。这就是连接的理想稳定状态。
- 带宽时延乘积:窗口应该设置为多大的问题,通道容量计算:
capacity (bit) = bandwidth (b/s) × round-trip time (s)
- 拥塞:
- 大管道向小管道发送报文的情况:当数据到达一个大的管道(如一个快速局域网)并向一个较小的管道(如一个较慢的广域网)发送时便会发生拥塞。
- 路由器过载:当多个输入流到达一个路由器,而路由器的输出流小于这些输入流的总和时也会发生拥塞。
- 紧急方式 - URG: 得出紧急数据的最后一个字节的序号 = 紧急 URG 包 SEQ+URG 偏移量
- TCP 提供了“紧急方式(urgent mode)”,它使一端可以告诉另一端有些具有某种方式的“紧急数据”已经放置在普通的数据流中。另一端被通知这个紧急数据已被放置在普通数据流中,由接收方决定如何处理。
- 可以通过设置 TCP 首部中的两个字段来发出这种从一端到另一端的紧急数据已经被放置在数据流中的通知。URG 比特被置 1,并且一个 16bit 的紧急指针被置为一个正的偏移量,该偏移量必须与 TCP 首部中的序号字段相加,以便得出紧急数据的最后一个字节的序号。
- 紧急方式有什么作用:交互用户键入中断键时,可以放弃文件的传输
- 没有一种单一的方法可以使用 TCP 进行成块数据的交换,这是一个依赖于许多因素的动态处理过程,有些因素我们可以控制(如发送和接收缓存的大小),而另一些我们则没有办法控制(如网络拥塞、与实现有关的特性等),进行成块数据有效传输的最重要的方法是 TCP 的滑动窗口协议。我们考察了 TCP 为使发送方和接收方之间的管道充满来获得最可能快的传输速度而采用的方法。我们用带宽时延乘积衡量管道的容量,并分析了该乘积与窗口大小之间的关系
3. TCP 的超时与重传
- TCP 相关实际算法:慢启动、拥塞避免、快速重传和快速恢复
- 慢启动算法:
- 慢启动算法是在一个连接上发起数据流的方法,但有时我们会达到中间路由器的极限,此时分组将被丢弃。
- 慢启动算法初始设置 cwnd 为 1 个报文段,此后每收到一个确认让窗口按指数方式增长:发送 1 个报文段,然后是 2 个,接着是 4 个……
- 拥塞避免算法:
- 拥塞避免算法是一种处理丢失分组的方法。该算法假定由于分组受到损坏引起的丢失是非常少的(远小于 1%),因此分组丢失就意味着在源主机和目的主机之间的某处网络上发生了拥塞。有两种分组丢失的指示:发生超时和接收到重复的确认,如果使用超时作为拥塞指示,则需要使用一个好的 RTT 算法
- 拥塞避免算法和慢启动算法是两个目的不同、独立的算法。但是当拥塞发生时,我们希望降低分组进入网络的传输速率,于是可以调用慢启动来作到这一点。在实际中这两个算法通常在一起实现。
- 拥塞避免算法和慢启动算法需要对每个连接维持两个变量:一个拥塞窗口 cwnd和一个慢启动门限 ssthresh。
- 对一个给定的连接,初始化 cwnd 为 1 个报文段,ssthresh 为 65535 个字节
- TCP 输出例程的输出不能超过 cwnd 和接收方通告窗口(滑动窗口)的大小,拥塞避免是发送方使用的流量控制,而通告窗口则是接收方进行的流量控制
- 当拥塞发生时(超时或收到重复确认),ssthresh 被设置为当前窗口大小的一半(cwnd 和接收方通告窗口大小的最小值,但最少为 2 个报文段)。此外,如果是超时引起了拥塞,则 cwnd 被设置为 1 个报文段(这就是慢启动)。
- 当新的数据被对方确认时,就增加 cwnd,但增加的方法依赖于我们是否正在进行慢启动或拥塞避免。如果 cwnd 小于或等于 ssthresh,则正在进行慢启动,否则正在进行拥塞避免。慢启动一直持续到我们回到当拥塞发生时所处位置的半时候才停止(因为我们记录了在步骤 2 中给我们制造麻烦的窗口大小的一半),然后转为执行拥塞避免。
- 术语“慢启动”并不完全正确。它只是采用了比引起拥塞更慢些的分组传输速率,但在慢启动期间进入网络的分组数增加的速率仍然是在增加的。只有在达到 ssthresh 拥塞避免算法起作用时,这种增加的速率才会慢下来。
- 快重传算法:
- 在收到一个失序的报文段时,TCP 立即需要产生一个 ACK(一个重复的 ACK)。这个重复的 ACK 不应该被迟延。该重复的 ACK 的在于让对方知道收到一个失序的报文段,并告诉对方自己希望收到的序号。由于我们不知道一个重复的 ACK 是由一个丢失的报文段引起的,还是由于仅仅出现了几个报文段的重新排序,因此我们等待少量重复的 ACK 到来。假如这只是一些报文段的重新排序,则在重新排序的报文段被处理并产生一个新的 ACK 之前,只可能产生 1~2 个重复的 ACK。如果一连串收到 3 个或 3 个以上的重复 ACK,就非常可能是一个报文段丢失了。
- 于是我们就重传丢失的数据报文段,而无需等待超时定时器溢出。这就是快速重传算法。接下来执行的不是慢启动算法而是拥塞避免算法,这就是快速恢复算法(快速恢复:流量过载,数据包丢失,通过拥塞避免将发送端分包入网速度降下来,没有直接使用慢启动,是因为考虑不希望让数据流突然慢下来)。
- 快速恢复:
- 当收到第 3 个重复的 ACK 时,将 ssthresh 设置为当前拥塞窗口 cwnd 的一半。重传丢失的报文段。设置 cwnd 为 ssthresh 加上 3 倍的报文段大小。
- 每次收到另一个重复的 ACK 时,cwnd 增加 1 个报文段大小并发送 1 个分组(如果新的 cwnd 允许发送)。
4. TCP 的定时器
- TCP 通过让接收方指明希望从发送方接收的数据字节数(即滑动窗口大小)来进行流量控制,这将有效地阻止发送方传送数据,直到窗口变为非 0 为止。
- 死锁情况:如果一个确认丢失了,则双方就有可能因为等待对方而使连接终止:接收方等待接收数据(因为它已经向发送方通告了一个非 0 的窗口),而发送方在等待允许它继续发送数据的窗口更新。
- 窗口探查(window probe):发送方使用一个坚持定时器(persist timer)来周期性(计算坚持定时器时使用了普通的 TCP 指数退避)地向接收方发送
ACK
查询报文,以便发现窗口是否已增大,从而避免死锁情况 - TCP 指数退避,发送端的查询退避这个过程将持续到或者窗口被打开,或者应用进程使用的连接被终止(TCP 连接超时)
- 糊涂窗口综合征
5. TCP 的保活定时器
- 我们可以启动一个客户与服务器建立一个连接,然后离去数小时、数天、数个星期或者数月,而连接依然保持。中间路由器可以崩溃和重启,电话线可以被挂断再连通,但是只要两端的主机没有被重启,则连接依然保持建立。这意味着两个应用进程—客户进程或服务器进程—都没有使用应用级的定时器来检测非活动状态,而这种非活动状态可以导致应用进程中的任何一个终止其活动,比如通过独立于 TCP 的保活定时器之外的应用定时器。
- 许多时候一个服务器希望知道客户主机是否崩溃并关机或者崩溃又重新启动,许多实现提供的保活定时器可以提供这种能力,但注意保活并不是 TCP 规范中的一部分(导致连接断开、带宽浪费、资金浪费), 许多人认为如果需要,TCP 连接保活的这个功能,不应该在 TCP 中提供,而应该由应用程序来完成。
- 实例:当个人计算机用户使用 TCP/IP 向一个使用 Telnet 的主机注册时,客户端未通过注销而是直接通过关闭电源,导致服务器上留下一个半开放连接,在等待来自客户的数据,则服务器将永远等待下去,保活功能就是试图在服务器端检测到这种半开放的连接。