TCP协议中的三次握手和四次挥手 状态图

建立TCP需要三次握手才能建立,而断开连接则需要四次握手。整个过程如下图所示:

1.jpg

 

先来看看如何建立连接的:

2.png

首先Client端发送连接请求报文,Server段接受连接后回复ACK报文,并为这次连接分配资源。Client端接收到ACK报文后也向Server段发送报文,并分配资源,这样TCP连接就建立了。

如何断开连接呢?简单的过程如下:

3.png

注意中断连接端可以是Client端,也可以是Server端

假设Client端发起中断请求,也就是发送FIN报文。Server端接到FIN报文后,意思是说“我client端要发给你了”,但是如果你还没有数据要发送完成,则不必急着关闭Socket,可以继续发送数据。所以所以你先发送ACK,"告诉Client端,你的请求我收到了,但是我还没准备好,请继续你等我的消息"。这个时候Client端就进入FIN_WAIT状态,继续等待Server端的FIN报文。当Server端确定数据已发送完成,则向Client端发送FIN报文,"告诉Client端,好了,我这边数据发完了,准备好关闭连接了"。Client端收到FIN报文后,"就知道可以关闭连接了,但是他还是不相信网络,怕Server端不知道要关闭,所以发送ACK后进入TIME_WAIT状态,如果Server端没有收到ACK则可以重传。“,Server端收到ACK后,"就知道可以断开连接了"。Client端等待了2MSL后依然没有收到回复,则证明Server端已正常关闭,那好,我Client端也可以关闭连接了。Ok,TCP连接就这样关闭了!

整个过程Client端所经历的状态如下:

4.png

而Server端所经历的过程如下:

5.png

注意在TIME_WAIT状态中,如果TCP client端最后一次发送的ACK丢失了,它将重新发送。TIME_WAIT状态中所需要的时间是依赖于实现方法的。典型的值为30秒、1分钟和2分钟。等待之后连接正式关闭,并且所有的资源(包括端口号)都被释放。

  1. 为什么连接的时候是三次握手,关闭的时候却是四次握手?

    因为当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当Server端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉Client端,"你发的FIN报文我收到了"。只有等到我Server端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四步握手。

  2. 为什么TIME_WAIT状态需要经过2MSL(最大报文段生存时间)才能返回到CLOSE状态?

    • 为了保证发送的最后一个ACK报文段能够到达B
    • 防止“已失效的连接请求报文段”出现在本连接中。在发送完最后一个ACK报文段后,再经过实践2MSL,就可以使本连接持续的时间内所产生的所有报文段,都从网络中消失。这样就可以使下一个新的连接中不会出现这种就得连接请求报文段。

TCP有限状态机

TCP有限状态机如下图:

  • 粗实箭头表示客户进程的正常迁移
  • 粗虚线箭头表示对服务器进程的正常迁移
  • 细线箭头表示异常变迁

6.png

 

 


链接:https://www.jianshu.com/p/9968b16b607e
来源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

 

 

=======================================================================================================

 

 

TCP三次握手,四次挥手,状态变迁图

     //加粗的线条是一般正常情况下的状态变迁图

正常情况:服务器端打开,客户端请求链接,三次握手后进入ESTABLISHED状态

客户端要关闭,4次挥手。

 

三次握手:

客服端:CLOSED(请求链接,发送SYN)->SYN_SENT(收到服务器端发送的SYN,ACK,客户端发送ACK)->ESTABLISHED

服务端:LISTEN(监听客户端请求)(收到客户端的SYN,发送ACK,SYN同意建立链接)->SYN收到(收到客户端的确认)->ESTABLISHED

客户端---------SYN------->服务端

客户端<-----ACK SYN-------服务端

客户端---------ACK------->服务端

 

 


四次挥手:

客户端:ESTABLISHED(发FIN)->FIN_WAIT_1->(收到服务器端的ACK确认)->FIN_WAIT_2->(客户端和服务端之间的管道是全双工,有两条,这时候关闭了一条,服务端发送FIN,客户端收到后发送ACK)->TIME_WAIT(2MSL超时关闭,默认服务端收到ACK,这时候两条管道都关闭了)

服务端:ESTABLISHED(收到客户端发送的FIN,发送ACK)->CLOSE_WAIT(客户端关闭了一条管道,这时候服务端发FIN)->LAST_ACK(等待最后确认关闭第二条管道)->(收到ACK,发送无)


户端主动关闭情况

进程终止时所有打开的TCP连接上都会发出一个FIN

客户端---------FIN-------->服务端

客户端<--------ACK---------服务端

/* 关闭了一条管道 */

客户端<--------FIN---------服务端

客户端---------ACK-------->服务端

/* 等待2MSL超时,关闭第二条管道 */

 

 

 


TIME_WAIT的持续时间是MSL(最长

生命周期Maximum Segment Lifttime)的2倍,2MSL,MSL一般情况是30S到2分钟,所以TIME_WAIT的时间一般为1~4分钟。

存在TIME_WAIT状态有两个理由:
1、实现终止TCP全双工连接的可靠性
假设最终的ACK丢失,服务器将重发最终的FIN,因此客户必须维护状态信息以允许它重发最终的ACK,如果不维护状态信息,它将响应以RST,而服务器则把该分节解释成一个错误。如果TCP打算执行所有必要的工作以彻底终止某个链接上两个方向的数据流,那么它必须能够处理连接终止序列四个分节中任何一个分节丢失的情况,也即主动关闭的那一端必须进入TIME_WAIT状态,因为它可能不得不重发最终的ACK。
2、允许老的重复分节在网络中消失
假设我们关闭某个TCP连接后,在以后的某个时候又重新建立相同的IP地址和端口之间的TCP连接。因为后一个连接和前一个连接它们的IP地址和端口号是相同的,TCP必须防止来自某个连接的老重复分组在连接终止后再现,从而被误解成属于同一个连接的化身。要实现这种功能,TCP不能给处于TIME_WAIT状态的连接启动新的化身,所有有TIME_WAIT,这段时间足够让某个方向上的分组最多存活MSL秒即被丢弃,另一个方向上的应答最多存活MSL秒也被丢弃。

 

 

===========================================================================================

 

TCP/IP状态变迁图和TCP三次握手与四次挥手

转自:http://network.chinabyte.com/262/13083762.shtml

TCP/IP状态变迁图:

说明: TCP/IP状态变迁图:

  各状态详细描述:

  CLOSED:表示初始状态。对服务端和C客户端双方都一样。

  LISTEN:表示监听状态。服务端调用了listen函数,可以开始accept连接了。

  SYN_SENT:表示客户端已经发送了SYN报文。当客户端调用connect函数发起连接时,首先发SYN给服务端,然后自己进入SYN_SENT状态,并等待服务端发送ACK+SYN。

  SYN_RCVD:表示服务端收到客户端发送SYN报文。服务端收到这个报文后,进入SYN_RCVD状态,然后发送ACK+SYN给客户端。

  ESTABLISHED:表示连接已经建立成功了。服务端发送完ACK+SYN后进入该状态,客户端收到ACK后也进入该状态。

  FIN_WAIT_1:表示主动关闭连接。无论哪方调用close函数发送FIN报文都会进入这个这个状态。

  FIN_WAIT_2:表示被动关闭方同意关闭连接。主动关闭连接方收到被动关闭方返回的ACK后,会进入该状态。

  TIME_WAIT:表示收到对方的FIN报文并发送了ACK报文,就等2MSL后即可回到CLOSED状态了。如果FIN_WAIT_1状态下,收到对方同时带FIN标志和ACK标志的报文时,可以直接进入TIME_WAIT状态,而无须经过FIN_WAIT_2状态。

  CLOSING:表示双方同时关闭连接。如果双方几乎同时调用close函数,那么会出现双方同时发送FIN报文的情况,就会出现CLOSING状态,表示双方都在关闭连接。

  CLOSE_WAIT:表示被动关闭方等待关闭。当收到对方调用close函数发送的FIN报文时,回应对方ACK报文,此时进入CLOSE_WAIT状态。

  LAST_ACK:表示被动关闭方发送FIN报文后,等待对方的ACK报文状态,当收到ACK后进入CLOSED状态。

  1. 连接建立

  1) Client

  当Client端调用socket函数调用时,相当于Client端产生了一个处于Closed状态的套接字。

  Client端又调用connect函数调用,系统为Client随机分配一个端口,连同传入connect中的参数(Server的IP和端口),这就形成了一个连接四元组,connect调用让Client端的socket处于SYN_SENT状态。

  当Server返回确认,并发送SYN,Client返回确认及SYN后,套接字处于ESTABLISHED阶段,此时双方的连接已经可以进行读写操作。

  2)Server

  当Server端调用socket函数调用时,相当于Server端产生了一个处于Closed状态的监听套接字,Server端调用bind操作,将监听套接字与指定的地址和端口关联,然后又调用listen函数,系统会为其分配未完成队列和完成队列,此时的监听套接字可以接受Client的连接,监听套接字状态处于LISTEN状态。

  当Server端调用accept操作时,会从完成队列中取出一个已经完成的client连接,同时在server这端会产生一个会话套接字,用于和client端套接字的通信,这个会话套接字的状态是ESTABLISH。

  2. 连接关闭

  与连接建立分为server/client不同,连接关闭并没有绝对的server/client之分,连接关闭分为主动关闭和被动关闭。Server和client都可以担任这两个角色中的任意一个。如client可以关闭它与server的连接,同样的server一样也可以关闭一些长时间无读写事件发生的连接。既然这么说了,下面就会分成两个部分:client主动关闭,server主动关闭。

  Client主动关闭,Server被动关闭:

  Client主动关闭,Server被动关闭的情况还是蛮多的,比如说短连接中,当一次会话结束时,client就可以关闭它与server之间的连接。

  我们这边直接说close而非shutdown。

  当client想要关闭它与server之间的连接,首先client这边会首先调用close函数,client端会发送一个FIN到server端,client端处于FIN_WAIT1状态。当server端返回给client ACK后,client处于FIN_WAIT2状态,server处于CLOSE_WAIT状态。

  当server端检测到client端的关闭操作(read返回为0),server端也需要调用close操作,server端会向client端发送一个FIN。此时server的状态为LAST_ACK,当 client收到来自server的FIN后,client端的套接字处于TIME_WAIT状态,它会向server端再发送一个ack确认,此时server端收到ack确认后,此套接字处于CLOSED状态。

  Server端主动关闭的流程与Client端关闭类似,就不再多讲,下面还需要关注的是TIME_WAIT这个状态,分别按照client/server两个部分讲述。

  首先说一下TCP/IP详解中描述的关于TIME_WAIT的描述及其存在的必要性:

  主动关闭的socket当收到对端的FIN操作后,该socket就会处于TIME_WAIT状态,处于TIME_WAIT状态的socket会存活2MSL(Max Segment Lifetime),

  之所以存活这么长时间是有理由的:

  一方面是可靠的实现TCP全双工连接的终止,也就是当最后的ACK丢失后,被动关闭端会重发FIN,因此主动关闭端需要维持状态信息,以允许它重新发送最终的ACK。

  另一方面TCP在2MSL等待期间,定义这个连接(4元组)不能再使用,任何迟到的报文都会丢弃。设想如果没有2MSL的限制,恰好新到的连接正好满足原先的4元组,这时候连接就可能接收到网络上的延迟报文就可能干扰最新建立的连接。重复的分节在网络中消逝。

  3. Server端的监听套接字与会话套接字的不同:

  当server端的socket调用bind和listen之后,监听套接字的状态就会变为LISTEN状态,监听套接字的工作决定了它只是监听连接。

  当server端调用accept,相当于从套接字的完成队列中取出一个client的连接,此时可以确定四元组,即:client的IP和port;server的IP和port,双方各有一个套接字进行交互,这里需要说一下server端的socket,我称server端accept后产生的套接字为会话套接字,这个套接字一出生的状态就是ESTABLISH。由server端得四元组我们可以看出,一个server从理论上可以接收的连接数量取决于文件描述符的个数。

  4,状态为TIME_WAIT是不是所有执行主动关闭的socket都会进入TIME_WAIT状态呢?

  有没有什么情况使主动关闭的socket直接进入CLOSED状态呢?

  主动关闭的一方在发送最后一个 ack 后就会进入 TIME_WAIT 状态 停留2MSL(max segment lifetime)时间,这个是TCP/IP必不可少的,也就是“解决”不了的。也就是TCP/IP设计者本来是这么设计的。

  主要有两个原因:

  1。防止上一次连接中的包,迷路后重新出现,影响新连接(经过2MSL,上一次连接中所有的重复包都会消失)

  2。可靠的关闭TCP连接。在主动关闭方发送的最后一个 ack(fin) ,有可能丢失,这时被动方会重新发 fin, 如果这时主动方处于 CLOSED 状态 ,就会响应 rst 而不是 ack。所以 主动方要处于 TIME_WAIT 状态,而不能是 CLOSED 。

  特别提示的是:为什么TIME_WAIT状态还需要等待2MSL才能回到CLOSED状态?或者为什么TCP要引入TIME_WAIT状态?

  《TCP/IP详解》中如此解释:当TCP执行一个主动关闭,并发回最后一个ACK后,该连接必须在TIME_WAIT状态停留的时间为2倍的MSL,这样可以让TCP再次发送最后的ACK以防止这个ACK丢失(另一端超时重发最后的FIN)。

  附注:MSL(Maximum Segment Lifetime)即最大生存时间,RFC 793中指出MSL为2分钟,但是实现中的常用值为30秒、1分钟或者2分钟。

  建立TCP连接(三次握手)

  由于TCP协议提供可靠的连接服务,于是采用有保障的三次握手方式来创建一个TCP连接。三次握手的具体过程如下:

  客户端发送一个带SYN标志的TCP报文(报文1)到服务器端,表示希望建立一个TCP连接。

  服务器发送一个带ACK标志和SYN标志的TCP报文(报文2)给客户端,ACK用于对报文1的回应,SYN用于询问客户端是否准备好进行数据传输。

  客户端发送一个带ACK标志的TCP报文(报文3),作为报文2的回应。

  至此,一个TCP连接就建立起来了。

  终止TCP连接(四次挥手)

  由于TCP连接是全双工的,因此每个方向都必须单独进行关闭。原则是主动关闭的一方(如已传输完所有数据等原因)发送一个FIN报文来表示终止这个方向的连接,收到一个FIN意味着这个方向不再有数据流动,但另一个方向仍能继续发送数据,直到另一个方向也发送FIN报文。

  四次挥手的具体过程如下:

  客户端发送一个FIN报文(报文4)给服务器,表示我将关闭客户端到服务器端这个方向的连接。

  服务器收到报文4后,发送一个ACK报文(报文5)给客户端,序号为报文4的序号加1。

  服务器发送一个FIN报文(报文6)给客户端,表示自己也将关闭服务器端到客户端这个方向的连接。

  客户端收到报文6后,发回一个ACK报文(报文7)给服务器,序号为报文6的序号加1。

  至此,一个TCP连接就关闭了。(4次挥手不是关闭TCP连接的唯一办法,见下文Q3疑问)

  TCP三次握手,四次挥手的时序图:

说明: TCP三次握手,四次挥手的时序图:

  TCP相关疑问

  几个常见的TCP/IP相关的疑问:

  Q1 为什么在TCP协议里,建立连接是三次握手,而关闭连接却是四次握手呢?

  A1因为当处于LISTEN 状态的服务器端SOCKET当收到SYN报文(客户端希望新建一个TCP连接)后,它可以把ACK(应答作用)和SYN(同步作用)放在同一个报文里来发送给客户端。但在关闭TCP连接时,当收到对方的FIN报文时,对方仅仅表示对方没有数据发送给你了,但未必你的所有数据都已经全部发送给了对方,所以你大可不必马上关闭SOCKET(发送一个FIN报文),等你发送完剩余的数据给对方之后,再发送FIN报文给对方来表示你同意现在关闭连接了,所以通常情况下,这里的ACK报文和FIN报文都是分开发送的。

  Q2为什么TIME_WAIT 状态还需要等2*MSL秒之后才能返回到CLOSED 状态呢?

  A2因为虽然双方都同意关闭连接了,而且握手的4个报文也都发送完毕,按理可以直接回到CLOSED 状态(就好比从SYN_SENT 状态到ESTABLISH 状态那样),但是我们必须假想网络是不可靠的,你无法保证你(客户端)最后发送的ACK报文一定会被对方收到,就是说对方处于LAST_ACK 状态下的SOCKET可能会因为超时未收到ACK报文,而重发FIN报文,所以这个TIME_WAIT 状态的作用就是用来重发可能丢失的ACK报文。

  Q3关闭TCP连接一定需要4次挥手吗?

  A3不一定,4次挥手关闭TCP连接是最安全的做法。但在有些时候,我们不喜欢TIME_WAIT 状态(如当MSL数值设置过大导致服务器端有太多TIME_WAIT状态的TCP连接,减少这些条目数可以更快地关闭连接,为新连接释放更多资源),这时我们可以通过设置SOCKET变量的SO_LINGER标志来避免SOCKET在close()之后进入TIME_WAIT状态,这时将通过发送RST强制终止TCP连接(取代正常的TCP四次握手的终止方式)。但这并不是一个很好的主意,TIME_WAIT 对于我们来说往往是有利的。

 

================================================================================================

大家对netstat -a命令很熟悉吧,但是,你有没有注意到STATE一栏呢,基本上显示着established,time_wait,close_wait等,这些到底是什么意思呢,在这篇文章,我将会详细的阐述。

大家很明白TCP初始化连接三次握手吧:发SYN包,然后返回SYN/ACK包,再发ACK包,连接正式建立。如下图:

wKioL1NJQtDjG6fiAAHG1DwiUds265.jpg

但是大家明白关闭连接的工作原理吗?

关闭连接要四次握手:发FIN包,ACK 包,FIN包,ACK包,四次握手!!

为什么呢,因为TCP连接是全双工,我关了你的连接,并不等于你关了我的连接。


客户端TCP状态迁移:
CLOSED->SYN_SENT->ESTABLISHED->FIN_WAIT_1->FIN_WAIT_2->TIME_WAIT->CLOSED
服务器TCP状态迁移:
CLOSED->LISTEN->SYN_RECEIVED->ESTABLISHED->CLOSE_WAIT->LAST_ACK->CLOSED

当客户端开始连接时,服务器还处于LISTENING,客户端发一个SYN包后,他就处于SYN_SENT状态,服务器就处于SYS收到状态,然后互相确认进入连接状态ESTABLISHED.


当客户端请求关闭连接时,客户端发送一个FIN包后,客户端就进入FIN_WAIT_1状态,等待对方的确认包,服务器发送一个ACK包给客户,客户端收到ACK包后结束FIN_WAIT_1状态,进入FIN_WAIT_2状态,等待服务器发过来的关闭请求,服务器发一个FIN包后,进入 CLOSE_WAIT状态,当客户端收到服务器的FIN包,FIN_WAIT_2状态就结束,然后给服务器端的FIN包给以一个确认包,客户端这时进入 TIME_WAIT,当服务器收到确认包后,CLOSE_WAIT状态结束了,这时候服务器端真正的关闭了连接.但是客户端还在TIME_WAIT状态下,什么时候结束呢.我在这里再讲到一个新名词:2MSL等待状态,其实TIME_WAIT就是2MSL等待状态,为什么要设置这个状态,原因是有足够的时间让ACK包到达服务器端,如果服务器端没收到ACK包,超时了,然后重新发一个FIN包,直到服务器收到ACK包,TIME_WAIT状态等待时间是在TCP重新启动后不连接任何请求的两倍.
大家有没有发现一个问题:如果对方在第三次握手的时候出问题,如发FIN包的时候,不知道什么原因丢了这个包,然而这边一直处在FIN_WAIT_2状态,而且TCP/IP并没有设置这个状态的过期时间,那他一直会保留这个状态下去,越来越多的FIN_WAIT_2状态会导致系统崩溃.

上面我碰到的这个问题主要因为TCP的结束流程未走完,造成连接未释放。现设客户端主动断开连接,流程如下

wKiom1NOnVrxqsNCAABEphByk80809.bmp

 

如上图所示,

Client 消息 Server

close()
------ FIN ------->
FIN_WAIT1 CLOSE_WAIT
<----- ACK -------
FIN_WAIT2
close()
<------ FIN ------
TIME_WAIT LAST_ACK

------ ACK ------->
CLOSED
CLOSED


由于Server的Socket在客户端已经关闭时而没有调用关闭,
造成服务器端的连接处在“挂起”状态,而客户端则处在等待应答的状态上。
此问题的典型特征是:
一端处于FIN_WAIT2 ,而另一端处于CLOSE_WAIT.

不过,根本问题还是程序写的不好,有待提高

-------------------------------------------------------------------------

CLOSE_WAIT,TCP的×××,TCP的朋友。

CLOSE_WAIT状态的生成原因
首先我们知道,如果我们的服务器程序APACHE处于CLOSE_WAIT状态的话,说明套接字是被动关闭的!

因为如果是CLIENT端主动断掉当前连接的话,那么双方关闭这个TCP连接共需要四个packet:

Client ---> FIN ---> Server

Client <--- ACK <--- Server

这时候Client端处于FIN_WAIT_2状态;而Server 程序处于CLOSE_WAIT状态。

Client <--- FIN <--- Server

这时Server 发送FIN给Client,Server 就置为LAST_ACK状态。

Client ---> ACK ---> Server

Client回应了ACK,那么Server 的套接字才会真正置为CLOSED状态。

Server 程序处于CLOSE_WAIT状态,而不是LAST_ACK状态,说明还没有发FIN给Client,那么可能是在关闭连接之前还有许多数据要发送或者其他事要做,导致没有发这个FIN packet。

通常来说,一个CLOSE_WAIT会维持至少2个小时的时间。如果有个流氓特地写了个程序,给你造成一堆的CLOSE_WAIT,消耗

你的资源,那么通常是等不到释放那一刻,系统就已经解决崩溃了。

只能通过修改一下TCP/IP的参数,来缩短这个时间:修改tcp_keepalive_*系列参数有助于解决这个问题。

连接进程是通过一系列状态表示的,这些状态有:

LISTEN,SYN-SENT,SYN-RECEIVED,ESTABLISHED,FIN-WAIT-1,FIN-WAIT-2,CLOSE-WAIT,CLOSING,LAST-ACK,TIME-WAIT 和 CLOSED。

各个状态的意义如下:

CLOSED- 初始状态,表示TCP连接是“关闭着的”或“未打开的”。

LISTEN - 表示服务器端的某个SOCKET处于监听状态,可以接受客户端的连接。

SYN-SENT - 这个状态与SYN_RCVD 状态相呼应,当客户端SOCKET执行connect()进行连接时,它首先发送SYN报文,然后随即进入到SYN_SENT 状态,并等待服务端的发送三次握手中的第2个报文。SYN_SENT 状态表示客户端已发送SYN报文。

SYN-RCVD - 表示接收到了SYN报文。在正常情况下,这个状态是服务器端的SOCKET在建立TCP连接时的三次握手会话过程中的一个中间状态,很短暂,基本上用netstat很难看到这种状态,除非故意写一个监测程序,将三次TCP握手过程中最后一个ACK报文不予发送。当TCP连接处于此状态时,再收到客户端的ACK报文,它就会进入到ESTABLISHED 状态。

ESTABLISHED- 表示TCP连接已经成功建立,数据可以传送给用户;

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()主动要求关闭连接。注意:FIN_WAIT_2 是没有超时的(不像TIME_WAIT 状态),这种状态下如果对方不关闭(不配合完成4次挥手过程),那这个FIN_WAIT_2 状态将一直保持到系统重启,越来越多的FIN_WAIT_2 状态会导致内核崩溃。

CLOSE-WAIT -  表示正在等待关闭。怎么理解呢?当对方close()一个SOCKET后发送FIN报文给自己,你的系统毫无疑问地将会回应一个ACK报文给对方,此时TCP连接则进入到CLOSE_WAIT状态。接下来呢,你需要检查自己是否还有数据要发送给对方,如果没有的话,那你也就可以close()这个SOCKET并发送FIN报文给对方,即关闭自己到对方这个方向的连接。有数据的话则看程序的策略,继续发送或丢弃。简单地说,当你处于CLOSE_WAIT 状态下,需要完成的事情是等待你去关闭连接。

CLOSING - 这种状态在实际情况中应该很少见,属于一种比较罕见的例外状态。正常情况下,当一方发送FIN报文后,按理来说是应该先收到(或同时收到)对方的ACK报文,再收到对方的FIN报文。但是CLOSING 状态表示一方发送FIN报文后,并没有收到对方的ACK报文,反而却也收到了对方的FIN报文。什么情况下会出现此种情况呢?那就是当双方几乎在同时close()一个SOCKET的话,就出现了双方同时发送FIN报文的情况,这时就会出现CLOSING 状态,表示双方都正在关闭SOCKET连接。

LAST-ACK - 当被动关闭的一方在发送FIN报文后,等待对方的ACK报文的时候,就处于LAST_ACK 状态。当收到对方的ACK报文后,也就可以进入到CLOSED 可用状态了。

TIME-WAIT - 等待足够的时间以确保远程TCP接收到连接中断请求的确认; 表示收到了对方的FIN报文,并发送出了ACK报文。TIME_WAIT状态下的TCP连接会等待2*MSL(Max Segment Lifetime,最大分段生存期,指一个TCP报文在Internet上的最长生存时间。每个具体的TCP协议实现都必须选择一个确定的MSL值,RFC 1122建议是2分钟,但BSD传统实现采用了30秒,Linux可以cat /proc/sys/net/ipv4/tcp_fin_timeout看到本机的这个值),然后即可回到CLOSED 可用状态了。如果FIN_WAIT_1状态下,收到了对方同时带FIN标志和ACK标志的报文时,可以直接进入到TIME_WAIT状态,而无须经过FIN_WAIT_2状态。


TCP连接过程是状态的转换,促使发生状态转换的是用户调用:OPEN,SEND,RECEIVE,CLOSE,ABORT和STATUS;传送过来的数据段,特别那些包括以下标记的数据段SYN,ACK,RST和FIN;还有超时,上面所说的都会使TCP状态发生变化。

wKioL1NOnU2iNgeMAAFH2LKK0pM721.jpg

wKiom1NJQQ7BrW4tAARo1r_pZgE817.jpg

 

TCP连接的状态转换图

这个图N多人都知道,它对排除和定位网络或系统故障时大有帮助,但是怎样牢牢地将这张图刻在脑中呢?那么你就一定要对这张图的每一个状态,及转换的过程有深刻地认识,不能只停留在一知半解之中。下面对这张图的11种状态详细解释一下,以便加强记忆!不过在这之前,先回顾一下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可用状态了。

 

最后有3个问题的回答,我自己分析后的结论(不一定保证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报文,并保证于此。

        3、关闭TCP连接一定需要4次挥手吗?

不一定,4次挥手关闭TCP连接是最安全的做法。但在有些时候,我们不喜欢TIME_WAIT状态(如当MSL数值设置过大导致服务器端有太多TIME_WAIT状态的TCP连接,减少这些条目数可以更快地关闭连接,为新连接释放更多资源),这时我们可以通过设置SOCKET变量的SO_LINGER标志来避免SOCKET在close()之后进入TIME_WAIT状态,这时将通过发送RST强制终止TCP连接(取代正常的TCP四次握手的终止方式)。但这并不是一个很好的主意,TIME_WAIT 对于我们来说往往是有利的。

 

==================================================================================================

TCP/IP 通信的的三次握手图解

TCP 通信握手图解

TCP是主机对主机层的传输控制协议,提供可靠的连接服务:

位码即tcp标志位,有6种标示:

  • SYN(synchronous建立联机)
  • ACK(acknowledgement 确认)
  • PSH(push传送)
  • FIN(finish结束)
  • RST(reset重置)
  • URG(urgent紧急)
  • Sequence number(seq 顺序号码)
  • Acknowledge number(ack 确认号码)

TCP握手详解

建立连接协议(三次握手):

  1. 第一次握手:客户端发送syn包(syn=x)的数据包到服务器,并进入SYN_SEND状态,等待服务器确认;
  2. 第二次握手:服务器收到syn包,必须确认客户的SYN(ack=x+1),同时自己也发送一个SYN包(syn=y),即SYN+ACK包,此时服务器进入SYN_RECV状态;
  3. 第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=y+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。

握手过程中传送的包里不包含数据,三次握手完毕后,客户端与服务器才正式开始传送数据。理想状态下,TCP连接一旦建立,在通信双方中的任何一方主动关闭连接之前,TCP连接都将被一直保持下去。

连接终止协议(四次握手)

由于TCP连接是全双工的,因此每个方向都必须单独进行关闭。这原则是当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向的连接。收到一个 FIN只意味着这一方向上没有数据流动,一个TCP连接在收到一个FIN后仍能发送数据。首先进行关闭的一方将执行主动关闭,而另一方执行被动关闭。

  1. 第一次挥手:主动关闭方发送一个FIN,用来关闭主动方到被动关闭方的数据传送,也就是主动关闭方告诉被动关闭方:我已经不会再给你发数据了(当然,在fin包之前发送出去的数据,如果没有收到对应的ack确认报文,主动关闭方依然会重发这些数据),但是,此时主动关闭方还可以接受数据。
  2. 第二次挥手:被动关闭方收到FIN包后,发送一个ACK给对方,确认序号为收到序号+1(与SYN相同,一个FIN占用一个序号, SYN 和 FIN 都有seq序号)
  3. 第三次挥手:被动关闭方发送一个FIN,用来关闭被动关闭方到主动关闭方的数据传送,也就是告诉主动关闭方,我的数据也发送完了,不会再给你发数据了。
  4. 第四次挥手:主动关闭方收到FIN后,发送一个ACK给被动关闭方,确认序号为收到序号+1,至此,完成四次挥手。

TCP状态转换图

TCP状态转换图

状态说明:
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状态。

注:MSL(最大分段生存期)指明TCP报文在Internet上最长生存时间,每个具体的TCP实现都必须选择一个确定的MSL值.RFC 1122建议是2分钟,但BSD传统实现采用了30秒.TIME_WAIT 状态最大保持时间是2 * MSL,也就是1-4分钟

结论:在TIME_WAIT下等待2MSL,只是为了尽最大努力保证四次握手正常关闭。确保老的报文段在网络中消失,不会影响新建立的连接.

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可用状态了。

思考

思考:

1、 为什么建立连接协议是三次握手,而关闭连接却是四次握手呢?

这是因为服务端的LISTEN状态下的SOCKET当收到SYN报文的建连请求后,它可以把ACK和SYN(ACK起应答作用,而SYN起同步作用)放在一个报文里来发送。但关闭连接时,当收到对方的FIN报文通知时,它仅仅表示对方没有数据发送给你了;但未必你所有的数据都全部发送给对方了,所以你可能未必会马上会关闭SOCKET,也即你可能还需要发送一些数据给对方之后,再发送FIN报文给对方来表示你同意现在可以关闭连接了,所以它这里的ACK报文和FIN报文多数情况下都是分开发送的。

2. 为什么不能用两次握手进行连接?

我们知道,3次握手完成两个重要的功能,既要双方做好发送数据的准备工作(双方都知道彼此已准备好),也要允许双方就初始序列号进行协商,这个序列号在握手过程中被发送和确认。
现在把三次握手改成仅需要两次握手,死锁是可能发生的。作为例子,考虑计算机S和C之间的通信,假定C给S发送一个连接请求分组,S收到了这个分组,并发送了确认应答分组。按照两次握手的协定,S认为连接已经成功地建立了,可以开始发送数据分组。可是,C在 S的应答分组在传输中被丢失的情况下,将不知道S是否已准备好,不知道S建立什么样的序列号,C甚至怀疑S是否收到自己的连接请求分组。在这种情况下,C认为连接还未建立成功,将忽略S发来的任何数据分组,只等待连接确认应答分组。而S在发出的数据分组超时后,重复发送同样的数据分组。这样就形成了死锁。
就好比打电话, A打给B, A要告诉B ,我打给你了, B要回应给A听,OK,连接成功了;然后B也要得到A的确认,才能开始正式通话

3. 为什么TIME_WAIT状态还需要等2MSL后才能返回到CLOSED状态?

什么是2MSL?MSL即Maximum Segment Lifetime,也就是报文最大生存时间,引用《TCP/IP详解》中的话:“它(MSL)是任何报文段被丢弃前在网络内的最长时间。”那么,2MSL也就是这个时间的2倍,当TCP连接完成四个报文段的交换时,主动关闭的一方将继续等待一定时间(2-4分钟),即使两端的应用程序结束。

1)为什么需要这个2MSL呢?

    1. 虽然双方都同意关闭连接了,而且握手的4个报文也都协调和发送完毕,按理可以直接回到CLOSED状态(就好比从SYN_SEND状态到ESTABLISH状态那样);但是因为我们必须要假想网络是不可靠的,你无法保证你最后发送的ACK报文会一定被对方收到,因此对方处于LAST_ACK状态下的SOCKET可能会因为超时未收到ACK报文,而重发FIN报文,所以这个TIME_WAIT状态的作用就是用来重发可能丢失的ACK报文。
    1. 报文可能会被混淆,意思是说,其他时候的连接可能会被当作本次的连接。直接引用《The TCP/IP Guide》的说法:The second is to provide a “buffering period” between the end of this connection and any subsequent ones. If not for this period, it is possible that packets from different connections could be mixed, creating confusion.

2)TIME_WAIT状态存在的理由:

    1. 可靠地实现TCP全双工连接的终止
      在进行关闭连接四路握手协议时,最后的ACK是由主动关闭端发出的,如果这个最终的ACK丢失,服务器将重发最终的FIN,因此客户端必须维护状态信息允许它重发最终的ACK。如果不维持这个状态信息,那么客户端将响应RST分节,服务器将此分节解释成一个错误(在java中会抛出connection reset的SocketException)。因而,要实现TCP全双工连接的正常终止,必须处理终止序列四个分节中任何一个分节的丢失情况,主动关闭 的客户端必须维持状态信息进入TIME_WAIT状态。
    1. 允许老的重复分节在网络中消逝
      TCP分节可能由于路由器异常而“迷途”,在迷途期间,TCP发送端可能因确认超时而重发这个分节,迷途的分节在路由器修复后也会被送到最终目的地,这个 原来的迷途分节就称为lost duplicate。在关闭一个TCP连接后,马上又重新建立起一个相同的IP地址和端口之间的TCP连接,后一个连接被称为前一个连接的化身 (incarnation),那么有可能出现这种情况,前一个连接的迷途重复分组在前一个连接终止后出现,从而被误解成从属于新的化身。为了避免这个情 况,TCP不允许处于TIME_WAIT状态的连接启动一个新的化身,因为TIME_WAIT状态持续2MSL,就可以保证当成功建立一个TCP连接的时 候,来自连接先前化身的重复分组已经在网络中消逝。
  • 对第二点的补充说明. 防止lost duplicate对后续新建正常链接的传输造成破坏。lost duplicate在实际的网络中非常常见,经常是由于路由器产生故障,路径无法收敛,导致一个packet在路由器A,B,C之间做类似死循环的跳转。IP头部有个TTL,限制了一个包在网络中的最大跳数,因此这个包有两种命运,要么最后TTL变为0,在网络中消失;要么TTL在变为0之前路由器路径收敛,它凭借剩余的TTL跳数终于到达目的地。但非常可惜的是TCP通过超时重传机制在早些时候发送了一个跟它一模一样的包,并先于它达到了目的地,因此它的命运也就注定被TCP协议栈抛弃。另外一个概念叫做incarnation connection,指跟上次的socket pair一摸一样的新连接,叫做incarnation of previous connection。lost duplicate加上incarnation connection,则会对我们的传输造成致命的错误。大家都知道TCP是流式的,所有包到达的顺序是不一致的,依靠序列号由TCP协议栈做顺序的拼接;假设一个incarnation connection这时收到的seq=1000, 来了一个lost duplicate为seq=1000, len=1000, 则tcp认为这个lost duplicate合法,并存放入了receive buffer,导致传输出现错误。通过一个2MSL TIME_WAIT状态,确保所有的lost duplicate都会消失掉,避免对新连接造成错误。

3)该状态为什么设计在主动关闭这一方:

(1)发最后ack的是主动关闭一方
(2)只要有一方保持TIME_WAIT状态,就能起到避免incarnation connection在2MSL内的重新建立,不需要两方都有

4)对服务器的影响

当某个连接的一端处于TIME_WAIT状态时,该连接将不能再被使用。事实上,对于我们比较有现实意义的是,这个端口将不能再被使用。某个端口处于TIME_WAIT状态(其实应该是这个连接)时,这意味着这个TCP连接并没有断开(完全断开),那么,如果你bind这个端口,就会失败。对于服务器而言,如果服务器突然crash掉了,那么它将无法在2MSL内重新启动,因为bind会失败。解决这个问题的一个方法就是设置socket的SO_REUSEADDR选项。这个选项意味着你可以重用一个地址。

5)如何正确对待2MSL TIME_WAIT

RFC要求socket pair在处于TIME_WAIT时,不能再起一个incarnation connection。但绝大部分TCP实现,强加了更为严格的限制。在2MSL等待期间,socket中使用的本地端口在默认情况下不能再被使用。若A 10.234.5.5:1234和B 10.55.55.60:6666建立了连接,A主动关闭,那么在A端只要port为1234,无论对方的port和ip是什么,都不允许再起服务。显而易见这是比RFC更为严格的限制,RFC仅仅是要求socket pair不一致,而实现当中只要这个port处于TIME_WAIT,就不允许起连接。这个限制对主动打开方来说是无所谓的,因为一般用的是临时端口;但对于被动打开方,一般是server,就悲剧了,因为server一般是熟知端口。比如http,一般端口是80,不可能允许这个服务在2MSL内不能起来。解决方案是给服务器的socket设置SO_REUSEADDR选项,这样的话就算熟知端口处于TIME_WAIT状态,在这个端口上依旧可以将服务启动。当然,虽然有了SO_REUSEADDR选项,但sockt pair这个限制依旧存在。比如上面的例子,A通过SO_REUSEADDR选项依旧在1234端口上起了监听,但这时我们若是从B通过6666端口去连它,TCP协议会告诉我们连接失败,原因为Address already in use.

Time_await 详解

因为TCP连接是双向的,所以在关闭连接的时候,两个方向各自都需要关闭。先发FIN包的一方执行的是主动关闭;后发FIN包的一方执行的是被动关闭。主动关闭的一方会进入TIME_WAIT状态,并且在此状态停留两倍的MSL时长。

穿插一点MSL的知识:MSL指的是报文段的最大生存时间,如果报文段在网络活动了MSL时间,还没有被接收,那么会被丢弃。关于MSL的大小,RFC 793协议中给出的建议是两分钟,不过实际上不同的操作系统可能有不同的设置,以Linux为例,通常是半分钟,两倍的MSL就是一分钟,也就是60秒,并且这个数值是硬编码在内核中的,也就是说除非你重新编译内核,否则没法修改它:

如果每秒的连接数是一千的话,那么一分钟就可能会产生六万个TIME_WAIT。

为什么主动关闭的一方不直接进入CLOSED状态,而是进入TIME_WAIT状态,并且停留两倍的MSL时长呢?这是因为TCP是建立在不可靠网络上的可靠的协议。例子:主动关闭的一方收到被动关闭的一方发出的FIN包后,回应ACK包,同时进入TIME_WAIT状态,但是因为网络原因,主动关闭的一方发送的这个ACK包很可能延迟,从而触发被动连接一方重传FIN包。极端情况下,这一去一回,就是两倍的MSL时长。如果主动关闭的一方跳过TIME_WAIT直接进入CLOSED,或者在TIME_WAIT停留的时长不足两倍的MSL,那么当被动关闭的一方早先发出的延迟包到达后,就可能出现类似下面的问题:

  • 旧的TCP连接已经不存在了,系统此时只能返回RST包
  • 新的TCP连接被建立起来了,延迟包可能干扰新的连接

不管是哪种情况都会让TCP不再可靠,所以TIME_WAIT状态有存在的必要性。

如何控制TIME_WAIT的数量?

如何控制TIME_WAIT的数量?

从前面的描述我们可以得出这样的结论:TIME_WAIT这东西没有的话不行,不过太多可能也是个麻烦事。下面让我们看看有哪些方法可以控制TIME_WAIT数量,这里只说一些常规方法,另外一些诸如SO_LINGER之类的方法太过偏门,略过不谈。

  • tcp_tw_recycle

顾名思义就是回收TIME_WAIT连接。可以说这个内核参数已经变成了大众处理TIME_WAIT的万金油,如果你在网络上搜索TIME_WAIT的解决方案,十有八九会推荐设置它,不过这里隐藏着一个不易察觉的陷阱

当多个客户端通过NAT方式联网并与服务端交互时,服务端看到的是同一个IP,也就是说对服务端而言这些客户端实际上等同于一个,可惜由于这些客户端的时间戳可能存在差异,于是乎从服务端的视角看,便可能出现时间戳错乱的现象,进而直接导致时间戳小的数据包被丢弃。参考:tcp_tw_recycle和tcp_timestamps导致connect失败问题

  • tcp_tw_reuse

顾名思义就是复用TIME_WAIT连接。当创建新连接的时候,如果可能的话会考虑复用相应的TIME_WAIT连接。通常认为「tcp_tw_reuse」比「tcp_tw_recycle」安全一些,这是因为一来TIME_WAIT创建时间必须超过一秒才可能会被复用;二来只有连接的时间戳是递增的时候才会被复用。官方文档里是这样说的:如果从协议视角看它是安全的,那么就可以使用。这简直就是外交辞令啊!按我的看法,如果网络比较稳定,比如都是内网连接,那么就可以尝试使用。

不过需要注意的是在哪里使用,既然我们要复用连接,那么当然应该在连接的发起方使用,而不能在被连接方使用。举例来说:客户端向服务端发起HTTP请求,服务端响应后主动关闭连接,于是TIME_WAIT便留在了服务端,此类情况使用「tcp_tw_reuse」是无效的,因为服务端是被连接方,所以不存在复用连接一说。让我们延伸一点来看,比如说服务端是PHP,它查询另一个MySQL服务端,然后主动断开连接,于是TIME_WAIT就落在了PHP一侧,此类情况下使用「tcp_tw_reuse」是有效的,因为此时PHP相对于MySQL而言是客户端,它是连接的发起方,所以可以复用连接。

说明:如果使用tcp_tw_reuse,请激活tcp_timestamps,否则无效。

  • tcp_max_tw_buckets

名思义就是控制TIME_WAIT总数。官网文档说这个选项只是为了阻止一些简单的DoS攻击,平常不要人为的降低它。如果缩小了它,那么系统会将多余的TIME_WAIT删除掉,日志里会显示:「TCP: time wait bucket table overflow」。

优化配置

用vim打开配置文件:#vim /etc/sysctl.conf
在这个文件中,加入下面的几行内容:

net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_tw_recycle = 1
net.ipv4.tcp_fin_timeout = 30

输入下面的命令,让内核参数生效:#sysctl -p

简单的说明上面的参数的含义:

net.ipv4.tcp_syncookies = 1
#表示开启SYN Cookies。当出现SYN等待队列溢出时,启用cookies来处理,可防范少量SYN攻击,默认为0,表示关闭;
net.ipv4.tcp_tw_reuse = 1
#表示开启重用。允许将TIME-WAIT sockets重新用于新的TCP连接,默认为0,表示关闭;
net.ipv4.tcp_tw_recycle = 1
#表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭;
net.ipv4.tcp_fin_timeout
#修改系統默认的 TIMEOUT 时间。

在经过这样的调整之后,除了会进一步提升服务器的负载能力之外,还能够防御小流量程度的DoS、CC和SYN攻击。

此外,如果你的连接数本身就很多,我们可以再优化一下TCP的可使用端口范围,进一步提升服务器的并发能力。依然是往上面的参数文件中,加入下面这些配置:

net.ipv4.tcp_keepalive_time = 1200
net.ipv4.ip_local_port_range = 10000 65000
net.ipv4.tcp_max_syn_backlog = 8192
net.ipv4.tcp_max_tw_buckets = 5000
#这几个参数,建议只在流量非常大的服务器上开启,会有显著的效果。一般的流量小的服务器上,没有必要去设置这几个参数。

案例详解

1、问题描述:高负载下,系统响应变慢,并出现超时或失误失败情况,TIME_WAIT积压
2、问题影响:系统设置的自动回收时间为60s,但在压测中如果涉及的服务较多的情况下,比如这次以100TPS压力单测1个接口,涉及4-6个服务,每秒就会创建400+的连接,1分钟就是2.4万的连接,系统无法及时回收,压测两分钟后,新的请求过来,无法创建连接或无法及时创建连接,导致请求失败,严重时会出现整个服务器挂死(新来的请求无法创建连接)。
3、问题原因:Linux环境配置存在问题(后check所有测试环境均未配置,包括生产环境)
4、问题背景:在对A接口进行负载测试时,在一定压力下,出现了比较诡异的现象:在100TPS压力下,开始响应时间很快,都在0.3s左右,但压测不到一分钟开始出现大量事务失败,响应时间倍增,最后系统甚至出现“挂死”。
5、问题解决:增加以下,

[root@aaa1 ~]# vim /etc/sysctl.conf

net.ipv4.tcp_tw_reuse = 1

这个是重点,表示开启重用。允许将TIME-WAIT sockets重新用于新的TCP连接,默认为0,表示关闭;
问题定位及验证:

netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'
TIME_WAIT 41735
CLOSE_WAIT 145
FIN_WAIT2 3
ESTABLISHED 413

不到两分钟,time_wait积压到4万,最后导致无法创建新的连接,事务全部失败。
添加参数后,再次验证,以下是压测十分钟中的time_wait数据,一致保持在5000以下,而且由于不需要重新创建连接,直接用已存在的,减少了资源开销,120TPS比之前压测100TPS的性能要好很多。

netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'
TIME_WAIT 4500
CLOSE_WAIT 7
ESTABLISHED 642

TIME_WAIT状态(2MSL)的作用
深入浅出TCP协议的2MSL TIME_WAIT状态
https://www.cnblogs.com/wujing-hubei/p/5699773.html
http://blog.sina.com.cn/s/blog_8e5d24890102w9yi.html
https://www.cnblogs.com/qlee/archive/2011/07/12/2104089.html
性能案例-Linux下解决time_wait连接过多(Linux内核优化)
优化Linux的内核参数来提高服务器并发处理能力
)
链接:https://www.jianshu.com/p/29868fb82890
来源:简书
==============================================================================================

 

TCP建立连接为什么是三次握手,而不是两次或四次?

TCP,名为传输控制协议,是一种可靠的传输层协议,IP协议号为6。

顺便说一句,原则上任何数据传输都无法确保绝对可靠,三次握手只是确保可靠的基本需要。

举个日常例子,打电话时我们对话如下:

 

对应为客户端与服务器之间的通信:

于是有了如下对话:

我:1+1等于几?

她:2,2+2等于几?

我:4

首先两个人约定协议

1.感觉网络情况不对的时候,任何一方都可以发起询问

2.任何情况下,若发起询问后5秒还没收到回复,则认为网络不通

3.网络不通的情况下等1min路由器之后再发起询问

 

对于我而言,发起 “1+1等于几”的询问后

1. 若5s内没有收到回复,则认为网络不通

2. 若收到回复,则我确认①我能听到她的消息 ②她能听到我的消息,然后回复她的问题的答案


对于她而言,当感觉网络情况不对的时候

1. 若没有收到我的询问,则她发起询问

2. 若收到“1+1等于几”,则她确认 ①她可以听到我的消息,然后回复我的问题的答案和她的问题“2,2+2等于几”

3. 若5s内没有收到我的回复“4”,则她确认 ②我听不见她的消息

4. 若5s内收到了我的回复“4”,则她确认 ②我可以听见她的消息

这样,如果上面的对话得以完成,就证明双方都可以确认自己可以听到对方的声音,对方也可以听到自己的声音!

这个故事可以解释TCP为什么要三次握手吗 ... 囧

 

关于四次挥手

先由客户端向服务器端发送一个FIN,请求关闭数据传输。

当服务器接收到客户端的FIN时,向客户端发送一个ACK,其中ack的值等于FIN+SEQ

然后服务器向客户端发送一个FIN,告诉客户端应用程序关闭。

当客户端收到服务器端的FIN是,回复一个ACK给服务器端。其中ack的值等于FIN+SEQ

 

 

为什么要4次挥手?

确保数据能够完整传输。

当被动方收到主动方的FIN报文通知时,它仅仅表示主动方没有数据再发送给被动方了。

但未必被动方所有的数据都完整的发送给了主动方,所以被动方不会马上关闭SOCKET,它可能还需要发送一些数据给主动方后,

再发送FIN报文给主动方,告诉主动方同意关闭连接,所以这里的ACK报文和FIN报文多数情况下都是分开发送的。

 

一、TCP报文格式

  TCP报文格式图:

 

  上图中有几个字段需要重点介绍下:

  (1)序号:Seq序号,占32位,用来标识从TCP源端向目的端发送的字节流,发起方发送数据时对此进行标记。

  (2)确认序号:Ack序号,占32位,只有ACK标志位为1时,确认序号字段才有效,Ack=Seq+1。

  (3)标志位:共6个,即URG、ACK、PSH、RST、SYN、FIN等,具体含义如下:

  (A)URG:紧急指针(urgent pointer)有效。

  (B)ACK:确认序号有效。

  (C)PSH:接收方应该尽快将这个报文交给应用层。

  (D)RST:重置连接。

  (E)SYN:发起一个新连接。

  (F)FIN:释放一个连接。

 

 需要注意的是:

  (A)不要将确认序号Ack与标志位中的ACK搞混了。

  (B)确认方Ack=发起方Req+1,两端配对。 

 

二、三次握手

TCP(Transmission Control Protocol) 传输控制协议

TCP是主机对主机层的传输控制协议,提供可靠的连接服务,采用三次握手确认建立一个连接

位码即tcp标志位,有6种标示:

 

SYN(synchronous建立联机)

ACK(acknowledgement 确认)

PSH(push传送)

FIN(finish结束)

RST(reset重置)

URG(urgent紧急)

 

Sequence number(顺序号码)

Acknowledge number(确认号码) 

establish  建立,创建

 

  所谓三次握手(Three-Way Handshake)即建立TCP连接,是指建立一个TCP连接时,需要客户端和服务端总共发送3个包以确认连接的建立。在socket编程中,这一过程由客户端执行connect来触发,整个流程如下图所示:



 

 

  (1)第一次握手:Client将标志位SYN置为1,随机产生一个值seq=J,并将该数据包发送给Server,Client进入SYN_SENT状态,等待Server确认。

 

  (2)第二次握手:Server收到数据包后由标志位SYN=1知道Client请求建立连接,Server将标志位SYN和ACK都置为1,ack (number )=J+1,随机产生一个值seq=K,并将该数据包发送给Client以确认连接请求,Server进入SYN_RCVD状态

 

  (3)第三次握手:Client收到确认后,检查ack是否为J+1,ACK是否为1,如果正确则将标志位ACK置为1,ack=K+1,并将该数据包发送给Server,Server检查ack是否为K+1,ACK是否为1,如果正确则连接建立成功,Client和Server进入ESTABLISHED状态,完成三次握手,随后Client与Server之间可以开始传输数据了。

 

 

  SYN攻击

  在三次握手过程中,Server发送SYN-ACK之后,收到Client的ACK之前的TCP连接称为半连接(half-open connect),此时Server处于SYN_RCVD状态,当收到ACK后,Server转入ESTABLISHED状态。SYN攻击就是Client在短时间内伪造大量不存在的IP地址,并向Server不断地发送SYN包,Server回复确认包,并等待Client的确认,由于源地址是不存在的,因此,Server需要不断重发直至超时,这些伪造的SYN包将长时间占用未连接队列,导致正常的SYN请求因为队列满而被丢弃,从而引起网络堵塞甚至系统瘫痪。SYN攻击时一种典型的DDOS攻击,检测SYN攻击的方式非常简单,即当Server上有大量半连接状态且源IP地址是随机的,则可以断定遭到SYN攻击了,使用如下命令可以让之现行:

  #netstat -nap | grep SYN_RECV

  

 三、四次挥手

 三次握手耳熟能详,四次挥手估计就..所谓四次挥手(Four-Way Wavehand)即终止TCP连接,就是指断开一个TCP连接时,需要客户端和服务端总共发送4个包以确认连接的断开。在socket编程中,这一过程由客户端或服务端任一方执行close来触发,整个流程如下图所示:



 

  由于TCP连接时全双工的,因此,每个方向都必须要单独进行关闭,这一原则是当一方完成数据发送任务后,发送一个FIN来终止这一方向的连接,收到一个FIN只是意味着这一方向上没有数据流动了,即不会再收到数据了,但是在这个TCP连接上仍然能够发送数据,直到这一方向也发送了FIN。首先进行关闭的一方将执行主动关闭,而另一方则执行被动关闭,上图描述的即是如此。

 (1)第一次挥手:Client发送一个FIN,用来关闭Client到Server的数据传送,Client进入FIN_WAIT_1状态。

  (2)第二次挥手:Server收到FIN后,发送一个ACK给Client,确认序号为收到序号+1(与SYN相同,一个FIN占用一个序号),Server进入CLOSE_WAIT状态。

 (3)第三次挥手:Server发送一个FIN,用来关闭Server到Client的数据传送,Server进入LAST_ACK状态。

  (4)第四次挥手:Client收到FIN后,Client进入TIME_WAIT状态,接着发送一个ACK给Server,确认序号为收到序号+1,Server进入CLOSED状态,完成四次挥手。



 

 

  上面是一方主动关闭,另一方被动关闭的情况,实际中还会出现同时发起主动关闭的情况,具体流程如下图:



  

  流程和状态在上图中已经很明了了,在此不再赘述,可以参考前面的四次挥手解析步骤。

 

四、附注

  关于三次握手与四次挥手通常都会有典型的面试题,在此提出供有需求的XDJM们参考:

  (1)三次握手是什么或者流程?四次握手呢?答案前面分析就是。

  (2)为什么建立连接是三次握手,而关闭连接却是四次挥手呢?

 

  这是因为服务端在LISTEN状态下,收到建立连接请求的SYN报文后,把ACK和SYN放在一个报文里发送给客户端。而关闭连接时,当收到对方的FIN报文时,仅仅表示对方不再发送数据了但是还能接收数据,己方也未必全部数据都发送给对方了,所以己方可以立即close,也可以发送一些数据给对方后,再发送FIN报文给对方来表示同意现在关闭连接,因此,己方ACK和FIN一般都会分开发送。

 

===============================================================================================

TCP/IP协议分层

![TCP分层2.jpg](//upload-images.jianshu.io/upload_images/2964446-94da7e7442050d15.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

TCP/IP协议族按照层次由上到下,层层包装。

应用层:
向用户提供一组常用的应用程序,比如电子邮件、文件传输访问、远程登录等。远程登录TELNET使用TELNET协议提供在网络其它主机上注册的接口。TELNET会话提供了基于字符的虚拟终端。文件传输访问FTP使用FTP协议来提供网络内机器间的文件拷贝功能。

传输层:
提供应用程序间的通信。其功能包括:一、格式化信息流;二、提供可靠传输。为实现后者,传输层协议规定接收端必须发回确认,并且假如分组丢失,必须重新发送。

网络层
负责相邻计算机之间的通信。其功能包括三方面。
一、处理来自传输层的分组发送请求,收到请求后,将分组装入IP数据报,填充报头,选择去往信宿机的路径,然后将数据报发往适当的网络接口。

二、处理输入数据报:首先检查其合法性,然后进行寻径--假如该数据报已到达信宿机,则去掉报头,将剩下部分交给适当的传输协议;假如该数据报尚未到达信宿,则转发该数据报。

三、处理路径、流控、拥塞等问题。

网络接口层
这是TCP/IP软件的最低层,负责接收IP数据报并通过网络发送之,或者从网络上接收物理帧,抽出IP数据报,交给IP层。

IP 是无连接的

IP 用于计算机之间的通信。

IP 是无连接的通信协议。它不会占用两个正在通信的计算机之间的通信线路。这样,IP 就降低了对网络线路的需求。每条线可以同时满足许多不同的计算机之间的通信需要。

通过 IP,消息(或者其他数据)被分割为小的独立的包,并通过因特网在计算机之间传送。

IP 负责将每个包路由至它的目的地。

IP地址

每个计算机必须有一个 IP 地址才能够连入因特网。

每个 IP 包必须有一个地址才能够发送到另一台计算机。

网络上每一个节点都必须有一个独立的Internet地址(也叫做IP地址)。现在,通常使用的IP地址是一个32bit的数字,也就是我们常说的IPv4标准,这32bit的数字分成四组,也就是常见的255.255.255.255的样式。IPv4标准上,地址被分为五类,我们常用的是B类地址。具体的分类请参考其他文档。需要注意的是IP地址是网络号+主机号的组合,这非常重要。

CP/IP 使用 32 个比特来编址。一个计算机字节是 8 比特。所以 TCP/IP 使用了 4 个字节。
一个计算机字节可以包含 256 个不同的值:
00000000、00000001、00000010、00000011、00000100、00000101、00000110、00000111、00001000 ....... 直到 11111111。
现在,你知道了为什么 TCP/IP 地址是介于 0 到 255 之间的 4 个数字。

TCP 使用固定的连接

TCP 用于应用程序之间的通信。

当应用程序希望通过 TCP 与另一个应用程序通信时,它会发送一个通信请求。这个请求必须被送到一个确切的地址。在双方“握手”之后,TCP 将在两个应用程序之间建立一个全双工 (full-duplex) 的通信。

这个全双工的通信将占用两个计算机之间的通信线路,直到它被一方或双方关闭为止。

UDP 和 TCP 很相似,但是更简单,同时可靠性低于 TCP。

IP 路由器

当一个 IP 包从一台计算机被发送,它会到达一个 IP 路由器。

IP 路由器负责将这个包路由至它的目的地,直接地或者通过其他的路由器。

在一个相同的通信中,一个包所经由的路径可能会和其他的包不同。而路由器负责根据通信量、网络中的错误或者其他参数来进行正确地寻址。

域名

12 个阿拉伯数字很难记忆。使用一个名称更容易。

用于 TCP/IP 地址的名字被称为域名。w3school.com.cn 就是一个域名。

当你键入一个像 http://www.w3school.com.cn 这样的域名,域名会被一种 DNS 程序翻译为数字。

在全世界,数量庞大的 DNS 服务器被连入因特网。DNS 服务器负责将域名翻译为 TCP/IP 地址,同时负责使用新的域名信息更新彼此的系统。

当一个新的域名连同其 TCP/IP 地址一同注册后,全世界的 DNS 服务器都会对此信息进行更新。

TCP/IP

TCP/IP 意味着 TCP 和 IP 在一起协同工作。

TCP 负责应用软件(比如你的浏览器)和网络软件之间的通信。

IP 负责计算机之间的通信。

TCP 负责将数据分割并装入 IP 包,然后在它们到达的时候重新组合它们。

IP 负责将包发送至接受者。

TCP报文格式

TCP报文格式1.jpg

16位源端口号:16位的源端口中包含初始化通信的端口。源端口和源IP地址的作用是标识报文的返回地址。

16位目的端口号:16位的目的端口域定义传输的目的。这个端口指明报文接收计算机上的应用程序地址接口。

32位序号:32位的序列号由接收端计算机使用,重新分段的报文成最初形式。当SYN出现,序列码实际上是初始序列码(Initial Sequence Number,ISN),而第一个数据字节是ISN+1。这个序列号(序列码)可用来补偿传输中的不一致。

32位确认序号:32位的序列号由接收端计算机使用,重组分段的报文成最初形式。如果设置了ACK控制位,这个值表示一个准备接收的包的序列码。

4位首部长度:4位包括TCP头大小,指示何处数据开始。

保留(6位):6位值域,这些位必须是0。为了将来定义新的用途而保留。

标志:6位标志域。表示为:紧急标志、有意义的应答标志、推、重置连接标志、同步序列号标志、完成发送数据标志。按照顺序排列是:URG、ACK、PSH、RST、SYN、FIN。

16位窗口大小:用来表示想收到的每个TCP数据段的大小。TCP的流量控制由连接的每一端通过声明的窗口大小来提供。窗口大小为字节数,起始于确认序号字段指明的值,这个值是接收端正期望接收的字节。窗口大小是一个16字节字段,因而窗口大小最大为65535字节。

16位校验和:16位TCP头。源机器基于数据内容计算一个数值,收信息机要与源机器数值 结果完全一样,从而证明数据的有效性。检验和覆盖了整个的TCP报文段:这是一个强制性的字段,一定是由发送端计算和存储,并由接收端进行验证的。

16位紧急指针:指向后面是优先数据的字节,在URG标志设置了时才有效。如果URG标志没有被设置,紧急域作为填充。加快处理标示为紧急的数据段。

选项:长度不定,但长度必须为1个字节。如果没有选项就表示这个1字节的域等于0。

数据:该TCP协议包负载的数据。

在上述字段中,6位标志域的各个选项功能如下。

URG:紧急标志。紧急标志为"1"表明该位有效。

ACK:确认标志。表明确认编号栏有效。大多数情况下该标志位是置位的。TCP报头内的确认编号栏内包含的确认编号(w+1)为下一个预期的序列编号,同时提示远端系统已经成功接收所有数据。

PSH:推标志。该标志置位时,接收端不将该数据进行队列处理,而是尽可能快地将数据转由应用处理。在处理Telnet或rlogin等交互模式的连接时,该标志总是置位的。

RST:复位标志。用于复位相应的TCP连接。

SYN:同步标志。表明同步序列编号栏有效。该标志仅在三次握手建立TCP连接时有效。它提示TCP连接的服务端检查序列编号,该序列编号为TCP连接初始端(一般是客户端)的初始序列编号。在这里,可以把TCP序列编号看作是一个范围从0到4,294,967,295的32位计数器。通过TCP连接交换的数据中每一个字节都经过序列编号。在TCP报头中的序列编号栏包括了TCP分段中第一个字节的序列编号。

FIN:结束标志。

TCP三次握手

所谓三次握手(Three-Way Handshake)即建立TCP连接,就是指建立一个TCP连接时,需要客户端和服务端总共发送3个包以确认连接的建立。在socket编程中,这一过程由客户端执行connect来触发,整个流程如下图所示:

TCP三次握手.png

(1)第一次握手:Client将标志位SYN置为1,随机产生一个值seq=J,并将该数据包发送给Server,Client进入SYN_SENT状态,等待Server确认。

(2)第二次握手:Server收到数据包后由标志位SYN=1知道Client请求建立连接,Server将标志位SYN和ACK都置为1,ack=J+1,随机产生一个值seq=K,并将该数据包发送给Client以确认连接请求,Server进入SYN_RCVD状态。

(3)第三次握手:Client收到确认后,检查ack是否为J+1,ACK是否为1,如果正确则将标志位ACK置为1,ack=K+1,并将该数据包发送给Server,Server检查ack是否为K+1,ACK是否为1,如果正确则连接建立成功,Client和Server进入ESTABLISHED状态,完成三次握手,随后Client与Server之间可以开始传输数据了。

简单来说,就是

1、建立连接时,客户端发送SYN包(SYN=i)到服务器,并进入到SYN-SEND状态,等待服务器确认

2、服务器收到SYN包,必须确认客户的SYN(ack=i+1),同时自己也发送一个SYN包(SYN=k),即SYN+ACK包,此时服务器进入SYN-RECV状态

3、客户端收到服务器的SYN+ACK包,向服务器发送确认报ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手,客户端与服务器开始传送数据。

SYN攻击

在三次握手过程中,Server发送SYN-ACK之后,收到Client的ACK之前的TCP连接称为半连接(half-open connect),此时Server处于SYN_RCVD状态,当收到ACK后,Server转入ESTABLISHED状态。SYN攻击就是Client在短时间内伪造大量不存在的IP地址,并向Server不断地发送SYN包,Server回复确认包,并等待Client的确认,由于源地址是不存在的,因此,Server需要不断重发直至超时,这些伪造的SYN包将产时间占用未连接队列,导致正常的SYN请求因为队列满而被丢弃,从而引起网络堵塞甚至系统瘫痪。SYN攻击时一种典型的DDOS攻击,检测SYN攻击的方式非常简单,即当Server上有大量半连接状态且源IP地址是随机的,则可以断定遭到SYN攻击了,使用如下命令可以让之现行:

#netstat -nap | grep SYN_RECV

TCP四次挥手

所谓四次挥手(Four-Way Wavehand)即终止TCP连接,就是指断开一个TCP连接时,需要客户端和服务端总共发送4个包以确认连接的断开。在socket编程中,这一过程由客户端或服务端任一方执行close来触发,整个流程如下图所示:

TCP四次挥手.png

由于TCP连接时全双工的,因此,每个方向都必须要单独进行关闭,这一原则是当一方完成数据发送任务后,发送一个FIN来终止这一方向的连接,收到一个FIN只是意味着这一方向上没有数据流动了,即不会再收到数据了,但是在这个TCP连接上仍然能够发送数据,直到这一方向也发送了FIN。首先进行关闭的一方将执行主动关闭,而另一方则执行被动关闭,上图描述的即是如此。

(1)第一次挥手:Client发送一个FIN,用来关闭Client到Server的数据传送,Client进入FIN_WAIT_1状态。

(2)第二次挥手:Server收到FIN后,发送一个ACK给Client,确认序号为收到序号+1(与SYN相同,一个FIN占用一个序号),Server进入CLOSE_WAIT状态。

(3)第三次挥手:Server发送一个FIN,用来关闭Server到Client的数据传送,Server进入LAST_ACK状态。

(4)第四次挥手:Client收到FIN后,Client进入TIME_WAIT状态,接着发送一个ACK给Server,确认序号为收到序号+1,Server进入CLOSED状态,完成四次挥手。

为什么建立连接是三次握手,而关闭连接却是四次挥手呢?

这是因为服务端在LISTEN状态下,收到建立连接请求的SYN报文后,把ACK和SYN放在一个报文里发送给客户端。而关闭连接时,当收到对方的FIN报文时,仅仅表示对方不再发送数据了但是还能接收数据,己方也未必全部数据都发送给对方了,所以己方可以立即close,也可以发送一些数据给对方后,再发送FIN报文给对方来表示同意现在关闭连接,因此,己方ACK和FIN一般都会分开发送。

为什么TIME_WAIT状态需要经过2MSL(最大报文段生存时间)才能返回到CLOSE状态?

原因有二:
一、保证TCP协议的全双工连接能够可靠关闭
二、保证这次连接的重复数据段从网络中消失

先说第一点,如果Client直接CLOSED了,那么由于IP协议的不可靠性或者是其它网络原因,导致Server没有收到Client最后回复的ACK。那么Server就会在超时之后继续发送FIN,此时由于Client已经CLOSED了,就找不到与重发的FIN对应的连接,最后Server就会收到RST而不是ACK,Server就会以为是连接错误把问题报告给高层。这样的情况虽然不会造成数据丢失,但是却导致TCP协议不符合可靠连接的要求。所以,Client不是直接进入CLOSED,而是要保持TIME_WAIT,当再次收到FIN的时候,能够保证对方收到ACK,最后正确的关闭连接。

再说第二点,如果Client直接CLOSED,然后又再向Server发起一个新连接,我们不能保证这个新连接与刚关闭的连接的端口号是不同的。也就是说有可能新连接和老连接的端口号是相同的。一般来说不会发生什么问题,但是还是有特殊情况出现:假设新连接和已经关闭的老连接端口号是一样的,如果前一次连接的某些数据仍然滞留在网络中,这些延迟数据在建立新连接之后才到达Server,由于新连接和老连接的端口号是一样的,又因为TCP协议判断不同连接的依据是socket pair,于是,TCP协议就认为那个延迟的数据是属于新连接的,这样就和真正的新连接的数据包发生混淆了。所以TCP连接还要在TIME_WAIT状态等待2倍MSL,这样可以保证本次连接的所有数据都从网络中消失。

=================================================================================================

 

TCP/IP协议簇 端口 三次握手 四次挥手 11种状态集

第一章:概念介绍

1.1 VLAN

1.1.1 什么是VLAN

  VLAN (Virturl LAN) ,翻译成中文是:“虚拟局域网”。VLAN可以是由少数几台家用计算机构成的网络,也可以是数以百计的计算机构成的企业网络。VLAN所指的LAN特指使用路由器分割的网络----也就是广播域。

1.1.2 划分VLAN的目的

1,提高安全性

 a) 举个例子:没有划分VLAN前,交换机端口连接下的所有PC都处于一个VLAN中即一个广播域中,实现ARP中间人攻击太简单了,划分VLAN之后,缩小了VLAN的攻击范围,ARP报文是一个2.5层的报文,只能在同一个VLAN中广播。

2,提高性能

 a) 不划分VLAN,整个交换机都处于一个广播域,随便一个PC机发的广播报文都能转播整个广播域,战用了很大带宽,划分了VLAN,缩小了广播域的大小,缩小了广播报文能够到达的范围。

 

1.3 TCP/IP协议簇

1.3.1组成

TCP/IP协议簇是Internet基础,也是当今最流行的组网形式,包括许多别的协议,组成了TCP/IP协议簇,其中比较重要的有SLIP协议,PPP协议,IP协议,ICMP协议,ARP协议,TCP协议,UDP协议,FTP协议,DNS协议,SMTP协议等

TCP/IP协议簇中相关的协议

 

 

1.3.3 协议名称与对应端口号

1.3.4 端口号

标识协议信息,便于计算机之间进行识别

0号端口在tcp和udp中都不会使用

1.4 TCP和UDP

TCP(传输控制协议)面向连接的网络协议

UDP(用户报文协议)面向无连接的网络协议

1.4.4  TCP/UD协议的对比

1.5 TCP报文结构

 

 1.5.1 确认字段ACK

当ACK=1时表示确认,且确认号有效;当ACK=0时 确认字段无效

1.5.2 请求连接字段SYN

当SYN=1时,表示发起一个连接请求

1.5.3 断开连接字段 FIN 

用来释放连接。当FIN=1时,表明此报文段的发送端的数据已发送完成,并要求释放连接。

1.5.4 序号

  每一个TCP报文段都会有一个序号,序号字段的值其实是本报文段所发送的数据的第一字节的序号。

 这是因为TCP是面向连接的可靠服务,其每一个字节都会对应一个序号,通过序号来确保服务的可靠性和有序性。

1.5.5 确认号

确认号,是期望收到对方的下一个报文段的数据的第一个字节的序号。

1.5.5.1 序号和确认号

发送端发送一个seq序列号X,接受端需要回应一个序列号 X+1 ,并发送一个序列号Y,

发送端接受一个序列号y ,接受端需要回应一共序列号y+1

第二章:三次握手和四次挥手

2.1 三次握手

 

2.1.1 含义

(1)客户端发送一个带SYN标志的TCP报文到服务器。

(2)服务器端回应客户端的,这个报文同时带ACK标志和SYN标志。因此它表示对刚才客户端SYN报文的回应;同时又标志SYN给客户端,询问客户端是否准备好进行数据通讯。

(3)客户必须再次回应服务端一个ACK报文。

2.1.2 五种状态

 

2.2 四次挥手

2.2.1文字说明

(1)TCP客户端发送一个FIN,用来关闭客户到服务器的数据转送。

(2)服务器收到一个FIN,它发回一个ACK,确认序号为收到的序号加1 。和SYN一样,一个FIN将占用一个序号。

(3)服务器关闭客户端的连接,发送一个FIN给客户端。

(4)客户端发回ACK报文确认,并将确认序号设置为收到序号加1

2.2.2 一形态

 

 

 

 

 

2.2.4 第二种形态(CLOSING)

 

2.3 十一种状态集

 

 

2.4 随机端口号范围(/proc/sys/net/ipv4/ip_local_port_range 文件)

[root@clsn ~]# cat /proc/sys/net/ipv4/ip_local_port_range
32768   60999

2.5 socket  监听

2.5.1 监听状态

[root@clsn ~]# netstat -lntup|grep sshd
tcp        0      0 0.0.0.0:22                  0.0.0.0:*                   LISTEN      1140/sshd          
tcp        0      0 :::22                       :::*                        LISTEN      1140/sshd 
 
[root@clsn ~]# netstat -an|grep ES
tcp        0     64 10.0.0.201:22               10.0.0.1:3868               ESTABLISHED

2.5.2 socket 条目 五元组概念

1、协议 tcp udp

2、目标ip地址

3、目标主机端口号

4、源ip地址

5、源端口号

2.6 ssh服务关闭 为什么连接不断

[root@clsn ~]# ps -ef |grep sshd
root       1140      1  0 Sep25 ?        00:00:00 /usr/sbin/sshd
root       5861   1140  0 11:40 ?        00:00:00 sshd: root@pts/0
root       5881   5863  0 11:40 pts/0    00:00:00 grep --color=auto sshd
 
[root@clsn ~]# /etc/init.d/sshd stop
Stopping sshd:                                             [  OK  ]
 
[root@clsn ~]# ps -ef |grep sshd
root       5861      1  0 11:40 ?        00:00:00 sshd: root@pts/0
root       5899   5863  0 11:41 pts/0    00:00:00 grep --color=auto sshd

因为有一个连接的进程存在。

没有进程就会断开。

==========================================================================================

 

TCP连接的建立。TCP连接的建立有主动打开,被动打开以及同时打开三种情况。

三次握手比较清楚,要强调的是ISN,就是初始序列号的选择问题,序列号是32位的,针对不同的OS,初始序列号的选择往往也是有规律的。

 

TCP传输的最大报文长度也是在三次握手中协商的。具体说是在也仅在SYN报文中协商的。MSS = MTU - ip_header_len - tcp_header_len。MSS这里也是为了防止分片,提高网络带宽利用率。

 

TCP三次握手中,最后一个报文ACK,不需要再有额外的确认机制,如果这个ACK在网络中丢弃了,TCP协议栈也有其他的机制来处理。

除了三次握手,还有一种很特殊的应用情况,就是TCP两端同时打开的情况(发送syn),这种情况没有描述在上面的状态机中。

举例子来说,A通过源端口7777发起到B的目的端口8888的连接的同时,B也通过源端口8888发起对A的目的端口7777的TCP连接。

 

b. TCP连接的关闭

TCP连接的关闭也有主动关闭,被动关闭和同时关闭三种情况,这三种情况在上面的TCP状态机中都有描述。

TCP连接的关闭需要报文四次交互,因为TCP是一个全双工的服务,所以每个方向的连接都关闭后,TCP的连接才是完整的拆除。

状态机中,主动关闭和同时关闭最后都会进入到一个TIME_WAITE状态。针对TCP主动关闭的最后一个报文应该是ACK,确认对端的FIN报文。这个状态的概念是该TCP连接的资源并没有完全释放,因为还要确保最后一个ACK报文能够无误的到达对端,确认对端的FIN,否则就仍然要重传ACK。

这个等待的过程(或者资源没有完全释放的过程)需要等待2MSL时间(考虑报文一次往返)。MSL是最大报文生存时间,RFC793中为2分钟,根据不同的TCP实现,一般是30s或者1分钟。

 

所以在TIME_WAITE状态内,该TCP连接所使用的端口和连接资源,不能被继续使用。但是很多TCP实现并没有这个限制,只要新的TCP连接所使用的ISN大于TIME_WAITE状态TCP连接所使用的最后序号即可。实现中往往使用

new ISN = latest ISN in time_waite + 128000

 

 

IP报文的最大生存时间是TTL值,TCP报文的最大生存时间是MSL,二层上没有报文最大生存时间的概念,存在风暴的可能。

 

 

IP报文的最大生存时间是TTL值,TCP报文的最大生存时间是MSL,二层上没有报文最大生存时间的概念,存在风暴的可能。

 

(3) TCP的滑动窗和定时器

a. TCP的报文确认机制。

TCP使用的是滑动窗口机制来发送数据流,所以TCP协议允许连续发送多个TCP分组而不等待对端的确认。所以发送的分组数据和确认不是一对一的关系。

TCP中,对数据的确认往往是延迟的,一般情况是两个TCP数据对应一个确认,在时延定时器没有溢出的情况下。如果时延定时器溢出了,那么自然也会发送确认报文。

但是,针对存在交互大量微小报文的TCP应用,过于频繁的确认会导致网络利用率的低效,所以TCP支持一种Nagle算法。

 

b. 延时定时器

当TCP收到报文时候,启动延时定时器,比如200ms。

 

c. Nagle算法

TCP连接上只能存在一个未被确认的微小报文(41字节的TCP报文),在该确认到达前,TCP仅仅收集微小报文,当确认到达后,以一个分组的形式发出去。

当然,某些应用需要关闭Nagle算法。

 

d. 滑动窗口机制

窗口合拢(左移):在收到对端数据后,自己确认了数据的正确性,这些数据会被存储到缓冲区,等待应用程序获取。但这时候因为已经确认了数据的正确性,需要向对方发送确认响应ACK,又因为这些数据还没有被应用进程取走,这时候便需要进行窗口合拢,缓冲区的窗口左边缘向右滑动。注意响应的ACK序号是对方发送数据包的序号,一个对方发送的序号,可能因为窗口张开会被响应(ACK)多次。

 

窗口张开(右移):窗口收缩后,应用进程一旦从缓冲区中取出数据,TCP的滑动窗口需要进行扩张,这时候窗口的右边缘向右扩张,实际上窗口这是一个环形缓冲区,窗口的右边缘扩张会使用原来被应用进程取走内容的缓冲区。在窗口进行扩张后,需要使用ACK通知对端,这时候ACK的序号依然是上次确认收到包的序号。

 

窗口收缩,窗口的右边缘向左滑动,称为窗口收缩,Host Requirement RFC强烈建议不要这样做,但TCP必须能够在某一端产生这种情况时进行处理。

 

e. 重传定时器

目的是为了获得对端的确认报文。如果多次重传仍然没有获得确认,则会发送复位报文RST。

 

这里我们再来看一下TCP的三次握手。

A(发起端) ---> syn ---> B(服务器)

A(发起端) <--- syn/ack <--- B(服务器)

A(发起端) ---> ack    ?   B(服务器)

如果TCP客户端A的最后一个ACK丢失了,TCP服务器B没有收到,会是一种什么情况?

这个时候A已经进入到了Establish状态,然而B还只是Syn_Recev状态,所以服务器会重传syn/ack报文,只到连接的最终建立。但是客户端A已经到建立状态了,所以A是有可能发送TCP数据给服务器B的。

所以TCP的两端,最终状态机是有可能不一致的。

 

后面会详细讲述重传和拥塞控制机制。

 

f. 坚持定时器

由于TCP没有对ACK的确认机制,所以当接收端窗口从0恢复到一定值的时候,如果接收端发给发送端的ACK报文(标识窗口大小)丢失了,发送端就永远不知道接收端的窗口恢复情况了。

所以发送端会定时发送带一个字节的ACK给接收端,查看接收端的确认报文中的窗口信息。

 

g. 保活定时器

由于物理原因,处于IDLE状态的TCP连接一端崩溃的时候,TCP有保活机制来判断对端是否仍然工作。这个设计存在争议,也许应用层应该实现该功能。RFC1122中有描述,保活定时器默认是关闭的。下面截取了一些RFC描述。

Implementors MAY include "keep-alives" in their TCP implementations, although this practice is not universally accepted.  If keep-alives are included, the application MUST be able to turn them on or off for each TCP connection, and they MUST default to off.

 

 

(4) TCP拥塞控制算法:慢启动、拥塞避免、快速重传和快速恢复

针对拥塞控制,主要有四种模型,即TCP TAHOE,TCP RENO,TCP NEWRENO和TCP SACK。TCP TAHOE模型是最早的TCP协议之一,它由Jacobson提出。

 

Jacobson观察到,TCP报文段(TCP Segment)丢失有两种原因,其一是报文段损坏,其二是网络阻塞,而当时的网络主要是有线网络,不易出现报文段损坏的情况,网络阻塞为报文段丢失的主要原因。针对这种情况,TCP TAHOE对原有协议进行了性能优化,其特点是,在正常情况下,通过重传计时器是否超时和是否收到重复确认信息(dupack)这两种丢包监测机制来判断是否发生丢包,以启动拥塞控制策略;在拥塞控制的情况下,采用慢速启动(Slow Start)算法和“拥塞避免”(Congestion Avoidance)算法来控制传输速率。 1990年出现的TCP Reno版本增加了“快速重传 ”(Fast Retransmit)、“快速恢复”(Fast Recovery)算法,避免了网络拥塞不严重时采用“慢启动”算法而造成过度减小发送窗口尺寸的现象,这样TCP的拥塞控制就主要由这4个核心算法组成。

a. 超时与重传

RTT的计算与RTO的计算

 

b. 慢启动和拥塞避免算法

慢启动算法的目的是为了保证TCP发送方发送分组的速率应该匹配收到该分组确认报文的速率,这样的设计能够应用于低速链路的广域网应用。为了实现慢启动机制,为TCP连接增加了一个新的窗口,拥塞窗口cwnd,该窗口初始化为一个报文段(非一个字节,而是一个TCP最大传输报文段大小,MSS)。这样一个方向上的TCP连接有两个窗口,一个是接收窗口用于接收方的流量控制,一个是拥塞窗口用于发送方的流量控制。发送方以这两个窗口中的小值作为方式上限。

 

慢启动算法:指数算法,cwnd默认为1,当收到一个ack确认时候,cwnd增加为2,当收到两个ack确认时候,cwnd增加为4,接着8,...

 

拥塞避免算法的目的,是为了防止中间路由器由于网络拥塞引起的数据包超时或者丢包。拥塞避免算法需要用到两个变量,一个是cwnd窗口大小,一个是ssthresh慢启动阈值,对于一个给定的初始连接,cwnd为1,ssthresh为65535。

当拥塞发生(超时或者重复确认),当拥塞发生时候,ssthresh被设置为cwnd和接收窗口中小值的一半,如果是超时引起的拥塞,则cwnd设置为1。

 

拥塞避免算法:如果cwnd大于ssthresh,每收到一个数据报文的确认,cwnd=cwnd+1/cwnd,cwnd窗口大小单位仍然是mss。

 

拥塞避免算法其实是和慢启动配合使用的。cwnd和ssthresh都是动态的值,虽然初始值为1和65535。

当真正拥塞发生的时候,如果是超时或重复ack引起的拥塞,ssthreash会置为cwnd和接收窗口大小的一半,cwnd会降为1,然后执行慢启动算法,直到cwnd大于ssthresh的时候,执行拥塞避免算法;

在慢启动算法期间和拥塞避免算法期间,TCP的发送速率都是在增长的,只是一个是指数增长方式,一个是线性增长方式。

 

 

c . 快速重传和快速恢复算法

TCP连接中有两种情况会引起重复的ack,一种是乱序报文,一种是丢包。

 

快速重传:当发送方收到三个重复的ack后,不会进入慢启动状态,而是立刻重传丢失的报文。因为只有接收方收到新的报文段的时候,才会发送重复的ack,这表明TCP连接上仍然有数据流动,所以应该避免使用慢启动降速。

 

快速恢复:

第一步,当收到第三个重复的ack的时候,ssthresh设置为当前cwnd的一半,重传丢失的报文。设置cwnd为ssthresh加上3倍的报文段大小(cwnd=cwnd/2 + 3)。

第二步,每收到一个重复的ack,cwnd增加1并发送一个分组。

第三步,当下一个确认新数据的ack到达的时候,设置cwnd为上面第一步中ssthresh值,这个ack应该是对重传报文的确认,同时也是对丢包后面的中间报文的确认。

 

最后,在收到三个重复ack的情况下,速度减半。

 

快速重传算法首次出现在4.3BSD的Tahoe版本,快速恢复首次出现在4.3BSD的Reno版本,也称之为Reno版的TCP拥塞控制算法。

 

可以看出Reno的快速重传算法是针对一个包的重传情况的,然而在实际中,一个重传超时可能导致许多的数据包的重传,因此当多个数据包从一个数据窗口中丢失时并且触发快速重传和快速恢复算法时,问题就产生了。因此NewReno出现了,它在Reno快速恢复的基础上稍加了修改,可以恢复一个窗口内多个包丢失的情况。具体来讲就是:Reno在收到一个新的数据的ACK时就退出了快速恢复状态了,而NewReno需要收到该窗口内所有数据包的确认后才会退出快速恢复状态,从而更一步提高吞吐量。

 

SACK就是改变TCP的确认机制,最初的TCP只确认当前已连续收到的数据,SACK则把乱序等信息会全部告诉对方,从而减少数据发送方重传的盲目性。比如说序号1,2,3,5,7的数据收到了,那么普通的ACK只会确认序列号4,而SACK会把当前的5,7已经收到的信息在SACK选项里面告知对端,从而提高性能,当使用SACK的时候,NewReno算法可以不使用,因为SACK本身携带的信息就可以使得发送方有足够的信息来知道需要重传哪些包,而不需要重传哪些包。

 

(5) TCP的应用

新的防火墙限速实现方案就用到了TCP窗口机制. 作所周知, QoS除了分类,测速,队列还有调度一类的借助硬件的算法以外,在基于缓存或者丢包的限速基础上,最好还要降低TCP端到端的真正发送的速率,否则容易引起TCP的一系列拥塞控制动作。我们软件新的设计,就是通过修改ACK方向的通告窗口大小,来控制发送发的速率,能够在限速的基础上,同时降低发送方的发送速率。

 

================================================================================================

TCP超时重传、快速重传、慢启动、拥塞避免、拥塞发生、快速恢复、滑动窗口之间的关系概括

 

1.滑动窗口

接收端使用的流量控制。当接收端的应用程序从TCP缓存中提取数据速度过慢,导致TCP缓存中还有未被提取的数据,由于TCP缓存大小是固定的,则接收端下次能接收的数据量就变小了。

2.糊涂窗口综合症

当滑动窗口变为0的时候,则发送端不能再继续发送任何数据。若之后应用程序从TCP缓存中提取了几个字节的数据,则接收端会向发送端通告窗口更新了,你可以发数据过来了。但窗口只是更新了几个字节,而IP首部+TCP首部就有40字节,为了发这几个字节过来,会造成大量的带宽浪费。所以,可以在发送端和接收端采取一定措施,避免糊涂窗口综合征。

3.超时重传

当发送端发送数据,发生丢包时,则丢掉的包的ACK一直不会返回。此时发送端就一直等那个ACK返回,若超时,则重传该数据包。对于超时时间RTO,有很多复杂的算法。RTO的选择很重要,选短了,可能只是返回时间长但并未丢包,却当做丢包。选长了,迟迟不发丢的包也是个问题。

4.快速重传

如3所示,丢包了的处理方法不仅只有超时重传,还有快速重传。机制是:丢包了,接收端重复发送丢包前的ACK,发送端每发送一个包过来,接收端就发相同的ACK回去,这个ACK是对丢包之前的确认。当接收端连续收到3个相同的ACK,它就知道发生丢包了,根据ACK序号就能重发丢的包。

5.慢启动

慢启动的意思是,刚刚加入网络的连接,一点一点地提速,不要一上来就像那些特权车一样霸道地把路占满。

慢启动的算法如下(cwnd全称Congestion Window):

1)连接建好的开始先初始化cwnd = 1,表明可以传一个MSS大小的数据。

2)每当收到一个ACK,cwnd++; 呈线性上升

3)每当过了一个RTT,cwnd = cwnd*2; 呈指数让升

4)还有一个ssthresh(slow start threshold),是一个上限,当cwnd >= ssthresh时,就会进入“拥塞避免算法”(后面会说这个算法)

6.拥塞避免
当cwnd >= ssthresh时,就会进入“拥塞避免算法”。一般来说ssthresh的值是65535,单位是字节,当cwnd达到这个值时后,算法如下:

1)收到一个ACK时,cwnd = cwnd + 1/cwnd

2)当每过一个RTT时,cwnd = cwnd + 1

这样就可以避免增长过快导致网络拥塞,慢慢的增加调整到网络的最佳值。很明显,是一个线性上升的算法。

7.拥塞发生
当丢包了,就意味着网络发生拥挤了,路由器缓存不够缓存包便丢弃。因此,丢包意味着拥塞发生。前面讲过丢包后,有两种不同的处理方法。而针对这两种不同的处理方法,会有不同的拥塞发生时的控制方法。

1)等到RTO超时,重传数据包。TCP认为这种情况太糟糕,反应也很强烈。

sshthresh =  cwnd /2
cwnd 重置为 1
进入慢启动过程
2)快速恢复算法,也就是在收到3个重复 ACK时就开启重传,而不用等到RTO超时。

TCP Reno的实现是:
ssthresh = cwnd /2
进入快速恢复算法——Fast Recovery

8.快速恢复算法

cwnd = ssthresh  + 3 * MSS (3的意思是确认有3个数据包被收到了)
重传Duplicated ACKs指定的数据包
因为重传丢掉的包到收到它的确认直接有个时间段,而这个时间段内接收端还是会发送重复的ACK,如果再收到 重复Acks,那么cwnd = cwnd +1
如果收到了新的Ack,那么,cwnd = ssthresh ,然后就进入了拥塞避免的算法了。

所以:

先慢启动,达到cwnd=ssthresh时,进入拥塞避免。

如果发生丢包,进入拥塞发生。因为丢包有两种不同处理办法,所以拥塞发生也有两种不同的处理办法。

1.计算ssthresh,cwnd,重新进入慢启动,再等到cwnd=ssthresh时,再次进入拥塞避免。

2.计算ssthresh,cwnd,快速重传,再等到cwnd=ssthresh时,再次进入拥塞避免。(步骤2就称为快速恢复)
————————————————---------------------------------------------------------------------------------------------------

tcp 慢启动,拥塞避免,快速重传,快速恢复

      

为了防止网络的拥塞现象,TCP提出了一系列的拥塞控制机制。最初由V. Jacobson在1988年的论文中提出的TCP的拥塞控制由“慢启动(Slow start)”和“拥塞避免(Congestion avoidance)”组成,后来TCP Reno版本中又针对性的加入了“快速重传(Fast retransmit)”、“快速恢复(Fast Recovery)”算法,再后来在TCP NewReno中又对“快速恢复”算法进行了改进,近些年又出现了选择性应答( selective acknowledgement,SACK)算法,还有其他方面的大大小小的改进,成为网络研究的一个热点。

       TCP的拥塞控制主要原理依赖于一个拥塞窗口(cwnd)来控制,在之前我们还讨论过TCP还有一个对端通告的接收窗口(rwnd)用于流量控制窗口值(rwnd)的大小就代表能够发送出去的但还没有收到ACK的最大数据报文段,显然窗口越大那么数据发送的速度也就越快,但是也有越可能使得网络出现拥塞,如果窗口值为1,那么就简化为一个停等协议,每发送一个数据,都要等到对方的确认才能发送第二个数据包,显然数据传输效率低下。TCP的拥塞控制算法就是要在这两者之间权衡,选取最好的cwnd值,从而使得网络吞吐量最大化且不产生拥塞。

      由于需要考虑拥塞控制和流量控制两个方面的内容,因此TCP的真正的发送窗口=min(rwnd, cwnd)。但是rwnd是由对端确定的,网络环境对其没有影响,所以在考虑拥塞的时候我们一般不考虑rwnd的值,我们暂时只讨论如何确定cwnd值的大小。关于cwnd的单位,在TCP中是以字节来做单位的,我们假设TCP每次传输都是按照MSS大小来发送数据的,因此你可以认为cwnd按照数据包个数来做单位也可以理解,所以有时我们说cwnd增加1也就是相当于字节数增加1个MSS大小。

慢启动

      慢启动:最初的TCP在连接建立成功后会向网络中发送大量的数据包,这样很容易导致网络中路由器缓存空间耗尽,从而发生拥塞。因此新建立的连接不能够一开始就大量发送数据包,而只能根据网络情况逐步增加每次发送的数据量,以避免上述现象的发生。具体来说,当新建连接时,cwnd初始化为1个最大报文段(MSS)大小,发送端开始按照拥塞窗口大小发送数据,每当有一个报文段被确认,cwnd就增加1个MSS大小。这样cwnd的值就随着网络往返时间(Round Trip Time,RTT)呈指数级增长,事实上,慢启动的速度一点也不慢,只是它的起点比较低一点而已。

我们可以简单计算下:

   开始           --->     cwnd = 1

   经过1个RTT后   --->     cwnd = 2*1 = 2

   经过2个RTT后   --->     cwnd = 2*2= 4

   经过3个RTT后   --->     cwnd = 4*2 = 8

如果带宽为W,那么经过RTT*log2W时间就可以占满带宽。

拥塞避免

      拥塞避免:从慢启动可以看到,cwnd可以很快的增长上来,从而最大程度利用网络带宽资源,但是cwnd不能一直这样无限增长下去,一定需要某个限制。TCP使用了一个叫慢启动门限(ssthresh)的变量,当cwnd超过该值后,慢启动过程结束,进入拥塞避免阶段。对于大多数TCP实现来说,ssthresh的值是65536(同样以字节计算)。拥塞避免的主要思想是加法增大,也就是cwnd的值不再指数级往上升,开始加法增加。此时当窗口中所有的报文段都被确认时,cwnd的大小加1,cwnd的值就随着RTT开始线性增加,这样就可以避免增长过快导致网络拥塞,慢慢的增加调整到网络的最佳值。

拥塞的检测

上面讨论的两个机制都是没有检测到拥塞的情况下的行为,那么当发现拥塞了cwnd又该怎样去调整呢?

首先来看TCP是如何确定网络进入了拥塞状态的,TCP认为网络拥塞的主要依据是它重传了一个报文段。上面提到过,TCP对每一个报文段都有一个定时器,称为重传定时器(RTO),当RTO超时且还没有得到数据确认,那么TCP就会对该报文段进行重传,当发生超时时,那么出现拥塞的可能性就很大,某个报文段可能在网络中某处丢失,并且后续的报文段也没有了消息,在这种情况下,TCP反应比较“强烈”:

1.把ssthresh降低为cwnd值的一半

2.把cwnd重新设置为1

3.重新进入慢启动过程。

从整体上来讲,TCP拥塞控制窗口变化的原则是AIMD原则,即加法增大、乘法减小。

 AIMD(加法增大乘法减小):

     1. 乘法减小:无论在慢启动阶段还是在拥塞控制阶段,只要网络出现超时,就是将cwnd置为1,ssthresh置为cwnd的一半,然后开始执行慢启动算法(cwnd<ssthresh)。

     2. 加法增大:当网络频发出现超时情况时,ssthresh就下降的很快,为了减少注入到网络中的分组数,而加法增大是指执行拥塞避免算法后,是拥塞窗口缓慢的增大,以防止网络过早出现拥塞。

   这两个结合起来就是AIMD算法,是使用最广泛的算法。拥塞避免算法不能够完全的避免网络
 

可以看出TCP的该原则可以较好地保证流之间的公平性,因为一旦出现丢包,那么立即减半退避,可以给其他新建的流留有足够的空间,从而保证整个的公平性。

               1. 当cwnd > ssthresh,使用慢启动算法,

               2. 当cwnd < ssthresh,使用拥塞控制算法,停用慢启动算法。

               3. 当cwnd = ssthresh,这两个算法都可以。
 

实例:
1.TCP连接进行初始化的时候,cwnd=1,ssthresh=16。

2.在慢启动算法开始时,cwnd的初始值是1,每次发送方收到一个ACK拥塞窗口就增加1,当ssthresh =cwnd时,就启动拥塞控制算法,拥塞窗口按照规律增长。

3.当cwnd=24时,网络出现超时,发送方收不到确认ACK,此时设置ssthresh=12,(二分之一cwnd),设置cwnd=1,然后开始慢启动算法,当cwnd=ssthresh=12,慢启动算法变为拥塞控制算法,cwnd按照线性的速度进行增长。
 

 

 

其实TCP还有一种情况会进行重传:那就是收到3个相同的ACK。TCP在收到乱序到达包时就会立即发送ACK,TCP利用3个相同的ACK来判定数据包的丢失,此时进行快速重传。

 

快速重传

TCP在收到乱序到达包时就会立即发送ACK,TCP利用3个相同的ACK来判定数据包的丢失,此时进行快速重传,快速重传做的事情有:

1.把ssthresh设置为cwnd的一半

2.把cwnd再设置为ssthresh的值(具体实现有些为ssthresh+3)

3.重新进入拥塞避免阶段。

è¿éåå¾çæè¿°

快速恢复

     后来的“快速恢复”算法是在上述的“快速重传”算法后添加的,当收到3个重复ACK时,TCP最后进入的不是拥塞避免阶段,而是快速恢复阶段。快速重传和快速恢复算法一般同时使用。快速恢复的思想是“数据包守恒”原则,即同一个时刻在网络中的数据包数量是恒定的,只有当“老”数据包离开了网络后,才能向网络中发送一个“新”的数据包,如果发送方收到一个重复的ACK,那么根据TCP的ACK机制就表明有一个数据包离开了网络,于是cwnd加1。如果能够严格按照该原则那么网络中很少会发生拥塞,事实上拥塞控制的目的也就在修正违反该原则的地方。

具体来说快速恢复的主要步骤是:

1.当收到3个重复ACK时,把ssthresh设置为cwnd的一半,把cwnd设置为ssthresh的值加3,然后重传丢失的报文段,加3的原因是因为收到3个重复的ACK,表明有3个“老”的数据包离开了网络。

2.再收到重复的ACK时,拥塞窗口增加1。

3.当收到新的数据包的ACK时,把cwnd设置为第一步中的ssthresh的值。原因是因为该ACK确认了新的数据,说明从重复ACK时的数据都已收到,该恢复过程已经结束,可以回到恢复之前的状态了,也即再次进入拥塞避免状态。

è¿éåå¾çæè¿°

快速重传算法首次出现在4.3BSD的Tahoe版本,快速恢复首次出现在4.3BSD的Reno版本,也称之为Reno版的TCP拥塞控制算法。

NewReno和SACK

      可以看出Reno的快速重传算法是针对一个包的重传情况的,然而在实际中,一个重传超时可能导致许多的数据包的重传,因此当多个数据包从一个数据窗口中丢失时并且触发快速重传和快速恢复算法时,问题就产生了。因此NewReno出现了,它在Reno快速恢复的基础上稍加了修改,可以恢复一个窗口内多个包丢失的情况。具体来讲就是:Reno在收到一个新的数据的ACK时就退出了快速恢复状态了,而NewReno需要收到该窗口内所有数据包的确认后才会退出快速恢复状态,从而更一步提高吞吐量。

SACK就是改变TCP的确认机制,最初的TCP只确认当前已连续收到的数据,SACK则把乱序等信息会全部告诉对方,从而减少数据发送方重传的盲目性。比如说序号1,2,3,5,7的数据收到了,那么普通的ACK只会确认序列号4,而SACK会把当前的5,7已经收到的信息在SACK选项里面告知对端,从而提高性能,当使用SACK的时候,NewReno算法可以不使用,因为SACK本身携带的信息就可以使得发送方有足够的信息来知道需要重传哪些包,而不需要重传哪些包。

------------------------------------------------------------------------------------------------------------------------------------------------------------------------

TCP超时重传、滑动窗口、拥塞控制、快重传和快恢复

TCP超时重传

原理是在发送某一个数据以后就开启一个计时器,在一定时间内如果没有得到发送的数据报的ACK报文,那么就重新发送数据,直到发送成功为止。
  影响超时重传机制协议效率的一个关键参数是重传超时时间(RTO,Retransmission TimeOut)。RTO的值被设置过大过小都会对协议造成不利影响。
  (1)RTO设长了,重发就慢,没有效率,性能差。
  (2)RTO设短了,重发的就快,会增加网络拥塞,导致更多的超时,更多的超时导致更多的重发。
  连接往返时间(RTT,Round Trip Time),指发送端从发送TCP包开始到接收它的立即响应所消耗的时间。

TCP滑动窗口

作用:(1)提供TCP的可靠性;(2)提供TCP的流控特性

 

image.png

image.png

image.png

image.png

TCP的滑动窗口的可靠性也是建立在“确认重传”基础上的。
发送窗口只有收到对端对于本段发送窗口内字节的ACK确认,才会移动发送窗口的左边界。
接收端可以根据自己的状况通告窗口大小,从而控制发送端的接收,进行流量控制。

TCP拥塞控制

拥塞控制是一个全局性的过程; 流量控制是点对点通信量的控制
  TCP拥塞控制4个核心算法:慢开始(slow start)、拥塞避免(Congestion Avoidance)、快速重传(fast retransmit)、快速回复(fast recovery)
  拥塞窗口(cwnd,congestion window),其大小取决于网络的拥塞程度,并且动态地在变化。

慢开始算法的思路就是,不要一开始就发送大量的数据,先探测一下网络的拥塞程度,也就是说由小到大逐渐增加拥塞窗口的大小。

为了防止cwnd增长过大引起网络拥塞,还需设置一个慢开始门限ssthresh状态变量。ssthresh的用法如下:
当cwnd < ssthresh时,使用慢开始算法。
当cwnd > ssthresh时,改用拥塞避免算法。
当cwnd = ssthresh时,慢开始与拥塞避免算法任意。

拥塞避免算法让拥塞窗口缓慢增长,即每经过一个往返时间RTT就把发送发的拥塞窗口cwnd加1,而不是加倍。

无论是在慢开始阶段还是在拥塞避免阶段,只要发送方判断网络出现拥塞,就把慢开始门限设置为出现拥塞时的发送窗口大小的一半。然后把拥塞窗口设置为1,执行慢开始算法。如下图:

 

image.png

image.png

拥塞控制的具体过程如下:
(1)TCP连接初始化,将拥塞窗口设置为1
(2)执行慢开始算法,cwnd按指数规律增长,直到cwnd=ssthresh时,开始执行拥塞避免算法,cwnd按线性规律增长
(3)当网络发生拥塞,把ssthresh值更新为拥塞前ssthresh值的一半,cwnd重新设置为1,按照步骤(2)执行

乘法减小和加法增大

乘法减小:是指不论在慢开始阶段还是拥塞避免阶段,只要出现超时,就把慢开始门限减半,即设置为当前的拥塞窗口的一半(于此同时,执行慢开始算法)。当网络出现频繁拥塞时,ssthresh值就下降的很快,以大大将小注入到网络中的分组数。

加法增大:是指执行拥塞避免算法后是拥塞窗口缓慢增大,以防止网络过早出现拥塞。

快重传和快恢复

快速重传(Fast retransmit)要求接收方在收到一个失序的报文段后就立即发出重复确认(为的是使发送方及早知道有报文段没有到达对方),而不要等到自己发送数据时捎带确认。
  快重传算法规定,发送方只要一连收到3个重复确认就应当立即重传对方尚未收到的报文段,而不必继续等待设置的重传计数器时间到期。

快速恢复(Fast Recovery)
(1)当发送方连续收到三个重复确认,就执行“乘法减小”算法,把慢开始门限ssthresh减半。这是为了预防网络发生拥塞。请注意:接下去不执行慢开始算法。
(2)由于发送方现在认为网络很可能没有发生拥塞,因此与慢开始不同之处是现在不执行慢开始算法(即拥塞窗口cwnd现在不设置为1),而是把cwnd值设置为慢开始门限ssthresh减半后的数值,然后开始执行拥塞避免算法(“加法增大”),使拥塞窗口缓慢地线性增大。

发送方窗口的上限值 = Min [ rwnd, cwnd ]
当rwnd < cwnd 时,是接收方的接收能力限制发送方窗口的最大值。
当cwnd < rwnd 时,则是网络的拥塞限制发送方窗口的最大

================================================================================================

 前言 
    对于每个TCP连接,TCP一般要管理4个不同的定时器:重传定时器、坚持定时器、保活定时器、2MSL定时器。

    重传定时器 
    很明显重传定时器是用来计算TCP报文段的超时重传时间的(至于超时重传时间的确定,这里涉及到一大堆的算法,书上有说,我这里不细谈了)。每发送一个报文段就会启动重传定时器,如果在定时器时间到后还没收到对该报文段的确认,就重传该报文段,并将重传定时器复位,重新计算;如果在规定时间内收到了对该报文段的确认,则撤销该报文段的重传定时器。

    坚持定时器 
    上篇文章中已经提到了,主要是为了应付零窗口大小通知可能导致的死锁问题。如果接收端在向发送端发送了零窗口报文段后不久,接收端的接收缓存又有了一些存储空间,于是接收端向发送端发送了一个非零窗口大小的报文段,然而这个报文段在传送过程中丢失了,发送端没有收到该报文段,就一直等待接收端发送非零窗口的报文通知,而接收端并不知道报文段丢失了,而是觉得已经告诉发送端了,就会一直等待发送端发送数据,如果没有任何措施的话,这话死锁的局面会一直延续下去。

    为了解决这个问题,TCP为每一个连接设有一个坚持定时器(也叫持续计数器)。只要TCP连接的一方收到对方的零窗口通知,就启动坚持定时器。若坚持定时器设置的时间到期,就发送一个零窗口控测报文段(该报文段只有一个字节的数据,它有一个序号,但该序号永远不需要确认,因此该序号可以持续重传),之后会出现以下三种情况:

    1、对方在收到探测报文段后,在对该报文段的确认中给出现在的窗口值,如果窗口值仍未零,则收到这个报文段的一方将坚持定时器的值加倍并重启。坚持计数器最大只能增加到约60秒,在此之后,每次收到零窗口通知,坚持计数器的值就定位60秒。

    2、对方在收到探测报文段后,在对该报文段的确认中给出现在的窗口值,如果窗口不为零,那么死锁的僵局就被打破了。

    3、该探测报文发出后,会同时启动重传定时器,如果重传定时器的时间到期,还没有收到接收到发来的响应,则超时重传探测报文。


    保活定时器 
    保活定时器是为了应对两个TCP连接间出现长时间的没有数据传输的情况。如果客户已与服务器建立了TCP连接,但后来客户端主机突然故障,则服务器就不能再收到客户端发来的数据了,而服务器肯定不能这样永久地等下去,保活定时器就是用来解决这个问题的。服务器每收到一次客户端的数据,就重新设置保活定时器,通常为2小时,如果2小时没有收到客户端的数据,服务端就发送一个探测报文,以后每隔75秒发送一次,如果连续发送10次探测报文段后仍没有收到客户端的响应,服务器就认为客户端出现了故障,就可以终止这个连接。

    2MSL定时器 
    2MSL定时器测量一个连接处于TIME—WAIT黄台的时间,通常为2MSL(报文段寿命的两倍)。2MSL定时器的设置主要是为了确保发送的最后一个ACK报文段能够到达对方,并防止之前与本连接有关的由于延迟等原因而导致已失效的报文被误判为有效
————————————————
 

 

TCP的定时处理函数。在前面的讨论中,我们看到了与TCP的各种定时器,包括重传定时器、持续定时器和保活定时器,此外TCP中还有几个定时器我们还未涉及。这里总的来看看TCP中的各个定时器。TCP为每条连接总共建立了七个定时器,依次为:

1 )“连接建立(connection establishment)”定时器在发送SYN报文段建立一条新连接时启动。如果在75秒内没有收到响应,连接建立将中止。

2 )“重传(retransmission)”定时器在TCP发送某个数据段时设定。如果该定时器超时而对端的确认还未到达,TCP将重传该数据段。重传定时器的值 (即TCP等待对端确认的时间)是动态计算的,与RTT的估计值密切相关,且还取决于该报文段已被重传的次数。

3 )“延迟ACK(delayed ACK)”定时器在TCP收到必须被确认但无需马上发出确认的数据时设定。如果在200ms内,有数据要在该连接上发送,延迟的ACK响应就可随着数据一起发送回对端,称为捎带确认。如果200ms后,该确认未能被捎带出去,则定时器超时,此时需要发送一个立即确认。

4 )“持续 (persist)”定时器在连接对端通告接收窗口为0,阻止TCP继续发送数据时设定。由于连接对端发送的窗口通告不可靠(只有数据才会被确认,ACK不会被确认),允许TCP继续发送数据的后续窗口更新有可能丢失。因此,如果TCP有数据要发送,但对端通告接收窗口为0,则持续定时器启动,超时后向对端发送 1字节的数据,判定对端接收窗口是否已打开。

5 )“保活(keep alive)”定时器在TCP控制块的so_options 字段设置了SOF_KEEPALIVE选项时生效。如果连接的连续空闲时间超过2小时,则保活定时器超时,此时应向对端发送连接探测报文段,强迫对端响应。如果收到了期待的响应, TCP可确定对端主机工作正常,在该连接再次空闲超过 2小时之前,TCP不会再进行保活测试。如果收到的是RST复位响应, TCP可确定对端主机已重启。如果连续若干次保活测试都未收到响应, TCP就假定对端主机已崩溃,但它无法区分是主机故障还是连接故障。

6) FIN_WAIT_2定时器,当某个连接从FIN_WAIT_1状态变迁到FIN_WAIT_2状态并且不能再接收任何新数据时,FIN_WAIT_2定时器启动,设为10分钟。定时器超时后,重新设为75秒,第二次超时后连接被关闭。加入这个定时器的目的是为了避免如果对端一直不发送 FIN,某个连接会永远滞留在FIN _ WAIT_ 2状态(假设TCP不选用半打开功能)。

7) TIME_WAIT定时器,一般也称为2MSL定时器。2MSL指两倍的MSL,即最大报文段生存时间。当连接转移到TIME_WAIT状态,即连接主动关闭时,定时器启动。状态转换图那一节中已经详细说明了需要2MSL等待状态的原因。连接进入TIME_WAIT状态时,定时器设定为1分钟,超时后,TCP控制块被删除,端口号可重新使用。

前面的7个定时器中,重传定时器使用rtime字段计数,持续定时器使用persist_cnt字段计数,其他五个定时器除延迟ACK定时器外都使用rtime字段计数,从上面的描述中可以看出,这四个定时器是TCP处于四种不同的状态时使用的,因此四个定时器完全独立的使用rtime字段而不会互相影响。延迟ACK定时器使用系统250ms周期性定时来完成的。

LWIP中包括两个定时器函数:一个函数每 250 ms调用一次(快速定时器);另一个函数每500ms调用一次(慢速定时器)。延迟ACK定时器与其他6个定时器有所不同:如果某个连接上设定了延迟ACK定时器,那么下一次250ms定时器超时后,延迟的ACK必须被发送(实际的ACK延迟时间在0~250ms之间)。其他的6个定时器每500 ms增加1,当计数值超过某些阈值时,则相应的动作被触发。

先看简单的快速定时器处理函数:

void tcp_fasttmr(void)

{

struct tcp_pcb *pcb;

for(pcb = tcp_active_pcbs; pcb != NULL; pcb = pcb->next) { //遍历整个active链表

if (pcb->refused_data != NULL) { // 如果某个控制块还没有数据未接收

err_t err;

TCP_EVENT_RECV(pcb, pcb->refused_data, ERR_OK, err); //调用上层函数接收数据

if (err == ERR_OK) {

pcb->refused_data = NULL; // 成功接收则复位指针

}

}

if (pcb->flags & TF_ACK_DELAY) {//若控制块开启了延迟ACK定时器

tcp_ack_now(pcb);   // 发送一个立即确认

pcb->flags &= ~(TF_ACK_DELAY | TF_ACK_NOW); //清除标志位

}

}// if

}// for

从上面可以看出,快速定时器处理函数主要做了两方面的工作,一是向上层递交上层一直未接收的数据,二是发送该连接上的立即确认数据段。与快速定时器相比,慢速定时器处理函数就显得相当的庞大了,这里我们只列出和各个定时器相关的部分,而对其他部分采用伪代码的方式加以描述,当然,这里所谓的其他部分就是我们在前面已经讲解过的部分了。

void tcp_slowtmr(void)

{

++tcp_ticks;

pcb = tcp_active_pcbs;

while (pcb != NULL) {

pcb_remove = 0;

该控制块的零窗口探查处理;

该控制块的超时重传处理;

if (pcb->state == FIN_WAIT_2) {  // FIN_WAIT_2定时器超时

if ((u32_t)(tcp_ticks - pcb->tmr) >

TCP_FIN_WAIT_TIMEOUT / TCP_SLOW_INTERVAL) {

++pcb_remove;

}

}

该控制块的超时保活处理;

#if TCP_QUEUE_OOSEQ

if (pcb->ooseq != NULL &&  // 丢弃在ooseq队列中长时间未被处理的数据

(u32_t)tcp_ticks - pcb->tmr >= pcb->rto * TCP_OOSEQ_TIMEOUT) {

tcp_segs_free(pcb->ooseq);

pcb->ooseq = NULL;

}

#endif

if (pcb->state == SYN_RCVD) { // SYN_RCVD状态超时

if ((u32_t)(tcp_ticks - pcb->tmr) >

TCP_SYN_RCVD_TIMEOUT / TCP_SLOW_INTERVAL) {

++pcb_remove;

}

}

if (pcb->state == LAST_ACK) { // LAST_ACK定时器超时

if ((u32_t)(tcp_ticks - pcb->tmr) > 2 * TCP_MSL / TCP_SLOW_INTERVAL) {

++pcb_remove;

}

}

若有超时则删除控制块;

无超时则周期性的外发数据包(poll);

取得active链表上的下一个控制块;

}// while

pcb = tcp_tw_pcbs; //处理TIME-WAIT链表

while (pcb != NULL) {

pcb_remove = 0;   // TIME-WAIT定时器超时

if ((u32_t)(tcp_ticks - pcb->tmr) > 2 * TCP_MSL / TCP_SLOW_INTERVAL) {

++pcb_remove;

}

若超时则删除控制块;

取得TIME-WAIT链表上的下一个控制块;

}// while

}//函数尾

可以看出各个定时器的实现都是利用全局变量tcp_ticks与tmr字段的差值来实现的。当TCP进入某个状态时,就会将相应tmr字段设置为当前的全局时钟tcp_ticks的值,所以上面的差值可以有效表示出TCP处于某个状态的时间。各个定时器超时后的处理也很相似,即将变量pcb_remove加1,pcb_remove变量是超时处理中最核心的变量了,当针对某个PCB控制块作完超时判断之后,函数通过判断pcb_remove的值来处理TCP控制块,当pcb_remove值大于1时,则表示该控制块上有超时事件发生,该控制块或被删除或被挂起。注意伪代码中的重传定时器超时并不会影响pcb_remove的值。如果细心,你还可以看到,上面的代码多了两个超时事件,即SYN_RCVD状态超时和ooseq队列数据超时,当然,这两个超时事件并不影响协议栈功能的实现。

最后来看看系统为每个超时时间设置的超时时间,从上面的代码中可以看出,它们是在各个宏定义里面实现的。

#define TCP_TMR_INTERVAL      250   //250ms

#define TCP_FAST_INTERVAL     TCP_TMR_INTERVAL  // 快速定时器

#define TCP_SLOW_INTERVAL     (2*TCP_TMR_INTERVAL) // 慢速定时器

 

#define TCP_FIN_WAIT_TIMEOUT   20000  // FIN_WAIT_2状态超时时间

#define TCP_SYN_RCVD_TIMEOUT  20000  // SYN_RCVD状态超时时间

 

#define TCP_OOSEQ_TIMEOUT   6U   //ooseq队列中数据等待的rto周期数

#define TCP_MSL  60000U   // MSL

到这里,我们的PCB控制块中的各个字段基本都已涉及到了,除了polltmr和pollinterval。这两个字段用于周期性调用函数tcp_output,用以发送控制块上残留的未发送数据段。

 

 

 

==============================================================================

,TCP/IP和Linux系统层级的很多设计都可以用于中间件系统架构上,比如说TCP 拥塞控制算法也可以用于以响应时间来限流的中间件。更深一层,像TCP/IP协议这种基础知识和原理性的技术,都是经过长时间的考验的,都是前人智慧的结晶,可以给大家很多启示和帮助。

 本文中会出现一些缩写,因为篇幅问题,无法每个都进行解释,如果你不明白它的含义,请自己去搜索了解,做一个主动寻求知识的人。

 TCP协议有两个比较重要的控制算法,一个是流量控制,另一个就是阻塞控制。

 TCP协议通过滑动窗口来进行流量控制,它是控制发送方的发送速度从而使接受者来得及接收并处理。而拥塞控制是作用于网络,它是防止过多的包被发送到网络中,避免出现网络负载过大,网络拥塞的情况。

 拥塞算法需要掌握其状态机和四种算法。拥塞控制状态机的状态有五种,分别是Open,Disorder,CWR,Recovery和Loss状态。四个算法为慢启动,拥塞避免,拥塞发生时算法和快速恢复。

Congestion Control State Machine

 和TCP一样,拥塞控制算法也有其状态机。当发送方收到一个Ack时,Linux TCP通过状态机(state)来决定其接下来的行为,是应该降低拥塞窗口cwnd大小,或者保持cwnd不变,还是继续增加cwnd。如果处理不当,可能会导致丢包或者超时。

状态机示意图

1 Open状态

 Open状态是拥塞控制状态机的默认状态。这种状态下,当ACK到达时,发送方根据拥塞窗口cwnd(Congestion Window)是小于还是大于慢启动阈值ssthresh(slow start threshold),来按照慢启动或者拥塞避免算法来调整拥塞窗口。

2 Disorder状态

 当发送方检测到DACK(重复确认)或者SACK(选择性确认)时,状态机将转变为Disorder状态。在此状态下,发送方遵循飞行(in-flight)包守恒原则,即一个新包只有在一个老包离开网络后才发送,也就是发送方收到老包的ACK后,才会再发送一个新包。

3 CWR状态

 发送方接收到一个拥塞通知时,并不会立刻减少拥塞窗口cwnd,而是每收到两个ACK就减少一个段,直到窗口的大小减半为止。当cwnd正在减小并且网络中有没有重传包时,这个状态就叫CWR(Congestion Window Reduced,拥塞窗口减少)状态。CWR状态可以转变成Recovery或者Loss状态。

4 Recovery状态

 当发送方接收到足够(推荐为三个)的DACK(重复确认)后,进入该状态。在该状态下,拥塞窗口cnwd每收到两个ACK就减少一个段(segment),直到cwnd等于慢启动阈值ssthresh,也就是刚进入Recover状态时cwnd的一半大小。
 发送方保持 Recovery 状态直到所有进入 Recovery状态时正在发送的数据段都成功地被确认,然后发送方恢复成Open状态,重传超时有可能中断 Recovery 状态,进入Loss状态。

5 Loss状态

 当一个RTO(重传超时时间)到期后,发送方进入Loss状态。所有正在发送的数据标记为丢失,拥塞窗口cwnd设置为一个段(segment),发送方再次以慢启动算法增大拥塞窗口cwnd。

 Loss 和 Recovery 状态的区别是:Loss状态下,拥塞窗口在发送方设置为一个段后增大,而 Recovery 状态下,拥塞窗口只能被减小。Loss 状态不能被其他的状态中断,因此,发送方只有在所有 Loss 开始时正在传输的数据都得到成功确认后,才能退到 Open 状态。

四大算法

 拥塞控制主要是四个算法:1)慢启动,2)拥塞避免,3)拥塞发生,4)快速恢复。这四个算法不是一天都搞出来的,这个四算法的发展经历了很多时间,到今天都还在优化中。

示意图

慢热启动算法 – Slow Start

 所谓慢启动,也就是TCP连接刚建立,一点一点地提速,试探一下网络的承受能力,以免直接扰乱了网络通道的秩序。

 慢启动算法:

1) 连接建好的开始先初始化拥塞窗口cwnd大小为1,表明可以传一个MSS大小的数据。
2) 每当收到一个ACK,cwnd大小加一,呈线性上升。
3) 每当过了一个往返延迟时间RTT(Round-Trip Time),cwnd大小直接翻倍,乘以2,呈指数让升。
4) 还有一个ssthresh(slow start threshold),是一个上限,当cwnd >= ssthresh时,就会进入“拥塞避免算法”(后面会说这个算法)

拥塞避免算法 – Congestion Avoidance

 如同前边说的,当拥塞窗口大小cwnd大于等于慢启动阈值ssthresh后,就进入拥塞避免算法。算法如下:

1) 收到一个ACK,则cwnd = cwnd + 1 / cwnd
2) 每当过了一个往返延迟时间RTT,cwnd大小加一。

 过了慢启动阈值后,拥塞避免算法可以避免窗口增长过快导致窗口拥塞,而是缓慢的增加调整到网络的最佳值。

拥塞状态时的算法

 一般来说,TCP拥塞控制默认认为网络丢包是由于网络拥塞导致的,所以一般的TCP拥塞控制算法以丢包为网络进入拥塞状态的信号。对于丢包有两种判定方式,一种是超时重传RTO[Retransmission Timeout]超时,另一个是收到三个重复确认ACK。

 超时重传是TCP协议保证数据可靠性的一个重要机制,其原理是在发送一个数据以后就开启一个计时器,在一定时间内如果没有得到发送数据报的ACK报文,那么就重新发送数据,直到发送成功为止。

 但是如果发送端接收到3个以上的重复ACK,TCP就意识到数据发生丢失,需要重传。这个机制不需要等到重传定时器超时,所以叫
做快速重传,而快速重传后没有使用慢启动算法,而是拥塞避免算法,所以这又叫做快速恢复算法。

 超时重传RTO[Retransmission Timeout]超时,TCP会重传数据包。TCP认为这种情况比较糟糕,反应也比较强烈:

  • 由于发生丢包,将慢启动阈值ssthresh设置为当前cwnd的一半,即ssthresh = cwnd / 2.
  • cwnd重置为1
  • 进入慢启动过程

 最为早期的TCP Tahoe算法就只使用上述处理办法,但是由于一丢包就一切重来,导致cwnd又重置为1,十分不利于网络数据的稳定传递。

 所以,TCP Reno算法进行了优化。当收到三个重复确认ACK时,TCP开启快速重传Fast Retransmit算法,而不用等到RTO超时再进行重传:

  • cwnd大小缩小为当前的一半
  • ssthresh设置为缩小后的cwnd大小
  • 然后进入快速恢复算法Fast Recovery。

cwnd曲线示意图

快速恢复算法 – Fast Recovery

 TCP Tahoe是早期的算法,所以没有快速恢复算法,而Reno算法有。在进入快速恢复之前,cwnd和ssthresh已经被更改为原有cwnd的一半。快速恢复算法的逻辑如下:

  • cwnd = cwnd + 3 MSS,加3 MSS的原因是因为收到3个重复的ACK。
  • 重传DACKs指定的数据包。
  • 如果再收到DACKs,那么cwnd大小增加一。
  • 如果收到新的ACK,表明重传的包成功了,那么退出快速恢复算法。将cwnd设置为ssthresh,然后进入拥塞避免算法。

快速重传示意图

 如图所示,第五个包发生了丢失,所以导致接收方接收到三次重复ACK,也就是ACK5。所以将ssthresh设置当当时cwnd的一半,也就是6/2 = 3,cwnd设置为3 + 3 = 6。然后重传第五个包。当收到新的ACK时,也就是ACK11,则退出快速恢复阶段,将cwnd重新设置为当前的ssthresh,也就是3,然后进入拥塞避免算法阶段。

后记

 本文为大家大致描述了TCP拥塞控制的一些机制,但是这些拥塞控制还是有很多缺陷和待优化的地方,业界也在不断推出新的拥塞控制算法,比如说谷歌的BBR。这些我们后续也会继续探讨,请大家继续关注。

个人博客:Remcarpediem

引用

本文为云栖社区原创内容,未经允许不得转载,如需转载请发送邮件至yqeditor@list.alibaba-inc.com;如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件至:yqgroup@service.aliyun.com 进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容。

  • 3
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值