38、Internet传输协议之TCP之一(传输层)

引言

  • 对于大多数Internet应用来说,它们需要可靠的、按序递交的传输特性。UDP不能提供这样的功能,所以Internet还需要另一个协议。这就是TCP,它是Internet上的主力军。

1、TCP概述

  • 传输控制协议(TCP)是为了在不可靠的互联网络上提供可靠的端到端字节流而专门设计的一个传输协议。因为互联网络的不同部分可能有不同的拓扑结构、带宽、延迟、数据包大小和其他参数。TCP的设计目的是能够动态地适应互联网络的这些特性,而且具备面对各种故障时的健壮性。
  • 每台支持TCP的机器都有一个TCP传输实体。TCP实体可以是一个库过程、一个用户进程,或者内核的一部分。在所有这些情形下,它管理TCP流,以及与IP层之间的接口。TCP传输实体接收本地进程的用户数据流,将它们分割成不超过64KB(实际上丢掉IP和TCP头,通常不超过1460数据字节)的分段,每个分段以单独的IP数据报形式发送。当包含TCP数据的数据报到达一台机器时,它们被递交给TCP传输实体,TCP传输实体重构出原始的字节流。为简化起见,我们有时候仅仅用“TCP”来代表TCP传输实体(一段软件)或者TCP协议(一组规则),例如,用户将数据交给TCP这句话中,很显然这里指的是TCP传输实体。
  • IP层并不保证数据报一定被正确地递交到接收方,也不指示数据报的发送速度有多快。正是TCP负责既要足够快地发送数据报,以便使用网络容量,但又不能引起网络拥塞;而且TCP超时后,要重传没有递交的数据报。即使被正确递交的数据报,也可能存在错序的问题,这也是TCP的责任,它必须把接收到的数据报重新装配成正确的顺序。简而言之,TCP必须提供可靠性的良好性能,这正是大多数用户所期望的而IP又没有提供的功能。

2、TCP服务模型

  • TCP服务由发送端和接收端创建一种称为套接字的端点来获得。每个套接字有一个套接字编号(地址),该编号由主机的IP地址以及一个本地主机的16位数值组成。这个16位数值称为端口,端口是TCP的TSAP名字。为了获得TCP服务,必须显式地在一台机器的套接字和另一台机器的套接字之间建立一个连接。一个套接字可能同时被用于多个连接。换句话说,两个或者多个连接可能终止于同一个套接字。每个连接可以用两端的套接字标识符来标识,即(socket1,socket2)。TCP不使用虚电路号或者其他的标识符。
  • 1024下的端口号被保留,只能用作由特权用户(比如UNIX系统的root)启动的标准服务。这些端口称为知名端口。例如一台机器上的任何一个进程希望检索邮件,那么它可以连接到目标主机的143端口与IMAP守护进程联系。可以在www.iana.org上找到所有知名端口的链表。目前已经分配了700多个(本书2010年出版)。表中列出了一些尤为知名的端口。
端口 协议 用途
20,21 FTP 文件传输
22 SSH 远程登录,Telnet的替代品
25 SMTP 电子邮件
80 HTTP 万维网
110 POP-3 访问远程邮件
143 IMAP 访问远程邮件
443 HTTPS 安全的Web(SSL/TLS之上的HTTP)
543 RTSP 媒体控制播放
631 IPP 打印共享
  • 1024~49151之间的其他端口可以通过IANA注册,由非特权用户使用,但是应用程序可以选择自己的端口号。例如,BitTorrent对等文件共享应用(非正式的)使用了6881~6887端口号,但也可以运行在其他端口号上。
  • 让FTP守护进程在系统启动时关联到21号端口,让SSH守护进程在系统启动时关联到22号端口等类似的做法是完全可能的。然而,这样做将会使内存散落在这些守护进程中,而且大多数时间这些进程都是空闲的。因此,一般的做法是让一个守护进程同时关联到多个端口上,然后等待针对这些端口的第一个入境连接请求,inetd就派生出一个新的进程,在这个进程中调用适当的守护程序,然后由这个守护程序来处理连接请求。按照这种方式,处理inetd,其他所有守护程序都只在确实有需要的时候才被激活。inetd通过一个配置文件知道哪个端口应该使用哪个守护程序。因此,系统管理员可以这样配置系统使得比较忙的端口(比如80端口)使用永久守护程序,而其他端口则让inetd处理。
  • 所有的TCP连接都是全双工的,并且是点到点的。所谓全双工,意味着同时可在两个方向传输数据;而点到点则意味着每个连接恰好有两个端点。TCP不支持组播或者广播传输模式。一个TCP连接就是一个字节流,而不是消息流。端到端之间不保留消息的边界。例如,如果发送进程将4个512字节的数据块写到一个TCP流中,那么这些数据有可能按4个512字节块、2个1024字节块、1个2048字节块或者其他的方式被递交给接收进程。接收端无法获知这些数据被写入字节流时的单元大小。
  • UNIX系统中的文件也具有这样的特性。读文件的程序无法判断该文件是一次写入一块,还是一次写入一个字节,或者整个文件被一次性写入。如同UNIX文件一样,TCP软件不理解字节流的含义。当一个应用将数据传递给TCP时,TCP可能立即将数据发送出去,也可能将它缓存起来(为了收集更多的数据一次发送出去),这完全由TCP软件自己来决定。然而,有时候应用程序确实希望数据立即被发送出去。例如一个交互式游戏的用户希望发送一个更新流,至关重要的是这个流应该被立即发送出去,而不是缓存起来。为了强制将数据发送出去,TCP有个PUSH标志位的概念,由数据包携带的PUSH标志位原意是让应用告诉TCP不要延迟传输。然而,应用程序在发送数据时不能从字面上设置PUSH标志,不同的操作系统已经演化出了不同的方案来加速传输(例如在Windows和Linux的TCP_NODELAY)。
  • TCP而一个有趣特性是紧急数据的处理。当一个具有高优先级数据的应用立即被处理时,例如一个交互式用户键入Ctrl + C来中断一个已经开始运行的远程计算时,发送端应用程序把一些控制信息放在数据流中,并将它连同URGENT标志一起交给TCP。这一事件导致TCP停止积累,该连接上已有的所有数据立即传输出去。当接收方收到紧急数据时,接收端应用程序被中断(比如按UNIX的术语是得到了一个信号),它停止当前正在做的工作,并且读入数据流以便找到紧急数据。紧急数据的尾部应该被标记出来,因而应用程序能够知道紧急数据的结束位置。紧急数据的起始处并没有标记出来,如何找出紧急数据取决于具体的应用程序。
  • 这个方案提供了一种略微粗糙的信号机制,而把一切留给应用程序来处理。然而,尽管紧急数据有潜在的应用价值,但在早期没有人发现它有人令人信服的应用,因而遭到淘汰。因为这种信号机制让应用程序自己来处理,现在它的用途因为各种版本实现上的差异而令人气馁,也许未来的传输协议能提供更好的信号机制。

3、TCP协议

  • TCP的一个关键特征,也是主导整个协议设计的特征是TCP连接上的每个字节都有它自己独有的32位序号。在Internet初期,路由器之间的线路绝大多数是56kbps的租用线路,所以一台满负荷全速运行的主机差不多一周才能遍历完所有的序号。以现代的网络的速度,序号的消耗非常大。数据包携带的32位序号可用在一个方向上的的滑动窗口协议以及另一个方向的确认。
  • 发送端和接收端的TCP实体以段的形式交换数据。TCP段由一个固定的20字节的头(加上可选部分)以及随后0个或多个数据字节构成。TCP软件决定了段的大小。它可以将多次写操作中的数据积累起来,放到一个段中发送,也可以将一次写操作中的数据分割到多个段中发送。有两个因素限制了段的长度。首先,包括TCP头在内的每个段,必须适合IP的65515个字节的有效载荷;其次,每个网络都有一个最大传输单元(MTU)。发送端和接收端的每个段必须适合MTU,才能以单个不分段的数据包发送和接收。实际上,MTU通常是1500字节(以太网的有效载荷大小)。
  • 然而,当IP数据包穿过一条网络路径,其上某条链路有更小的MTU时,还是有可能要对携带TCP段的该IP数据包进行分段操作。如果这种情况发生,则会降低性能并会引起其他问题。现代MTU使用RFC 1191给出的技术实现了“路径MTU发现”功能。该技术利用ICMP错误消息来发现某条路径上任意一条链路的最小MTU。
  • TCP实体使用的基本协议是具有动态窗口大小的滑动窗口协议。当发送端传送一段时,它启动一个计时器。当该段到达接收方时,接收端的TCP实体返回一个携带了确认号和剩余窗口大小的段(如果有数据要发送,则包含数据),并且确认号的值等于接收端期望接收的下一个序号。如果发送端的计时器在确认段到达之前超时,则发送端将再次发送原来的段。
  • 尽管这个协议听起来简单,但是段到达的顺序可能是错误的(比如3072~4095字节的数据比2048~3071字节还没有到达);段在传输过程中可能会被延迟,因而发送端重传段,而重传的段可能包含于与原来传输的段字节范围不同,然而,由于数据流中的每个字节都有它自己的偏移值,所以这项管理工作是可以完成的。TCP必须以有效的方法解决这些问题。

4、TCP段的头

  • 图中显示了TCP段的结构。每个段的起始部分是一个固定格式的20字节头。固定头部之后可能有头的选项。如果该数据段有数据部分的话,那么在选项后是最多可达65535-20-20=65495个字节的数据,这里的第一个20是IP头,第二个是TCP头。没有任何数据的TCP段通常用作确认和控制消息。在这里插入图片描述
  • 源端口和目标端口字段标识了连接的本地端点。TCP端口加上所在主机的IP地址组成了48位(16+32)的唯一端点。源端点和目标端点一起标识了一条连接。这个连接标识符称为5元组,由5个信息组成:协议(TCP)、源IP地址和端口、目标IP地址和端口。
  • 序号和确认号字段执行它们的常规功能。后者是指定的下一个期待的字节,而不是已经正确接收到的最后一个字节。它是累积确认,因为它用一个数字概括了接收到的所有数据,它不会超过丢失数据的序号数字。这两个字段都是32位长,因为一个TCP流中的每一个数据字节(这里应该叫段比较合适,寒注)都已经被编号了。
  • TCP头长度指明了TCP头包含多少个32位的字。这个信息是必须的,因为选项字段是可变长的。从技术上讲,这个字段指明了数据部分在段内的起始位置(以32位作为单位进行计量),因为TCP长度字段正好是按字(32位)0为单位进行计量的。
  • 接下来是4个没有被使用的字段。这些位(原先的6位中只有2位被重新声明使用)间接证明了TCP使用运行良好。接着是8个1比特的标志位。当采用RFC 3168说明的显式拥塞通知(ECN)时,CWR和ECE就用作拥塞控制的信号。当TCP接收端收到了来自网络的拥塞指示后,就设置ECE以便给TCP发送端发送ECN-Echo信号,告诉发送段放慢速率。TCP发送端设置CWR,给TCP接收端发送CWR信号,这样接收端就知道发送端已经放慢速率,不必再给发送端发送ECN-Echo规则。
  • 如果使用了紧急指针,则将URG设置为1。紧急指针指向当前序号开始找到紧急数据字节的偏移量。这个设施是中断消息的另一种途径。这种设施很少用到。
  • ACK被设置为1表示确认号字段是有效的。几乎所有的数据包都会用到这个标志位,如果ACK为0,则该段不包含确认信息,所以确认号字段可以被忽略。
  • PSH指出这是被推送的数据。特此请求接收端一旦收到数据后立即将数据递交给应用程序,而不是将它缓冲起来直到整个缓冲区满为止。
  • RST被用于突然重置一个已经变得混乱的连接,混乱有可能是由于主机崩溃,或者其他原因。该标志位也可以用来拒收一个无效的字段,或者拒绝一个连接请求。
  • SYN被用于建立连接过程。在连接请求中,SYN=1和ACK=0表示该段没有使用捎带确认字段。但是连接应答捎带了一个确认字段,因此SYN=1和ACK=1。本质上,SYN为被用来表示CONNECTION REQUSET 和 CONNECTION ACCEPTED,然后进一步用ACK位来区分这两种可能情况。
  • FIN被用来释放一个连接。它表示发送端已经没有数据需要传输了。然而在关闭一个连接之后,关闭进程可能会在一段不确定的时间内继续接受数据。SYN和FIN段都有序号,从而保证了这两种段以正确的顺序被处理。
  • TCP中的流量控制是通过一个可变大小的滑动窗口来处理的。窗口大小字段指定了从被确认的字节算起可以发送多少个字节。窗口大小字段为0是合法的,说明到现在为止已经接收到了多达确认号-1个字节,但是接收端没有更多的空间消耗数据,希望别再发送数据。以后接收端可以通过发送一个具有同样确认号但是非零窗口大小字段的段来通知发送端继续发送端。
  • .在数据链路层章节的协议中,确认接收到的帧和允许发送新帧是捆绑在一起的,这是每个协议采取固定窗口大小的固定结果。在TCP中,确认和允许发送额外数据时完全分离的两种机制。实际上,接收端可以这样说:“我已经接收到了序号k之前的字节,但是现在我不想接收更多的数据了。”这种分离(事实上是可变大小的窗口)带来了格外的灵活性。
  • 校检和提供了额外的可靠性。它校检的范围包括头、数据,以及与UDP一样的概念性伪头,除了伪头的协议号为TCP(6),且校检和必须强制执行。
  • 选项字段提供了一种添加额外设施的途径,主要针对常规头覆盖不到的地方。协议定义了许多选项,有几个已经被广泛使用。选项的长度可变长,但必须是32位的倍数,不足部分用0填充。选项可以扩展到40个字节(10个选项,寒注),这是说明的最长TCP头。某些选项用在连接建立期间,主要协商或者通知另一端的能力;其他选项用在连接生存期间,通过数据包携带。每个选项具有类型-长度-值编码。
  • 用途最广的选项允许每台主机指定它愿意接收的最大段长(MSS)。采用大的段通常比小的段更有效率,因为这20字节头的开销可以分摊到更多的数据上,但是小型主机可能处理不了大的段。在连接建立过程中,每一端可以宣布它的最大段长,并且查看对方给出的最大值。如果一台主机没有这个选项,那么它默认可以接受536字节的有效载荷。所有Internet主机都要求能够接受536+20=556个字节的TCP段。两个方向上的段长可以不同。
  • 对于具有高带宽、高延迟,或者两者兼备的线路,64KB的窗口对应于16位的字段是个问题。在一条OC-12的线路上(大约600Mbps),只需要1毫秒就可输出一个完整的64KB的窗口。如果往返传播延迟是50毫秒(越洋光纤的典型值),则发送端将有98%空闲等待确认的到来。大的窗口允许发送端不停地送出数据。窗口尺度选项允许发送端和接收端在连接建立阶段协商窗口尺寸因子。双方使用尺度因子将窗口大小字段向左移至多14位,因此允许窗口最大可达230个字节。大多数的TCP实现都支持这个选项。
  • 时间戳选项携带由发送端发出的时间戳,并被接收端回应。一旦在连接建立选项阶段启用了它,那么每个数据包都要包含这个选项,主要用来计算来回时间样值,该样值被用在估算多久之后数据包可以被认为丢失。它还可以被用作32位序号的逻辑扩展。在一条快速链路上,序号空间很快回绕到零,这样讲导致分不清新数据包和老数据包。防止序号回绕方案根据时间戳丢弃入境段,从而解决序号回绕问题。
  • 最后选择确认选项使得接收端可以告诉发送端已经接收到段的序号范围。这是对确认的补充,可用在一个数据包已丢失但后续(或重复)到达的特定情况下。有了选择确认,发送端可以明显感知到接收端已经接收了什么数据,并据此确定应该重传什么数据。该选项由RF 2108 和 RFC 2883定义。

5、TCP连接建立

  • TCP使用了三次握手法来建立连接。为了建立一个连接,某一端比如说服务器必须先依次执行LISTEN和ACCEPT原语,然后被动地等待入境连接请求;并且可以指定只接受一个特定的请求源,也可以不指定。另一端比如说客户端,执行CONNECT原语,同时说明它希望连接的IP地址和端口。它愿意接受的最大TCP段长,以及一些可选的用户数据(比如口令)等参数。CONNECT原语发送一个SYN标志位置为on和ACK标志位为off的TCP段,然后等待服务器的响应。
  • 当这个段到达接收方时,那里的TCP实体检查是否有一个进程已经在目标端口字段指定的端口上执行了LISTEN。如果没有,则它发送一个设置了RST的应答报文,拒接客户的连接请求。如果某个进程正在改端口上监听,那么TCP实体将入境的TCP段交给该进程处理。该进程可以接受或拒绝这个连接请求。如果接受就发送一个确认段。正常请求下,发送的TCP段顺序如图a所示。请注意,SYN段值消耗了1个字节的序号空间(是指序号空间也有SYN值吗?),所以它可以被毫无异议地确认。在这里插入图片描述
  • 如果两台主机同时企图在一对套接字之间建立连接,则事件序列如图b所示。这些事件的结果恰好只建立了一个连接,因为所有的连接都是由它们的端点来标识的。如果第一个请求产生了一个由(X,Y)标识的连接,第二个请求也建立了这样一个连接,那么实际上只有一个表项即(X,Y)。
  • 每个主机选择的初始序号应该循环比较慢,而不是一个常数。这个规则是为了防止被延迟的重复数据包。最初采用了一个基于时钟的方案,时钟每4微秒滴答一次。(现在是应时间戳辅助,寒注)
  • 然而,三次握手的实现方式有个漏洞,当监听用户进程接受一个连接请求,并立即以SYN段作为回应,它必须记住该SYN段的序号。这意味着一个恶意的发送端很容易占据一个主机资源。具体做法是这样的:恶意用户绵绵不断地发送SYN请求段请求服务器的连接,但是又故意不完成连接建立后的后续过程,由此可消耗掉一台主机的资源。这种攻击称为SYN泛洪。抵御这种攻击的一种方法是使用SYN“小甜饼(Cookie)”。主机不去记忆序号,而是选择一个加密生成的序号,将它放在出境段中,并且忘记它。如果三次握手完成后,该序号(+1)将返回主机。主机运行相同的加密函数,只要该函数的输入是已知的(例如其他主机的IP地址和端口,和一个本地密钥),它就能重新生成正确的序号。这个过程允许主机检查确认号是否正确,而不必记住单独的序号。这里存在一些注意事项,比如无法处理TCP选项,所以只有当主机容易受到SYN泛洪时,才可以用SYN Cookie。

6、TCP连接释放

  • 虽然TCP连接时全双工,但是为了理解TCP连接是如何释放的,最好将TCP连接看成是一对单工连接。每个单工连接的释放彼此独立。为了释放一个连接,任何一方都可以发送一个设置了FIN标志位的TCP段,这表示它已经没有数据发送了。当FIN段被另一方确认后,这个方向上的连接就被关闭,不再发送任何数据。然而,另一个方向上或许还在传输数据流。当两个方向都关闭后,连接才算被彻底释放。通常情况下,释放一个连接需要4个TCP段:每个方向上一个FIN和一个ACK。然而,第一个ACK和第二个FIN有可能被组合在同一个段中,从而将所需段总数降低至3个。
  • 一个TCP连接的两端也可能会同时发送FIN端。这两个段按常规的方法被单独确认,然后关闭连接。实际上,两台主机可以先后释放连接,或者同时释放连接,这两者没有本质区别。为了避免两军对垒问题,需要使用计时器。如果在两倍于最大数据包生存期内,针对FIN的响应没有出现,那么FIN的发送端直接释放连接。另一方最终会注意到没人监听连接,因而也会超时。虽然这种方案并不是完美无缺的,但理论上也不存在完美的解决方案。实际上,这种方案很少产生问题。

7、TCP连接管理模型

  • 建立连接和释放连接所需要的步骤可以用一个有限状态机来表示,该状态机的11中状态如表所示。在每一种状态中,都存在特定的合法事件。当一个合法事件发生时,可能需要采取某个动作。当发生其他事件时,则报告一个错误。
状态 描述
CLOSED 没有活跃的连接或者挂起
LISTEN 服务器等待入境呼叫
SYN RCVD 到达一个连接请求:等待ACK
SYN SENT 应用已经启动了打开一个连接
ESTABLISHED 正常的数据传送状态
FIN WAIT1 应用没有数据传送了
FIN WAIT2 另一端同意释放连接
TIME WAIT 等待所有数据包寿终正寝
CLOSING 两端同时试图关闭连接
CLOSE WAIT 另一端已经发起关闭连接
LAST ACK 等待所有数据包寿终正寝
  • 每个连接都是从CLOSED状态开始的。当它执行了一个被动打开操作(LISTEN),或者一个主动打开操作(CONNECT)后,它就离开CLOSED状态。如果另一端执行了相应的操作,则连接就建立起来了,当前状态变成ESTABLISHED。连接的释放过程可以由任何一方发起。当释放状态完成,状态又回到CLOSED。
  • 有限状态机本身如图所示。在图中用粗线表示客户主动连接到一个被动服务器上的一般情况,其中客户部分用的是实现,服务器部分用的是虚线。细线表示了不常用的事件序列。图中的每条线都标记成一对“事件/动作”。这里的事件既可以是用户发起的系统调用(CONNECT\LISTEN\SEND\CLOSE),也可以是到达了一个段(SYN\FIN\ACK\RST),或者是发生了两倍于最大数据包生存期的超时事件。动作可以是发送一个控制段(SYN\FIN\RST)或者什么也不做(图中标记为-),括号中是说明。在这里插入图片描述
  • 当客户机上的一个应用程序发出CONNECT请求,本地的TCP实体创建一条连接记录,并将它标记为SYN SENT状态,然后发送一个SYN段。请注意,在一台机器上可以有多个连接处于打开(或正在打开)状态,它们可能代表了多个应用程序,所以状态是针对每个连接的,并且每个连接被记录在相应的连接记录中。当SYN+ACK到达的时候,TCP发出三次握手过程的最后一个AKC段,然后切换到ESTABLISHED状态。现在可以发送和接收数据了。
  • 当一个应用结束时,它执行COLSE原语,从而使本地的TCP实体发送一个FIN段,并等待对应的ACK(虚线框标记了主动关闭)。当ACK到达时,状态迁移到FIN WAIT2,而且连接的一个方向被关闭。当另一刚也关闭时,会到达一个FIN段,然后它被确认。现在,双方都已经关闭了连接,但是TCP要等待一段长度为最大数据包生存期的两倍的时间才能保证该连接上的所有数据包都已寿终正寝,以防万一发生确认被丢失的情形。当计时器超时后,TCP删除该连接记录。
  • 现在从服务器的角度来看连接管理的情形。服务器执行LISTEN,并等待入境连接请求。当收到一个SYN时,服务器就确认该段并且进入到SYN RCVD状态。当服务器本身的SYN被确认后,就标志着三次握手过程的结束,服务器进入到ESTABLISHED状态。从现在开始双方可以传输数据了。当客户完成了自己的数据传输,它就执行CLOSE,从而导致TCP实体发送一个FIN到服务器(虚线框标记了被动关闭)。然后,服务器接收到信号;当它也执行了CLOSE时,TCP实体给客户发送一个FIN段。当该段的来自客户的确认返回后,服务器释放该连接并且删除相应的连接记录。

8、TCP滑动窗口

  • TCP的窗口管理将正确接收段的确认和接收端的接收缓冲区分配分离开来。例如,假设接收端有一个4096字节的缓冲区,如图所示。如果发送端传送了一个2048字节的数据段,并且该数据段已被正确地接收,那么接收端将确认该数据段。然而,由于接收端现在只剩下2048字节的缓冲区空间(在应用程序从缓冲区中取走数据之前),所以它将宣告下一个期望字节开始窗口为2048。在这里插入图片描述
  • 现在发送端又传输了另一个2048字节长的段,该段被确认后接收端宣告窗口大小为0。因此发送端停止发送,等待接收端的应用程序从缓冲区取走数据。当窗口变为0后,有两种特殊情形可以发送数据:第一,紧急数据可以发送(比如允许用户杀掉远程机器上运行的某一个进程);第二,发送端可以发送一个1字节的段,以便强制接收端重新宣告下一个期望的字节和窗口大小,这种数据包称为窗口探测。TCP标准明确地提出来这个选项,来防止窗口更新数据包丢失后发生死锁。
  • 发送端不一定接到应用程序传递来的数据就马上将数据传输出去;同样,接收端也不一定必须尽可能快地发送确认段。例如,在上图中,当一块2KB数据到达缓冲区后,TCP知道它有4KB的窗口可以发送,所以它完全可以等待缓冲区集满4KB之后一起发送以提高传输性能。
  • 考虑一个远程终端连接,比如使用SSH 或者Telnet,该连接对用户的每次击键动作都做出响应。在最差的情况下,当一个字符到达发送端的TCP 实体, TCP 创建一个21 字节的TCP 段,并将它交给IP 组成一个41 字节的IP数据报,然后发送出去;在接收端, TCP立即发送一个40 字节的确认( 20 字节的TCP 头加上20 字节的IP 头)。以后, 当远程终端读取了这个字节之后, TCP 发送一个窗口更新段,它将窗口向前移动1 个字节。这个数据包也是40 字节长。最后,当远程终端处理了该字符以后,它发送一个41 字节的数据包作为该字符的回显。总共累计起来,对于每次敲入的字符,需要使用162 字节的带宽,并发送4 个数据段。对于带宽紧缺的场合,这种处理方法显然并不合适。
  • 针对这样的情况,许多TCP 实现采用了一种称为延迟确认( delayed acknowledgement)的优化方法。基本想法是将确认和窗口更新延迟50毫秒,希望能够获得一些数据免费搭载过去。假设远程终端在500 毫秒内回显,则现在远程用户只需要送回一个41 字节的数据包即可,从而将数据包数和所用带宽减少了一半。
  • 尽管通过延迟确认减少了接收端给予网络的负载,但是发送端发送多个小数据包的工作方式仍然非常低效(比如, 41 字节的数据包只包含1 个字节的数据)。避免这种用法的一种办法是采用Nagle 算法( Nagle, 1984 )。Nagle 的建议非常简单:当数据每次以很少量方式进入到发送端时,发送端只是发送第一次到达的数据字节,然后将其余后面到达的字节缓冲起来,直到发送出去的那个数据包被确认:然后将所有缓冲的字节放在一个TCP 段中发送出去,并且继续开始缓冲字节,直到下一个段被确认。这就是说,任何时候只有第一个发送的数据包是小数据包。如果在一个来回时间内应用程序发送了许多数据,那么Nagle 算法可以将这些数据放置在一个段中发送,由此大大地减少所需的带宽。另外,如果应用传递来的数据足够多,多到可以填满一个最大数据段,则该算法也允许发送一个新的段。
  • 什么时候不用它更好?特别是,在Internet 上玩互动游戏时,玩家通常需要一个快速的短数据包流。如果把更新收集起来以突发的方式发送将使得游戏的响应不稳定,这显然会引起用户不满。一个更微妙的问题是Nagle 算法有时可能会延迟确认的相互作用,从而造成暂时的死锁:接收端等待(上层)数据到来以便可以捎带确认,而发送端等待确认的到来以便能够发送更多的数据。这种相互作用可能导致延迟网页的下载。由于存在这些问题,可以禁用Nagle 算法(这就是所谓的TCP_NODELA Y 选项〉。
  • 降低TCP 性能的另一个问题是低能窗口综合症( silly window syndrome) (Clark, 1982 )。当数据以大块形式被传递给发送端TCP 实体,但是接收端的交互式应用每次仅读取一个字节数据的时候,这个问题就会发生。为了看清此问题,请参考下图 。初始时,接收端的TCP 缓冲区为满,发送端知道这一点(即它有一个大小为0 的窗口〉。然后,交互式应用从TCP 流中读取一个字符,这个动作使得接收端的TCP立刻发送一个窗口更新段给发送端,告诉它现在可以发送1 个字节过来。发送端立即发送1 个字节。现在缓冲区又满了,所以,接收端对这1 字节的数据段进行确认,同时设置窗口大小为0 。这种行为可能会永久地持续下去。在这里插入图片描述
  • Clark 的解决方案是禁止接收端发送只有1 个字节的窗口更新段。相反,它强制接收端必须等待一段时间,直到有了一定数量的可用空间之后再通告给对方。特别是,只有当接收端能够处理它在建立连接时宣告的最大数据段,或者它的缓冲区一半为空时(相当于两者之中取较小的值〉,它才发送窗口更新段。而且,发送端不发送太小的段也会有所帮助。相反,它应该等待一段时间,直到可以发送一个满的段,或者至少包含接收端缓冲区一半大小的段。
  • Nagle 的算法和Clark 针对低能窗口综合症的解决方案相互补充。Nagle 试图解决由于发送端应用每次向TCP 传递一个字节而引起的问题: Clark 则试图解决由于接收端应用每次从TCP 流中读取一个字节而引起的问题。这两种方案都有效的,而且可以一起工作。发送端的目标是不发送太小的数据段,接收端也不要请求太小的段。
  • 接收端的TCP 实体除了向发送端宣告较大的窗口以外,还可以进一步提高性能。如同发送端的TCP 一样,接收端的TCP 也可以缓冲数据,所以它可以阻塞上层应用的READ请求,直至它积累了大块的数据。这样做可以减少调用TCP 的次数,从而减少额外的开销。当然,这样做也增加了响应的时间,但是,对于像文件传输这样的非交互式应用来说,效率可能比单个请求的响应时间更加重要。
  • 接收端必须处理的另一个问题是乱序到达的数据段。接收端将缓冲这些数据,直至可以按照顺序递交给应用程序为止。实际上,没有什么比丢弃乱序到达的段更糟糕的事情,因为若丢弃这些段,那么它们将被发送端重传,这是一种浪费。
  • 只有当确认字节之前的所有数据都到达之后才能发送确认,这种方式称为累计确认(cumulative acknowledgement )。如果接收端己经获得段0、1 、2 、4、5 、6 、7,它可以确认直到段2 (包括段2 )之前的数据。当发送端超时,然后重发段3 。因为接收端己经缓冲了段4~段7,一旦它收到段3 就可立即确认直到段7 的全部字节。(笔者感觉自己读书的方式实在太蠢了,有点不想坚持了,或许应该只写写书后答案就好)

猜你喜欢

转载自blog.csdn.net/ao__ao/article/details/88416408