首页 > 技术文章 > “三次握手”与“四次挥手”

jakelin 2021-01-25 14:36 原文

TCP是面向连接的协议。运输连接是用来传送TCP报文的。运输链接就有三个阶段:连接建立数据传送连接释放

TCP建立连接

TCP建立连接过程中的三个重要问题:

  1. 要使每一方能够确知对方的存在;
  2. 要允许双方协商一些参数(eg:最大窗口值);
  3. 能够对运输实体资源(eg:缓存大小、连接表中的项目等)进行分配。

TCP连接采用客户服务器方式。主动发起连接建立的应用进程叫做客户端。被动等待连接建立的应用程序叫做服务器

TCP三次握手.png

一开始,服务器进程创建传输控制块 TCB,准备接受客户进程的连接请求。然后服务器进程处于 LISTEN 状态,等待客户的连接请求。

  1. 客户进程创建传输控制块 TCB。向服务器进程发送连接请求报文段(同步位 SYN=1 ,同时选择一个随机的初始序号值 seq=x ),客户进程进入 SYN_SENT 状态;

    序号值是用来标记TCP数据流中的每一个字节的。同步报文段,不携带数据,但仍要消耗掉一个序号

  2. 服务器进程接收到连接请求报文段,如果同意建立连接,则向客户进程发送确认(同步位 SYN=1,确认位 ACK=1,确认号 ack=x+1,同时也为自己选择一个初始序号 seq=y),服务器进程进入 SYN_RCVD 状态;

    同步报文段,不携带数据,但仍要消耗掉一个序号。报文段的数据可理解为:我已收到 x,期待收到 x+1

  3. 客户进程收到确认后,还要向服务器进程发送确认,客户进程进入 ESTABLISED 状态。

    如果不携带数据则不消耗序号,即不带序号。

  4. 服务器进程收到确认后,也进入 ESTABLISED 状态。TCP连接建立,接下来进行数据传送。

TCP建立连接的过程,有三个报文段,故该过程叫做三报文握手,即“三次握手”。如果将服务器进程确认报文段拆分成两个报文段:1、确认报文段(ACK=1, ack=x+1),2、同步报文(SYN=1, seq=1)。这样的过程就变成了四报文握手

# sudo tcpdump -i eth0 -nt '(src 172.20.12.30 and dst 172.20.12.27) or (src 172.20.12.27 and dst 172.20.12.30)'
# 1、同步报文段:SYN=1, seq=3411381857 
IP 172.20.12.27.38012 > 172.20.12.30.80: Flags [S], seq 3411381857, win 29200, options [mss 1460,sackOK,TS val 3763862309 ecr 0,nop,wscale 7], length 0
# 2、同步报文段:SYN=1, seq=3063678231, ack=3411381858
IP 172.20.12.30.80 > 172.20.12.27.38012: Flags [S.], seq 3063678231, ack 3411381858, win 28960, options [mss 1460,sackOK,TS val 2338707997 ecr 3763862309,nop,wscale 7], length 0
# 3、确认报文段:ACK=1, ack=1   没有携带数据不消耗序号,没-S参数,ack值是相对偏移值
IP 172.20.12.27.38012 > 172.20.12.30.80: Flags [.], ack 1, win 229, options [nop,nop,TS val 3763862310 ecr 2338707997], length 0

TCP释放连接

释放连接时中断端可以是客户端,也可以是服务器端。假设是客户进程发起中断连接请求。

TCP四次挥手.png

一开始,客户进程和服务器进程均处于 ESTABLISED 状态。

  1. 客户进程向服务器进程发出TCP连接释放报文段(终止控制位 FIN=1,序号 seq=u),并停止再发送数据。客户进程进入 FIN-WAIT-1 状态。

    不携带数据,但也要消耗掉一个序号。

  2. 服务器进程收到连接释放报文段后,向客户进程发送确认(ACK=1, ack=u+1, seq=v)。服务器进程进入 CLOSE-WAIT 状态,半关闭状态。此时,服务器进程向客户进程发送还未传输完的数据。

    客户进程没有数据要发送,但若服务器进程发送数据,客户进程仍要接收。

    实际抓包的数据:ACK=1, ack=u

  3. 客户进程收到确认后,进入 FIN-WAIT-2 状态,等待服务器进程发出释放连接请求报文段。

  4. 若服务器进程已经没有要向客户进程发送的数据,其通知TCP释放连接。向客户进程发送释放连接请求(FIN=1, ACK=1, ack=u+1, seq=w)。服务器进程进入 LAST-ACK 状态,等待客户进程的确认。

  5. 客户进程收到服务器进程的释放连接请求报文,对其进行确认(ACK=1, ack=w+1, seq=u+1)。进入 TIME-WAIT 状态,等待 2MSL,客户进程TCP连接最终关闭。

  6. 服务器进程收到客户进程的确认,进入 CLOSED 状态。

# 1、结束报文段:FIN=1, ACK=1, seq=484, ack=5
IP 172.20.12.30.80 > 172.20.12.27.38728: Flags [F.], seq 484, ack 5, win 227, options [nop,nop,TS val 2340566655 ecr 3765720984], length 0
# 2、确认报文段:ACK=1, ack=484  注意:此处ack值没有+1 
IP 172.20.12.27.38728 > 172.20.12.30.80: Flags [.], ack 484, win 237, options [nop,nop,TS val 3765720984 ecr 2340566655], length 0
# 3、结束报文段:FIN=1, ACK=1, seq=5, ack=485  此处ack值+1了
IP 172.20.12.27.38728 > 172.20.12.30.80: Flags [F.], seq 5, ack 485, win 237, options [nop,nop,TS val 3765720984 ecr 2340566655], length 0
# 4、确认报文段:ACK=1, ack=5
IP 172.20.12.30.80 > 172.20.12.27.38728: Flags [.], ack 6, win 227, options [nop,nop,TS val 2340566656 ecr 3765720984], length 0

问题注意

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

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

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

  1. 保证客户进程发送的最后一个 ACK 报文能够到达服务进程。

  2. 防止“已失效的连接请求报文段”出现在本连接中。

    客户端发送最后一个ACK报文后,再经过2MSL,本连接的的时间内所产生的所有报文段都从网络中消失。这样下一个新的连接中不会出现就连接的请求报文段。

【问题3】建立连接为什么需要第三个确认报文(为什么客户进程还要发送一次确认)?

为了防止已失效的连接请求报文段突然又传到服务器进程(被动连接端),因而产生错误。

客户进程发出的连接请求,未收到回复确认,于是重传请求报文段。后来建立连接,数据传输完毕后释放了连接,此时延迟的失效报文段到达服务器进程,被误以为一次新的连接请求。于是服务器进程发出确认报文段,同一建立连接。假定没有第三个报文段进行确认,新的连接就建立了。白白浪费资源。

推荐阅读