一文彻底搞懂 TCP 三次握手、四次挥手过程及原理

TCP 协议简述

TCP 提供面向有连接的通信传输,面向有连接是指在传送数据之前必须先建立连接,数据传送完成后要释放连接。
无论哪一方向另一方发送数据之前,都必须先在双方之间建立一条连接。在 TCP/IP 协议中,TCP 协议提供可靠的连接服务,连接是通过 三次握手 进行初始化的。
同时由于 TCP 协议是一种面向连接的、可靠的、基于字节流的运输层通信协议,TCP 是 全双工模式,所以需要 四次挥手 关闭连接。

TCP 包首部

网络中传输的数据包由两部分组成:一部分是协议所要用到的首部,另一部分是上一层传过来的数据。首部的结构由协议的具体规范详细定义。在数据包的首部,明确标明了协议应该如何读取数据。反过来说,看到首部,也就能够了解该协议必要的信息以及所要处理的数据。包首部就像协议的脸。

所以在学习 TCP 协议之前,首先要知道 TCP 在网络传输中处于哪个位置,以及它的协议的规范,下面我们就看看 TCP 首部的网络传输起到的作用:

image

下面的图是 TCP 头部的规范定义,它定义了 TCP 协议如何读取和解析数据:

image

TCP 首部承载这 TCP 协议需要的各项信息,下面我们来分析一下:

TCP 端口号

TCP 的连接是需要四个要素确定唯一一个连接:

(源IP,源端口号)+ (目地IP,目的端口号)

所以 TCP 首部预留了两个 16 位作为端口号的存储,而 IP 地址由上一层 IP 协议负责传递,源端口号和目地端口各占 16 位两个字节,也就是端口的范围是 2^16=65535,另外 1024 以下是系统保留的,从 1024-65535 是用户使用的端口范围。

TCP 的序号和确认号

32位序号 seq:Sequence number 缩写 seq ,TCP 通信过程中某一个传输方向上的字节流的每个字节的序号,通过这个来确认发送的数据有序,比如现在序列号为 1000,发送了 1000,下一个序列号就是 2000。
32位确认号 ack:Acknowledge number 缩写 ack,TCP 对上一次 seq 序号做出的确认号,用来响应 TCP 报文段,给收到的 TCP 报文段的序号 seq 加 1。

TCP 的标志位

每个 TCP 段都有一个目的,这是借助于 TCP 标志位选项来确定的,允许发送方或接收方指定哪些标志应该被使用,以便段被另一端正确处理。
用的最广泛的标志是 SYNACKFIN,用于建立连接,确认成功的段传输,最后终止连接。

SYN:简写为S,同步标志位,用于建立会话连接,同步序列号;
ACK:简写为.,确认标志位,对已接收的数据包进行确认;
FIN:简写为F,完成标志位,表示我已经没有数据要发送了,即将关闭连接;
PSH:简写为P,推送标志位,表示该数据包被对方接收后应立即交给上层应用,而不在缓冲区排队;
RST:简写为R,重置标志位,用于连接复位、拒绝错误和非法的数据包;
URG:简写为U,紧急标志位,表示数据包的紧急指针域有效,用来保证连接不被阻断,并督促中间设备尽快处理;

TCP 三次握手建立连接

所谓三次握手 (Three-way Handshake),是指建立一个 TCP 连接时,需要客户端和服务器总共发送3个报文。

三次握手的目的是连接服务器指定端口,建立 TCP 连接,并同步连接双方的序列号和确认号,交换 TCP 窗口大小信息。在 socket 编程中,客户端执行 connect() 时,将触发三次握手。

三次握手过程的示意图如下:

image

第一次握手

客户端将 TCP 报文标志位 SYN 置为 1,随机产生一个序号值 seq = J,保存在 TCP 首部的序列号 (Sequence Number) 字段里,指明客户端打算连接的服务器的端口,并将该数据包发送给服务器端,发送完毕后,客户端进入 SYN_SENT 状态,等待服务器端确认。

第二次握手

服务器端收到数据包后由标志位 SYN = 1 知道客户端请求建立连接,服务器端将 TCP 报文标志位 SYN 和 ACK 都置为 1,ack = J + 1,随机产生一个序号值 seq = K ,并将该数据包发送给客户端以确认连接请求,服务器端进入 SYN_RCVD 状态。

第三次握手

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

第三次握手允许携带数据

注意:上面写的 ack 和 ACK 不是同一个概念

小写的 ack 代表的是头部的确认号 Acknowledge number, 缩写 ack ,是对上一个包的序号进行确认的号,ack=seq+1。
大写的 ACK 是上面说的 TCP 首部的标志位,用于标志的 TCP 包是否对上一个包进行了确认操作,如果确认了,则把 ACK 标志位设置成1。

下面我自己做实验,开一个 HTTP 服务,监听 80 端口,然后使用 Tcpdump 命令抓包,看一下 TCP 三次握手的过程:

1
2
3
4
5
6
7
8
9
10
11

sudo tcpdump -n -t -S -i enp0s3 port 80

第一次握手,标志位Flags=S
IP 10.0.2.2.51323 > 10.0.2.15.80: Flags [S], seq 84689409, win 65535, options [mss 1460], length 0
第二次握手,标志位Flags=[S.]
IP 10.0.2.15.80 > 10.0.2.2.51323: Flags [S.], seq 1893430205, ack 84689410, win 64240, options [mss 1460], length 0
第三次握手,标志位Flags=[.]
IP 10.0.2.2.51323 > 10.0.2.15.80: Flags [.], ack 1893430206, win 65535, length 0
建立连接后,客户端发送http请求
IP 10.0.2.2.51321 > 10.0.2.15.80: Flags [P.], seq 1:753, ack 1, win 65535, length 752: HTTP: GET / HTTP/1.1

tcpdump命令解析一下:

-i: 指定抓包的网卡是enp0s3
-n: 把域名转成IP显示
-t: 不显示时间
-S: 序列号使用绝对数值,不指定-S的话,序列号会使用相对的数值
port: 指定监听端口是80
host: 指定监听的主机名

实战中TCP的三次握手过程:

第一次握手,客户端51323端口号向服务器端80号端口发起连接,此时标志位flags=S,即SYN=1标志,表示向服务端发起连接的请求,同时生成序列号seq=84689409
第二次握手,服务端标志位flags=[S.],即SYN+ACK标志位设置为1,表示对上一个请求连接的报文进行确认,同时设置ack=seq+1=184689410,生成序列号seq=1893430205
第三次握手,客户端对服务端的响应进行确认,所以此时标志位是[.]即ACK=1,同时返回对上一个报文的seq的确认号,ack=1893430206
至此,三次握手完成,一个TCP连接建立完成,接下来就是双端传输数据了

为什么需要三次握手

假设 client 发出的第一个连接请求报文段并没有丢失,而是在某个网络结点长时间的滞留了,以致延误到连接释放以后的某个时间才到达 server。

本来这是一个早已失效的报文段。但 server 收到此失效的连接请求报文段后,就误认为是 client 再次发出的一个新的连接请求。于是就向 client 发出确认报文段,同意建立连接。

假设不采用“三次握手”,那么只要 server 发出确认,新的连接就建立了。由于现在 client 并没有发出建立连接的请求,因此不会理睬 server 的确认,也不会向 server 发送数据。但 server 却以为新的运输连接已经建立,并一直等待 client 发来数据。这样,server 的很多资源就白白浪费掉了。

所以,采用“三次握手”的办法可以防止上述现象发生。例如刚才那种情况,client 不会向 server 的确认发出确认。server 由于收不到确认,就知道 client 并没有要求建立连接。

TCP 四次挥手关闭连接

四次挥手即终止 TCP 连接,就是指断开一个 TCP 连接时,需要客户端和服务端总共发送 4 个包以确认连接的断开。

在 socket 编程中,这一过程由客户端或服务端任一方执行 close 来触发。

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

四次挥手过程的示意图如下:

image

挥手请求可以是 Client 端,也可以是 Server 端发起的,我们假设是 Client 端发起:

第一次挥手

Client 端发起挥手请求,向 Server 端发送标志位是 FIN 报文段,设置序列号 seq ,此时,Client 端进入 FIN_WAIT_1 状态,这表示 Client 端没有数据要发送给 Server 端了。

第二次挥手

Server 端收到了 Client 端发送的 FIN 报文段,向 Client 端返回一个标志位是 ACK 的报文段,ack 设为 seq + 1,Client 端进入 FIN_WAIT_2 状态,Server 端告诉 Client 端,我确认并同意你的关闭请求。

第三次挥手

Server 端向 Client 端发送标志位是 FIN 的报文段,请求关闭连接,同时 Client 端进入 LAST_ACK 状态。

第四次挥手

Client 端收到 Server 端发送的 FIN 报文段,向 Server 端发送标志位是 ACK 的报文段,然后 Client 端进入 TIME_WAIT 状态。Server 端收到 Client 端的 ACK 报文段以后,就关闭连接。此时,Client 端 等待 2MSL 的时间后依然没有收到回复,则证明 Server 端已正常关闭,那好,Client 端也可以关闭连接了。

为什么关闭的时候却是四次握手?

建立连接时因为当 Server 端收到 Client 端的 SYN 连接请求报文后,可以直接发送 SYN + ACK 报文。其中 ACK 报文是用来应答的,SYN 报文是用来同步的,所以建立连接只需要三次握手。

由于TCP协议是一种面向连接的、可靠的、基于字节流的运输层通信协议,TCP是全双工模式。这就意味着,关闭连接时,当 Client 端发出 FIN 报文段时,只是表示 Client 端告诉 Server 端数据已经发送完毕了。当 Server 端收到 FIN 报文并返回 ACK 报文段,表示它已经知道 Client 端没有数据发送了,但是 Server 端还是可以发送数据到 Client 端的,所以 Server 很可能并不会立即关闭 SOCKET ,直到 Server 端把数据也发送完毕。当 Server 端也发送了 FIN 报文段时,这个时候就表示 Server 端也没有数据要发送了,就会告诉 Client 端,我也没有数据要发送了,之后彼此就会愉快的中断这次TCP连接。

为什么要等待 2MSL ?

MSL:报文段最大生存时间,它是任何报文段被丢弃前在网络内的最长时间。

有以下两个原因:

第一点:保证 TCP 协议的全双工连接能够可靠关闭。由于 IP 协议的不可靠性或者是其它网络原因,导致了 Server 端没有收到 Client 端的 ACK 报文,那么 Server 端就会在超时之后重新发送 FIN ,如果此时 Client 端的连接已经关闭处于 CLOESD 状态,那么重发的 FIN 就找不到对应的连接了,从而导致连接错乱,所以,Client 端发送完最后的 ACK 不能直接进入 CLOSED 状态,而要保持 TIME_WAIT ,当再次收到 FIN 的收,能够保证对方收到 ACK ,最后正确关闭连接。

第二点:保证这次连接的重复数据段从网络中消失。如果 Client 端发送最后的 ACK 直接进入 CLOSED 状态,然后又再向 Server 端发起一个新连接,这时不能保证新连接的与刚关闭的连接的端口号是不同的,也就是新连接和老连接的端口号可能一样了,那么就可能出现问题:如果前一次的连接某些数据滞留在网络中,这些延迟数据在建立新连接后到达Client端,由于新老连接的端口号和IP都一样,TCP协议就认为延迟数据是属于新连接的,新连接就会接收到脏数据,这样就会导致数据包混乱。所以TCP连接需要在TIME_WAIT状态等待2倍MSL,才能保证本次连接的所有数据在网络中消失。

----------本文结束感谢您的阅读----------
xiaolong wechat
一只程序猿对世界的不完全理解