TCP三次握手
TCP三次握手
TCP 是面向连接的协议,所以使用 TCP 前必须先建立连接,而建立连接是通过三次握手来进行的。三次握手的过程如下图:
一开始,客户端和服务端都处于
CLOSE
状态。先是服务端主动监听某个端口,处于LISTEN
状态客户端会随机初始化序号(
client_isn
),将此序号置于 TCP 首部的「序号」字段中,同时把SYN
标志位置为1
,表示SYN
报文。接着把第一个 SYN 报文发送给服务端,表示向服务端发起连接,该报文不包含应用层数据,之后客户端处于SYN-SENT
状态。服务端收到客户端的
SYN
报文后,首先服务端也随机初始化自己的序号(server_isn
),将此序号填入 TCP 首部的「序号」字段中,其次把 TCP 首部的「确认应答号」字段填入client_isn + 1
, 接着把SYN
和ACK
标志位置为1
。最后把该报文发给客户端,该报文也不包含应用层数据,之后服务端处于SYN-RCVD
状态。客户端收到服务端报文后,还要向服务端回应最后一个应答报文,首先该应答报文 TCP 首部
ACK
标志位置为1
,其次「确认应答号」字段填入server_isn + 1
,最后把报文发送给服务端,这次报文可以携带客户到服务端的数据,之后客户端处于ESTABLISHED
状态。服务端收到客户端的应答报文后,也进入
ESTABLISHED
状态。
为什么是三次握手?而不是两次、四次
原因一:避免历史连接
我们考虑一个场景,客户端先发送了 SYN(seq = 90)
报文,然后客户端宕机了,而且这个 SYN
报文还被网络阻塞了,服务端并没有收到,接着客户端重启后,又重新向服务端建立连接,发送了 SYN(seq = 100)
报文(注意!不是重传 SYN
,重传的 SYN
的序列号是一样的)。
- 一个「旧
SYN
报文」比「最新的SYN
」 报文早到达了服务端,那么此时服务端就会回一个SYN + ACK
报文给客户端,此报文中的确认号是91(90+1)
。 - 客户端收到后,发现自己期望收到的确认号应该是
100 + 1
,而不是90 + 1
,于是就会回RST
报文。 - 服务端收到
RST
报文后,就会释放连接。 - 后续最新的
SYN
抵达了服务端后,客户端与服务端就可以正常的完成三次握手了。
如果服务端在收到 RST
报文之前,先收到了「新 SYN
报文」,也就是服务端收到客户端报文的顺序是:「旧 SYN
报文」->「新 SYN
报文」,此时会发生什么?
- 当服务端第一次收到
SYN
报文,也就是收到 「旧SYN
报文」时,就会回复SYN + ACK
报文给客户端,此报文中的确认号是91(90+1)
。 - 然后这时再收到「新
SYN
报文」时,就会回Challenge Ack
报文给客户端,这个 ack 报文并不是确认收到「新 SYN 报文」的,而是上一次的 ack 确认号,也就是91(90+1)
。所以客户端收到此ACK
报文时,发现自己期望收到的确认号应该是101
,而不是91
,于是就会回RST
报文。
如果是两次握手连接,就无法阻止历史连接,那为什么 TCP 两次握手为什么无法阻止历史连接呢?
- 主要是因为在两次握手的情况下,服务端没有中间状态给客户端来阻止历史连接,导致服务端可能建立一个历史连接,造成资源浪费。
- 在两次握手的情况下,服务端在收到 SYN 报文后,就进入
ESTABLISHED
状态,意味着这时可以给对方发送数据,但是客户端此时还没有进入ESTABLISHED
状态,假设这次是历史连接,客户端判断到此次连接为历史连接,那么就会回RST
报文来断开连接,而服务端在第一次握手的时候就进入ESTABLISHED
状态,所以它可以发送数据的,但是它并不知道这个是历史连接,它只有在收到RST
报文后,才会断开连接。
客户端发送三次握手(ack 报文)后就可以发送数据了,而被动方此时还是 syn_received 状态,如果 ack 丢了,那客户端发的数据是不是也白白浪费了?
- 不是的,即使服务端还是在 syn_received 状态,收到了客户端发送的数据,还是可以建立连接的,并且还可以正常收到这个数据包。这是因为数据报文中是有 ack 标识位,也有确认号,这个确认号就是确认收到了第二次握手
原因二:同步双方初始序列号
TCP 协议的通信双方, 都必须维护一个「序列号」, 序列号是可靠传输的一个关键因素,它的作用:
- 接收方可以去除重复的数据;
- 接收方可以根据数据包的序列号按序接收;
- 可以标识发送出去的数据包中, 哪些是已经被对方收到的(通过
ACK
报文中的序列号知道);
四次握手其实也能够可靠的同步双方的初始化序号,但由于第二步和第三步可以优化成一步,所以就成了「三次握手」。
而两次握手只保证了一方的初始序列号能被对方成功接收,没办法保证双方的初始序列号都能被确认接收
原因三:避免资源浪费
如果只有「两次握手」,当客户端发生的 SYN
报文在网络中阻塞,客户端没有接收到 ACK
报文,就会重新发送 SYN
,由于没有第三次握手,服务端不清楚客户端是否收到了自己回复的 ACK
报文,所以服务端每收到一个 SYN
就只能先主动建立一个连接,这会造成什么情况呢?
如果客户端发送的 SYN
报文在网络中阻塞了,重复发送多次 SYN
报文,那么服务端在收到请求后就会建立多个冗余的无效链接,造成不必要的资源浪费。
为什么每次建立TCP连接时,初始化的序列号都要求不一样
主要原因有两个方面:
- 为了防止历史报文被下一个相同四元组的连接接收(主要方面);
- 为了安全性,防止黑客伪造的相同序列号的 TCP 报文被对方接收;
接下来,详细说说第一点。
过程如下:
- 客户端和服务端建立一个 TCP 连接,在客户端发送数据包被网络阻塞了,然后超时重传了这个数据包,而此时服务端设备断电重启了,之前与客户端建立的连接就消失了,于是在收到客户端的数据包的时候就会发送 RST 报文。
- 紧接着,客户端又与服务端建立了与上一个连接相同四元组的连接;
- 在新连接建立完成后,上一个连接中被网络阻塞的数据包正好抵达了服务端,刚好该数据包的序列号正好是在服务端的接收窗口内,所以该数据包会被服务端正常接收,就会造成数据错乱。
可以看到,如果每次建立连接,客户端和服务端的初始化序列号都是一样的话,很容易出现历史报文被下一个相同四元组的连接接收的问题。
第一次握手丢失了,会发生什么?
当客户端想和服务端建立TCP
连接的时候,首先第一个发的就是 SYN 报文,然后进入到 SYN_SENT
状态。
在这之后,如果客户端迟迟收不到服务端的SYN-ACK
报文(第二次握手),就会触发「超时重传」机制,重传 SYN 报文,而且重传的 SYN 报文的序列号都是一样的。
不同版本的操作系统可能超时时间不同,有的 1 秒的,也有 3 秒的,这个超时时间是写死在内核里的,如果想要更改则需要重新编译内核,比较麻烦。
当客户端在 1 秒后没收到服务端的SYN-ACK
报文后,客户端就会重发SYN
报文,那到底重发几次呢?
在 Linux 里,客户端的SYN
报文最大重传次数由 tcp_syn_retries
内核参数控制,这个参数是可以自定义的,默认值一般是 5
通常,第一次超时重传是在 1 秒后,第二次超时重传是在 2 秒,第三次超时重传是在 4 秒后,第四次超时重传是在 8 秒后,第五次是在超时重传 16 秒后。没错,每次超时的时间是上一次的 2 倍。
当第五次超时重传后,会继续等待 32 秒,如果服务端仍然没有回应ACK
,客户端就不再发送SYN
包,然后断开TCP
连接。
所以,总耗时是1+2+4+8+16+32=63
秒,大约 1 分钟左右。
第二次握手丢失了,会发生什么?
第二次握手的 SYN-ACK
报文其实有两个目的 :
- 第二次握手里的 ACK, 是对第一次握手的确认报文;
- 第二次握手里的 SYN,是服务端发起建立 TCP 连接的报文;
对于客户端
因为第二次握手报文里是包含对客户端的第一次握手的 ACK 确认报文,所以,如果客户端迟迟没有收到第二次握手,那么客户端就觉得可能自己的 SYN 报文(第一次握手)丢失了,于是客户端就会触发超时重传机制,重传 SYN 报文。
对于服务端
因为第二次握手中包含服务端的 SYN 报文,所以当客户端收到后,需要给服务端发送 ACK 确认报文(第三次握手),服务端才会认为该 SYN 报文被客户端收到了。
那么,如果第二次握手丢失了,服务端就收不到第三次握手,于是服务端这边会触发超时重传机制,重传 SYN-ACK 报文。
因此,当第二次握手丢失了,客户端和服务端都会重传:
- 客户端会重传 SYN 报文,也就是第一次握手,最大重传次数由
tcp_syn_retries
内核参数决定; - 服务端会重传 SYN-ACK 报文,也就是第二次握手,最大重传次数由
tcp_synack_retries
内核参数决定。
第三次握手丢失,会发生什么?
因为这个第三次握手的 ACK 是对第二次握手的 SYN 的确认报文,所以当第三次握手丢失了,如果服务端那一方迟迟收不到这个确认报文,就会触发超时重传机制,重传 SYN-ACK 报文,直到收到第三次握手,或者达到最大重传次数。
注意,ACK 报文是不会有重传的,当 ACK 丢失了,就由对方重传对应的报文。