以下分析,均在linux环境下进行抓包;
抓包命令:
tcpdump -i any port 端口 -s 0 -w 文件
,使用root用户进行抓包;
并用wireshark进行包分析。
以下为博主机器下的抓包信息:
1.三次握手
1.1建立握手
三次握手连接的步骤:
-
首先,客户端先对服务端发起SYN包(请求建立连接)(seq = 0),发送完毕之后,客户端将自己状态设置为SYN_SENT状态;
-
服务端收到客户端的连接请求,将自己状态设置为SYN_RCVD,并且给客户端发送SYN包(请求建立连接),ACK包(
该包有两层含义:①当前服务端收到了客户端发来的SYN包(计算该包的大小),②下一次客户端给服务端发送数据包的时候,包序为该ack的值)
(seq = 0, ack = 1);
-
客户端收到服务端发来的SYN + ACK数据包之后,表明建立连接成功了,将自身状态设置为ESTABLISHED状态,并给服务端发送ACK数据包(
该数据包中,①包含客户端下一次发送的包序,②服务端下一次给客户端发送数据时的包序)
(seq = 1, ack = 1),服务端将状态设置为ESTABLISHED。
包序:在客户端和服务端中,各存在着一套包序。建立连接时,SYN(seq = 0),表明当前客户端(服务端)的包序是从0开始的,则后面发送数据包时,其包序应该大于当前的包序(seq = 0),包序也不一定是从0开始的。
ACK数据包不消耗包序,是用来确认发送的数据包是否被接收到,如果在一段时间内发送数据方没有收到ACK数据包,则会对数据进行重发。
为什么客户端还要发送一次ACK确认数据包呢?
主要是为了防止已失效的连接请求数据包(SYN)突然又传到了服务端;
-
正常情况:客户端发起连接请求,但因连接请求数据包(SYN)丢失而没有收到确定数据包(ACK),于是客户端再重传一次连接请求(SYN),后续收到了确认数据包(ACK),建立了连接。数据传输完毕之后就释放了连接,客户端发送了两次连接请求数据包,第一个丢失,第二个抵达了服务端,没有“已失效的连接请求数据包”。
-
异常情况:客户端发出的第一个连接请求数据包(SYN)并没有丢失,而是在某些网络结点长时间滞留了,以致延误到连接释放以后的某个时间才到达服务端,这是一个已经失效的数据包(SYN),但服务端收到此失效的连接请求数据包(SYN)后,误认为客户端又发起了一次新的连接,于是向客户端发出了确认数据包(ACK),同意建立连接。如果不采用三次握手,那么服务端发出确认数据包(ACK)后,新的连接就建立了;
-
但是目前客户端并没有发起连接请求(SYN),则不会对服务端的确定数据包(ACK)进行确认,也不会对服务端发送数据。但服务端却以为新的连接已经建立,并一直等待客户端发送数据,服务端的资源就被白白浪费了;
-
三次握手,就可解决上面的问题,客户端不会向服务端发送确认数据包(ACK),服务端由于收不到确认,就知道客户端并没有请求建立连接。
1.2 抓包分析
客户端对服务端发起连接。
源端口 目的端口 发送数据包 数据长度
客户端 37828 9999 SYN seq = 0 len = 0
服务端 9999 37828 SYN ACK seq = 0 ack = 1 len = 0
客户端 37828 9999 ACK seq = 1 ack = 1 len = 0
连接请求数据包SYN不发送数据,但消耗一个包序。
2.数据收发
三次握手连接建立后,就可以正常的数据收发。
上图中,客户端先给服务端发送数据(nihaoa),该数据长度为6个字节;自三次握手建立之后,客户端维护的seq序列为1,则服务端给客户端确认应答时,ack = 1 + 6 = 7;
服务端再给客户端发送数据(wohenhao),该数据长度为8个字节;自三次握手建立之后,服务端维护的seq序列为1,则客户端给服务端确认应答时,ack = 1 + 8 = 9;
源端口 目的端口 发送数据包 数据长度
客户端 37828 9999 PSH ACK seq = 1 ack = 1 len = 6
服务端 9999 37828 ACK seq = 1 ack = 7 len = 0
服务端 9999 37828 PSH ACK seq = 1 ack = 7 len = 8
客户端 37828 9999 ACK seq = 7 ack = 9 len = 0
3.四次挥手
FIN数据包不发送数据,但消耗一个包序。
四次挥手步骤:
-
客户端先发起断开连接,客户端状态变为
FIN_WAIT_1
,序列号为前面数据收发之后的序列号; -
服务端收到连接释放请求,将状态设置为
CLOSE_WAIT
,并对释放请求进行应答,
ACK(seq = 9, ack = 8)
;此时TCP连接处于
半关闭状态
,即
客户端已经没有要发送的数据了,但若是服务端要发送数据,客户端仍需要接收,服务端到客户端这个方向的连接并未关闭
,可能会持续一段时间。 -
客户端收到服务端的
ACK
应答后,就进入
FIN_WAIT_2
状态,等待服务端发出的连接释放请求; -
若服务端没有向客户端发送的数据了,则服务端发送FIN数据包,并将状态设置为
LAST_ACK
,等待客户端的确定,然后断开连接; -
客户端收到服务端的释放请求后,必须对该请求进行应答,发送ACK数据包,并且进入到
TIME_WAIT
状态。在
TIME_WAIT
状态下,TCP连接还没有释放掉,必须经过时间等待计时器(
TIME-WAIT timer
)设置的时间
2MSL
后,客户端才进入到
CLOSED
状态; -
MSL:最长报文段寿命,RFC793建议设置为2分钟,TCP允许根据实际情况设置更小的MSL值,因此从客户端进入到
TIME_WAIT
状态之后,要经过4分钟才能进入到
CLOSED
状态,才能开始建立下一个新的连接。
为什么客户端在TIME_WAIT必须等待2MSL的时间?
-
为了保证客户端最后一次发送的
ACK
数据包能够到达服务端,这个
ACK
数据包很可能会丢失,则服务端(
LAST_ACK
状态)接收不到对方的
ACK
应答,服务端就会超时重传这个
FIN
数据包,而客户端在2MSL时间内能收到这个FIN数据包,接着客户端重传一个ACK应答,重新启动
2MSL
计时器,最后客户端和服务端都会进入到
CLOSED
状态。如果不这样,服务端就无法按照正常步骤进入
CLOSED
状态;
-
防止“已失效的连接请求数据包”出现在本连接中,客户端在发送完最后一个
ACK
应答后,再经过
2MSL
时间,就可以使本连接持续时间内产生的所有数据包都消失,这样就可以使下一个新的连接种不会出现这种旧的连接请求数据包。
2MSL = 丢失ACK的MSL + 重传的FIN报文的MSL。
客户端先断开连接,但是客户端要等待2MSL时间,所以一般情况下,都是客户端先断开连接,服务端作为被断开方,可节约服务端的资源。
3.1 TIME_WAIT状态导致服务端无法快速启动的问题
TIME_WAIT
状态只有主动断开连接方才会拥有:
-
当进程正常调用close之后,进程很快的就结束掉了,但之前进程所占的端口并没有被释放;
-
原因在于主动断开链接方的状态是
TIME_WAIT
状态到CLOSED状态需要等待2MSL的时间。
ACK + 重传的FIN的MSL。
既然要收到重传MSL的FIN报文,那么刚刚监听/使用的端口一定不能被释放,如果释放了,从网卡中接收数据,经过网络协议栈层层分用之后,到达TCP之后,TCP就无法处理这个数据包,因为不知道这个数据之前是哪一个链接的。
主动断开连接方在TIME_WAIT到CLOSED这个状态之间,之前监听/使用的端口并没有释放。
设置端口复用:
int opt = 1;
setsockopt(listen, SOL_SOCKET, SO_REUSERADDR, &opt, sizeof(opt));
参数 | 解释 |
---|---|
listen | 侦听套接字 |
SOL_SOCKET | 套接字选项 |
SO_REUSERADDR | 重用端口 |
opt | 设置为1,则开启端口重用 |
3.2 抓包分析
这是博主机器上抓的,只有三次挥手。
分析为什么只有三次挥手:
-
ACK作为TCP协议的头部,则无论是否有数据发送,ACK都是存在的;
-
客户端先发送一个FIN ACK数据包,代表此时客户端没有数据要发送了,需要断开连接了;
-
服务端此时,也没有数据需要发送,则不单独发送一个ACK应答,服务端也直接进行断开连接请求(FIN) + ACK ,这里的ACK也就直接应答了客户端的FIN请求;
-
客户端对服务端的断开连接请求进行应答(ACK)。
源端口 目的端口 发送数据包 数据长度
客户端 37828 9999 FIN ACK seq = 7 ack = 9 len = 0
服务端 9999 37828 FIN ACK seq = 9 ack = 8 len = 0
客户端 37828 9999 ACK seq = 8 ack = 10 len = 0