TCP
是面向连接的、可靠的、基于字节流的传输层通信协议。
TCP
是面向连接的协议,所以使用 TCP
前必须先建立连接,而建立连接是通过三次握手来进行的。
TCP包头结构
在讲解三次握手的过程之前,我们先来看一下 TCP
包的结构:
TCP
包头大小在大多数情况下是固定的,它通常是 20
字节(不包括任何选项),但如果启用了选项,则最多可以达到 60
字节。下面是 TCP
包头的一般结构:
- 源端口号(16位):表示发送方的端口号。
- 目标端口号(16位):表示接收方的端口号。
- 序列号(32位):用于对数据流中的字节进行编号,以便对方能够按顺序重新组装数据。
- 确认号(32位):表示期望接收到的下一个序列号。
- 数据偏移(4位):指示
TCP
包头的长度,以4字节
为单位。因此,数据偏移的值乘以4
就是TCP
包头的总长度。这个字段也被称为头部长度
。 - 保留位(6位):保留供将来使用,目前全部为 0。
- 标志位(6位):用于控制
TCP
连接的状态,包括SYN
、ACK
、FIN
、RST
、PSH
、URG
等。 - 窗口大小(16位):表示发送方的接收窗口大小,用于流量控制。
- 校验和(16位):用于验证
TCP
报文的完整性。 - 紧急指针(16位):当
URG
标志被设置时,紧急指针表示紧急数据的末尾位置。 - 选项(可选):可以包含各种选项,如最大报文段大小(
MSS
)、窗口缩放因子等,每个选项的大小不定。
在三次握手过程中,我们主要关注序列号、确认号以及标志位中的SYN和ACK
三次握手过程
通常来说,服务器会开放监听端口,而客户端则主动连接这个端口,创建连接的时候,会进行三次握手,过程如下图所示:
- 客户端发送
SYN
包到服务器,附上一个随机生成的序列号(ISN
)。此时客户端处于SYN_SEND
状态。 - 服务器返回
SYN+ACK
包到客户端,附上一个随机生成的序列号,确认号则是客户端上传的序列号+1。此时服务端处于SYN_RECV
状态。 - 客户端返回
ACK
到服务器,确认号是服务器下发的序列号+1。此时客户端处于ESTABLISHED
状态,连接已建立,这个包可以顺带发一些数据。 - 服务端收到
ACK
后,也进入ESTABLISHED
状态,可以收发数据。
三次握手的一个重要功能是客户端和服务端交换 ISN(Initial Sequence Number)
,以便让对方知道接下来接收数据的时候如何按序列号组装数据。同时也确保了服务端和客户端的收发都能正常进行。
使用 wireshark
抓包工具,我们可以看到三次握手的数据:
为什么是三次握手?不是两次、四次?
1. TCP 连接使用三次握手的首要原因,是为了防止旧的重复连接初始化造成混乱。
想像一个场景,客户端发了SYN
之后宕机了,重启后又发了新的SYN
。如果只有两次握手的话,当服务器收到旧的SYN
之后,发送ACK
给客户端,就直接进入ESTABLISHED
状态,这时候就可以发数据了。
但是客户端期待的是新的SYN
的序列号,发现服务端发的确认号不对应,会关闭这个连接,而服务器此时已经发了数据过来,这就造成了混乱。
而三次握手,客户端可以收到ACK
之后,判断确认号,正确则返回ACK
,错误则返回RST
告诉服务器关闭这个连接。
使用三次握手和RST
控制消息,将是否建立连接的最终控制权交给了客户端,因为只有客户端有足够的上下文来判断当前连接是否是错误的或者过期的,这也是TCP
使用三次握手建立连接的最主要原因。
2. 三次握手的第二个原因,是为了交互双方的序列号。
TCP
协议的通信双方,都必须维护一个序列号,用来保证数据包的有序,以及丢包时能够重发,所以这个初始化的序列号是很重要的。当客户端发SYN
给服务器时,服务器需要返回ACK
确认,而服务器发SYN
给客户端时,客户端也需要发ACK
确认,才能确保两边都有正确的序列号。服务器在发SYN
和ACK
时,可以合并成一条消息发送,所以是不需要四次握手的。