在讲解TCP之前(你需要了解运输层的相关知识),先来明确几个概念。
最大报文段长度(MSS):MSS是指在报文段里应用层数据的最大长度,而不是指包括首部的TCP报文段的最大长度。通常根据最初确定的由本地主机发送的最大链路层帧长度(即所谓的最大传输单元MTU)来设置。
TCP连接的组成:一台主机上的缓存、变量和与进程连接的套接字,以及另一台主机上的另一组缓存、变量和与进程连接的套接字。两端都有各自的发送缓存和接收缓存。在这两台主机之间的网络元素(路由器、交换机、中继器),没有为该连接分配任何缓存和变量。
全双工服务:TCP提供的是全双工服务,可以类比为双向通行的道路。
TCP报文段结构
TCP报文段分为首部字段和数据字段,数据字段包含一块应用数据,MSS限制了报文段数据字段的最大长度。当TCP传送大文件时,会将该文件划分为长度为MSS的若干块。
TCP首部包含字段如下。
- 源端口号(16b)&目的端口号(16b):用于多路复用/分解来自或送到上层应用的数据。
- 检验和字段(16b):用于差错校验。
- 序号字段(32b)&确认号字段(32b):用于实现可靠数据传输服务。
- 接收窗口字段(16b):用于流量控制,指示接收方愿意接受的字节数量。
- 首部长度字段(4b):用于指示以32比特的字为单位的TCP首部长度。
- 可选与变长的选项字段:用于发送方与接收方协商最大报文长度(MSS)时,或在高速网络环境下用于窗口调节因子时使用。
- 标志字段(8b):
- ACK:用于指示确认字段中的值是有效的。
- RST、SYN、FIN:用于连接的建立和拆除。
- CWR、ECE:用于明确拥塞通告。
- PSH:用于指示接收方应立即将数据交给上层。
- URG:用于指示报文段里存在着被发送端的上层实体置为紧急的数据。
- 紧急数据指针字段(16b):当紧急数据存在,TCP必须通知接收端的上层实体。
序号和确认号
TCP把数据看成无结构的、有序的字节流。序号是建立在传送的字节流之上,而不是建立在传送的报文段的序列之上。
假设主机A上一个进程想通过TCP连接向主机B上的一个进程发送一个数据流,主机A中的TCP隐式的对数据流中的每一个字节编号。假定数据流由一个包含1000字节的文件组成,其MSS为100字节。TCP将为该数据流构建10个报文段,第一个报文段的序号是0,第二个报文段的序号是100,以此类推。每一个序号被填入到相应的TCP报文段中(这里我们假设了初始序号为0,但实际上TCP连接的双方可以随机选取初始序号,这样做可以减少仍在网络中存在的两台主机之间先前已经终止的连接的报文段误认为是后来这两台主机之间新建连接所产生的有效报文段的可能性)。
主机A填充进报文段的确认号是主机A期望从主机B接收到的下一字节的序号。
假设主机A已收到一个来自主机B的包含字节0-535的报文段,以及另一个包含字节900-1000的报文段,由于某种原因,主机A还没有收到字节536-899的报文段。在这个例子中,主机A为了重构主机B的数据流,仍在等待字节536及其后面的字节。因为A到B的下一个报文段将在确认号字段中包含536。因为TCP值确认该流中到第一个丢失字节为止的字节,所以TCP被称为提供累积确认。
考虑这个例子中第三个报文段先于第二个报文段失序到达,对于失序报文段TCP并没有明确规定处理方式,而是留给TCP的编程人员处理。处理方式:①接收方立即丢弃失序报文段;②接收方保留失序字节,并等待缺少的字节以填补该间隔。后一种方式是实践中采用的方法。
RTT的估计与超时
TCP采用超时/重传机制来处理报文段的丢失问题,超时时间间隔必须大于该连接的往返时间(RTT),否则会造成不必要的重传。
报文段的样本RTT(SampleRTT)就是从某报文段被发出到对该报文段的确认被收到之间的时间量,由于路由器的拥塞和端系统负载的变化,这些报文段的SampleRTT的值会随之波动。TCP维持了一个SampleRTT均值(EstimatedRTT),一旦获得一个新的SampleRTT时,TCP就会更新EstimatedRTT。
RTT偏差(DevRTT)用于估算SampleRTT一般会偏离EstimatedRTT的程度。
超时时间间隔应该大于EstimatedRTT,否则会造成不必要的重传,但也不能比EstimatedRTT大太多,否则会导致数据传输时延大。因此当SampleRTT的波动较大时,加上的余量应该大一些,反之则小一些,这时就需要DevRTT了。
推荐的初始TimeoutInterval值为1秒,当出现超时后,TimeoutInterval值将加倍,然而只要收到报文段并更新EstimatedRTT,就重新计算TimeoutInterval。
可靠数据传输
在之前讲述可靠数据传输时,假定每一个已发送但未被确认的报文段都与一个定时器相关联,但定时器的管理却需要相当大的开销。因此推荐的定时器管理过程仅使用单一的重传定时器,即使有多个已发送但还未被确认的报文段。TCP协议遵循了这种单一定时器的推荐。
TCP发送方有3个与发送和重传有关的主要事件:①从上层应用接收数据;②定时器超时;③收到ACK。
一旦①发生,TCP从应用程序接收数据,并封装在报文段中,并把该报文段交给网络层。如果定时器还没有为某些其他报文段而运行,则当报文段被传给网络层时,TCP就启动该定时器。(将定时器关联到最早的未被确认的报文段)。该定时器的过期时间间隔是TimeoutInterval。
一旦②发生,TCP通过重传引起超时的报文段来响应超时事件,然后TCP重启定时器。
一旦③发生,TCP将ACK的值y与它的SendBase(最早未被确认字节序号,即窗口开始)比较。y确认了字节编号在y之前的所有字节都以收到,如果y>SendBase,则更新SendBase。如果当前有未被确认的报文段,TCP还要重启定时器。
超时间隔加倍
TCP重传具有最小序号的还未被确认的报文段,每次重传都会将下一次的超时间隔设为先前值的两倍,直到再次收到ACK或收到上层应用的数据时会重新计算超时间隔。因此超时间隔在每次重传后会呈指数型增长。
这种机制提供了一个形式受限的拥塞控制,定时器过期很可能是由网络拥塞引起的,在拥塞的时候如果持续重传分组,会使拥塞更加严重。相反TCP提供了更加文雅的方式,每个发送方的重传都是经过越来越长的时间间隔后进行的。
快速重传
超时触发重传的问题之一超时周期可能相对较长。幸运的是发送方通常可在超时发生之前通过冗余ACK来较好的检测到丢包的情况。冗余ACK就是再次确认某个报文段的ACK,而发送方先前已经受到对该报文段的确认。
TCP不使用否定确认,只是对已经接受到的最后一个按序字节进行重复确认,即产生一个冗余ACK。如果TCP发送方接收到对相同数据的3个冗余ACK,说明跟在这个已经被确认过3次的报文段之后的报文段已经丢失。一但收到3个冗余ACK,TCP就执行快速重传,即在该报文段的定时器过期之前重传丢失的报文段。
GNB or SR ?
TCP确认是累积式的,正确接收但失序的报文段是不会被接收方逐个确认的。因此TCP发送方仅需维持已发送过但未被确认的字节的最小序号和下一个要发送的字节的序号,不同于GBN的是TCP会将正确接收但失序的报文段缓存起来。
对TCP提出的一种修改意见是所谓的选择确认,允许TCP接收方有选择的确认失序报文段,而不会累积的确认最后一个正确接收的有序报文段,当该机制与选择重传机制结合起来使用时,TCP就看起来很像SR协议。因此TCP的差错机制可以看做GBN和SR的混合体。
流量控制
TCP连接的两端都为该连接设置了接收缓存,当TCP收到正确按序的字节后,就将数据放入接收缓存。相关联的应用程序从缓存中读取数据。事实上应用程序也许正忙于其他任务,如果应用程序读数据相对缓慢,而发送方发送的数据太多太快,就会很容易的导致接收缓存溢出。
TCP为了解决该问题提供了流量控制服务。流量控制是一个速度匹配服务,即发送方的发送速率与接收方的接收速率相匹配。为了易于理解我们假设TCP接收方会丢弃失序报文段。
通过让发送方维护一个接收窗口的变量来提供流量控制。接收窗口用于告知发送方该接收方还有多少可以的缓存空间。
假设主机A通过一条TCP连接向主机B发送一个大文件,主机B为该连接分配了一个接收缓存,用RcvBuffer表示其大小。主机B上的应用进程不时从该缓存读取数据。
定义有如下变量。
- LastByteRead:主机B上的应用程序从缓存读出的数据流的最后一个字节的编号。
- LastByteRcvd:从网络到达的并且已放入主机B接收缓存中的数据流的最后一个字节的编号。
- rwnd:接收窗口。
rwnd = RcvBuffer - [LastByteRcvd - LastByteRead]
主机B通过把当前的rwnd放入它发给主机A的报文段接收窗口字段中,通知主机A它在该连接的缓存中还有多少可用空间。
LastByteRcvd - LastByteRead就是主机A发送到连接中但是未被确认的数据量。通过将该值控制在rwnd内,就可以保证主机A不会使主机B的接收缓存溢出,因此主机A在该连接的整个生命周期必须保证:LastByteRcvd - LastByteRead <= rwnd。
一个小问题是,当主机B发送了rwnd=0后,没有再发送新的报文段,接着主机B的接收缓存有空间,但是主机A不知道,即主机A被阻塞而不能再发送新数据。因此TCP规定:当主机B的接收窗口为0时,主机A继续发送只有一个字节数据的报文段,这些报文段将会被接收方确认。最后缓存开始清空,并且确认报文里将包含一个非0的rwnd值。
UDP没有提供流量控制,因此接收方缓存可能会溢出,并且丢失报文段。
TCP连接管理
三次握手
第一步:客户端的TCP首先向服务端的TCP发送一个特殊的TCP报文段。该报文段不包含应用层数据。但是在报文段的首部SYN被置1.因此该特殊的报文段被称为SYN报文段。另外客户端会随机选择一个初始序号(client_isn),并将此编号放置于该起始的SYN报文段的序号中,该报文段会被分装在一个IP数据报中,并发送给服务器。
第二步:一旦SYN报文段的IP数据到达服务器,服务器会为该TCP连接分配TCP缓存和变量,并向该客户TCP发送允许连接的报文段。这个允许连接的报文段也不包含应用层数据。但是首部包含3个重要信息:①SYN置1;②确认号置为client_isn+1;③服务器选择初始序号(server_isn),并放置到序号字段。该允许连接的报文段被称为SYNACK报文段。
第三步:收到SYNACK报文段后,客户也要给该连接分配缓存和变量,客户主机向服务器发送另外一个报文段。这最后一个报文段对服务器的允许连接报文段进行了确认(确认号server_isn+1)。因为连接已经建立了,所以SYN置0。该报文段可以携带应用层数据。
一旦完成这3个步骤,客户端和服务端就可以相互发送包括数据的报文段了。以后每一个报文段中,SYN都置0。
四次挥手
当连接结束后,主机中的缓存和变量将被释放。
第一步:客户进程发出一个关闭连接命令,引起客户TCP向服务器进程发送一个特殊的TCP报文段,首部FIN置1。
第二步:服务器接收FIN报文段后,会送一个确认报文段。
第三步:服务器发送它自己的FIN报文段。
第四步:客户对该服务器的FIN报文段进行确认。
在一个TCP连接的生命周期内,运行在每台主机的TCP协议在各种TCP状态之间变迁。
TCP状态
客户端:
客户TCP开始时处于CLOSED状态。发送SYN报文段后,客户TCP进入了SYN_SENT状态。收到SYNACK报文段后,客户TCP进入了ESTABLISHED状态,该状态下TCP客户就能发送和接收包含应用层数据的TCP报文段了。
客户TCP发送FIN报文段,进入FIN_WAIT_1状态。收到确认报文段后进入FIN_WAIT_2状态。收到FIN报文段并确认后进入TIME_WAIT状态。假定ACK丢失,TIME_WAIT状态使客户TCP重传最后的确认报文。TIME_WAIT时间与具体实现有关,典型的值为30秒、1分钟或2分钟。经过等待后进入CLOSED状态,连接正式关闭,客户端所有资源(包括端口号)将被释放。
服务端:
服务TCP开始时处于CLOSED状态。创建监听套接字后处于LISTEN状态。接收SYN发送SYNACK后处于SYN_RCVD状态。接收ACK后处于ESTABLISHED状态。
服务TCP接收FIN发送ACK后处于CLOSE_WAIT状态。发送FIN后处于LAST_ACK状态。接收ACK后处于CLOSED状态,连接正式关闭,服务端所有资源(包括端口号)将被释放。
客户SYN发出后可能会接收到一个RST报文段(即RST标志位置1),表示当前服务端并没有运行在目的端口上的应用程序。当然客户也可能什么都收不到,这很可能表示SYN报文段被防火墙阻挡,无法到达目标主机。
拥塞控制
拥塞控制通过遏制发送方来缓解IP网络拥塞。网络拥塞会导致路由器缓存溢出,接着导致分组重传。在最宽泛的级别上,我们可以根据网络层是否为运输层拥塞控制提供了显示帮助来区分拥塞控制方法。
- 端到端拥塞控制:网络层没有为运输层拥塞控制提供显示支持,即使网络中存在拥塞,端系统也必须通过对网络行为的观察(如分组丢失与时延)来推断。TCP采用端到端的方法解决拥塞控制,因为IP层不会向端系统提供有关网络拥塞的反馈信息。TCP报文段的丢失(超时或3次冗余ACK)被认为是网络拥塞的迹象,TCP会相应的减小其窗口长度。
- 网络辅助的拥塞控制:路由器向发送方提供关于网络中拥塞状态的显示反馈信息。这种反馈可以简单的用一个比特来指示链路中的拥塞情况。
TCP的一个关键部分是可靠传输,另一个关键部分就是拥塞控制。
TCP让每一个发送方根据感知到的网络拥塞程度在限制其能向连接发送流量的速率,如果一个TCP发送方感知从它到目的地之间的路劲上没有什么拥塞,则TCP发送方增加其发送速率。反之则降低其发送速率。
三个问题:①如何限制发送速率;②如何感知网络拥塞;③如何确定发送速率。
对于①,TCP连接的每一端都是由一个接收缓存,一个发送缓存和几个变量(LastByteRead、rwnd等)组成。运行在发送方的TCP拥塞控制机制跟踪一个额外的变量,即拥塞窗口,表示为cwnd,它对一个发送方能向网络中发送流量的速率进行了限制。
LastByteSent - LastByteAcked(发送方未被确认的数据量) <= min(cwnd, rwnd)
对于②,定义TCP发送方丢包事件为超时或收到3个冗余ACK,当发生丢包事件时,发送方就认为路径上出现了拥塞。
对于③,一个丢失的报文段意味着拥塞,因此当丢失报文段时应降低TCP发送方的速率;一个确认报文段指示该网络正向接收方交付发送方的报文段,因此当对先前未确认报文段的确认到达时,能够增加发送方的速率。
TCP拥塞控制算法
初始处于慢启动状态,cwnd = 1(MSS),ssthresh = 64(KB)。
慢启动
- 收到新的ACK:cwnd += 1(MSS),dupACKcount = 0。
- 收到冗余ACK:dupACKcount++。
- 超时:ssthresh = cwnd/2,cwnd = 1(MSS),dupACKcount = 0。
- dupACKcount == 3:ssthresh = cwnd/2,cwnd = sstresh + 3(MSS),进入快速恢复。
- cwnd >= ssthresh:进入拥塞避免。
拥塞避免
- 收到新的ACK:cwnd += MSS/cwnd,dupACKcount = 0。
- 收到冗余ACK:dupACKcount++。
- 超时:ssthresh = cwnd/2,cwnd = 1(MSS),dupACKcount = 0,进入慢启动。
- dupACKcount == 3:ssthresh = cwnd/2,cwnd = sstresh + 3(MSS),进入快速恢复。
快速恢复
- 收到冗余ACK:cwnd = cwnd + MSS。
- 超时:ssthresh = cwnd/2,cwnd = 1(MSS),dupACKcount = 0,进入慢启动。
- 收到新的ACK:cwnd = ssthresh,dupACKcount = 0,进入拥塞避免。
总结一下tcp的拥塞控制,发送方发送未确认需要同时小于接收窗口和拥塞窗口,发送方通过限制拥塞窗口大小,限制发送速率,不拥塞时缓慢增加拥塞窗口大小,拥塞时骤然减少拥塞窗口大小。