《TCP/IP详解:卷一》-TCP部分讲解

TCP/IP协议

作者:Danbo 2015-7-2
本文为参考TCP/IP详解卷一,某些知识点加上了作者自己的理解,如有错误,欢迎指正,可以微博联系我!

TCP包格式和IP包格式如下:

TCP的正常建立与关闭

建立连接

TCP协议提供可靠的面向连接服务,采用三次握手建立连接。
第一次握手:建立连接时,客户端发送SYN包(syn=j)到服务器,并进入SYN_SEND状态,等待服务器确认;
第二次握手:服务器收到SYN包,向客户端返回ACK(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RCVD状态;
第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。
完成三次握手,客户端与服务器开始传送数据,也就是ESTABLISHED状态。 

 

终止连接

采用四次挥手断开双向连接。
(1) TCP客户端发送一个FIN,用来关闭客户到服务器的数据传送。
(2) 服务器收到这个FIN,它发回一个ACK,确认序号为收到的序号加1。和SYN一样,一个FIN将占用一个序号。
(3) 服务器关闭客户端的连接,发送一个FIN给客户端。
(4) 客户端发回ACK报文确认,并将确认序号设置为收到序号加1。

TCP状态变迁图

 

客户端的状态可以用一下流程图来表示:

CLOSED->SYN_SENT->ESTABLISHED->FIN_WAIT_1->FIN_WAIT_2->TIME_WAIT->CLOSED

服务器的状态可以流程图:

CLOSED->LISTEN->SYN收到 ->ESTABLISHED->CLOSE_WAIT->LAST->ACK->CLOSED

 

2MSL等待状态(两个作用)

TIME_WAIT状态也称为2MSL等待状态。每个具体TCP实现必须选择一个报文段最大生存时间MSL(Maximum Segment Lifetime)。它是任何报文段被丢弃前在网络内的最长时间。处理原则:当TCP执行一个主动关闭,并发回最后一个ACK,该链接必须在TIME_WAIT状态停留的时间为2MSL。这样可让TCP有机会在此发送最后一个ACK以防这个ACK丢失(在另一端发送FIN前提)

但是,在连接处于2MSL等待时,任何迟到返回的报文段将被丢弃。因为处于2MSL等待的、由该插口对(socket pair)定义的连接在这段时间内不能被再用,对于客户程序还好 一些,但是对于服务程序,例如httpd,它总是要使用同一个端口80来进行服务,而在 2MSL时间内,启动httpd就会出现错误(插口被使用)。为了避免这个错误,服务器给出了一个平静时间(quit time)的概念,这是说在2MSL时间内,虽然可以重新启动服务器,但是这个服务器还是要平静等待MSL时间才能进行下一次连接,让后来返回的数据包没有机会影响到发送端,因为返回的包和重新建立的包使用同一个四元组,发送端无法区分这两个包属于不同连接。(建议MSL时间为2min,不过这个与操作系统有关。)

 

半打开状态(Half-Open)

如果以防已经关闭或异常终止连接而另一方却不知道,我们将这样的TCP连接称为半打开的。这种状态可以通过Keepalive选项来进行发现两一段已经消失。还有一种形式是:本端发送SYN,对端回应ACK+SYN,此时本段不回应ACK。

当处于半打开状态的一方重启并重新连接后,它将丢失复位前的所有信息,因此它并不知道数据报文段中提到的连接。此时就会返回RST(异常终止要发送RST置位的包)包应答,已关闭此次连接。此时只需要等待MSL时间,因为TCP默认机器重启的时间大于MSL。PIX防火墙和IDS入侵检测系统都可以伪装攻击目标发送RST的包去终止异常的TCP连接。(比如限定连接的时间,减少半开连接限制超时时间)当我们Telnet一个不存在的端口号时,本段立马收到一个拒绝访问的包,这个就是对方发送的RST包导致的。

 

半关闭状态(Half-Close)

单方向链路关闭。即TCP连接一端在结束它的发送后还能接收来自另一端数据的能力。程序调用的是shutdown,而不是close,不过大多数程序都是调用close终止两个方向的连接。

 

最大报文段长度MSS(Option字段)

最大报文段长度表示TCP传往另一端的最大块数据的长度。当建立一个连接时,每一方都受到对方通告的MSS值(MSS选项只能出现在SYN报文中)。如果一方收不到另一方的MSS值,那么就设为默认的536字节。MSS是最长见的选项字段,还有另一个选项叫做窗口放大因子(Window*Shift Count即可以发送超大的数据包,即乘以目前窗口的倍数为实际一次发送的数据量。解决高速链路和高速主机普通TCP发包过慢问题。)。还有一些HASH值也会放在Option字段。而防火墙默认则会清掉IP和TCP的Option选项字段。

 

纳格算法

网络中某个应用程序不断地送出小单位的资料,且某些常是1字节大小。因为TCP封包具有40字节的总头部(加上20字节的IP头部),这导致41字节大小的包只有一字节的数据,这造成了极大的资源浪费,更糟糕的是在慢速网络下,这类包造成拥塞碰撞(Congestion Collapse)。TCP连接最多只能有一个未被确认的小分组。只适用于低速链路。
 
Nagle算法过程:
1.发送端TCP将它从发送应用程序收到的第一个数据发送出去,哪怕只有一个字节;
2.在发送出第一个报文段后,发送端的TCP数据包就会在输出缓存中积累并等待,当从接收端收到对上一个数据包的ACK或者缓存中积累到一个最大报文段后,发送端TCP就可以发送这个报文段了。
Nagle算法的优点就是简单,并且它考虑到应用程序产生数据的速率,以及网络运输数据的速率。若应用程序比网络更快,则报文段就更大(最大报文段)。若应用程序比网络慢,则报文段就较小(小于最大报文段)。
不过有时候我们必须要关闭纳格算法的:比鼠标的移动,这个必须无时延的发送;还有功能键的发送,比如F1发送的不止一个字符,此时就不能启用纳格算法。
 

经受时延的确认

通常TCP在接收到数据时并不立即发送ACK;相反,它推迟发送,以便将ACK与需要沿该方向发送的数据一起发送(有时称这种现象为数据捎带ACK)。绝大多数实验为200ms)也就是说,TCP将以最大200ms的时延等待是否有属于一起发送。

一个例子:这里我们将举另外一个例子:在一个交互注册过程中键入中断的一个特殊功能键。这个功能键通常可以产生多个字符序列,经常从ASCII码的转移(escape)字符开始,如果TCP每次得到一个字符,它很可能会发送序列中的第一个字符(ASCII码的ESC),然后缓存其他字符并等待对该字符的确认。但当服务器收到该字符后,它并不发送确认而是继续的等待接受序列中的其他字符。这就会经常触发服务器的经受时延的确认算法,表示剩下的字符没有在200ms内发送。对于交互用户而言,这将产生明显的时延。

注意只有客户端这边有经受时延的确认,因为客户端这边输入的比较慢,服务器那边收到数据就会立即确认。最大等待200ms还没有数据发送的话,客户端就直接返回ACK了。

 

滑动窗口

TCP采用滑动窗口来进行传输控制,滑动窗口的大小意味着接收方有多大的缓存区可以用于接收数据。发送方可以通过滑动窗口的大小来确定应该发送多少字节的数据。当滑动窗口为0时,发送方一般不能再发送数据,但是紧急数据除外,例如:允许用户终止在远端机上的运行进程。另一种情况是发送方可以发送一个1字节的数据报来通知接收方重新声明它希望接收的下一字节及发送方的滑动窗口大小。
滑动窗口机制的基本原理就是在任意时刻,发送方都维持了一个连续的允许发送的帧的序号,称为发送窗口,同时,接收方也维持了一个允许接收的帧的序号,称为接收窗口。发送窗口和接收窗口的序号的上下界不一定要一样,甚至大小也可不同。不同的滑动窗口协议窗口大小一般不同。发送窗口内的序号的上下界不一定要一样,但是还没有被确认的帧,或者是哪些可以被发送的帧。

  1. 称窗口左边向右边靠近为窗口合拢。这种现象发生在数据被发送和确认时。
  2. 当窗口右边沿想右移动时将允许发送更多的数据,我们称之为窗口张开。这种先发发送在另一端的接收进程读取已经确认的数据并释放了TCP的接收缓存时。
  3. 当右边装口左移时,我们称之为窗口收缩。

发送方打开几号窗口表示发送方已经发送了该序列的帧,但是如果没有得到接收方ack确认的话,此时该序号的帧仍然在发送窗口中。接收方方打开几号窗口代表接收端收到几号的帧,但是并没有返回ack确认。当接收方返回该序号的ack时,该序号关闭(合拢),接收方收到ack时,该序号窗口关闭(此时窗口张开,注意始终不能大于通告窗口大小)。

对于发送窗口来说窗口打开代表发送了该序列数据,但没有收到确认,收到ACK后窗口合拢。
对于接受窗口来说窗口打开代表接受了该序列数据,但没有发送确认,当发ACK后窗口合拢。

 

1比特滑动窗口协议
当发送窗口和接收窗口的大小固定为1时,滑动窗口协议退化为停等协议(stop-and-wait)。该协议规定发送方每发送一帧后就要停下来,等待接收方已正确接收的确认(Acknowledgement)返回后才能继续发送下一个帧。由于发送方需要判断接收到的帧是新习发的帧还是重新发送的帧,因此发送放要为每个帧加一个序号。由于停等协议规定只要一帧完全发送成功后才能发送新的帧,因而只用一比特来编号就够了。

后退n协议
由于停等协议要为每一个帧进行确认后才继续发送下一帧,大大降低了信道利用率,因此又提出了后退n协议。后退n协议中,发送方在发完已给数据帧后 ,不停下来等待应答帧,而是连续发送若干个帧,即使在连续发送过程中收到了接收方发来的应答帧,也可以继续发送。且发送方在每发完一个数据帧时都要设置超时定时器。只要在所设置的超时时间内未收到确认帧,就要重发相应的数据帧。如:当发送方发送了N个帧后,若发现该N帧的前一个帧在计时器超后仍未返回其确认信息,该帧被判定为出错或者丢失,此时发送方就不得不重新发送出错帧及其后的N帧。

从这里不难看出,后退n协议一方面因连续发送数据帧而提高了效率,但是另一方面,在重传时又必须把原已正确重传的数据帧进行重传(仅因这些数据这之前有一个数据帧出错),这种做法又使重传效率降低。由此可见,若传输信道的传输质量很差因而导致误码率较大,连续测协议不一定优于停止等待协议。此协议中的发送窗口的大小为k,接收窗口仍未1.

选择重传协议
在后退n协议中,接收方若发现错误帧就不再接收后续的帧,及时是正确的帧到达,这显然是一种浪费。另一种效率跟高的策略是当接收方发现某帧出错后,其后继续送来的正确的帧虽然不能立即递交给接收方的高层,但接收方扔可收下来,存放在一个缓冲区,同时要求发送方重新传输出错的那一帧。一旦收到重新传来的帧后,就可以源存与缓冲区中国的其余帧一并按正确的顺序递交高层。这种方法称为选择重发(Selectice Repeat),显然,选择重发减少了浪费,但要求接收方有足够大的缓冲区空间。

 

糊涂窗口综合症

当发送端应用程序产生数据很慢、或者接收端应用程序处理接收缓存区的数据很慢的时候,就会在链路中传送很小的报文段,极端情况下有小负载只有1字节而报文段却又41字节。这种现象叫做糊涂窗口综合症(Silly Window Syndrome)。
 
可以在发送方或接收方任意一方采取措施来避免这种现象
在接收端避免措施
接收方不通告小窗口,通常算法是接收方不通告一个比当前窗口大的窗口除非窗口可以增加一个报文段大小(将要接收MSS的大小)、或者可以在增加接收方缓存空间的一半。
 
在发送端避免措施
发送方在满足一下条件之一后才会发送数据:1.可以发送一个满长度的报文段;2.可以发送至少是接收方通告窗口大小一半的报文段;3.能够发送手头的所有数据并且不希望接收ACK或者改连接禁用了纳格算法。
 

慢启动

如果发送方一开始便向网络发送多个报文段,知道达到接收方通告的窗口大小为止。当发送方和接收方位于同一局域网还好。但是如果发送方和接收方之间存在多个路由器和速率较慢的链路时,可能出现问题。中间的路由器必须缓存分组,并有可能耗尽存储器的空间。现在TCP支持一种被称为“慢启动(slow start)”的算法。该算法核心是让新分组进入网络的速率与另一端返回确认的速率相同而进行工作。
 
慢启动为发送方的TCP增加了另一窗口:拥塞窗口(congestion window,cwnd)当与另一个网络的主机建立TCP连接时,拥塞窗口被初始化为1个报文段。每收到一个ACK,拥塞窗口就增加一个报文段。以此成指数增长方式。发送方取拥塞窗口和通告窗口的最小值作为发送上限。拥塞窗口是发送方使用的流量控制,而通告窗口则是接收方使用的流量控制。
 

TCP超时与重传

TCP超时重传采用指数退避的算法(exponential backoff)对连续重传之间不同的时间差,他们取整后分别为1\3\6\12\24\48\64(最大值为64)
 

拥塞避免算法

拥塞算法是一种处理丢失分组的方法。网络发生分组丢失的指示:发生超时和收到重复的ACK(3个或3个以上)
拥塞避免算法和慢启动算法是两个目的不同、独立的算法。但是方发生拥塞时,我们下午给你降低分组进入网络的传输速率,于是可以调用慢启动来作到这一点。在实际中这两个算法通常在一起使用。
 
拥塞避免算法和慢启动算法需要对每个连接维持两个变量:一个拥塞窗口(cwnd)和一个慢启动门限(ssthresh)。这样得到的算法的工作过程如下:
1)对一个给定的连接,初始化cwnd为1个报文段,ssthresh为65535个字节;
2)TCP输出数据大小不能超过cwnd和接收方通告窗口的大小。拥塞避免是发送方使用的流量控制,而通告窗口是接收方进行的流量控制。前置是发送方感受到网络拥塞的估计,后者则与接收方在该连接上的可用缓存大小有关;
3)当拥塞发生时(超时或收到重复确认),ssthresh被设置为当前窗口的一半,但至少为2个报文段大小。如果是超时引起的拥塞,则cwnd被设置为1个报文段(这就是慢启动);
4)当新的数据被对方确实时,就增加cwnd,但增加的方法依赖于我们是否在进行慢启动或拥塞避免。如果cwnd≤ssthresh,则正在进行慢启动,反之进行拥塞避免。慢启动一直持续到我们我们回到当拥塞发生时所处位置一半的时候才停止(即新的ssthresh)然后转为执行拥塞避免。
慢启动算法初始cwnd为1个报文段,每收到一个ack后cwnd就增加1(注意TCP是累计确认),那样,窗口会以指数方式增长。
拥塞避免算法要求每次收到确认时将cwnd增加1/cwnd,这是个线性增长。我们希望在一个往返时间内最多为cwnd增加1个报文段,不管在这个RTT中收到了多少个ACK,然后慢启动则是根据这个往返时间中所收到的确认的个数增加cwnd。
 
下图是慢启动和拥塞避免的可视化描述
 
解释:上图中,假定当cwnd为32个报文段时就会发生拥塞。于是设置ssthresh为16个报文段,而cwnd为1个报文段。在时刻0发送一个报文段,并假设在时刻1接收到它的ACK,此时cwnd增加为2.接着发送了2个报文段,并假设在时刻2接收到他们的ACK,于是cwnd增加为4(对每个ACK增加1次)。这种指数增加算法一直进行到在时刻3和时刻4之间收到8个ACK后cwnd等于ssthresh时才停止,从该时刻起,cwnd以线性方式增加,在每个往返时间内最多增加1个报文段。
正如我们在这个图中看到的那样,术语“慢启动”并不完全正确。它只是采用了比引起拥塞更慢的分组传输速率,但在慢启动期间进入网络分组速率依然是增加的。只有在达到ssthresh拥塞避免算法起作用时,这种增加的速率才会慢下来。
 

快速重传与快速恢复算法

算法通常按如下过程进行实现:
1)当收到第3个重复的ACK时,将ssthresh设置为当前cwnd的一半。重传丢失的报文段。然后设置cwnd为当前ssthresh加上3倍的报文段大小。
代码实现为:[java] view plaincopy step1:if ( dupacks >= 3 ) { ssthresh = max( 2 , cwnd / 2 ) ;cwnd = ssthresh + 3 * SMSS ;}
2)每次收到另一个重复的ACK时,cwnd++,并发送1个分组。注意!!是先按照上次cwnd发送数据包,然后再使cwnd增加一个报文段大小。
3)当下一个确认新数据的ACK达到时,设置cwnd为ssthresh。这个ACK应该是在进行重传后的一个往返时间内的所有中间报文段的确认。这一步采用的是拥塞避免,因为当分组丢失时我们将当前的速率减半。
下图是拥塞避免的一个例子:
我们注意当cwnd为512时进行慢启动,因为只有当cwnd大于ssthresh才进行拥塞避免,当cwnd为768时此时进行的还是慢启动,注意因为cwnd的增加是进行发送数据包之后的时,代码实现是:cwnd++即:进行完发包后才进行自加的!
 

TCP坚持定时器

当一个通告窗口变化的ACK丢失后,则双方就有可能因为等待对方而使连接终止:接收方等待接收数据(因为它已经向发送方通告了一个非0的窗口),而发送方在等待允许它继续发送数据的窗口更新。为防止这种死锁情况的发生,发送方使用一个坚持定时器(persist timer)来周期性地向接收方查询,以便发现窗口是否增大。这些从发送发发出的报文段称为窗口探查(window probe)。
同样当TCP一直收到窗口为0的ACK时,使用指数退避的方式发送坚持定时器,TCP从不放弃发送窗口探查。这些探查每隔60s发送一次,这个过程将持续到或者窗口被打开,或者应用程序使用的连接被终止。
 

TCP保活定时器

keepalive
 

版权声明:本文为博主原创文章,未经博主允许不得转载。

posted @ 2015-07-02 00:41  Danbocn  阅读(30561)  评论(0编辑  收藏  举报