本文系翻译文章1。上一篇:传输层:TCP和UDP
传输控制协议(TCP):高级内容
传输层是网络层和应用层之间的桥梁,传输控制协议(Transmission Control Protocol,TCP)可以以可靠的方式实现衔接。在之前的文章 2中,上述已经解释了UDP和TCP的区别,现在是时候去理解如何改善TCP的性能以满足现代应用程序的需求,以及如何以最佳的方式使用基础网络。这样,我们将在主机上学习到管理TCP连接的原理,例如状态和窗口操作:欢迎阅读有关高级TCP的文章。
TCP 状态
TCP状态理论
为了运行TCP,任一设备都必须落实一些原则:设备必须知道当没有收到ACK的时候重传数据,设备必须知道对收到的数据发送相应的确认号,等等。所有的这些规则都可以用状态图或流程图定义。任意给定时间,设备必须在图中某一个状态,且只能在一个状态。基于设备所处的状态,它可以执行什么操作,不能执行什么操作,并且可以转移至特定状态。一眼看去,图很复杂,但实际上并不复杂。看看下图以及它的含义吧!
下面把它拆解来看。如您所见,图中有11种不同的状态,没有真正的开始点。这意味着任何一个状态(有箭头指向的)都可以从至少一个状态转移而来。每一个矩形代表一种状态,箭头指向代表当前状态可以转移到的状态。箭头上的文本指明了状态成功转移需要的触发条件。最后,所有状态分成了不同颜色的类别:同一组的是带有同样目的的。
注意:您可能发现不同书中有不同的状态表示方式:使用下划线、虚线或没有连接词。
尽管这里没有一个真正的开始点,可以认为CLOSED
是开始点:此种状态下,连接不能是打开状态。因为任何连接都是从没有连接开始;考虑一下,开始没有连接,三次握手之后协商建立连接。一个期望建立连接的设备,可以依据其角色从CLOSED
状态转移至其它两个状态。首先考虑服务器(这里称之为响应者)。服务器不向客户端初始化连接,而是等待客户端的连接请求。这意味着服务器端必须准备好接收请求,并在应用程序启动时从CLOSED
状态转移至LISTEN
状态。这个状态表明服务器端已经做好了建立连接的准备,正等待从客户端发来的SYN段。
图中决定主动建立连接的设备称之为发起者,哦通常是客户端。它在向服务器端发送SYN段后状态从CLOSED
转移至SYN_SENT
。一旦响应者在LISTEN
状态收到了SYN段,它会立即回复SYN+ACK段,并且其状态转移至SYN_RECEIVED
。一旦发起者收到了SYN+ACK段,它在向响应者发送ACK段之后状态转移至ESTABLISHED
,响应者收到这个段之后的状态也转移至ESTABLISHED
。这意味着连接建立了,数据可以交换了。
注意:一旦一个连接初始化了,传输控制块(Transmission Control Block)就定义好了。它是一个在整个连接过程中保存在设备内存中的信息和变量的集合。其中包含了源端口和目的端口。
一旦数据交换完成,双方之一想要关闭连接。这种情况下,有一个设备(称之为Initiator,发起者)初始化关闭,另一个设备对此进行响应,但是这不是必须要求一方是客户端,另一方是服务器端。连接关闭发起者只是主动发起连接关闭请求的设备,而响应者是响应关闭连接的一方(被动关闭)。发起者会发送一个FIN报文段,状态从ESTABLISHED
转移到FIN_WAIT_1
状态,响应者的状态也随着它接收到FIN报文段而转移到CLOSE_WAIT
,并发送一个ACK报文段。当发起者收到ACK报文段,它的状态转移到FIN_WAIT_2
并处在这种状态不做任何事。响应者还是处于CLOSE_WAIT
状态,它在等着应用程序完成所有的事情并关闭。一旦应用程序完事了,响应者设备将会给发起者发送一个FIN报文段,告知其也要关闭连接了,此时它的状态转移到LAST_ACK
。一旦发起者接收到响应者的FIN报文段,它会回发一个ACK报文段,且其状态转移到TIME_WAIT
。响应者已收到ACK报文段,就会状态转移至CLOSED
。两毫秒之后,发起者状态也会转移至CLOSED
。这时,连接正式关闭。
TCP的状态
这看起来有些复杂,但是很快您将会看到如何用到连接上并使之有意义。这是一个带有设备状态的典型的TCP连接过程图。
在解释之前,请记住状态对于设备连接非常重要:它们不是连接自身的状态,所以一个设备可以处于一种状态,另一个设备可能处于不同的状态。如您所见,客户端状态从CLOSED
开始,但是此时服务器端已经处于LISTEN
状态了,因为服务器端的应用程序必须监听客户端请求并产生响应,所以需要提前开启。客户端发送一个SYN报文段并进入SYS_SENT
状态。一旦服务器端收到上个SYN报文段,它会回送SYN+ACK报文段并进入SYN_RECEIVED
状态,之后客户端回送ACK报文段并进入ESTABLISHED
状态,而服务器端收到ACK报文段后也进入ESTABLISHED
状态。
注意:对于每一个收到的连接,服务器端的状态都会从LISTEN
转移到SYN_RECEIVED
状态,因为服务器端会并发收到很多连接请求。这意味着每次服务器端收到一个SYN
报文段,服务器端都会为之创建一个状态图所示师力,当连接关闭时,这个也会随着销毁。这样总会有一个LISTEN
的状态实例等待接收新的客户端的新的连接。
一旦数据交换结束了,本例子中是客户端要求结束连接。它通过发送一个FIN报文段,之后转移至FIN_WAIT_1
状态。当服务器端收到了FIN报文段,它会回送一个ACK报文段并转移至CLOSE_WAIT
状态。当客户端收到了ACK报文段,会转移至FIN_WAIT_2
状态,等着服务器端关闭。一旦服务器端传送完了数据,它会发送FIN报文,并转移到LAST_ACK
状态。当客户端收到了FIN段报文,会转移到TIME_WAIT
状态,并在2ms之后,转移到CLOSED
状态(其实要建立在没有收到报文的情况下,否则继续等着)。当服务器端收到ACK段报文后转移到CLOSED
状态。连接结束。
同时关闭
一个有趣的案例:同时关闭一个TCP连接。本例中,没有主动发起和被动响应关闭的双方,而是双方同时准备关闭连接。
如图所示,关闭流程时完美同步的。客户端向服务器端发送一个FIN报文段,来关闭连接并转移到FIN_WAIT_1
状态。同时,服务器出于同样目的,发送一个FIN报文段后也转移到FIN_WAIT_1
状态。客户端收到FIN报文段,之后它转移到CLOSING
状态并发送一个ACK报文段。服务器端也一样,服务器端也会收到FIN报文段并发送一个ACK报文段后转移到CLOSING
状态。当任何一方收到了ACK报文段,它会转移到TIME_WAIT
状态,超时之后,再转移到CLOSED
状态。基本上,伴随同时关闭,FIN标志实在FIN_WAIT_1
状态收到的。
也有同时打开连接的情况,类似于同步关闭连接(在SYN_SENT
状态收到一个SYN报文段),但是在现实世界中很不常见,因为C/S架构模式,这种模式是客户端主动连接服务器。
重置TCP连接
众所周知,设备可以随时使用RST报文转移到CLOSED
状态。如同下图高亮部分,展示了两种可能的RST报文及重置方式。
在第一幅图中,重置被发送并收到了。如您所见,它可以在任意状态的时候发送并随之关闭连接。这意味着它不会监听并接收连接发来的任何信息,所以当其它设备收到了RST报文段,它只能选择关闭连接。
第二幅图片中的例子,展示了当RST报文段丢失的场景。作为最后一个发送的出去的报文段,没有相应的重传机制。发送方发送了就立即关闭连接,不会再处理后续了。所以,对端设备没有收到RST时,再发送数据也不会收到确认回复,它会在超时后断掉连接。
TCP 窗口
TCP报头中有一个16位长的字段,叫做窗口大小。它标识了接收窗口,即入流量的缓冲区。平铺直叙,不拽名词,即窗口大小是发送端设备用于存储的等待应用程序处理的字节数量。这只用于接收数据。通过发送报文段,设备说:“嗨老兄,你可以给我发送X字节,我保证我可以临时储存它们,以使得应用程序后面处理它们。”X就是窗口大小。
串口大小在每个报文段中都有,但是是对端程序来使用它:窗口大小指示了接收窗口的大小,并不指定已经用了多少。为了跟踪对端程序用了多少内存,本端蓄婢比较发送了多少数据以及多少数据得到了确认回复。每一个发送后的报文段都会占用对端的接收窗口,每一个确认回复都会从对端的接收窗口移除对应的报文段。请记住,所述操作都没有考虑报文段的数量,而是每个报文段中字节数量。如下图所示。
为了简化,称客户端是”A“,服务器端是”B“。当通过三次握手协商了连接,两个设备互告之窗口大小:A的接收窗口(RWIN或RWND)大小是32KB,B的接收窗口大小是16KB。每一个设备都会记住对方的接收窗口大小并考虑对方是否还有足够的缓冲区空间。然而,因为信息描述“缓冲区占用率”,设备必须保持跟踪。好消息是只有本端发送的消息会占用对端的缓冲区空间(其实就是一个TCP流一个呗):本端知晓对端缓冲区总大小,也知晓其何时为空,还知悉本端发送了多少数据以及对端确认了多少数据。本例为了简单:客户端每次发送1K数据,服务器端每次都少了1K可用缓冲。重复了3次,同时服务器端开始处理数据,并回送确认报文,每收到一个确认报文,客户端都会检查确认了多少并增加对应字节数量的可用空间。
一会之后,服务器端要向客户端发送数据。然而它有较多的数据需要发送:它每次发送1K,重复发送20次(图中省略了),可用空间从32K减小到了11K。客户端花费了大量时间处理数据,一旦它处理不过来了(如本例),它会通过发送一个“窗口大小现在为0”的报文告知服务器停止传送。
注意:窗口大小在每个报文段中都有,其定义了当前发送该报文段的设备中总可用缓冲空间,所以除非该值和三次握手时原始接收值(或最后一个值,如果在三次握手过程中多次更改)不一样,否则设备不会考虑它。
这是因为一个设备会尽可能的多传送数据,但是没有收到回复的字节数量不能超过窗口大小。接收窗口经过专门设计,可以避免设备过载,就像它们缓冲区满了之后,任何无法容纳在缓冲区中的接收段都将会被丢弃。现代设备,没有这么严格了。它可以和拥塞窗口连用,来处理网络拥塞。
选择确认
选择确认,也称为Selective ACK或简称SACK(RFC 2018),是对TCP性能的另一改善,它允许设备单独确认报文段。传统的TCP实现中,一个报文段丢失的时候,不得不重传,丢失报文段所有后续的报文段也不得不重传,尽管它们被正确接收了。这是因为确认号的本质所导致的,确认号告知对端设备它下面想要接收的是哪一个。这种逻辑可以确认连续的字节流,但是中间不能有中断(例如,“OK,我接受到了0-8以及10-20,但是没有收到8-10”)。SACK机制改变了这一状况,它允许设备独自决定确认哪些报文段,所以只有丢失的报文段会重传,整体上改善了吞吐量。下图所示是对比传统确认机制和选择重传机制。
在左边,带有警告标志的报文段收发了两次。
传统的确认机制,服务器端向客户端先后发送了三个报文段,分别包含了1-1460字节,1461-2920字节以及2921-4380字节。然而,第二个包含1461-2920字节的报文段因为暂时的网络原因丢失了,所以客户端只确认了第一个报文段(确认号是1461),但是丢弃了第三个报文段,因为如果发送了确认号是4381的报文段,将无法表示中间丢失了的报文了。所以,服务器端收到确认号是1461的报文段后,又重新发送了1461-2920(丢失的报文段)以及2921-4380两个报文段。这样总共发送了两次第三个有效报文段,浪费了带宽和时间。
为了解决上述问题,选择重传机制实现了两个不同的TCP报头选项。第一个是Sack-Permitted Option(允许SACK选项),它的标识为设置为4。这个选项用在三次握手时验证连接双方都支持SACK机制。第一个阶段成功后,**Sack Option(SACK选项,类型设置为5)**解释选项格式超出了本文范围,只需知道它是TCP报头重用于告知对端设备哪一个报文段使用SACK确认了。了解更多请查看RFC2018。基本上,一个报文段丢失的时候,后续报文段正确接收,所有丢失报文之前的报文已经按传统确认机制确认了,丢失报文之后的报文都使用SACK机制确认了,使用的是TCP“Sack Option”报头扩展中适当的字段。如图所示,因为唯一的丢失的报文段是1461-2920,客户端使用传统确认机制发送了确认号为1461,使用SACK机制确认了2920-4380。
报头压缩
报头压缩是很棒的TCP功能,它使得低速连接,如卫星连接等的带宽增强了。这一功能非常简单,但也与众不同,因为它不是在TCP主机上实现的。其它的功能都是由TCP双方实施的增强功能,然而这里不是这样子的。实际上,它是在路径上的路由器上实现的。
想象一下,路由器处理包含了TCP报文段的IP包,如果您想要大量使用连接(例如从服务端下载文件),路由器会处理大量源IP地址和目的IP地址相同的数据包,而且报文段中的源端口和目的端口也一样。但是这些数据包不得不一层层传送出去。然而,报头开销占据的带宽即使在低速连接上也会降低性能。但是所有的这些报头中带有的相同的连接信息,难道不能只发送一次吗?通过报头压缩可以做到。
实现了报头压缩功能的路由器将报头中的IP和TCP地址,以及在连接期间不会发生改变的其它字段,通过算法产生唯一的标识符(Hash ID),该标识符要小得多。大多数情况下,我们从40字节报头压缩到4字节的标识符。很明显,所有的路由器收到了一个压缩报头,需要知道其扩展后的值,并发送至正确的目的地址。最后,路由器会解压缩报头并发送至目的设备(或者下一个不支持报头压缩的路由器)。
如图所示,两个路由器在卫星链接通过Hash ID来交互,每一个Hash都对应唯一一个数据流。图中只有单向数据流,但是实际上也会有一个回应数据流的Hash ID。
处理网络拥塞
快速重传
快速重传是一个用于实现主动重传数据的非常简单的功能。在传统TCP实现中,我们应该等待ACK以决定我们应该重传的数据。有了快速重传功能,如果在超时前没有收到ACK,为了节省时间,设备会自动重传尚未确认的报文段。这在高延迟低带宽网络中不太好,因为信息可能并不是丢失了,而是还没有到达对端。
TCP 拥塞控制
构建网络越来越便宜,现在网络可以实现10年前无法想象的吞吐量。然而,随着网络速度的提升,现代应用程序的要求也在上升,日常的带宽杀手应用,如VoIP,视频或大量的网络游戏。因此,100%使用网络性能是必须的,但是如果我们尝试发送的数据量超过了网络的处理能力,则会导致性能下降。需要在带宽利用率和网络性能之间做出取舍。网络管理需要一些工具来控制拥塞,而TCP正好有这能力。
TCP两端是网络边缘的两个主机,它们无法知晓网络的整体状况,因此,它们无法知道它们之间有效的吞吐量。但是,它们必须能够知晓这种情况。TCP使用了拥塞窗口(congestion window, CWND),其中是在发送必须停止并等待确认前,发送端能够发送的字节数量。拥塞窗口不像接收窗口处于每个报文中,它是设备本地的,不会出现在连接中。无论何时,设备最多可以发送由接收器窗口和拥塞窗口之间的最小值指定的最大字节数,如以下公式所示。
transmittable bytes = min(cwnd, rwnd)
这意味着,如果拥塞窗口小于接收窗口,则设备可以在等待确认之前最多发送由拥塞窗口中定义的字节数。反之,如果接收窗口比拥塞窗口小,那么设备可以在等待确认之前最多发送由接收窗口定义的字节数。
拥塞窗口根据网络拥塞程度动态调整。每次一个报文未收到确认,都认为是网络拥塞导致的。拥塞窗口的变化是由实现使用的算法决定的。下面是最常见的一种算法,它遵从的规则如下:
- 拥塞窗口从1个分段大小开始(大约是1KB);
- 定义拥塞窗口门限值(ssthresh);
- 如果收到了确认报文,且当前拥塞窗口小于门限值,拥塞窗口翻倍;
- 如果收到了确认报文,但是拥塞窗口大小大于等于门限值,则拥塞窗口只增加其初始值(例:1KB);
- 如果没有收到确认报文段,出发重传,拥塞窗口减半,门限值设置为此值;
- 拥塞窗口不能大于接收窗口。
为了解释方便,请看下方一个使用了拥塞控制机制的报文交换示例。如您所见,每个设备都有其自己的拥塞窗口(CWND,绿色的)以及对方的的接收窗口(RWND)。左边记录了报文交换的编号(行列),方便后方单行参考。
当协商连接的时候,两个设备互换接收窗口(本例中各有32KB)。它们的拥塞窗口大小都从1KB开始,但是由于客户端是本例唯一发送数据的一方,所以它是唯一会使用拥塞窗口的一方。第2行,客户端收到一个ACK报文并把其CWND翻倍(现在是2k),服务器端也一样(第3行)。然后,客户端发送两个1k数据报文,这两个报文分别于第6行和第7行得到确认回复,同时收到确认后拥塞窗口分别再次翻倍(4k再到8k)。然后,客户端继续发送了1k报文并立即得到了服务器确认,拥塞窗口也变成了16k(第9行)。第10、11行重复上述过程,客户端的CWND达到了32k。这时客户端拥塞窗口达到了最大值,如果再增加,需要服务器端的接收窗口也增大。在第12行,发生了报文丢失,等待超时(没有体现在图中),拥塞窗口和ssthresh(门限值)设置为16k,在13-14行再次发送并接收到确认回复,但是此时拥塞窗口没有翻倍,因为其已经达到了门限值(ssthresh),它只是增加了1k。第16行,服务器端缩减了其接收窗口到8k,所以客户端的拥塞窗口也设置为8k。
虽然上例不错,但是让我们看一个更加复杂的。这次我们使用的是一个拥塞窗口值随时间变化的图表。
如图所示,拥塞窗口从1k开始,在达到阈值之前,每接收到一个ACK都会翻倍,然后开始线性增长知道丢失了一个报文。然后,阈值缩小为拥塞窗口最大值的一半,然后拥塞窗口从这个一半开始继续线性增长。最后,接收窗口缩减了,阈值和拥塞窗口也随之变化。
UDP 的占优和 TCP 的饥饿(UDP predominance and TCP starvation)
知道了拥塞控制,下面将探讨同一个网络上的UDP和TCP。TCP实现了一个复杂的回退机制以适应网络拥塞,而UDP并没有。所以,如果大量流量使用了UDP,TCP和UDP一起将超出网络的能力,TCP将会因为拥塞控制算法而产生回退。UDP则不会,它会继续占用其已经使用的带宽,如果一些UDP流量因为网络拥塞而排队,则由于(TCP拥塞控制使得)网络不再拥塞,将立即触发它。如果网络再次饱和,TCP会再次执行拥塞控制算法直到网络全部都是UDP流量。如下图所示:
这在局域网(LAN)中不是问题,因为局域网带宽较高,很难达到网络饱和装填。但是在广域网(WAN,连接地理位置相距较远的站点)中,需要采取措施来避免UDP流量的占优。如**服务质量(Quality of Service,QoS)**使用一系列规则定义了网络应该在拥塞时做出何种应对【好像一般是UDP随机丢包或者全丢】。这些规则设置在路由器上,并定义了网络拥塞时丢掉哪些包。QoS的规则可以分开UDP和TCP的流量,可使得UDP只能使其可用的网络饱和。QoS规则还能给一些应用分配百分比带宽,或给一些应用保留带宽(也就是说,这部分带宽只能被一些应用使用,就算它们不用别的也不能用)。
本文,我们覆盖了所有的TCP高级功能。现在您可以探索TCP如何在CCNP级别上工作了【本文其实是CCNA的免费文章,Cisco认证】。这些知识在谈论防火墙的时候也有用【防火墙有一种工作方式就是两方发送3个RST重置报文】,下一篇文章将探讨UDP以及会话层和表示层的内容。