首部格式:http://en.wikipedia.org/wiki/Transmission_Control_Protocol
建立和关闭连接时,SYN和FIN需要占一个序列号,其它的纯ACK不占序列号。
伪首部计算:http://www.tcpipguide.com/free/t_TCPChecksumCalculationandtheTCPPseudoHeader-2.htm
TCP协议:http://www.networksorcery.com/enp/protocol/tcp.htm
三、TCP建立连接和关闭的三次握手
1.2 关闭一个 TCP 连接
TCP 连接建立起来后,就可以在两个方向传送数据流。当 TCP 的应用进程再没有数据需要发送时,就发关闭命令。 TCP 通过发送控制位 FIN=1 的数据片来关闭本方数据流,但还可以继续接收数据,直到对方关闭那个方向的数据流,连接就关闭。
TCP 协议使用修改的三次握手协议来关闭连接, 如图 3-11 所示,即终止一个连接要经过 4 次握手。这是因为 TCP 的半关闭( half-close )造成的。由于一个 TCP 连接是全双工(即数据在两个方向上能同时传递),因此每个方向必须单独地进行关闭。关闭的原则就是当一方完成它的数据发送任务后就能发送一个 FIN 来终止这个方向连接。当一端收到一个 FIN ,它必须通知应用层另一端已经终止了那个方向的数据传送。发送 FIN 通常是应用层进行关闭的结果。
从一方的 TCP 来说,连接的关闭有三种情况:
• 本方启动关闭
收到本方应用进程的关闭命令后, TCP 在发送完尚未处理的报文段后,发 FIN = 1 的报文段给对方,且 TCP 不再受理本方应用进程的数据发送。在 FIN 以前发送的数据字节,包括 FIN ,都需要对方确认,否则要重传。注意 FIN 也占一个顺序号。一旦收到对方对 FIN 的确认以及对方的 FIN 报文段,本方 TCP 就对该 FIN 进行确认,在等待一段时间,然后关闭连接。等待是为了防止本方的确认报文丢失,避免对方的重传报文干扰新的连接。
• 对方启动关闭
当 TCP 收到对方发来的 FIN 报文时,发 ACK 确认此 FIN 报文,并通知应用进程连接正在关闭。应用进程将以关闭命令响 应。 TCP 在发送完尚未处理的报文段后,发一个 FIN 报文给对方 TCP ,然后等待对方对 FIN 的确认,收到确认后关闭连接。若对方的确认未及时到达,在等待一段时间后也关闭连接。
• 双方同时启动关闭
连接双方的应用进程同时发关闭命令,则双方 TCP 在发送完尚未处理的报文段后,发送 FIN 报文。各方 TCP 在 FIN 前所发报文都得到确认后,发 ACK 确认它收到的 FIN 。各方在收到对方对 FIN 的确认后,同样等待一段时间再关闭连接。这称之为同时关闭( simultaneous close )。
1.3 TCP 状态机
TCP 协议的操作可以使用一个具有 11 种状态的有限状态机( Finite State Machine )来表示,图 3-12 描述了 TCP 的有限状态机,图中的圆角矩形表示状态,箭头表示状态之间的转换,各状态的描述如表 3-2 所示。图中用粗线表示客户端主动和被动的服务器端建立连接的正常过程:客户端的状态变迁用粗实线,服务器端的状态变迁用粗虚线。细线用于不常见的序列,如复位、同时打开、同时关闭等。图中的每条状态变换线上均标有“事件/动作”:事件是指用户执行了系统调用( CONNECT 、 LISTEN 、 SEND 或 CLOSE )、收到一个报文段( SYN 、 FIN 、 ACK 或 RST )、或者是出现了超过两倍最大的分组生命期的情况;动作是指发送一个报文段( SYN 、 FIN 或 ACK )或什么也没有(用“-”表示)。
1. 正常状态转换
我们用图 3-13 来显示在正常的 TCP 连接的建立与终止过程中,客户与服务器所经历的不同状态。读者可以对照图 3-12 来阅读,使用图 3-12 的状态图来跟踪图 3-13 的状态变化过程,以便明白每个状态的变化:
在此状态下,双方可以自由传输数据。当一个应用程序完成数据传输任务后,它需要关闭 TCP 连接。假设仍由客户端发起主动关闭连接。
1、建立连接协议(三次握手)
(1)客户端发送一个带SYN标志的TCP报文到服务器。这是三次握手过程中的报文1。
(2) 服务器端回应客户端的,这是三次握手中的第2个报文,这个报文同时带ACK标志和SYN标志。因此它表示对刚才客户端SYN报文的回应;同时又标志SYN给客户端,询问客户端是否准备好进行数据通讯。
(3) 客户必须再次回应服务段一个ACK报文,这是报文段3。
2、连接终止协议(四次握手)
由于TCP连接是全双工的,因此每个方向都必须单独进行关闭。这原则是当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向的连接。收到一个 FIN只意味着这一方向上没有数据流动,一个TCP连接在收到一个FIN后仍能发送数据。首先进行关闭的一方将执行主动关闭,而另一方执行被动关闭。
(1) TCP客户端发送一个FIN,用来关闭客户到服务器的数据传送(报文段4)。
(2) 服务器收到这个FIN,它发回一个ACK,确认序号为收到的序号加1(报文段5)。和SYN一样,一个FIN将占用一个序号。
(3) 服务器关闭客户端的连接,发送一个FIN给客户端(报文段6)。
(4) 客户段发回ACK报文确认,并将确认序号设置为收到序号加1(报文段7)。
CLOSED: 这个没什么好说的了,表示初始状态。
LISTEN: 这个也是非常容易理解的一个状态,表示服务器端的某个SOCKET处于监听状态,可以接受连接了。
SYN_RCVD: 这个状态表示接受到了SYN报文,在正常情况下,这个状态是服务器端的SOCKET在建立TCP连接时的三次握手会话过程中的一个中间状态,很短暂,基本上用netstat你是很难看到这种状态的,除非你特意写了一个客户端测试程序,故意将三次TCP握手过程中最后一个ACK报文不予发送。因此这种状态时,当收到客户端的ACK报文后,它会进入到ESTABLISHED状态。
SYN_SENT: 这个状态与SYN_RCVD遥想呼应,当客户端SOCKET执行CONNECT连接时,它首先发送SYN报文,因此也随即它会进入到了SYN_SENT状态,并等待服务端的发送三次握手中的第2个报文。SYN_SENT状态表示客户端已发送SYN报文。
ESTABLISHED:这个容易理解了,表示连接已经建立了。
FIN_WAIT_1: 这个状态要好好解释一下,其实FIN_WAIT_1和FIN_WAIT_2状态的真正含义都是表示等待对方的FIN报文。而这两种状态的区别是:FIN_WAIT_1状态实际上是当SOCKET在ESTABLISHED状态时,它想主动关闭连接,向对方发送了FIN报文,此时该SOCKET即进入到FIN_WAIT_1状态。而当对方回应ACK报文后,则进入到FIN_WAIT_2状态,当然在实际的正常情况下,无论对方何种情况下,都应该马上回应ACK报文,所以FIN_WAIT_1状态一般是比较难见到的,而FIN_WAIT_2状态还有时常常可以用netstat看到。
FIN_WAIT_2:上面已经详细解释了这种状态,实际上FIN_WAIT_2状态下的SOCKET,表示半连接,也即有一方要求close连接,但另外还告诉对方,我暂时还有点数据需要传送给你,稍后再关闭连接。
TIME_WAIT: 表示收到了对方的FIN报文,并发送出了ACK报文,就等2MSL后即可回到CLOSED可用状态了。如果FIN_WAIT_1状态下,收到了对方同时带FIN标志和ACK标志的报文时,可以直接进入到TIME_WAIT状态,而无须经过FIN_WAIT_2状态。
CLOSING: 这种状态比较特殊,实际情况中应该是很少见,属于一种比较罕见的例外状态。正常情况下,当你发送FIN报文后,按理来说是应该先收到(或同时收到)对方的ACK报文,再收到对方的FIN报文。但是CLOSING状态表示你发送FIN报文后,并没有收到对方的ACK报文,反而却也收到了对方的FIN报文。什么情况下会出现此种情况呢?其实细想一下,也不难得出结论:那就是如果双方几乎在同时close一个SOCKET的话,那么就出现了双方同时发送FIN报文的情况,也即会出现CLOSING状态,表示双方都正在关闭SOCKET连接。
CLOSE_WAIT: 这种状态的含义其实是表示在等待关闭。怎么理解呢?当对方close一个SOCKET后发送FIN报文给自己,你系统毫无疑问地会回应一个ACK报文给对方,此时则进入到CLOSE_WAIT状态。接下来呢,实际上你真正需要考虑的事情是察看你是否还有数据发送给对方,如果没有的话,那么你也就可以close这个SOCKET,发送FIN报文给对方,也即关闭连接。所以你在CLOSE_WAIT状态下,需要完成的事情是等待你去关闭连接。
LAST_ACK: 这个状态还是比较容易好理解的,它是被动关闭一方在发送FIN报文后,最后等待对方的ACK报文。当收到ACK报文后,也即可以进入到CLOSED可用状态了。
最后有2个问题的回答,我自己分析后的结论(不一定保证100%正确)
1、 为什么建立连接协议是三次握手,而关闭连接却是四次握手呢?
这是因为服务端的LISTEN状态下的SOCKET当收到SYN报文的建连请求后,它可以把ACK和SYN(ACK起应答作用,而SYN起同步作用)放在一个报文里来发送。但关闭连接时,当收到对方的FIN报文通知时,它仅仅表示对方没有数据发送给你了;但未必你所有的数据都全部发送给对方了,所以你可以未必会马上会关闭SOCKET,也即你可能还需要发送一些数据给对方之后,再发送FIN报文给对方来表示你同意现在可以关闭连接了,所以它这里的ACK报文和FIN报文多数情况下都是分开发送的。
2、 为什么TIME_WAIT状态还需要等2MSL后才能返回到CLOSED状态?
这是因为:虽然双方都同意关闭连接了,而且握手的4个报文也都协调和发送完毕,按理可以直接回到CLOSED状态(就好比从SYN_SEND状态到ESTABLISH状态那样);但是因为我们必须要假想网络是不可靠的,你无法保证你最后发送的ACK报文会一定被对方收到,因此对方处于LAST_ACK状态下的SOCKET可能会因为超时未收到ACK报文,而重发FIN报文,所以这个TIME_WAIT状态的作用就是用来重发可能丢失的ACK报文,并保证于此。
注意:在主动关闭方发送的最后一个 ack(fin) ,有可能丢失,这时被动方会重新发fin, 如果这时主动方处于 CLOSED 状态 ,就会响应 rst 而不是 ack。所以主动方要处于 TIME_WAIT 状态,而不能是 CLOSED 。
一般来说,tcp正常关闭需要四个包。比如a和b关闭连接,a先给b发一个fin,b会进行确认ack,然后b也会发出fin,当a接受到这个fin,并发出最后一个ack后,就会处于time_wait状态。这个时间长短跟操作系统有关,一般会在1-4分钟,也就是两倍的数据包(2msl)最大生存时间。TCP主动关闭方采用TIME_WAIT主要是为了实现终止 TCP全双工连接的可靠性及允许老的重复分节在网络中消逝,等过了2msl(大约1~4分钟)后TIME_WAIT就会消失。
TIME_WAIT状态的目的是为了防止最后a发出的ack丢失,让b处于LAST_ACK超时重发FIN
所以说,主动发起关闭连接的一方会进入time_wait状态,这个时候,进程所占用的端口号不能被释放。除非在你的程序中用setsockopt设置端口可重用(SOCK_REUSE)的选项,但这不是所有操作系统都支持的
解决TIME_WAIT的办法主要有以下几种:
1、修改LINGER值,缩短关闭时间
LINGER lingerStruct;
lingerStruct.l_onoff = 1;
lingerStruct.l_linger = 0;
setsockopt(m_socket,SOL_SOCKET,SO_LINGER,(char *)&lingerStruct,sizeof(lingerStruct));
不过这种办法不是很安全的,不过现在网络都很好啦,不会有问题的。
2、修改注册表
[HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Services\Tcpip\Parameters]
"TcpTimedWaitDelay"=dword:00000005
这个值好像是300秒到30秒之间,改成30秒后你会发现TIME_WAIT很快就会消失了。
3、禁用LINGER
//如果你使用的是Socket API,可以这样
BOOL bDontLinger=FALSE;
setsockopt(m_socket,SOL_SOCKET,SO_DONTLINGER,(LPCTSTR)&bDontLinger,sizeof(BOOL));
closesocket(s);
//如果你使用的是CAsyncSocket,需要响应的修改,例如禁用LINGER可以这样
BOOL bDontLinger=FALSE;
m_socket->SetSockOpt(SO_DONTLINGER,(const char *)&bDontLinger,sizeof(bDontLinger),SOL_SOCKET);
m_socket->Close();
4、客户端可以不BIND(),这样,即使断开连接后再次连接,SOCKET将使用不同的端口(1025-5000),
等几分钟后,原有的端口就会自动关闭。
关闭BITCOMET后系统出现的几个TCP状态
Time_wait状态
TIME_WAIT状态是TCP协议中最容易被误解的特性之一。这很可能是因为最初的规约
RFC793中只对该状态做了扼要的解释,尽管后来的RFC,如RFC1185,对TIMEWAIT状态
做了详细说明。设置TIMEWAIT状态的原因主要有两个:
1)它实现了全双工的连接关闭。
2)它使过时的重复报文段作废。
下面我们对这两个原因做进一步的讨论。
TCP全双工关闭
图4-4给出了一般情况下连接关闭时的报文段交换过程。图中还给出了连接状态的变迁和在服务器端测得的RTT值。图中左侧为客户端,右侧为服务器端。要注意,其中的任何一端都可以主动关闭连接,但一般都是客户端执行主动关闭。下面我们来看看最后一个报文段(最后一个ACK)丢失时会发生什么现象。这个现象就给在图4-5中。
由于没有收到客户的最后一个确认,服务器会超时,并重传最后一个FIN报文段。我们特意把服务器的重传超时(RTO)给得比图4-4中的RTT大,这是因为RTO的取值是估计的RTT值加上若干倍的RTT方差(卷2的第25章详细论述了如何测量RTT值以及如何计算RTO)。处理最后一个FIN报文段丢失的方法也是一样:服务器在超时后继续重传FIN。
这个例子说明了为什么TIMEWAIT状态要出现在执行主动关闭的一端:该端发出最后一个ACK报文段,而如果这个ACK丢失或是最后一个FIN丢失了,那么另一端将超时并重传最后的FIN报文段。因此,在主动关闭的一端保留连接的状态信息,这样它才能在需要的时候重传最后的确认报文段;否则,它收到最后的FIN报文段后就无法重传最后一个ACK,而只能发出RST报文段,从而造成虚假的错误信息。图4-5还说明了另一个问题,即如果重传的FIN报文段在客户端主机仍处于TIMEWAIT状态的时候到达,那么不仅仅最后一个ACK会重传,而且TIMEWAIT状态也重新开始。这时,TIMEWAIT状态的持续时间定时器重置为2倍的报文段最大生存时间,即2MSL。
问题是,执行了主动关闭的一端,为了处理图4-5所示的情况,需要在TIMEWAIT状态保持多长的时间?这取决于对端的RTO值;而RTO又取决于该连接的RTT值。RFC1185中指出RTT的值超过1分钟不太可能。但实际上RTO却很有可能长达1分钟:在广域网发生拥塞期间时就会有这种情形。这是因为拥塞会导致多次重传的报文段仍然丢失,从而使TCP的指数退避算法生效,RTO的值越来越大。过时的重复报文段失效设置TIMEWAIT状态的第二个原因是为让过时的重复报文段失效。TCP协议的运行基于一个基本的假设,即:互连网上的每一个IP数据报都有一个有限的生存期限,这个期限值是由IP首部的TTL(生存时间)字段决定的。每一台路由器在转发IP数据报时都要将其TTL值减1;但如果该IP数据报在路由器中等待的时间超过1秒,那就要把TTL的值减去等待的时间。实际上,很少有IP数据报在路由器中的等待时间超过1秒的,因而每个路由器通常都是把TTL的值减1(RFC1812[Baker1995])的5.3.1节)。由于TTL字段的长度是8比特,因此每个IP数据报所能经历的转发次数至多为255。
RFC793把该限制定义为报文段最大生存时间MSL,并规定其值为2分钟。该RFC同时指出,将报文段最大生存时间MSL定义为2分钟是一个工程上的选择,其值可以根据经验进行修改。最后,RFC793规定TIMEWAIT状态的持续时间为MSL的2倍。
图4-6给出的是一个连接关闭后在TIME_WAIT状态保持了2倍报文段最大生存时间(2MSL),然后发起建立新的连接替身。
由于该连接的新的替身必须在前一个连接替身关闭2MSL之后才能再次发起,而且由于前一个连接替身的过时重复报文段在TIMEWAIT状态的第1个报文段最大生存时间里就已经消失,因此我们可以保证前一次连接的过时重复报文段不会在新的连接中出现,也就不可能被误认为是第二次连接的报文段。
TIMEWAIT状态的自结束
RFC793中规定,处于TIMEWAIT状态的连接在收到RST后变迁到CLOSED状态,这称
为TIME_WAIT状态的自结束。RFC1337[Braden1992a]中则建议不要用RST过早地结束TIMEWAIT状态。
主要有两个原因
1。防止上一次连接中的包,迷路后重新出现,影响新连接(经过2MSL,上一次连接中所有的重复包都会消失)
2。可靠的关闭TCP连接在主动关闭方发送的最后一个 ack(fin) ,有可能丢失,这时被动方会重新发fin, 如果这时主动方处于 CLOSED 状态 ,就会响应 rst 而不是 ack。所以主动方要处于 TIME_WAIT 状态,而不能是 CLOSED 。
四、TCP确认与重传机制
1累计确认 确认针对数据流中的数据,不是报文段
优点:无二义性,确认丢失不一定会引起重传。
实例:A向B发送数据
1)设发送端窗口为{1201,2000};分2报文段传送;
2)第一个报文段{1201,1600},第二个报文段{1601,2000};
B首先发回确认{1601},再发回确认{2001}
3)若A在定时器未到之前只收到确认{2001},而{1601}丢失,则A不会重传 {1201,1600}报文段;
2超时与重传
(1)定时器时间是由报文段的平均往返时间确定的
TimeOut=B*RTT(Round Trip Time)
(2) RTT的确定
某个报文段的 RTT=收到确认的时间-发送的时间(但是在实际过程中很难测出)
平均RTT=a*Old_RTT+(1-a)*New_RTT
a接近于1的含义:时间不敏感型
a接近于0的含义:时间敏感型
(3) RTT的不精确性
当重传一个数据时,此时收到的确认报文,主机无法判断出到底是对哪个报文的确认。
若认为是对原报文确认,增加了对RTT的估值,降低了效率,该重传也没重传;若认为是对重传报文的确认,RTT值变小,导致正确传送的数据也重传,这就造成了不必要的重传。
RTT的Karn补偿方法
1) 忽略重传报文,不用估算其RTT。
2) 当然就不用重传报文的RTT来修正定时器的Timeout值。
3) 发生重传时,增加Timeout
New_Timeout=r*Old_Timeout
4) Timeout不会无限增加,受上限约束,无重传时,重新估计
Timeout值。实验证明r=2网络就能很好地工作了,小于2会导致时限不稳定。
Karn算法:计算往返估计值时,忽略对应于重传报文段的样本,但是要使用补偿策略,对一个重传分组的许多后续分组,其定时时限均不变,直到获得一个新的有效样本时再修改时限值。
五、拥塞控制
拥塞(Congestion)
拥塞是因为报文在一个或多个路由器发生过栽而导致严重延迟。一旦发生拥塞,延迟显著增加,路由器开始将报文缓存起来,直到它有能力将其发送出去。最坏情况下,到达拥塞路由器的报文持续增加到超过器存储能力,此时路由器就丢弃 (discard)报文。
TCP拥塞控制策略
(1)拥塞发生时,报文传送时延增加、使得定时器超时。在拥塞情况下,定时器超时重传会加重拥塞至网络瘫痪。
(2)TCP拥塞处理策略
1)假设报文丢弃大多数都是拥塞引起的
2)加速递减机制:发生拥塞时的抑制机制:指数递减
3)慢启动:拥塞解除时的恢复机制:指数增加
4)拥塞避免:避免慢起动窗口增长过大的机制恢复:指数增加-〉线性增加
路由器报文丢弃策略与拥塞控制
(1) 早期路由器使用尾部每端丢弃(TAIL-DROP) 策略来处理拥塞:
若报文到来时队列已满,则丢弃该报文。所以,路由器丢弃队列的尾部。尾部丢弃带来的问题就是全局性同步(Global synchronization),路由器不是丢弃一个连接的n个报文段,而是丢弃n个连接的各一个报文段,造成所有 N 个连接的发送端同时进入慢起动。慢启动会显著降低流量。
(2) 解决办法
随机丢弃策略 (Random Early Discard、RED)
1)RED 尽可能避免末端丢弃
2)RED 用两个阈值分别表示队列的位置:Tmin 与 Tmax
图17
RED 基本操作:若目前队列的报文数小于 Tmin,则把新报文加入队列中。若目前队列的报文数大于 Tmax,丢弃新报文。若目前队列的报文数在Tmin 与 Tmax之间,根据一概率P来随机决定是否丢弃此报文。
选择Tmin和Tmax时注意:Tmin 必须大到确保输出链路有高使用率,Tmax - Tmin必须大于在一个tcp往返时间内增加的队列大小(例如 Tmax 为 Tmin 的两倍)。
利用平均队列长度Avg来确定p
这种策略可以在队列满时p较大;在突发情况下不会丢弃数据。加权平均队列大小为avg=(1–γ)*Old_avg+γ*Current_queue_size
其中γ介于0和1之间。若γ足够小(例如0.002),平均值会呈长期稳定的趋势(亦即old_avg),从而不受突发通讯的影响。
六、糊涂窗口综合症与短分组
主要特征是发送短的报文段,造成带宽浪费。
当收发两端的应用程序以不同的速率工作时,软件性能会出现严重的问题。如接收方每次仅接收一个八位组,发送方能够快速地生成数据,发送方TCP软件所传输的报文段就能很快填满整个缓冲区。接收方每读取一个八位组数据就通告发送方一个八位组的窗口。如此这样,每次就只有一个八位组窗口正常工作,而在传输过程中加上各种协议数据的首部,TCP20字节,IP20字节,有效数据比1/41,加上物理帧头比例更小。这就浪费了网络带宽。
1、接收方糊涂窗口的避免
启发式策略:接收方为当前可用的窗口值维护一个内部记录,并在窗口大小显著增加之前推迟发送增加窗口的通告。窗口大小至少为缓冲区空间的一半或最长报文段数据的字节数时TCP认为是显著的。
通俗点说就是当接收方缓冲区达到饱和时通告一个0窗口,当接收应用程序取到至少占缓冲区一半的八位组数据时再发送更收的窗口通告。
推迟确认技术:就是在收到一个报文段后并不马上发送确认。
优点:降低通信量提高吞吐率。为了使发送方正确估计RTT,至少每隔一个报文段要进行正常的确认。 确认时间最多500ms.
缺点:(1)当接收方确认迟延太大时,发送方会进行报文段的重传,不必要的重传浪费了网络带宽,降低了吞吐率,还加大了收发双方的计算负载。
(2)确认迟延混乱了往返时间的估计值。
2、发送方对糊涂窗口的避免
目的是防止发送短报文段,所以发送方在发送报文段之前必须延迟,以积聚合理的数据量,这就叫组块技术(clumping)。
策略:在一个连接上已经传输的数据还未被确认的情况下,发送方的应用程序又生成了后续数据,并照常将数据送到缓冲区中。但并不发送后续报文段,而是等到数据足以填满一个最大长度的报文段之后再把缓冲区中的数据发送出去。(Nagle算法)
TCP的计时器.
重传计时器---若在计时器截止时间之前收到了对此数据段的确认,则撤销此计时器;若在收到对此特定数据段的确认之前计时器截止期到,则重传此数据段,并将计时器复位.设一次数据传输的往返时间为RTT,则重传时间=2RRT....数据传输的往返时间RTT是利用发送端发送数据时产生的时间戳和当前时间计算得来的.时间戳存放在TCP数据首部的可选项中.
坚持计时器---假定接收端的TCP宣布了窗口大小为0,发送端的TCP将停止传送数据段,直到接收端的TCP发送确认并宣布一个非0的窗口大小为止.但应该注意一点的是,在TCP中对确认是不需要确认的.若确认丢失,接收端的TCP就认为它完成任务了,并等待发送端的TCP发送更多的数据段.发送端的TCP由于没有收到确认,就等等对方发送确认来通知窗口的大小.双方进入了死锁等待的情况.为了解开这个死锁,TCP为每个连接使用一个坚持计时器.当发送端收到一个窗口大小为0的确认时,就启动坚持计时器.当坚持计时器期限到时,发送端的TCP就发送一个特殊的数据段,称为探测数据段,这个数据段只有一个字节的数据.它有一个序号,但它的序号永远不需要确认,它只是提醒接收端的TCP:确认已经丢失,必须重传.坚持计时器的时间值设置是重传时间的数值.但若没有收到接收端来的响应.就需要发送另一个探测数据段,并将坚持计时器的值加倍和复位,直到这个值增大到极限值(通常是60s)为止.在此之后,如果还没有得到响应,发送端每隔60s就发送一个探测数据段,直到窗口重新打开.
保活计时器---用来防止在两个TCP之间的连接长时间空闲.假定客户打开了到服务器的连接,传送了一些数据,然后就保持沉默了,也许这个客户出现了故障.在这种情况下,这个连接将永远地处于打开状态,白白浪费了服务器宝贵的资源...所以每当服务器收到客户的信息就将保活计时器复位.超时时间通常设为2小时.若服务器过了2小时还没有收到客户的信息,它就发送探测数据段.若发送10个探测数据段(每个相隔75s)还没有响应,就假定客户出了故障,因而就中止该连接.
时间等待计时器---TCP协议在断开的时候,如果A发送完最后一个ACK就立即关闭连接,而这时,如果这个ACK数据段丢失了,B无法判断是FIN丢失还是ACK丢失,因此B会重传FIN数据段,而此时A已经关闭了连接,B永远也无法收到A的ACK字段了.
因此TCP协议设置了一个时间等待计时器,A在发送了最后一个ACK报文后,并不立即关闭连接,而是经过一个时间等待计时器的时间再关闭.这个时间可以保证A能收到重复的FIN数据段...由于此时数据连接已经完成了建立和断开的生存周期,所以数据段的寿命期已经知道.时间等待计时器的值通常设置为一个数据段寿命期的两倍..
拥塞控制算法
Reno是目前应用最广泛且较为成熟的算法。该算法所包含的慢启动、拥塞避免和快速重传、快速恢复机制,是现有的众多算法的基础。
1.慢启动与拥塞避免
TCP发送端采用慢启动和拥塞避免算法来控制向网络输送的数据量。为了实现这些算法,必须向TCP每个连接状态加入3个参量:
丢失窗口(lw):丢失窗口是在一个TCP根据它的重传定时器检测到了数据丢失之后,拥塞窗口的尺寸。
重启窗口(rw):重启窗口是TCP在一段闲置期之后重新开始传送后拥塞窗口的尺寸(如
果使用慢启动算法;
(1)拥塞窗口(cwnd),如前所述,它是对发送端收到确认(ACK)之前能向网络传送的最大数据量的一个发送端的限制。
(2)接收端通知窗口(rwnd),它是对未完成数据量的接收端的限制,cwnd和rwnd的最小值决定了数据传送。
(3)慢启动阀值(ssthresh),被用来确定是用慢启动还是用拥塞避免算法来控制数据传送,具体用法如下:当cwnd<ssthresh时使用慢启动算法;cwnd>ssthresh时使用拥塞避免算法;当cwnd=ssthresh时,发送端既可以使用慢启动也可以使用拥塞避免。ssthresh的初始值可以任意大(比如,一些实现中使用接收端通知窗口的尺寸),但是一旦对拥塞响应之后,其大小可能会被减小。
在不清楚网络环境的情况下向网络传送数据,要求TCP缓慢地探测网络以确定可用带宽,以避免突然传送大量数据而使网络拥塞。为达此目的,在传送开始时,采用了慢启动机制,这个机制在修复了由重发定时器探测到的数据丢失之后也被采用。
首先要确定的是cwnd的初始值IW(初始窗口大小),这里规定它必须小于或等于2*SMSS字节而且不能大于两个数据段。
在慢启动期间,每收到一个新的ACK,cwnd最多增长1。直到cwnd超过ssthresh或者检测到拥塞时,停止执行慢启动算法,转入拥塞避免阶段。在拥塞避免期间,cwnd在每个ACK以1/cwnd(或每个RTT增加SMISS个字节)的速度递增。拥塞避免算法一直保持直到检测出拥塞。等式(5.1.1)给出了一个在拥塞避免期间用来修正cwnd值的公式:
cwnd+=1/cwnd (5.1.1)
每收到一个非重复的ACK都采用等式(5.1.1)来调整cwnd。等式(5.1.1)用于近似拥塞避免算法的增长。
在实现中,在拥塞避免期间常用公式:
cwnd+=SMSS*SMSS/cwnd
来修正cwnd的值,当SMSS*SMSS/cwnd<1时,cwnd+=1。
另一种改进的方案是每当新的ACK到来时记下被新确认的字节数,然后cwnd就可增加相应字节数,这个增加的数目最多可达到SMSS字节。
一旦TCP发送端使用重传定时器检测到包丢失时,ssthresh的值就如下设置:
Ssthresh=max(FlightSize/2,2*SMSS) (5.1.2)
式中,Filght Size是已发送但未收到ACK的数据的大小。
在重发了丢失的数据段之后,cwnd必须被设置成LW(丢失窗口),它等于一个满尺寸数据段的大小。再发丢失的数据段之后,发送端起用慢启动算法增长窗口直到该窗口大小增长到等于新设置的ssthresh值之后,又采用拥塞避免算法了。
2.快速重传与快速恢复
当接收端收到一个失序的数据报时,会立即发回一个重复ACK,这个ACK的目的是告知发送端收到一个失序的数据报并说明其所期望的接受序号。从发送端的角度看,重复ACK可能是许多网络问题引起的。首先,它们有可能是因为包丢失而引起。在此情况下,在此数据段之后的所有数据段都会触发重复ACK。其次,重复ACK可能是由于网络对数据段的重新排序引起的。最后,重复ACK有可能是ACK或数据段被网络复制所引起的。此外,当接收端部分或完整地填补了序号空缺应立即发送一个ACK,这样可以更及时地通知发送端,使其迅速从重发状态中恢复过来。快速重传算法如图5-2所示。
TCP发送端应该使用快速重传算法来探测或者修复数据丢失,在收到3个重复ACK(即连续的4个相同的ACK,标志着1个数据段已丢失)时,TCP不等重传定时器超时就立即重传看来已丢失的数据段。此后起用快速恢复算法来进行新的数据传输,直到1个非重复 ACK到达。
下面是快速传送/快速恢复算法的实现:
(1)当第二个重复ACK收到时,ssthresh根据等式(5.1.2)设值。即
ssthresh=max(FlightSize/2,2*SMSS)
Filght Size是已发送但未收到ACK的数据的大小。
(2)重传丢失的数据段并将cwnd的值设置为ssthresh+3*SMSS,称之为给拥塞窗口“充气”。
cwnd=ssthresh+3*SMSS
(3)此后对每个接收到一个重复ACK,将cwnd增大SMSS字节,这将人为地扩充拥塞窗口用以反映已经离开网络的附加数据段。
(4)如果cwnd和接收端的通知窗口值允许的话,发送一个数据段。
(5)当下一个确认新数据的ACK到达时,设定cwnd值为ssthresh(步骤1设置的值),这称作给窗口“放气”。这个ACK必须是步骤1触发的重发引起的确认,重发之后一个RTT(在接收端有次序紊乱的数据段的情况下,它可能一会儿就到达)。另外,此ACK应该确认丢失数据段和第二个重复ACK期间的数据段,如果它们一个也没有丢失的话。
Reno算法的性能分析
从Reno运行机制中很容易看出,为了维持一个动态平衡,必须周期性地产生一定量的丢失,再加上AIMD机制--减少快,增长慢,尤其是在大窗口环境下,由于一个数据报的丢失所带来的窗口缩小要花费很长的时间来恢复,这样,带宽利用率不可能很高且随着网络的链路带宽不断提升,这种弊端将越来越明显。
公平性方面,根据统计数据,Reno的公平性还是得到了相当的肯定,它能够在较大的网络范围内理想地维持公平性原则。
快速重传与快速恢复算法
在收到一个失序的报文段时, T C P立即需要产生一个A C K(一个重复的A C K)。这个重复的A C K不应该被迟延。该重复的A C K的目的在于让对方知道收到一个失序的报文段,并告诉对方自己希望收到的序号。
由于我们不知道一个重复的A C K是由一个丢失的报文段引起的,还是由于仅仅出现了几个报文段的重新排序,因此我们等待少量重复的A C K到来。假如这只是一些报文段的重新排序,则在重新排序的报文段被处理并产生一个新的A C K之前,只可能产生1 ~ 2个重复的A C K。如果一连串收到3个或3个以上的重复A C K,就非常可能是一个报文段丢失了。于是我们就重传丢失的数据报文段,而无需等待超时定时器溢出。这就是快速重传算法。接下来执行的不是慢启动算法而是拥塞避免算法。这就是快速恢复算法。
没有执行慢启动的原因是由于收到重复的A C K不仅仅告诉我们一个分组丢失了。由于接收方只有在收到另一个报文段时才会产生重复的A C K,而该报文段已经离开了网络并进入了接收方的缓存。也就是说,在收发两端之间仍然有流动的数据,而我们不想执行慢启动来突然减少数据流。这个算法通常按如下过程进行实现:
1) 当收到第3个重复的A C K时,将s s t h re s h设置为当前拥塞窗口c w n d的一半。重传丢失的报文段。设置c w n d为s s t h re s h加上3倍的报文段大小。
2) 每次收到另一个重复的A C K时, c w n d增加1个报文段大小并发送1个分组(如果新的c w n d允许发送)。
3) 当下一个确认新数据的A C K到达时,设置c w n d为s s t h re s h(在第1步中设置的值)。这个A C K应该是在进行重传后的一个往返时间内对步骤1中重传的确认。另外,这个A C K也应该是对丢失的分组和收到的第1个重复的A C K之间的所有中间报文段的确认。这一步采用的是拥塞避免,因为当分组丢失时我们将当前的速率减半。