TCP是面向连接的协议。运输连接是用来传送TCP报文的。运输链接就有三个阶段:连接建立、数据传送、连接释放。
TCP建立连接
TCP建立连接过程中的三个重要问题:
- 要使每一方能够确知对方的存在;
- 要允许双方协商一些参数(eg:最大窗口值);
- 能够对运输实体资源(eg:缓存大小、连接表中的项目等)进行分配。
TCP连接采用客户服务器方式。主动发起连接建立的应用进程叫做客户端。被动等待连接建立的应用程序叫做服务器。
![TCP三次握手.png](https://i.loli.net/2020/10/25/BUtYroWNihSe6V7.png)
一开始,服务器进程创建传输控制块 TCB,准备接受客户进程的连接请求。然后服务器进程处于 LISTEN 状态,等待客户的连接请求。
-
客户进程创建传输控制块 TCB。向服务器进程发送连接请求报文段(同步位
SYN=1
,同时选择一个随机的初始序号值seq=x
),客户进程进入 SYN_SENT 状态;序号值是用来标记TCP数据流中的每一个字节的。同步报文段,不携带数据,但仍要消耗掉一个序号。
-
服务器进程接收到连接请求报文段,如果同意建立连接,则向客户进程发送确认(同步位
SYN=1
,确认位ACK=1
,确认号ack=x+1
,同时也为自己选择一个初始序号seq=y
),服务器进程进入 SYN_RCVD 状态;同步报文段,不携带数据,但仍要消耗掉一个序号。报文段的数据可理解为:我已收到 x,期待收到 x+1。
-
客户进程收到确认后,还要向服务器进程发送确认,客户进程进入 ESTABLISED 状态。
如果不携带数据则不消耗序号,即不带序号。
-
服务器进程收到确认后,也进入 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](https://i.loli.net/2020/10/25/A7n6OUMl8YfCpjg.png)
一开始,客户进程和服务器进程均处于 ESTABLISED 状态。
-
客户进程向服务器进程发出TCP连接释放报文段(终止控制位
FIN=1
,序号seq=u
),并停止再发送数据。客户进程进入 FIN-WAIT-1 状态。不携带数据,但也要消耗掉一个序号。
-
服务器进程收到连接释放报文段后,向客户进程发送确认(
ACK=1, ack=u+1, seq=v
)。服务器进程进入 CLOSE-WAIT 状态,半关闭状态。此时,服务器进程向客户进程发送还未传输完的数据。客户进程没有数据要发送,但若服务器进程发送数据,客户进程仍要接收。
实际抓包的数据:
ACK=1, ack=u
。 -
客户进程收到确认后,进入 FIN-WAIT-2 状态,等待服务器进程发出释放连接请求报文段。
-
若服务器进程已经没有要向客户进程发送的数据,其通知TCP释放连接。向客户进程发送释放连接请求(
FIN=1, ACK=1, ack=u+1, seq=w
)。服务器进程进入 LAST-ACK 状态,等待客户进程的确认。 -
客户进程收到服务器进程的释放连接请求报文,对其进行确认(
ACK=1, ack=w+1, seq=u+1
)。进入 TIME-WAIT 状态,等待2MSL
,客户进程TCP连接最终关闭。 -
服务器进程收到客户进程的确认,进入 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状态?
-
保证客户进程发送的最后一个 ACK 报文能够到达服务进程。
-
防止“已失效的连接请求报文段”出现在本连接中。
客户端发送最后一个ACK报文后,再经过2MSL,本连接的的时间内所产生的所有报文段都从网络中消失。这样下一个新的连接中不会出现就连接的请求报文段。
【问题3】建立连接为什么需要第三个确认报文(为什么客户进程还要发送一次确认)?
为了防止已失效的连接请求报文段突然又传到服务器进程(被动连接端),因而产生错误。
客户进程发出的连接请求,未收到回复确认,于是重传请求报文段。后来建立连接,数据传输完毕后释放了连接,此时延迟的失效报文段到达服务器进程,被误以为一次新的连接请求。于是服务器进程发出确认报文段,同一建立连接。假定没有第三个报文段进行确认,新的连接就建立了。白白浪费资源。