面向连接的运输:TCP

阅读本章前,需要了解:

运输层

可靠数据传输原理

一、TCP连接

TCP被称为是面向连接(connection-oriented),这是因为在一个应用进程可以开始向另一个应用进程发送数据之前,两个进程必须先相互“握手”,即他们必须先相互发送某些预备报文,以建立确保数据传输的参数。连接的双方都将初始化与TCP连接相关的许多TCP状态变量。

TCP连接不是电路交换网中的物理连接,而是一条逻辑连接,其共同状态仅保留在两个通信端系统的TCP程序中。由于TCP协议只在端系统中运行,而不在中间的网络元素(路由器和链路层交换机)中运行,所以中间的网络元素不会维持TCP连接状态。事实上,中间路由器对TCP连接完全视而不见,它们看到的是数据报,而不是连接。

TCP连接是全双工服务(full-duplex service):如果一台主机上的进程A与另一台主机上的进程B存在一条TCP连接,那么应用层数据就可在从进程B流向进程A的同时,也从进程A流向进程B。TCP连接总是点对点(point-to-point)的,即在单个发送方与单个接收方之间的连接。

TCP连接建立时会进行三次握手(three-way handshake),其简单过程是:客户首先发送一个特殊的TCP报文段,服务器用另一个特殊的TCP报文段来响应,最后客户再用第三个特殊报文段作为响应。两个主机之间发送了三个报文段,前两个报文段不包含应用层数据,第三个报文段可以包含应用层数据。

TCP连接建立好后,客户进程通过套接字将数据传给TCP,而后数据就由TCP控制。TCP将这些数据引导到该连接的发送缓存(send buffer,是三次握手期间设置的缓存)中。之后TCP将不时从发送缓存中取出一块数据,并传递给网络层。TCP可从缓存中取出并放入报文段中的数据数量受限于最大报文段长度(Maximum Segment Size,MSS),MSS通常分局最初确定的由本地发送主句发送的最大链路层帧长度(即最大传输单元(MTU))来设置。设置MSS要保证一个TCP报文段加上TCP/IP的首部长度将适合单个链路层帧。注:MSS是指在报文段里应用层数据的最大程度,而不是指包括首部的TCP报文段的最大长度。

在数据配上TCP首部形成TCP报文段后,经下层网络到达另一端,该报文段数据会被放在该TCP连接的接收缓存中,应用程序从此缓存中读取数据。TCP连接的每端都有各自的发送缓存和接收缓存。

clip_image001

                                                        图1.1 TCP发送缓存和接收缓存

二、TCP报文段结构

TCP报文段由首部字段和一个数据字段组成。数据字段包含一块应用数据,但MSS限制了报文段数据字段的最大长度。

例如当TCP发送一个大文件(如一个图像),TCP通常是将该文件划分成长度为MSS的若干块。但像交互式应用,如Telnet远程登录应用,其传送的数据块通常小于MSS。

clip_image002

                                                            图2.1 TCP报文段结构

TCP报文段首部包括源端口号目的端口号,被用于多路复用/分解操作;包含检验和字段(checksum field)。除此之外,还包含:

  • 32比特的序号字段(sequence number field)和32比特的确认序号字段(acknowledgement number field),被发送方和接收方用来实现可靠传输。
  • 16比特的接收出窗口字段(receive window field),该字段用于流量控制,用于指示接收方愿意接受的字节数量。
  • 4比特的首部字段长度(header length field),该字段指示了以32比特的字为单位的TCP首部长度。因为TCP有一个选项字段,TCP首部长度是可变的。若选项字段为空,则TCP首部典型长度为20字节。
  • 可选与变长的选项字段(options field),该字段用于发送方与接收方协商最大报文段长度(MSS)时,或在高速网络环境下用作窗口调节因子时使用。
  • 6比特的标志字段(flag field)。ACK比特用于指示确认字段中的值是有效的,即该报文段包括一个对已被成功接收报文段的确认。RST、SYN和FIN比特用于连接建立和拆除。在明确拥塞通告中使用了CWR和ECE比特。当PSH比特被置位时,就指示接收方应立即将数据交给上层。URG比特用来指示报文段里存在着被发送端的上层实体置为“紧急”的数据。紧急数据的最后一个字节由16比特的紧急数据指针字段(urgent data pointer field)指出。当紧急数据存在并给出指向紧急数据尾指针的时候,TCP必须通知接收端的上层实体。

2.1 序号和确认序号

这两个字段是可靠传输的关键部分,在“可靠数据传输原理”一章,我们举例时均以0、1……作为序号和确认序号,但我们也提到TCP序号是按字节流中的字节进行计数的,而不是建立在传送的报文段的序列上,现在我们具体来看。

TCP把数据看成是一个无结构的、有序的字节流。一个报文段的序号(sequence number for a segment)因此是该报文段首字节的字节流序号。例如,A向B发送一个数据流,A中的TCP将隐式地对数据流中的每一个字节编号。假定数据流由一个包含500 000字节的文件组成,其MSS为1000字节,数据流的首字节编号是0。那么该TCP将为该数据流构建500个报文段,给第一个报文段分配序号0,第二个分配序号1000,第三个分配2000,以此类推。每一个序号被填入到相应TCP报文段首部的序号字段中,如图2.2。

clip_image003

                                                                    图2.2 文件数据划分成TCP报文段

确认序号比序号难处理,TCP是全双工的,因此主机A在发数据的同时,也接收来自B的数据,从主机B到达的每个报文段中都有一个序号用于从B流向A的数据。因此主机A填充进报文段的确认号是主机A期望从主机B收到的下一个字节的序号。例如,主机A已收到一个来自B的包含字节0~535的报文段,以及另一个包含字节900~1000的报文段,由于A还没收到字节535~899的报文段,因此A到B的下一个报文段将在确认号字段中包含536。因为TCP只确认该流中至第一个字节为止的字节,所以TCP被称为提供累计确认(cumulative acknowledgement)。

上面的例子也引发一个问题,第二个报文丢失,第三个报文已经到达,那么对于这第三个失序的报文该怎么处理。TCP RFC并没有为此明确规定,而是留给编程人员处理,因此要么丢弃该报文,要么缓存该失序的报文。实践中是怎么处理的,我们后续说。

另外,一条TCP连接的双方均可以随机地选择初始序号,这样可以减少将那些仍在网络中存在的来自两台主机之间先前已终止的连接的报文段,误认为是后来这两台主机之间新建连接所产生的有效报文段的可能性。

2.2 序号和确认号学习案例

clip_image004

                                  图2.3 一个经TCP的简单Telnet应用的确认号和序号

客户A输入字符‘C’,我们假设客户和服务器的起始序号分别是42和79。这里假设客户第一个报文段序号为42,前面说到,确认号就是主机正在等待的数据的下一个字节序号,因此在TCP建立好后,该客户端期望收到来自服务器字节79。

然后第二个报文段由服务器发送,它首先需要通过填入确认号表明已收到42,因为确认号就是主机正在等待的数据的下一个字节序号,所以它在确认号中填入43,表明收到42正在等待43。它的序号是79,是服务器到客户的数据流的起始序号,是服务器要发送的第一个字节的数据(这里客户第一个报文段确认号79,即需要79,服务器正确收到后即反馈79,客户收到79后就知道服务器已收到)。

第三个报文段是客户确认服务器已收到数据,该报文段data域为空。该报文序号填入43,确认序号填入80(因为客户已收到79及之前所有的字节,正在等待80字节)。

我们从整体上可以看到这与选择重传类似,双方都维护着一个窗口,需要互相确认。那么TCP

到底是采用的哪种可靠协议呢?我们继续看。

2.3 超时

我们简短说一下超时问题,TCP也采用超时/重传机制处理丢失报文,超时的时间是通过一个公式计算的。首先超时间隔必须大于该连接的往返时间(RTT)即从一个报文段发出到它被确认的时间,该公式就是通过对这个值测量估计得到相对合理的RTT值。除此之外,不能直接利用该值,而是应该在该值的基础上给一个余量。在网络拥塞时,余量大些;网络不拥塞时,余量小些。

三、可靠数据传输

在可靠传输原理一章,我们最后的到“回退N步”和“选择重传”两种较优的协议,TCP采用的是哪一种呢?我先给出答案,TCP结合了两种两种协议,我们看具体实例。

首先TCP使用的是单一的重传定时器(为每个分组设置定时器开销太大),然后我们用两个递增步骤讨论来TCP的可靠实现。先给出一个TCP发送方的高度简化的描述,该发送方只用超时来恢复报文段的丢失;然后再给出一个更全面的描述,该描述中除使用超时机制外,还使用冗余确认机制。我们假定数据仅向一个方向发送,即从A到B,且A正在发送一个大文件。(注:我们这里只给出了发送方,但TCP是全双工服务,连接的一侧既是一个接收方,也是一个发送方,因此实际中客户端和服务端都会进行超时重传)

clip_image005

                                                    图3.1 简化的TCP发送方

如图3.1在该简化描述中,TCP发送方有3个与发送和重传有关的主要事件:从上层收到数据、定时器超时和收到ACK。前两个事件很容易理解,我们看第三个事件。当发送方收到一个有效ACK字段值,TCP将ACK值y与SendBase比较,SendBase是最早未被确认的字节序号,采用累积确认,所以y确认了在y之前所有的字节都已经收到。如果y>SendBase,则ACK是确认了一个或多个先前未被确认的报文段,因此发送方更新的它的SendBase值;如果当前仍无任何应答报文段(即最早未被确认报文段被确认,SendBase更新后的报文段还没收到应答报文,此时该报文段又成为最早未被确认报文段),TCP重新启动定时器。

对于描述中的if (y>SendBase),是因为底层信道不可靠,流水线工作方式下,报文段到达顺序不一定和发送顺序相同。例如,可能接收方会在极短时间内发送ACK=10,ACK=14,假如ACK=10在信道中经历过大时延,而ACK=14先到达发送方,发送方知道14之前的报文段已全被接收方收到,更新SendBase。这时ACK=10到达发送方,比SendBase小,就不会执行if语句内的操作。

3.1 结合累计确认与丢失分组缓存

根据上面的简化描述,我们讨论一些有趣的情况。

clip_image006

                                             图3.2 由于确认序号丢失而重传

如图3.2第一种情况,A向B发送一个报文段,假设该报文段序号为92,包含8个字节。A发出后等待B来自B的确认号为100的报文段,但该确认报文丢失了。这种情况就会发生超时,A会重传相同报文段。当B收到重传报文时,它通过序号发现该报文之前已收到,则B会丢弃该报文重发确认序号。

clip_image007

                                                              图3.3 报文段100没有重传

如图3.3第二种情况,A连续发送两个报文段,第一个报文段序号92,包含8字节;第二个100,包含20字节。两报文段都到达B,B为每个报文段都发送一个确认报文段。第一个确认报文的确认序号为100,第二个为120。假设在超时之前,两个报文都没有达到A。这时发生超时时间,A重传序号为92的报文段(最早未被确认),并重启定时器。主机B收到A的重传报文后,返回确认序号为120的确认报文,如图3.4。因为主机B已收到序号为119及之前所有的字节,所以直接返回它下一次期待接收的字节。

clip_image008

                                                  图3.5 累计确认避免了第一个报文段的重传

针对第二种情况,我想到这样一个问题:A连续发送序号为92和100两个报文段,假设序号92的报文丢失,序号100的报文到达B,那么B返回ACK=120,则发送方y=120>SendBase,SendBase被更新。这时序号92的报文段岂不是永远丢失了。事实上这是不会发生的,TCP采用累积确认,主机B接收到序号100的报文段时,发现它与上一次接收到的连续报文段的序号不是连续的,因此判断有丢失,返回的上一次接收到的连续报文段的确认报文。发送方收到冗余确认报文段,等待超时重发。

这里可能又有另一个问题,A发送序号92和100的报文段,B如何知道100序号之前还有另一个报文段未到达,即B为什么会判断到序号100不是第一个。这个问题就是TCP建立连接时解决的,在三次握手期间,序号和确认号的初始值就被设置好了。因此在即使92是真正第一个带数据的报文,但在这之前A和B之间已经交换过了特殊报文。

3.2 超时间隔加倍

每当超时事件发生时,TCP重传具有最小序号的还未被确认的报文段。但需要注意的是,每次TCP重传时都将会下一次的超时间隔设为先前值的两倍。例如,第一次定时器超时时间设置为0.75秒,发生第一次超时后,TCP重传报文,同时把新的过期时间设置为1.5秒。如此超时间隔在每次重传后会呈指数型增长。不过,每当定时器在另两个时间(即收到上层应用数据和收到ACK)中的任意一个启动时,超时间隔由另外的算法推算得到。

这种修改是实现拥塞控制的一部分。因为超时很可能是由于网络拥塞控制的,如果此时发送端继续不断发送分组,只会让拥塞更严重。通过延长时间间隔,等待一段时间后再试,可能会缓解拥塞。

3.3 快速重传

在3.1中针对第二种情况提到过这样的问题,A发送序号为92和100的报文段,假设序号92丢失,序号100到达。B发现序号100与上一次接收到的正确报文存在间隔,因此返回上一个连续报文的确认报文段。A收到该返回确认报文段后,就知道序号92丢失了,但A并不处理只是等待定时器超时再重发。这种机制很明显会使发送方延迟重传丢失分组,从而增加了时延。

TCP不使用否定确认(NAK),只是对已经接受到的最后一个按序字节数据进行重复确认(即产生一个冗余ACK),因为是流水线式发送,所以一个报文段丢失,发送方可能就会收到大量的冗余ACK。为了避免这两个问题,TCP采用快速重传(fast retransmit),即如果TCP发送方接收到对相同数据的3个冗余ACK,它把这当做一种指示,说明跟在这个已被确认过3次的报文段之后的报文段已经丢失,就立即重传丢失的报文。

clip_image009

                                 图3.6 收到冗余ACK的处理过程

clip_image010

                   图3.7 快速重传:在某报文段的定时器过期之前重传丢失的报文段

3.4 产生TCP ACK的建议

                                                              事件                                                                                        TCP接收方动作
具有所期望序号的按序报文段到达。所有在期望序号以及以前的数据都已经被确认 延迟的ACK。对另一个按序报文段的到达最多等待500ms。如果下一个按序报文段在这个时间间隔内没有到达,则发送一个ACK。
具有所期望序号的按序报文到达。另一个按序报文段等待ACK传输。 立即发送单个累积ACK,以确认两个按序报文段。
比期望序号大的失序报文段到达,检测出间隔 立即发送冗余ACK,指示下一个期待字节的序号。((即已收到1、3、4,在收到2后,立即返回ACK=5)
能部分或完全填充接收数据间隔的报文段到达 倘若该报文段起始于间隔的低端,立即发送ACK

3.5 小结

TCP是GBN和SR协议的混合体,它使用了单个定时器,每次为最早未被确认报文段设置;采用GBN中的累积确认方式和SR中的报文段缓存(发送方和接收方拥有各自的接收发送缓存),维护两个滑动窗口。

当接收方收到按序到达报文段,若缓存中没有和当前接收报文段连续的序号更大的报文段,返回该报文段确认报文段(例如收到1,返回1的确认报文段);若缓存中有和当前接收报文段连续的序号更大的报文段,则返回这个连续的更大报文段的确认报文段(例如收到报文段1,缓存中有报文段2和3,返回3的确认报文段)。当接收方收到一个非连续报文段,则返回上一个按序到达报文段的确认报文段(例如已收到报文段1,又收到报文段3,则缓存报文段3,返回报文段1的确认报文段)。

四、流量控制

一条TCP连接的每一侧主机都为该连接设置了缓存,当该连接接收到正确、失序的字节后,都会将数据放入接收缓存。应用程序从缓存中读取数据,但是接收方应用不一定立即读取数据。这时如果发送方发送得太多、太快,就会使接收方缓存溢出。

为此TCP为应用程序提供流量控制服务(flow-control service)以消除发送方使接收方缓存溢出的可能性。因此流量控制是一个速度匹配服务,即发送方的发送速率和接收方应用程序的读取速率相匹配。TCP发送方也可能因为网络拥塞而被遏制,这种是拥塞控制服务(congestion control),尽管所采取动作相似,但是针对的不同原因采取的措施。

具体实现是,TCP让发送方维护了一个接收窗口(receive window)的变量来提供流量控制,接收窗口用于给发送方一个指示——该接收方还有多少可用的缓存空间。

因为TCP是全双工通信,在连接两端的发送方都各自维护一个接收窗口。假设主机A通过一条TCP连接向B发送一个大文件,主机B为该连接分配一个接收缓存,并用RcvBuffer表示其大小,我们定义一下变量:

  • LastByteRead:主机B上的应用进程从缓存读出的数据流的最后一个字节的编号。
  • LastByteRcvd:从网络中到达的并且已放在主机B接收缓存中的数据流的最后一个字节的编号。

由于TCP不允许已分配的缓存溢出,因此必须成立:

LastByteRcvd - LastByteRead <= RcvBuffer

接收窗口用rwnd表示,如图4.1,它是动态变化的,根据缓存可用空间的数量来设置:

rwnd = RcvBuffer - [LastByteRcvd - LastByteRead]

clip_image011

                                                       图4.1 接收窗口(rwnd)和接收缓存(RcvBuffer)

连接就使用rwnd来控制流量,具体操作为:主机B通过把当前rwnd值放入它发给主机A的报文段接收窗口字段中,通知主机A它在该连接的缓存中还有多少可用空间。

开始时,主机B设定rwnd=RcvBuffer。主机A轮流跟踪两个变量LastByteSent和LastByteAcked,这两个变量之差(LastByteSent - LastByteAcked)就是A发送到连接但未被确认的数据量。通过将未确认的数据量控制在rwnd以内,就能保证A不会使B的接收缓存溢出。因此,在连接的整个生命周期内A必须保证:

LastByteSent - LastByteAcked <= rwnd

这里还存在一个问题,假设B接收缓存已满则rwnd=0,同时假设B在这时没有任何其他数据要发给A。则A在得知rwnd=0后不再向B发送数据。这时TCP不再向主机A发送带有新的rwnd新值的报文段,主机A就不可能知道B的接受缓存已有了新的空间,那么A就会被永远阻塞。为解决这个问题,TCP在当B的接收窗口为0时,A继续发送只有一个字节的数据报文段,这些报文段将被正确接收。最终缓存空间将开始清空,并且确认报文里将包含一个非0的rwnd值。

注意:UDP没有流量控制,因此报文段可能由于缓存溢出而在接收方丢失(拿到的都丢了,耻辱)。

五、TCP连接管理

5.1 三次握手及相关问题

一台主机上的进程想与另一台主机上进程建立TCP连接,首先客户应用进程通知客户TCP,然后客户TCP会用以下方式与服务器中的TCP建立一条TCP连接(如图5.1):

(1)第一步:客户端的TCP首先向服务器端的TCP发送一个特殊的TCP报文段。该报文段中不包含应用层数据,但在报文段首部中有一个特殊的标志位(即SYN比特)被置为1,因此该报文段被称为SYN报文段。另外客户会随机选择一个初始序号(client_isn),并将该序号放在SYN报文段的序号字段中。

(2)第二步:当SYN报文段正确到达服务器主机,服务器会为该TCP连接分配TCP缓存和变量,并向该客户TCP发送允许连接的报文段(这里可能会产生SYN洪泛攻击)。这个允许连接的报文段也不包含应用层数据,但该报文段首部SYN比特被置为1,确认号字段被置为client_isn+1,最后服务器选择自己的初始序号(server_isn)。该报文段被称为SYNACK报文段,目的是表明同意建立该连接。

(3)第三步:在收到SYNACK报文段后,客户也要给该连接分配缓存和变量。客户又向主机发送建立连接时的最后一个报文段,这个报文段对服务器的允许连接的报文段进行了确认(即将值server_isn+1放在该报文段的确认字段上)。因为连接已建立,所以该SYN比特被置为0。与前两个报文段不同的是,该报文段可以携带客户要发送的数据。

clip_image012

                                                图5.1 TCP三次握手:报文段交换

此后所有发送的报文,SYN比特都被置为0。这里就有疑问:为什么需要随机的初始序号?为什么需要3次握手而不是两次握手?

问题一:序号是按字节流中字节进行计数的,这样接收方就知道自己收到的分组是否有序。开始连接时随机初始序号有两个作用,一是如果有人知道固定序号的顺序,就可能伪造报文段,不安全;二是如果每次使用固定的序号,假定这样一个情况,A和B采用非持续连接,频繁发送文件时,就会反复建立TCP连接。假如其中有一次,A和B新建的连接用到的端口号恰好是上一个断开的连接用过的,而在上一个连接时,有一个报文段由于网络拥塞一直在信道中,在新的连接建立好后才到达B。这时由于源和目的端口相同,这个报文到达当前连接,如果每次使用固定序号,这个报文可能就会被当前连接正确接收,不安全。

问题二:这里有两个原因,一是为了保证客户端和服务端能都够确认对方已经准备好了。首先A发送SYN报文段,这时A还没有B的初始序号,所以一些状态和变量未初始化,等待来自B的信息。B收到A的报文段后,初始化后向A发送SYNACK报文段(B准备好了,具备收发能力),A收到后初始化成功(A准备好,具备收发能力)。若两次握手,A能确认B准备好了,但B不能确认A是否准备好,则两端信息就不一定对称。

二是防止已超时的SYN报文段在服务端产生错误,其实这并不是根本原因,只是原因一的其中一种可能性而已。假设A发送SYN报文段,但该报文阻塞了,超时后A又发送了一个SYN报文,这次报文到达B。B返回SYNACK报文段,A收到后发送带数据确认报文后连接关闭。若如果这时第一个SYN报文段到达,B又会初始化,并且由于两次握手,A不进行反馈,B就会保持该开销。

5.2 四次挥手及相关问题

参与一条TCP连接的两个进程中的任何一个都能终止该连接,当连接结束后,主机中的“资源”(缓存和变量)将被释放(如图5.2)。假设客户应用进程发出一个关闭连接命令,这会引起客户TCP向服务器进程发送一个特殊的TCP报文段,这个特殊报文段让其首部中的一个标志位即FIN比特被设置为1。当服务器收到该报文段后,就向发送方发回一个确认报文段,然后服务器发送它自己的终止报文段,其FIN比特被置为1。最后该客户对这个服务器的终止报文段进行确认,此时两台主机上用于该连接的所有资源被释放。

clip_image013

                                                      图5.2 关闭一条TCP连接

同样的,这里也需要提:为什么连接的时候是三次握手,关闭的时候却是四次握手?

当服务端收到客户端的SYN连接请求报文后,因为是建立连接,资源是初始化的,因此将SYN和ACK放在一个报文段中发送。其中ACK报文是用来应答的,SYN报文是用来同步(Synchronous)的。但是关闭连接时,收到FIN报文段表示对方不再发送但还能接受报文。因此当服务端收到FIN报文时,服务端还有报文需要发送和处理,不会立即关闭,所以只能先回复一个ACK报文,告诉客户端自己收到了报文。等到服务端所有的报文都处理完了,再发送FIN报文,因此一般是将FIN和ACK分开发送。

5.3 TCP连接状态

在一个TCP连接的生命周期内,运行在每台主机的TCP协议在各种TCP状态间变迁(因为TCP是全双工服务,所以一台主机既可能是服务器,也可能是客户端)。

客户端:如图5.3,是客户TCP经历的典型TCP状态。客户TCP开始时出于CLOSE(关闭)状态。客户的应用程序发起一个新的连接,这使客户中的TCP向服务器的TCP发送一个SYN报文段,发送过该报文段后客户TCP进入SYN_SENT状态。当客户TCP出于SYN_SENT状态时,它等待服务器TCP对客户所发报文段进行确认,且确认报文段SYN被置为1。当客户TCP收到该确认报文后,客户TCP进入ESTABLISHED(已建立)状态。当处在ESTABLISHED状态时,TCP客户就能发送和接收包含有效载荷数据的报文段了。

clip_image014

                                                       图5.3 客户TCP经历的典型的TCP状态序列

假设客户应用程序决定要关闭连接(服务器也能选择关闭连接)。这时客户TCP发送一个带有FIN比特被置为1的TCP报文段,并进入FIN_WAIT_1状态。当处于FIN_WAIT_1状态时,客户TCP等待一个来自服务器的带有确认的TCP报文段。当它收到该报文段时,客户TCP进入FIN_WAIT_2状态。当处在FIN_WAIT_2状态时,客户等待来自服务器的FIN比特被置为1的另一个报文段;当收到该报文段后,客户TCP对服务器的报文段进行确认并进入TIME_WAIT状态。假定客户端发送的ACK丢失,TIME_WAIT状态使TCP客户重传最后的确认报文。在TIME_WAIT状态中所消耗的时间是与具体实现有关的,而典型的值是2MSL(报文段在网络中的最大生存时间)。经过等待后,连接就正式关闭,客户端所有资源包括端口号被释放。

问题:为什么TIME_WAIT要等一段时间才进入CLOSED?

这里有两个原因,一是尽量保证被动关闭的一端收到它自己发出去的FIN报文的ACK确认报文(即可能最后一个确认报文丢失,需要超时超时重传);二是处理延迟的重复报文,避免前后两个使用相同四元组的连接中的前一个连接的报文干扰后一个连接。

服务端:如图5.4是服务器端TCP通常要经历的一系列状态,其中假设客户和服务器都准备通信,即服务器正在监听客户发送的SYN报文段的端口,假设客户开始拆除连接的。现在我们考虑这一种情况,假如主机收到一个TCP报文段,其端口号或源IP地址与该主机上进行中的套接字都不匹配的情况。例如一台主机收到了具有目的端口号80的一个TCP SYN分组,但该主机在端口80不接受连接(即它不在端口80上运行Web服务器)。则该主机向源发送一个重置报文段,该TCP报文段将RST标志位置为1,即告诉源主机不要再发送该报文段了。注:当主机收到一个目的端口与进行中的UDP套接字不匹配的UDP分组时,该主机发送一个ICMP数据报。

因此,假如我们用端口扫描工具,对某端口发送一个TCP SYN报文段时,可能得到三种结果:

  • 源主机从目标主机接收到一个TCP SYNACK报文段。说明目的主机上的一个应用程序使用TCP该端口运行,该端口打开。
  • 源主机接收到一个TCP RST报文段。说明SYN报文段到达,但目标主机没有一个应用使用该端口,不过至少说明源和目标主机之间没有防火墙阻挡SYN报文段。
  • 源什么都没接收到。说明很可能SYN报文段被中间的防火墙所阻挡。

clip_image015

                                            图5.4 服务器端TCP经历的典型的TCP状态序列

这里还有另一个情况,就是服务器主动断开连接的情况,事实上还有很多情况,如服务器进程终止、服务器主机奔溃/奔溃后重启、服务器关机等异常情况。这种情况下发现四次挥手过程中服务器和客户端的状态颠倒了,也就是说,服务端和客户端的进程那个先向对方发送FIN 字段报文,那么哪个就先进入FIN_WAIT2状态。

整个过程是,当服务器进程被终止时,会关闭其打开的所有文件描述符,此时就会向客户端发送一个FIN 的报文,客户端则响应一个ACK 报文,但是这样只完成了“四次挥手”的前两次挥手,也就是说这样只实现了半关闭,客户端仍然可以向服务器写入数据。

但当客户端向服务器写入数据时,由于服务器端的套接字进程已经终止,此时连接的状态已经异常了,所以服务端进程不会向客户端发送ACK 报文,而是发送了一个RST 报文请求将处于异常状态的连接复位;如果客户端此时还要向服务端发送数据,将诱发服务端TCP向服务端发送SIGPIPE信号,因为向接收到RST的套接口写数据都会收到此信号. 所以说,这就是为什么我们主动关闭服务端后,用客户端向服务端写数据,还必须是写两次后连接才会关闭的原因。这里有一篇博文https://www.cnblogs.com/549294286/p/5208357.html,有较为详细的解释。

5.4 小结及SYN洪泛攻击

如图5.5和5.6(图来源于网络),我们将TCP状态和过程放在一起看:

clip_image016

                                                                           图5.5 三次握手

clip_image017

                                                                                 图5.6 四次挥手

最后说一下SYN洪泛攻击,在三次握手时,服务器响应SYN报文段,分配并初始化连接变量和缓存,然后返回SYNACK报文段。加入最后客户端没有发第三个报文段,最终服务器将终止该半开连接并回收资源。

SYN洪泛攻击(SYN flood attack),就是攻击者发送大量的SYN报文段,而不完成第三次握手的步骤。这种SYN报文段太多,就会导致服务器不断为这些半开连接分配资源,最终导致资源被消耗殆尽。这种攻击通过SYN cookie进行防御,其工作方式是:

  • 当服务器收到SYN报文段时,它不知道该报文的目的,因此不会为该报文生成一个半开连接。相反,服务器会生成一个初始TCP序列号,该序列号是SYN报文段的源和目的IP地址与端口号以及仅有服务器知道的秘密数的一个复杂函数。这个序列号被称为“cookie”,服务器则发送具有这种特殊初始序列号的SYNACK分组。
  • 如果客户合法,则它返回ACK报文段,当服务器收到该ACK,需要验证该ACK是与前面发送的某些SYN报文段相对应的(这里就借助了cookie的作用)。然后就为这个合法的连接生成具有套接字的全开连接。
  • 如果客户没有返回ACK报文段,则初始的SYN并不会对服务器产生危害,因为服务器并没有为它分配资源。

六、拥塞控制

对网络来说拥塞显然不好,这里我们提出三种拥塞的情况,一步步分析它的问题:

(1)情况一:两个发送方和一台具有无穷大缓存的路由器。这种情况中,当分组到达速率接近链路容量时,分组将经历巨大的排队时延。

(2)情况二:两个发送方和一台具有有限缓存的路由器。这种情况,发送方必须执行重传以补偿因为缓存溢出而丢弃的分组。这就会带来,发送方在遇到大时延时所进行不必要重传会引起路由器利用其链路带宽来转发不必要的分组副本。

(3)情况三:多个发送方和具有有限缓存的多台路由器及多条路径。这种情况是(2)的复杂版,当一个分组沿一条路径被丢弃时,每个上游路由器用于转发该分组到丢弃该分组而使用的传输容量最终被浪费。

6.1 拥塞控制方法

显然我们需要一些机制在面临网络拥塞时能遏制发送方,这里我们提出两种在实践中所采用的主要方法。

  • 端到端拥塞控制。在该方法中,网络层没有为运输层拥塞控制提供显式支持,TCP通过网络中行为(超时、三次冗余等)认为网络陷入拥塞,TCP会相应地减小其窗口长度。
  • 网络辅助的拥塞控制。在该方法中,路由器向发送方提供关于网络拥塞情况的显式反馈。有两种反馈方式,一是路由器发送一种阻塞分组(choke packet);二是路由器标记或者更新从发送方流向接收方的分组中的某个字段来指示拥塞的产生,一旦接收方收到该标记分组,接收方会向发送方通知网络拥塞。

在TCP中,IP层不向端系统提供显式的网络拥塞反馈,所以TCP使用的是端到端的拥塞控制。

6.2 拥塞控制实现

TCP所采取的方法是让每一个发送方根据所感知到的网络拥塞程度来限制其能向连接发送流量的速率。我们根据这句总结,从三个问题来逐步阐述拥塞控制实现。

第一,一个TCP发送方如何限制它向其连接发送流量的速率呢?

在流量控制的时候,我们用到了接收窗口(rnwd)变量。同样地,运行在发送方TCP拥塞控制机制跟踪一个额外的变量——拥塞窗口(congestion window),缩写cwnd。它对一个TCP发送方能向网络中发送流量的速率进行了限制。结合流量控制,那么在一个发送方中未被确认的数量不会超过cwnd和rwnd中的最小值。

LastByteSent - LastByteACKed <= min {cwnd,rwnd}

通过这样的约束限制了发送方中未被确认的数据量,因此间接地限制了发送方的发送速率。我们假设一个丢包和发送时延均可以忽略不计的连接,因此粗略地讲,在每个往返时间(RTT)的起始点,拥塞窗口的限制条件允许发送方向该连接发送cwnd个字节的数据,在该RTT结束时发送方接收对数据的确认报文。因此该发送方的发送速率大概是 cwnd/RTT 字节/秒。通过调节cwnd的值,发送方能调整它向该连接发送数据的速率。

这里以及后面我们都假设TCP的接收缓存足够大,因此可以忽略流量控制机制的限制,即在发送方中未被确认的数量仅受限于cwnd,并且假设发送方总是有数据要发送,即在拥塞窗口中的所有报文段要被发送。

第二,一个TCP发送方如何感知从它到目的地之间的路径上存在拥塞呢?

当出现过度拥塞时,在这条路径上的一台路由器的缓存会溢出,会引起一个数据报被丢弃,丢弃的数据报就会引起发送方的丢包事件(要么超时或收到3个冗余ACK),发送方就认为在发送方到接收方的路径上出现了拥塞的指示。

TCP使用确认报文段的指示来触发(或计时)增大它的拥塞窗口长度,TCP被说成是自计时(self-clocking)的。如果发送方收到确认报文是以相当慢的速率到达(可能在路径中存在一段高时延或一段低带宽链路),则该拥塞窗口将以相当慢的速率增大;如果确认以高速率到达,则该拥塞窗口将更为迅速地增大。

第三,当发送方感知到端到端的拥塞时,采用何种算法来改变其发送速率?

TCP发送方怎样确定它应当发送的速率呢?如果TCP发送方发送太快,就可能导致网络拥塞;如果发送太慢,又不能充分利用网络带宽。这里有三个指导性原则:

  • 一个丢失报文段意味着拥塞,因此当丢失报文段时应当降低TCP发送方的速率。
  • 一个确认报文段知识该网络正在向接收方交付发送方的报文段,因此当对先前未确认报文段的确认到达时,能够增加发送方的速率。
  • 带宽探测。给定ACK指示源到目的地路径无拥塞,而丢包事件指示拥塞,TCP调节其传输速率的策略是增加其速率以响应到达的ACK,除非出现丢包事件,才减小传输速率。

接下来就是在这三个原则之下的TCP拥塞控制算法(TCP congestion control algorithm),该算法包括三个主要部分:慢启动;拥塞避免;快速恢复。前两项是TCP强制部分,两者的主要差别是在于对收到的ACK做出反应时增加cwnd长度的方式。最后一项是推荐部分,对TCP发送方并非必须的。

6.2.1 慢启动

当一条TCP连接开始时,cwnd的值通常初始设置为一个MSS(最大报文长度)的较小值,这就使得初始发送速率大约为MSS/RTT。例如,如果MSS=500字节且RTT=200ms,则初始发送速率大约只有20kbps。然而在实际中,可用带宽明显比MSS/RTT大得多,TCP发送方希望迅速找到可用带宽的数量。因此慢启动(slow-start)状态,cwnd的值以1个MSS开始并且每当传输的报文段首次被确认就增加一个MSS。如图6.1,这个过程每过一个RTT,发送速率就翻番,因此TCP发送速率起始慢,但在慢启动阶段就以指数增长。

clip_image018

                                      图6.1 TCP慢启动

这种增长不可能一直持续,它会在几种情况下结束。

方式一,如果存在一个由超时指示的丢包事件(即拥塞),TCP发送方将cwnd设置为1并重新开始慢启动过程。它还将第二个状态变量的值ssthresh(“慢启动阈值”的速记)设置为cwnd/2,即当检测到拥塞时将ssthresh值为拥塞窗口的一半。

方式二,通过ssthresh的值,当检测到拥塞时ssthresh会被设置为cwnd的一半,当到达或超过ssthresh的值时,继续使用cwnd翻番可能太大了。因此当cwnd的值等于ssthresh时,结束慢启动并且进入拥塞避免模式。

方式三,如果检测到3个冗余ACK,这时TCP执行一种快速重传并进入快速恢复状态。

clip_image019

                                                                                 图6.2 TCP拥塞控制的FSM描述

6.2.2 拥塞避免

进入拥塞避免状态时,cwnd是达到或超过ssthresh的值,因此cwnd的值大约是上次遇到拥塞时的值的一半。这时TCP不会每过一个RTT再将cwnd的值翻番,而是采用一种较为保守的办法,每个RTT只将cwnd的增加一个MSS。一种通用方法是对于TCP发送方无论何时到达一个新的确认,就将cwnd增加一个MSS字节。

它的结束方式有,当出现超时时,cwnd的值被设置为一个MSS;当丢包事件出现时,ssthresh的值被更新为cwnd的一半;当收到三个冗余ACK时,网络继续从发送方向接收方交付报文,TCP将cwnd的值减半,将ssthresh的值记录为cwnd的值的一半。接下来进入快速恢复状态。

6.2.3 快速恢复

在快速恢复中,对于引起TCP进入快速恢复状态的缺失报文段,对收到的每个冗余的ACK,cwnd后进入拥塞避免状态。如果出现超时事件,快速恢复在执行如同在慢启动和拥塞避免中相同的动作后,迁移到慢启动状态:当丢包事件出现时,cwnd的值被设置为1个MSS,并且ssthresh的值设置为cwnd的一半。

6.3 公平性

考虑K条TCP连接,每条都有不同的端到端路径,但是都经过一段传输速率为R bps的瓶颈链路。如果每条连接的平均传输速率接近R/K,即每条连接都得到相同份额的链路带宽,则认为该拥塞控制机制是公平的。

在这里我不解释TCP使用到的AIMD算法,仅直观的想(个人看法,还是研究AIMD算法才是正道),两条TCP对链路使用就是一种博弈的过程:即两条连接都想得到最大带宽,其中一方可能就会先被拥塞,而减小发送速率,另一个方在这时就会增加发送速率,但增加到一定程度又会被拥塞,从而减小。两者相互博弈,能够使带宽得到公平的利用。UDP没有内置的拥塞控制机制,因此不会与其他连接合作,适时调节其传输速率。

但对TCP的公平性我们提出这样一个问题,即我们没什么办法阻止基于TCP的应用使用多个并行连接。当一个应用使用多条并行连接时,它占用了一条拥塞链路中较大比例的带宽。例如,一个段速率为R且支持9个在线客户-服务器应用的链路,每个应用使用一条TCP连接。如果一个新的应用加进来,也使用一条TCP连接,则每个应用得到差不多相同的传输速率R/10。但是如果这个新的应用这次使用了11个并行TCP连接,则这个新应用就不公平地分配到超过R/2的带宽。这种情况在Web应用中非常常见。

posted @ 2020-11-26 16:17  Aidan_Chen  阅读(491)  评论(0编辑  收藏  举报