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
状态。
从上面的过程可以发现第三次握手是可以携带数据的,前两次握手是不可以携带数据的,一旦完成三次握手,双方都处于 ESTABLISHED
状态,此时连接就已建立完成,客户端和服务端就可以相互发送数据了。
如何在 Linux 系统中查看 TCP 状态?
TCP 的连接状态查看,在 Linux 可以通过 netstat -napt
命令查看。
为什么是三次握手?不是两次、四次?
相信大家比较常回答的是:“因为三次握手才能保证双方具有接收和发送的能力。”
这回答是没问题,但这回答是片面的,并没有说出主要的原因。
在前面我们知道了什么是 TCP 连接:
用于保证可靠性和流量控制的某些状态信息,这些信息包括Socet,序列号,窗口大小。
所以,重要的是为什么三次握手才可以初始化 Socket、序列号和窗口大小并建立 TCP 连接。
接下来,以三个方面分析三次握手的原因:
- 三次握手才可以阻止重复历史连接的初始化(主要原因)
- 三次握手才可以同步双方的初始序列号
- 次握手才可以避免资源浪费
原因一:阻止重复历史连接的初始化
RFC 793 指出的 TCP 连接使用三次握手的首要原因:三次握手的首要原因是为了防止旧的重复连接初始化造成混乱。
我们考虑一个场景,客户端先发送了 SYN(seq = 90)报文,然后客户端宕机了,而且这个 SYN 报文还被网络阻塞了,服务端并没有收到,接着客户端重启后,又重新向服务端建立连接,发送了 SYN(seq =100)报文(注意!不是重传 SYN,重传的 SYN 的序列号是一样的)。
客户端连续发送多次 SYN(都是同一个四元组)建立连接的报文,在网络拥堵情况下:
一个「旧 SYN 报文」比「最新的 SYN」 报文早到达了服务端,那么此时服务端就会回一个 SYN +ACK
报文给客户端,此报文中的确认号是 91(90+1)。
客户端收到后,发现自己期望收到的确认号应该是 100 + 1,而不是 90 + 1,于是就会回 RST 报文。
服务端收到 RST 报文后,就会释放连接。
上述中的「旧 SYN 报文」称为历史连接,TCP 使用三次握手建立连接的最主要原因就是防止「历史连接」初始化了连接。
原因二:同步双方初始序列号
TCP 协议的通信双方, 都必须维护一个「序列号」, 序列号是可靠传输的一个关键因素,它的作用:
- 接收方可以去除重复的数据;
- 接收方可以根据数据包的序列号按序接收;
- 可以标识发送出去的数据包中, 哪些是已经被对方收到的(通过 ACK 报文中的序列号知道);
两次握手只保证了一方的初始序列号能被对方成功接收,没办法保证双方的初始序列号都能被确认接收。
初始序列号 ISN 是如何随机产生的?
起始 ISN
是基于时钟的,每 4 微秒 + 1,转一圈要 4.55 个小时。
RFC793 提到初始化序列号 ISN 随机生成算法:ISN = M + F(localhost, localport, remotehost,remoteport)。
M
是一个计时器,这个计时器每隔 4 微秒加 1。F
是一个 Hash 算法,根据源 IP、目的 IP、源端口、目的端口生成一个随机数值。
以看到,随机数是会基于时钟计时器递增的,基本不可能会随机成一样的初始化序列号。
既然 IP 层会分片,为什么 TCP 层还需要 MSS 呢?
MTU
:一个网络包的最大长度,以太网中一般为1500
字节;MSS
:除去 IP 和 TCP 头部之后,一个网络包所能容纳的 TCP 数据的最大长度;
原因
- 性能开销大:IP 分片需在路由器拆分数据包并添加额外头部,接收端需缓存所有分片并重组,消耗网络和主机资源。
- 可靠性低:分片丢失会导致整个原始数据包失效,需重传全部数据,而非仅丢失的片段。
TCP 通过三次握手协商 MSS,直接限制数据段大小,从源头避免分片,减少网络负担。,如果一个 TCP 分片丢失后,进行重发时也是以 MSS 为单位,而不用重传所有的分片,大大增加了重传的效率。
第一次握手丢失了,会发生什么?
当客户端想和服务端建立 TCP 连接的时候,首先第一个发的就是 SYN 报文,然后进入到 SYN_SENT
状态。因为丢失了,所以服务器收不到SYN报文,客户端就无法得到回应,就会一直发送。重传的 SYN 报文的序列号都是一样的。
间隔一段时间,发一次。不同版本的操作系统可能超时时间不同,有的 1 秒的,也有 3 秒的,这个超时时间是写死在内核里的,一般是5次
第二次握手丢失了,会发生什么?
客户端发SYN 报文给服务器,服务器回复ACK和SYN报文,但是它丢失了,这就导致客户端收不到回应,就一直发SYN,服务端也不知道未发出去,得不到客户端的ACK,服务端也重发ACK和SYN,所以他们就全部重新发送
第三次握手丢失了,会发生什么?
第三握手丢失,服务器得不到客户端的ACK,因为单独的ACK不会重传,
- 重传 ACK 无意义(数据重传会自然触发新的 ACK);
当服务端超时重传 2 次 SYN-ACK 报文后,由于 tcp_synack_retries 为 2,已达到最大重传次数,于是再等待一段时间(时间为上一次超时时间的 2 倍),如果还是没能收到客户端的第三次握手,那么服务端就会断开连接。
什么是 SYN 攻击?如何避免 SYN 攻击?
我们都知道 TCP 连接建立是需要三次握手,假设攻击者短时间伪造不同 IP 地址的 SYN
报文,服务端每接收到一个 SYN
报文,就进入SYN_RCVD
状态,但服务端发送出去的 ACK + SYN
报文,无法得到未知IP 主机的 ACK
应答,久而久之就会占满服务端的半连接队列,使得服务端不能为正常用户服务。
在 TCP 三次握手的时候,Linux 内核会维护两个队列,分别是:
- 半连接队列,也称 SYN 队列;
- 全连接队列,也称 accept 队列;
正常流程:
1、当服务端接收到客户端的 SYN 报文时,会创建一个半连接的对象,然后将其加入到内核的「 SYN 队列」;
2、接着发送 SYN + ACK 给客户端,等待客户端回应 ACK 报文;
3、服务端接收到 ACK 报文后,从「 SYN 队列」取出一个半连接对象,然后创建一个新的连接对象放入到「 Accept 队列」;
4、应用通过调用 accpet()
socket 接口,从「 Accept 队列」取出连接对象。
SYN 攻击方式最直接的表现就会把 TCP 半连接队列打满,这样当 TCP 半连接队列满了,后续再在收到SYN 报文就会丢弃,导致客户端无法和服务端建立连接。
避免 SYN 攻击方式
方式一:调大 netdev_max_backlog
当网卡接收数据包的速度大于内核处理的速度时,会有一个队列保存这些数据包。控制该队列的最大值如下参数,默认值是 1000,我们要适当调大该参数的值,比如设置为 10000
方式二:增大 TCP 半连接队列
方式三:加cookie身份验证
方式四:减少 SYN+ACK 重传次数