Last Updated: 2023-04-18 09:44:07 Tuesday
-- TOC --
本文详细介绍TCP连接的三次握手,四次挥手,2MSL,TIME_WAIT状态等概念。下图去掉CLOSED,刚好是TCP的11个状态:
下图来自RFC793:
+---------+ ---------\ active OPEN
| CLOSED | \ -----------
+---------+<---------\ \ create TCB
| ^ \ \ snd SYN
passive OPEN | | CLOSE \ \
------------ | | ---------- \ \
create TCB | | delete TCB \ \
V | \ \
+---------+ CLOSE | \
| LISTEN | ---------- | |
+---------+ delete TCB | |
rcv SYN | | SEND | |
----------- | | ------- | V
+---------+ snd SYN,ACK / \ snd SYN +---------+
| |<----------------- ------------------>| |
| SYN | rcv SYN | SYN |
| RCVD |<-----------------------------------------------| SENT |
| | snd ACK | |
| |------------------ -------------------| |
+---------+ rcv ACK of SYN \ / rcv SYN,ACK +---------+
| -------------- | | -----------
| x | | snd ACK
| V V
| CLOSE +---------+
| ------- | ESTAB |
| snd FIN +---------+
| CLOSE | | rcv FIN
V ------- | | -------
+---------+ snd FIN / \ snd ACK +---------+
| FIN |<----------------- ------------------>| CLOSE |
| WAIT-1 |------------------ | WAIT |
+---------+ rcv FIN \ +---------+
| rcv ACK of FIN ------- | CLOSE |
| -------------- snd ACK | ------- |
V x V snd FIN V
+---------+ +---------+ +---------+
|FINWAIT-2| | CLOSING | | LAST-ACK|
+---------+ +---------+ +---------+
| rcv ACK of FIN | rcv ACK of FIN |
| rcv FIN -------------- | Timeout=2MSL -------------- |
| ------- x V ------------ x V
\ snd ACK +---------+delete TCB +---------+
------------------------>|TIME WAIT|------------------>| CLOSED |
+---------+ +---------+
TCP Connection State Diagram
Figure 6.
TCP传输的可靠性,是通过对每个传输字节的seq进行确认和checksum来保证的!
客户端状态: SYN_SENT ----> ESTAB
客户端报文: 发送SYN ----> 接收ACK(带SYN标志) ----> 发送ACK
服务器端状态: LISTEN ----> SYN_RCVD ----> ESTAB
服务器端报文: 接收SYN ----> 发送ACK(带SYN标志)----> 接收ACK
客户端会先于服务器端一丢丢,进入ESTAB状态。
通信双方同步Sequence Number
通信双方在SYN报文中,都随机初始化了一个Seq序列号,这个序列号是保障数据可靠接收的关键。SYN报文中含有一个随机的序列号SEQ,应答ACK报文中含有SEQ+1。TCP是全双工通信,两个发送方使用自己随机初始化的序列号。
检测TCP端口是否开放?
从原理上看,发送SYN后如果能收到对应的ACK,这个端口就可以进行TCP连接,这就是TCP端口扫描的基本原理。因此,connect调用能成功,端口就是open的。(用connect接口,就能够写一个简单的端口扫描工具,或tcping工具)
服务器端的TCP,其实就是不断地产生与客户端一样的TCP,因此在ESTAB状态下,任何一方都可以首先离开,发FIN。
因此,首先发起关闭TCP的一方,最终会进入著名的TIME_WAIT状态!
由于TCP连接是全双工的,因此每个方向都必须单独进行关闭。原则是当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向的连接。收到一个FIN只意味着这一方向上没有数据流动,一个TCP连接在收到一个FIN后仍能发送数据。首先进行关闭的一方执行主动关闭,而另一方执行被动关闭。
我们说的四次挥手,具体是那四次呢?其实就是双方各自发出的FIN报文和各自给对方的ACK报文,加起来一共4个报文。
主动关闭方A的状态变化: ESTAB ----> FINWAIT-1 ----> FINWAIT-2 ----> TIME_WAIT ----> CLOSED
主动方A的报文:发送FIN ----> 收到ACK ----> 收到FIN ----> 发送ACK
被动关闭方B的状态变化:ESTAB ----> CLOSE_WAIT ----> LAST-ACK ----> CLOSED
被动方B的报文:收到FIN ----> 发送ACK ----> (完成数据发送后)发送FIN ----> 收到ACK
由于网络传输,主动方A有可能先收到被动方B的FIN,然后才收到ACK,所以就多了一个进入 CLOSING 状态,然后跳过 FINWAIT-2 状态,直接到 TIME_WAIT 状态的转移路径!
FIN_WAIT_1和FIN_WAIT_2状态,表示等待对方的FIN_ACK和FIN报文,而这两种状态的区别是,FIN_WAIT_1状态实际上是当SOCKET在ESTABLISHED状态时,它要主动关闭连接,向对方发送了FIN报文,此时该SOCKET即进入到FIN_WAIT_1状态。而当对方回应ACK报文后,则进入到FIN_WAIT_2状态。正常情况下,无论对方处于何种情况下,都应该马上回应ACK 报文,所以FIN_WAIT_1状态一般是比较难见到的,而FIN_WAIT_2状态还有时常常可以用netstat看到。
也有可能是三次挥手,比如收到对端的FIN,在回复ACK的时候,自己没有数据要发送,有可能将FIN和ACK合并在一起发给对端。就像三次握手的时候,Server将SYN和ACK合并在一起一样。
特别注意:Python的close接口,不一定会发出FIN!请先shutdown!
要先记住,主动发起FIN的那一方A,才会进入这个著名的TIME_WAIT状态。主动发FIN,就意味着自己的数据传输已经完成了,后面不会再发送数据了!
也有可能不发FIN,直接发RST,此时就没有TIME_WAIT,参考close和shutdown
此时,被动方B回应ACK后,进入CLOSE_WAIT状态,B虽然收到了FIN,也回应了ACK,但B的数据可能还没有发完,它还要继续发送数据。被动方B要完成数据发送后,才会发出自己的FIN,进入LAST-ACK状态。主动方A会进入TIME_WAIT状态,就是因为被动方B可能还有数据在发送,网络中可能还存在B发出的,A还未接收到的数据。
socket有个shutdown接口,可以只关闭发送或接收,调用shutdown可以发出FIN,而调用close有时会直接发RST。
由于网络传输的各种不确定性,在TIME_WAIT状态下,主动的A虽然占用了一组(ip,port)资源,暂时不会被系统回收再利用,直到TIME_WAIT超时结束。主动方A这样长时间的等待,确保了新的连接(假如继续使用相同的ip和port资源),绝对不会收到旧连接在网络上重传的报文。如下图:
假如TIME_WAIT的时间很短,或者没有,新的socket又不小心复用了相同的ip和port资源,seq=3的报文,被新的连接错误的接收(此时正好seq=3能够跟其它报文的seq对应上,如果seq对不上,会被RST)。
上图是收到错误的数据包的情况,下图是收到错误类型的报文:
被动的B发送FIN后,一直收不到ACK(丢了),此时如果正好A端不小心复用了ip和port资源,并用来建立新的连接,主动的A会发送SYN,B收到后认为这是个错误的报文,直接RST!
因此,TIME_WAIT状态其实就是等待网络出清,规避各种可能因为网络传输的不确定性造成的问题。
2MSL是什么?
MSL:Max Segment Lifetime,是网络报文的最大生存时间!2MSL,就是2倍这个时间。(TCP报文使用segment这个术语,这暗指了它可以处理很长的数据流stream,而segment只是stream的一部分)
这个2MSL时间有多长呢?
2MSL的时间是从主动方A接收到FIN,回应ACK后,开始计时的。如果在TIME-WAIT时间内,因为主动方A的ACK没有传输到被动房B,主动方A又重复接收到了被动房B重发的FIN报文,那么2MSL时间将重新计时。(这会是一种攻击方式吗?)
修改TIME_WAIT的时长,需要重新编译内核:
$ grep -rnE 'TCP_TIMEWAIT_LEN' --include=*.h
include/net/tcp.h:121:#define TCP_TIMEWAIT_LEN (60*HZ) /* how long to wait to destroy TIME-WAIT
include/net/tcp.h:123:#define TCP_FIN_TIMEOUT TCP_TIMEWAIT_LEN
Server在accept之后,会创建一个新的socket,不会影响那个用于accept的socket。新的socket的端口号也是系统随机分配的,当Server主动关闭了这个连接,进入TIME_WAIT状态的也仅仅只是这个新的socket。Server还可以继续创建新的socket,用不同的port。
两个应用程序同时执行主动打开的情况是可能的,虽然发生的可能性较低。
每一端都发送一个SYN,并传递给对方,且每一端都使用对端所知的端口作为本地端口(主动bind)。但多数伯克利版的tcp/ip实现并不支持同时打开。
这种情况比较糟糕,通信两端都会进入TIME_WAIT状态。
如果应用程序同时发送FIN,或者由于网络传输的原因,导致双方都在发出自己的FIN后,收到对方的FIN,则双方在发送后会首先进入FIN_WAIT_1状态。在收到对端的FIN后,回复一个ACK,会进入CLOSING状态。在收到对端的ACK后,进入TIME_WAIT状态。这种情况称为同时关闭。同时关闭也需要有4次报文交换,与典型的关闭相同。
过多的 TIME-WAIT 状态主要的危害有两种:
主动FIN的一方,主要受系统资源限制:
由于一个四元组表示 TCP 连接,理论上主动关闭连接的服务端可以建立很多连接,因为服务端只监听一个端口,不会因为 TCP 连接过多而导致端口资源受限。但是 TCP 连接过多,会占用系统资源,比如文件描述符、内存资源、CPU 资源、线程资源等。
被动连接方,主动关闭TCP,此TCP对应的不占用port资源的socket,会有TIME_WAIT状态吗?
会的。但是主动连接方或者Server方,都有可能直接发RST,这样就没有TIME_WAIT状态。
Server在12345号端口上listen, accept之后,发送一个welcome消息,然后等待10秒后,close。Client直接在Python解释器里面随手输入。
10:02:31.526543 IP localhost.55056 > localhost.12345: Flags [S], seq 1926553857, win 65495, options [mss 65495,sackOK,TS val 2537488315 ecr 0,nop,wscale 7], length 0
10:02:31.526572 IP localhost.12345 > localhost.55056: Flags [S.], seq 1990950355, ack 1926553858, win 65483, options [mss 65495,sackOK,TS val 2537488315 ecr 2537488315,nop,wscale 7], length 0
10:02:31.526582 IP localhost.55056 > localhost.12345: Flags [.], ack 1, win 512, options [nop,nop,TS val 2537488315 ecr 2537488315], length 0
10:02:31.526955 IP localhost.12345 > localhost.55056: Flags [P.], seq 1:9, ack 1, win 512, options [nop,nop,TS val 2537488315 ecr 2537488315], length 8
10:02:31.526961 IP localhost.55056 > localhost.12345: Flags [.], ack 9, win 512, options [nop,nop,TS val 2537488315 ecr 2537488315], length 0
10:02:41.537577 IP localhost.12345 > localhost.55056: Flags [F.], seq 9, ack 1, win 512, options [nop,nop,TS val 2537498326 ecr 2537488315], length 0
10:02:41.580922 IP localhost.55056 > localhost.12345: Flags [.], ack 10, win 512, options [nop,nop,TS val 2537498369 ecr 2537498326], length 0
10:02:46.088801 IP localhost.55056 > localhost.12345: Flags [F.], seq 1, ack 10, win 512, options [nop,nop,TS val 2537502877 ecr 2537498326], length 0
10:02:46.088815 IP localhost.12345 > localhost.55056: Flags [.], ack 2, win 512, options [nop,nop,TS val 2537502877 ecr 2537502877], length 0
完美的三次握手和四次挥手!
10:04:47.825547 IP localhost.55058 > localhost.12345: Flags [S], seq 262689593, win 65495, options [mss 65495,sackOK,TS val 2537624614 ecr 0,nop,wscale 7], length 0
10:04:47.825560 IP localhost.12345 > localhost.55058: Flags [S.], seq 2935847414, ack 262689594, win 65483, options [mss 65495,sackOK,TS val 2537624614 ecr 2537624614,nop,wscale 7], length 0
10:04:47.825569 IP localhost.55058 > localhost.12345: Flags [.], ack 1, win 512, options [nop,nop,TS val 2537624614 ecr 2537624614], length 0
10:04:47.825988 IP localhost.12345 > localhost.55058: Flags [P.], seq 1:9, ack 1, win 512, options [nop,nop,TS val 2537624614 ecr 2537624614], length 8
10:04:47.825995 IP localhost.55058 > localhost.12345: Flags [.], ack 9, win 512, options [nop,nop,TS val 2537624614 ecr 2537624614], length 0
10:04:57.832942 IP localhost.12345 > localhost.55058: Flags [F.], seq 9, ack 1, win 512, options [nop,nop,TS val 2537634621 ecr 2537624614], length 0
10:04:57.876939 IP localhost.55058 > localhost.12345: Flags [.], ack 10, win 512, options [nop,nop,TS val 2537634665 ecr 2537634621], length 0
10:05:00.360197 IP localhost.55058 > localhost.12345: Flags [R.], seq 1, ack 10, win 512, options [nop,nop,TS val 2537637148 ecr 2537634621], length 0
注意:此时client直接发出了RST报文。
10:07:59.675050 IP localhost.55060 > localhost.12345: Flags [S], seq 4215475499, win 65495, options [mss 65495,sackOK,TS val 2537816463 ecr 0,nop,wscale 7], length 0
10:07:59.675075 IP localhost.12345 > localhost.55060: Flags [S.], seq 2210383874, ack 4215475500, win 65483, options [mss 65495,sackOK,TS val 2537816463 ecr 2537816463,nop,wscale 7], length 0
10:07:59.675095 IP localhost.55060 > localhost.12345: Flags [.], ack 1, win 512, options [nop,nop,TS val 2537816463 ecr 2537816463], length 0
10:07:59.676064 IP localhost.12345 > localhost.55060: Flags [P.], seq 1:9, ack 1, win 512, options [nop,nop,TS val 2537816464 ecr 2537816463], length 8
10:07:59.676081 IP localhost.55060 > localhost.12345: Flags [.], ack 9, win 512, options [nop,nop,TS val 2537816464 ecr 2537816464], length 0
10:08:02.314109 IP localhost.55060 > localhost.12345: Flags [R.], seq 1, ack 9, win 512, options [nop,nop,TS val 2537819102 ecr 2537816464], length 0
不给Server发Fin的机会,Client直接RST。
10:10:28.424276 IP localhost.55062 > localhost.12345: Flags [S], seq 2883372796, win 65495, options [mss 65495,sackOK,TS val 2537965212 ecr 0,nop,wscale 7], length 0
10:10:28.424313 IP localhost.12345 > localhost.55062: Flags [S.], seq 4093837342, ack 2883372797, win 65483, options [mss 65495,sackOK,TS val 2537965212 ecr 2537965212,nop,wscale 7], length 0
10:10:28.424343 IP localhost.55062 > localhost.12345: Flags [.], ack 1, win 512, options [nop,nop,TS val 2537965212 ecr 2537965212], length 0
10:10:28.425555 IP localhost.12345 > localhost.55062: Flags [P.], seq 1:9, ack 1, win 512, options [nop,nop,TS val 2537965214 ecr 2537965212], length 8
10:10:28.425575 IP localhost.55062 > localhost.12345: Flags [.], ack 9, win 512, options [nop,nop,TS val 2537965214 ecr 2537965214], length 0
10:10:30.362500 IP localhost.55062 > localhost.12345: Flags [P.], seq 1:4, ack 9, win 512, options [nop,nop,TS val 2537967151 ecr 2537965214], length 3
10:10:30.362526 IP localhost.12345 > localhost.55062: Flags [.], ack 4, win 512, options [nop,nop,TS val 2537967151 ecr 2537967151], length 0
10:10:33.361355 IP localhost.55062 > localhost.12345: Flags [R.], seq 4, ack 9, win 512, options [nop,nop,TS val 2537970149 ecr 2537967151], length 0
没想到,这个时候Client还是直接发RST。
10:14:05.555841 IP localhost.55066 > localhost.12345: Flags [S], seq 569604261, win 65495, options [mss 65495,sackOK,TS val 2538182344 ecr 0,nop,wscale 7], length 0
10:14:05.555887 IP localhost.12345 > localhost.55066: Flags [S.], seq 1036382583, ack 569604262, win 65483, options [mss 65495,sackOK,TS val 2538182344 ecr 2538182344,nop,wscale 7], length 0
10:14:05.555917 IP localhost.55066 > localhost.12345: Flags [.], ack 1, win 512, options [nop,nop,TS val 2538182344 ecr 2538182344], length 0
10:14:05.556892 IP localhost.12345 > localhost.55066: Flags [P.], seq 1:9, ack 1, win 512, options [nop,nop,TS val 2538182345 ecr 2538182344], length 8
10:14:05.556914 IP localhost.55066 > localhost.12345: Flags [.], ack 9, win 512, options [nop,nop,TS val 2538182345 ecr 2538182345], length 0
10:14:08.567662 IP localhost.55066 > localhost.12345: Flags [P.], seq 1:4, ack 9, win 512, options [nop,nop,TS val 2538185356 ecr 2538182345], length 3
10:14:08.567687 IP localhost.12345 > localhost.55066: Flags [.], ack 4, win 512, options [nop,nop,TS val 2538185356 ecr 2538185356], length 0
10:14:15.568236 IP localhost.12345 > localhost.55066: Flags [R.], seq 9, ack 4, win 512, options [nop,nop,TS val 2538192356 ecr 2538185356], length 0
第一次观察到Server直接发RST的情况!
这不是推荐的做法,TIME_WAIT是我们的朋友,如果服务器有太多TIME_WAIT,可以尝试不要主动关闭TCP,让分散在各个地方的Client去关闭。
强制在close的时候发RST,使用SO_LINGER
:
struct linger so_linger;
so_linger.l_onoff = 1;
so_linger.l_linger = 0;
setsockopt(s, SOL_SOCKET, SO_LINGER, &so_linger,sizeof(so_linger));
如果l_onoff为非 0, 且l_linger值为 0,那么调用close后,会立该发送一个RST标志给对端,该 TCP 连接将跳过四次挥手,也就跳过了TIME_WAIT状态,直接关闭。
Python版本:
import socket
import struct
s = socket.socket()
s.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, struct.pack('ii',1,0))
下面介绍TCP头中,ECE和CWR标志位是如何工作的:
RFC3168还规定的一些实现细节:
太多细节....
本文链接:https://cs.pynote.net/net/tcp/202302281/
-- EOF --
-- MORE --