大家好,我是三叔,很高兴这期又和大家见面了,一个奋斗在互联网的打工人。
这篇博客依旧是记录 tcp 的基础知识的学习总结相关内容!备注:图片均来自作者:小林哥
学习了解 tcp 三次握手四次挥手首先就要了解 tcp 的头结构,可能各位读者心中大概都知道三次握手四次挥手的基本概念,但是真的有了解过其中的原理是什么吗?tcp 为什么要有三次握手四次挥手?而不是两次或者四次,带着这些疑问去思考,去学习,会有意外收获。
头部结构最重要的就是序列号和确认应答号:对应的握手中的一个来回。
序列号:在建立连接时由计算机生成的随机数作为其初始值,通过 SYN 包传给接收端主机,每发送一次数据,就累加一次该数据字节数的大小,序列号是用来解决网络包乱序问题。SYN 的全称就叫 Synchronize Sequence Numbers(同步序列号)。
确认应答号:指下一次期望收到的数据的序列号,发送端收到这个确认应答以后可以认为在这个序号以前的数据都已经被正常接收,用来解决丢包的问题。
笔者在TCP 重传机制 滑动窗口 流量控制 拥塞控制 学习总结!一文中讲到过TCP/IP的网络分层,分别是应用层、传输层、网络层、网络接口层,而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 状态。
注意:第三次握手是可以携带数据的,前两次握手是不可以携带数据的,这样也是为了提高 TCP 的效率
前面笔者让大家思考,为什么三次握手不是两次也不是四次?
主要是以下三个方面:
我们考虑一个场景,客户端先发送了 SYN(seq = 90)报文,然后客户端宕机了,而且这个 SYN 报文还被网络阻塞了,服务端并没有收到,接着客户端重启后,又重新向服务端建立连接,发送了 SYN(seq = 100)报文(注意!不是重传 SYN,重传的 SYN 的序列号是一样的)。
当服务端第一次收到 SYN 报文,也就是收到 「旧 SYN 报文」时,就会回复 SYN + ACK 报文给客户端,此报文中的确认号是 91(90+1)。
然后这时再收到「新 SYN 报文」时,就会回 Challenge Ack (opens new window)报文给客户端,这个 ack 报文并不是确认收到「新 SYN 报文」的,而是上一次的 ack 确认号,也就是91(90+1)。所以客户端收到此 ACK 报文时,发现自己期望收到的确认号应该是 101,而不是 91,于是就会回 RST 报
文。
如果是两次握手连接,就无法阻止历史连接,因为在两次握手的情况下,服务端没有中间状态给客户端来阻止历史连接,导致服务端可能建立一个历史连接,造成资源浪费。
在两次握手的情况下,服务端在收到 SYN 报文后,就进入 ESTABLISHED 状态,意味着这时可以给对方发送数据,但是客户端此时还没有进入 ESTABLISHED 状态,假设这次是历史连接,客户端判断到此次连接为历史连接,那么就会回 RST 报文来断开连接,而服务端在第一次握手的时候就进入 ESTABLISHED 状态,所以它可以发送数据的,但是它并不知道这个是历史连接,它只有在收到 RST 报文后,才会开连接。
可以看到,如果采用两次握手建立 TCP 连接的场景下,服务端在向客户端发送数据前,并没有阻止掉历史连接,导致服务端建立了一个历史连接,又白白发送了数据,妥妥地浪费了服务端的资源。要解决这种现象,最好就是在服务端发送数据前,也就是建立连接之前,要阻止掉历史连接,这样就不会造成资源浪费,而要实现这个功能,就需要三次握手。
TCP 协议的通信双方, 都必须维护一个序列号, 序列号是可靠传输的一个关键因素,它的作用:接收方可以去除重复的数据、接收方可以根据数据包的序列号按序接收、可以标识发送出去的数据包中, 哪些是已经被对方收到的(通过 ACK 报文中的序列号知道)。
如下图所示:四次握手其实也能够可靠的同步双方的初始化序号,但由于第二步和第三步可以优化成一步,所以就成了三次握手。而两次握手只保证了一方的初始序列号能被对方成功接收,没办法保证双方的初始序列号都能被确认接收。
从上面的两项可以看出:
主要原因有两个方面:
为了防止历史报文被下一个相同四元组的连接接收(主要方面);
TCP 四元组可以唯一的确定一个连接,四元组包括如下
源地址和目的地址的字段(32 位)是在 IP 头部中,作用是通过 IP 协议发送报文给对方主机。
源端口和目的端口的字段(16 位)是在 TCP 头部中,作用是告诉 TCP 协议应该把报文发给哪个进程。
为了安全性,防止黑客伪造的相同序列号的 TCP 报文被对方接收;
如下图所示:如果每次序列号都从 0 开始
如果每次建立连接客户端和服务端的初始化序列号都不一样,就有大概率因为历史报文的序列号不在对方接收窗口,从而很大程度上避免了历史报文,比如下图:
TCP 连接建立是需要三次握手,假设攻击者短时间伪造不同 IP 地址的 SYN 报文,服务端每接收到一个 SYN 报文,就进入SYN_RCVD 状态,但服务端发送出去的 ACK + SYN 报文,无法得到未知 IP 主机的 ACK 应答,久而久之就会占满服务端的半连接队列,使得服务端不能为正常用户服务。
在 TCP 三次握手的时候,Linux 内核会维护两个队列,分别是:
a. 半连接队列,也称 SYN 队列;
b. 全连接队列,也称 accept 队列;
出于效率考虑,虽然都叫队列,但半连接队列其实被设计成了哈希表,而全连接队列本质是链表。
不管是半连接队列还是全连接队列,都有最大长度限制,超过限制时,默认情况都会丢弃报文。
SYN 攻击方式最直接的表现就会把 TCP 半连接队列打满,这样当 TCP 半连接队列满了,后续再在收到 SYN 报文就会丢弃,导致客户端无法和服务端建立连接。
当服务端收到 SYN 包后,服务端会立马回复 SYN+ACK 包,表明确认收到了客户端的序列号,同时也把自己的序列号发给对方。
此时,服务端出现了新连接,状态是 SYN_RCV。在这个状态下,Linux 内核就会建立一个「半连接队列」来维护「未完成」的握手信息,当半连接队列溢出后,服务端就无法再建立新的连接。
要想增大半连接队列,不能只单纯增大 tcp_max_syn_backlog 的值,还需一同增大 somaxconn 和 backlog,也就是增大 accept 队列。否则,只单纯增大 tcp_max_syn_backlog 是无效的。
避免 SYN 攻击方式,可以有以下四种方法:
开启 syncookies 功能就可以在不使用 SYN 半连接队列的情况下成功建立连接,相当于绕过了 SYN 半连接来建立连接。如下图所示:
可以看到,当开启了 tcp_syncookies 了,即使受到 SYN 攻击而导致 SYN 队列满时,也能保证正常的连接成功建立。
syncookies 的工作原理:服务器根据当前状态计算出一个值,放在己方发出的 SYN+ACK 报文中发出,当客户端返回 ACK 报文时,取出该值验证,如果合法,就认为连接建立成功,如下图所示:
前面主要讲的是 TCP 如何建立连接,接下来,主要介绍 TCP 如何断开连接:
每个方向都需要一个 FIN 和一个 ACK,因此通常被称为四次挥手;主动关闭连接的,才有 TIME_WAIT 状态。
关闭连接时,客户端向服务端发送 FIN 时,仅仅表示客户端不再发送数据了但是还能接收数据。
服务端收到客户端的 FIN 报文时,先回一个 ACK 应答报文,而服务端可能还有数据需要处理和发送,等服务端不再发送数据时,才发送 FIN 报文给客户端来表示同意现在关闭连接。服务端通常需要等待完成数据的发送和处理,所以服务端的 ACK 和 FIN 一般都会分开发送,因此是需要四次挥手。
有趣的是,如果主动关闭方在没有收到第三次会怎样?在不采取任何措施的情况下,会发生死锁:
所以需要对重试机制进行优化,在规定时间内如果没有收到握手信息,进行重试,重试一定次数后还没收到,则断开连接。
TCP 的连接信息是由内核维护的,所以当服务端的进程崩溃后,内核需要回收该进程的所有 TCP 连接资源,于是内核会发送第一次挥手 FIN 报文,后续的挥手过程也都是在内核完成,并不需要进程的参与,所以即使服务端的进程退出了,还是能与客户端完成 TCP 四次挥手的过程。