TCP三次握手
什么是TCP连接
- TCP连接⽤于保证可靠性和流量控制维护的某些状态信息,这些信息的组合,包括Socket、序列号和窗口⼤⼩称为连接。建⽴⼀个 TCP 连接是需要客户端与服务器端达成上述三个信息的共识。即:
- Socket:由 IP 地址和端⼝号组成;
- 序列号:⽤来解决乱序问题等;
- 窗⼝⼤⼩:⽤来做流量控制;
- TCP 四元组可以唯⼀的确定⼀个连接,四元组包括如下:
- 源地址
- 源端口
- 目的地址
- 目的端口
- TCP的特点:
- 使用TCP进行数据传输前需要三次握手建立连接,且是端对端连接
- 可靠性,数据可以无差错、不丢失、不重复、按需到达;
- 流量控制、拥塞控制:保证数据传输的安全性和网络的稳定性;
- 提供全双工通信,TCP连接的两端都设有发送缓存和接收缓存;
- 面向字节流:
- 通过头部序列号、确认应答号字段将每次传输的内容精确到字节为单位;
- TCP把应用程序交下来的数据仅看成是一连串无结构的字节流;
- 发送方可以组装、分片应用程序下发的数据块后发送给接收方,但是最后接收方收到的数据块组装后包含的字节流应该是完全一致的;
TCP头部格式
- 序列号:在建⽴连接时由计算机⽣成的随机数作为其初始值,通过 SYN 包传给接收端主机,每发送⼀次数据,就
「累加」⼀次该「数据字节数」的⼤⼩。⽤来解决⽹络包乱序问题。 - 确认应答号:指下⼀次「期望」收到的数据的序列号,发送端收到这个确认应答以后可以认为在这个序号以前的数
据都已经被正常接收。⽤来解决不丢包的问题。 - 控制位:
- ACK:该位为 1 时,「确认应答」的字段变为有效,TCP 规定除了最初建⽴连接时的 SYN 包之外该位必须设置为 1;
- RST:该位为 1 时,表示 TCP 连接中出现异常必须强制断开连接;
- SYN:该位为 1 时,表示希望建⽴连接,并在其「序列号」的字段进⾏序列号初始值的设定。
- FIN:该位为 1 时,表示今后不会再有数据发送,希望断开连接。当通信结束希望断开连接时,通信双⽅的主机之间就可以相互交换 FIN 位为 1 的 TCP 段。
TCP建立连接(三次握手)
建立连接过程:
- ⼀开始,客户端和服务端都处于 CLOSED 状态。先是服务端主动监听某个端⼝,处于 LISTEN 状态;
- 客户端会随机初始化序号( client_isn ),将此序号置于 TCP⾸部的「序号」字段中,同时把 SYN 标志位置为 1 ,表示 SYN 报⽂。接着把第⼀个 SYN报⽂发送给服务端,表示向服务端发起连接,该报⽂不包含应⽤层数据,之后客户端处于 SYN-SENT 状态;
- 服务端收到客户端的 SYN 报⽂后,⾸先服务端也随机初始化⾃⼰的序号(server_isn),将此序号填⼊TCP ⾸部的「序号」字段中,其次把 TCP ⾸部的「确认应答号」字段填⼊ client_isn + 1 , 接着把SYN和ACK标志位置为1。最后把该报⽂发给客户端,该报⽂也不包含应⽤层数据,之后服务端处于 SYN_RCVD 状态。
- 从操作系统内核的角度看,当服务端收到客户端发起的 SYN 请求后,内核会把该连接存储到半连接队列(也称SYN队列),并向客户端响应 SYN+ACK;
- 客户端收到服务端报⽂后,还要向服务端回应最后⼀个应答报⽂,⾸先该应答报⽂ TCP ⾸部 ACK 标志位置为 1 ,其次「确认应答号」字段填⼊ server_isn + 1 ,最后把报⽂发送给服务端,这次报⽂可以携带客户到服务器的数据,之后客户端处于 ESTABLISHED 状态。
- 服务端收到第三次握⼿的ACK后,内核会把连接从半连接队列移除,然后创建新的完全的连接,并将其添加到全连接队列(accept队列),等待进程调⽤ accept 函数时把连接取出来;
- 后续进程间的通信就是通过调用操作系统提供的函数读写这个连接的套接字(文件描述符)进行的,就像往一个文件流里面读写东西一样;
- 服务器收到客户端的应答报⽂后,也进⼊ ESTABLISHED 状态。
建立连接过程异常:
TCP 第⼀次握⼿的 SYN 丢包了:
- 当客户端发起的 TCP 第⼀次握⼿ SYN 包,在超时时间内没收到服务端的
ACK,就会在超时重传SYN数据包,每次超时重传的RTO是翻倍上涨的,直到SYN包的重传次数达到tcp_syn_retries值后,客户端不再发送 SYN 包。 - RTO (Retransmission Timeout 超时重传时间);
- tcp_syn_retries 默认值为5,也就是 SYN 最⼤᯿传次数是 5 次。
- 修改tcp_syn_retries值: echo 2 > /proc/sys/net/ipv4/tcp_syn_retries
TCP 第⼆次握⼿的 SYN、ACK 丢包了:
- 服务端收到客户端的SYN包后,就会回SYN、ACK包(丢失了)。但是包丢失了,所以客户端一直没有回ACK,服务端在超时后,重传了SYN、ACK包,接着⼀会,客户端超时重传的SYN包⼜抵达了服务端,服务端收到后,服务端的超时定时器就重新计时,然后回了SYN、ACK包,所以相当于服务端的超时定时器只触发了⼀次,⼜被重置了。
- 最后,客户端 SYN 超时重传次数达到了 5 次(tcp_syn_retries 默认值 5 次),就不再继续发送 SYN 包了。
- 当第⼆次握⼿的SYN、ACK丢包时,客户端会超时重发SYN包,服务端也会超时重传SYN、ACK 包。
- TCP 第⼆次握⼿ SYN、ACK 包的最⼤重传次数是通过 tcp_synack_retries 内核参数限制的,其默认值为5次
- 修改tcp_synack_retries值:echo 2 > /proc/sys/net/ipv4/tcp_synack_retries
TCP 第三次握⼿的 ACK 包丢了:
- 第三次握手客户端回ACK包给服务端,但是丢失了。服务端收不到客户端的ACK包,那么就会超时重发SYN、ACK包给客户端,当超过tcp_synack_retries次数,还没有收到客户端回的ACK包,那么服务端的TCP连接主动终止了;
- 虽然服务端的TCP终止了,但是客户端在第三次握手,回送了ACK包给服务端之后,客户端就会进入ESTABLISHED 状态。等待后续传送数据;
- 此时由于服务端已经断开连接,客户端发送的数据报⽂,⼀直在超时重传,每⼀次重传,RTO的值是指数增⻓的,总共需要重传15次,客户端的 TCP连接才会报错退出;
- TCP 建⽴连接后的数据包传输,最⼤超时重传次数是由 tcp_retries2 指定,默认值是 15 次
- 修改tcp_retries2值:echo 5 > /proc/sys/net/ipv4/tcp_retries2
- 假如客户端不发送数据,那么客户端什么时候才会断开处于 ESTABLISHED 状态的连接?
- TCP保活机制:定义⼀个时间段,在这个时间段内,如果没有任何连接相关的活动,TCP保活机制会开始作⽤,每隔⼀个时间间隔,发送⼀个「探测报⽂」,该探测报⽂包含的数据⾮常少,如果连续⼏个探测报⽂都没有得到响应,则认为当前的TCP连接已经死亡,系统内核将错误信息通知给上层应⽤程序。
- 保活时间:net.ipv4.tcp_keepalive_time=7200 表示保活时间是 7200 秒(2⼩时),也就 2 ⼩时内如果没有任何连接相关的活
动,则会启动保活机制 - 保活探测的次数:net.ipv4.tcp_keepalive_intvl=75 表示每次检测间隔 75 秒;
- 保活探测的时间间隔:net.ipv4.tcp_keepalive_probes=9 表示检测 9 次⽆响应,认为对⽅是不可达的,从⽽中断本次的连接。
- 也就是说在 Linux 系统默认设置中,最少需要经过 2 ⼩时 11 分 15 秒才可以发现⼀个「死亡」连接。
为什么是三次握手:
- 三次握⼿的⾸要原因是为了防⽌旧的重复连接初始化造成混乱。
- 在网络拥堵情况下,客户端连续发送了多次 SYN 建⽴连接的报⽂;
- 如果其中一个SYN连接报文,在服务端与客户端已经完成通信并且已经完成四次挥手断开连接之后,才到达服务端;
- 假设建立连接只需要两次握手,那么服务端在接收到这个迟到的SYN连接报文,会回送ACK确认报文并且进行连接初始化,等待客户端传输数据。但其实客户端完全不知道服务端在等待自己传输数据,所以服务端的资源就被白白浪费了;
- 当建立TCP连接是三次握手的话,就能很好解决上述这个问题,因为服务端在接收到这个迟到的SYN连接报文时,会回送一个SYN+ACK报文给客户端。那么客户端在收到这个SYN报文时,可以根据⾃身的上下⽂,判断这是⼀个历史连接(序列号过期或超时),那么客户端就会发送RST 报⽂给服务端,表示中⽌这⼀次连接。
- 三次握手可以同步双⽅初始序列号:TCP 协议的通信双⽅, 都必须维护⼀个「序列号」,序列号是可靠传输的⼀个关键因素:
- 接收⽅可以去除重复的数据;
- 接收⽅可以根据数据包的序列号按序接收;
- 可以标识发送出去的数据包中, 哪些是已经被对⽅收到的;
- 四次握⼿其实也能够可靠的同步双⽅的初始化序号,但由于服务端在收到SYN连接报文时,可以把ACK报文和SYN报文合并成一个报文发送给客户端,于是就可以简化成三次握手;
半连接队列与全连接队列:
- 服务端收到客户端发起的SYN请求后,内核会把该连接存储到半连接队列,并向客户端响应 SYN+ACK,接着客户端会返回 ACK,服务端收到第三次握⼿的 ACK 后,内核会把连接从半连接队列移除,然后创建新的完全的连接,并将其添加到 accept 队列,等待进程调⽤ accept 函数时把连接取出来。
全连接队列:
- 当超过了 TCP 最⼤全连接队列,服务端则会丢掉后续进来的 TCP 连接;
- 丢掉的 TCP 连接的个数会被统计起来,可以使⽤ netstat-s命令查看:
- 查看全连接队列大小:
- 在「LISTEN 状态」时, Recv-Q/Send-Q 表示的含义如下:
- Recv-Q:当前全连接队列的⼤⼩,也就是当前已完成三次握⼿并等待服务端 accept() 的 TCP 连接;
- Send-Q:当前全连接最⼤队列⻓度,上⾯的输出结果说明监听 8088 端⼝的 TCP 服务,最⼤全连接⻓度为128;
- 在「⾮ LISTEN 状态」时, Recv-Q/Send-Q 表示的含义如下:
- Recv-Q:已收到但未被应⽤进程读取的字节数;
- Send-Q:已发送但未收到确认的字节数;
- 当服务端并发处理⼤量请求时,如果TCP全连接队列过⼩,就容易溢出。发⽣TCP 全连接队溢出的时候,后续的请求就会被丢弃,这样就会出现服务端请求数量上不去的现象。
- 如何增大TCP全连接队列:
- TCP 全连接队列的最⼤值取决于somaxconn和backlog之间的最⼩值,也就是 min(somaxconn, backlog);
- somaxconn 是 Linux 内核的参数,默认值是 128,可以通过 /proc/sys/net/core/somaxconn 来设置;
- backlog 是 listen(int sockfd, int backlog) 函数中的 backlog ⼤⼩,Nginx 默认值是 511,可以通过修改配置⽂件设置其⻓度;
- 可以结合wrk工具进行压测来调整上述两个参数值;
半连接队列
- TCP半连接溢出也会导致服务端丢掉后续进来的TCP连接;
- 模拟TCP 半连接溢出场景,实际上就是对服务端⼀直发送TCP SYN包,但是不回第三次握⼿ACK,这样就会使得服务端有⼤量的处于 SYN_RECV状态的 TCP 连接。这其实也就是所谓的 SYN 洪泛、SYN 攻击、DDos 攻击。
- 查看当前半连接队列大小:
- 如何增大TCP半连接队列:
- 半连接队列最⼤值不是单单由 max_syn_backlog 决定,还跟 somaxconn 和 backlog 有关系。
- 当 max_syn_backlog > min(somaxconn, backlog) 时, 半连接队列最⼤值 max_qlen_log = min(somaxconn,backlog) * 2;
- 当 max_syn_backlog < min(somaxconn, backlog) 时, 半连接队列最⼤值 max_qlen_log =max_syn_backlog * 2;
- max_qlen_log 是理论半连接队列最⼤值,并不⼀定代表服务端处于 SYN_REVC 状态的最⼤个数。
- 如果没有开启 tcp_syncookies,并且 max_syn_backlog 减去 当前半连接队列⻓度⼩于(max_syn_backlog >> 2),也会丢弃连接;
- 如何防止syn攻击使得服务器丢弃连接:
- 开启 syncookies功能就可以在不使⽤SYN半连接队列的情况下成功建⽴连接:
- 0 值,表示关闭该功能;
- 1 值,表示仅当 SYN 半连接队列放不下时,再启⽤它;
- 2 值,表示⽆条件开启功能;
- 那么在应对 SYN 攻击时,只需要设置为 1 即可:
- 增大半连接队列;
- 减少SYN+ACK重传次数;
- 开启 syncookies功能就可以在不使⽤SYN半连接队列的情况下成功建⽴连接: