当前位置: 首页 > news >正文

猎头做单网站百度资源搜索

猎头做单网站,百度资源搜索,深圳注册公司的基本流程,企业策划书范文TCP 三次握手和四次挥手 TCP 基本认识 序列号#xff1a;在建立连接时由计算机生成的随机数作为其初始值#xff0c;通过 SYN 包传给接收端主机#xff0c;每发送一次数据#xff0c;就「累加」一次该「数据字节数」的大小。用来解决网络包乱序问题。 确认应答号#xf…TCP 三次握手和四次挥手 TCP 基本认识 序列号在建立连接时由计算机生成的随机数作为其初始值通过 SYN 包传给接收端主机每发送一次数据就「累加」一次该「数据字节数」的大小。用来解决网络包乱序问题。 确认应答号指下一次「期望」收到的数据的序列号发送端收到这个确认应答以后可以认为在这个序号以前的数据都已经被正常接收。用来解决丢包的问题。 控制位 ACK该位为 1 时「确认应答」的字段变为有效TCP 规定除了最初建立连接时的 SYN 包之外该位必须设置为 1 。RST该位为 1 时表示 TCP 连接中出现异常必须强制断开连接。SYN该位为 1 时表示希望建立连接并在其「序列号」的字段进行序列号初始值的设定。FIN该位为 1 时表示今后不会再有数据发送希望断开连接。当通信结束希望断开连接时通信双方的主机之间就可以相互交换 FIN 位为 1 的 TCP 段。 IP 层是「不可靠」的它不保证网络包的交付、不保证网络包的按序交付、也不保证网络包中的数据的完整性。为了能够可靠传递就需要上层传输层的 TCP 协议负责。TCP 是一个工作在传输层的可靠数据传输的服务它能确保接收端接收的网络包是无损坏、无间隔、非冗余和按序的。 TCP 是面向连接的、可靠的、基于字节流的传输层通信协议。面向连接就是指一定是「一对一」的连接不像 UDP 可以一对多可靠的就是指 TCP 能保证一个报文到达接收端字节流是指 TCP 报文有序即使后面的报文先到达也不会给应用层而是等待同时重复报文会自动丢弃。 用于保证可靠性和流量控制维护的某些状态信息这些信息的组合包括 Socket、序列号和窗口大小称为连接。Socket 就是由 IP 地址和端口号组成序列号之前已经学习就是解决乱序问题窗口大小就是用来流量控制。 那么确认连接依靠的就是 TCP 的四元组包含了源地址和目的地址32 位在 IP 头部中源端口和目的端口16 位在 TCP 头部。 上述是 TCP 理论上服务端的最大连接数对 IPv4 来说客户端 IP 最多 2^32客户端端口最多 2^16也就是说理论上最大连接数为 2^48。当然还会有以下的影响因素 文件描述符限制每个 TCP 连接都是一个文件如果文件描述符被占满了会发生 Too many open files。Linux 对可打开的文件描述符的数量分别作了三个方面的限制 系统级当前系统可打开的最大数量通过 cat /proc/sys/fs/file-max 查看用户级指定用户可打开的最大数量通过 cat /etc/security/limits.conf 查看进程级单个进程可打开的最大数量通过 cat /proc/sys/fs/nr_open 查看 内存限制每个 TCP 连接都要占用一定内存操作系统的内存是有限的如果内存资源被占满后会发生 OOM。 那么为什么有 UDP 还有 TCP 呢因为 UDP 非常简单但不能提供复杂机制头部只有 8 个字节 两者的区别如下 连接 TCP 是面向连接的传输层协议传输数据前先要建立连接。 UDP 是不需要连接即刻传输数据。 服务对象 TCP 是一对一的两点服务即一条连接只有两个端点。 UDP 支持一对一、一对多、多对多的交互通信。 可靠性 TCP 是可靠交付数据的数据可以无差错、不丢失、不重复、按序到达。 UDP 是尽最大努力交付不保证可靠交付数据。但是我们可以基于 UDP 传输协议实现一个可靠的传输协议比如 QUIC 协议。 拥塞控制、流量控制 TCP 有拥塞控制和流量控制机制保证数据传输的安全性。 UDP 则没有即使网络非常拥堵了也不会影响 UDP 的发送速率。 首部开销 TCP 首部长度较长会有一定的开销首部在没有使用「选项」字段时是 20 个字节如果使用了「选项」字段则会变长的。 UDP 首部只有 8 个字节并且是固定不变的开销较小。 传输方式 TCP 是流式传输没有边界但保证顺序和可靠。 UDP 是一个包一个包的发送是有边界的但可能会丢包和乱序。 分片不同 TCP 的数据大小如果大于 MSS 大小则会在传输层进行分片目标主机收到后也同样在传输层组装 TCP 数据包如果中途丢失了一个分片只需要传输丢失的这个分片。 UDP 的数据大小如果大于 MTU 大小则会在 IP 层进行分片目标主机收到后在 IP 层组装完数据接着再传给传输层。 所以TCP 是保证数据的可靠性的多用于 FTP 文件传输以及 HTTP/HTTPS而 UDP 处理简单常用于包总量较少的通信 DNS、SNMP 等还有视频和广播通信等。 TCP 有「首部长度」字段因为 TCP 是可变长的UDP 不会变化所以没有这个字段。 TCP 的数据长度IP 相关的内容在 IP 首部格式中都已知而 TCP 首部长度也可以通过头部格式中的信息获取所以 TCP 数据长度是可以计算的不需要「包长度」字段UDP 则因为各种问题是有「包长度」字段的靠谱的说法是首部长度需要是 4 的倍数加上这个字段就正好。 **这里注意TCP 和 UDP 是可以使用同一个端口的。**数据链路层MAC 地址来寻找局域网中主机网络层通过 IP 地址寻找主机传输层通过端口来寻址找到同一计算机中通信的不同应用程序。端口号就是为了区分同一主机上不同的应用程序的数据包。所以说TCP、UDP 端口号相互独立不冲突。 TCP 连接建立 三次握手 建立连接是通过三次握手来进行的。三次握手的过程如下图 最开始客户端和服务端都是 CLOSE 状态。服务端主动监听某个端口处于 LISTEN 状态。 客户端随机初始化序列号client_isn然后 SYN 位置 1表示 SYN 报文。然后发送 SYN 报文这里不包含应用层数据之后客户端处于 SYN-SENT 状态。 服务端收到 SYN 报文也随机初始化自己序号server_isn然后把 TCP 头部的「确认应答号」字段填入 client_isn 1, 接着把 SYN 和 ACK 标志位置为 1。最后发回给客户端也不包含应用层数据之后服务端处于 SYN-RCVD 状态。 客户端收到后需要回应应答报文。把 ACK 标志位置 1其次「确认应答号」字段填入 server_isn 1 最后把报文发送给服务端这次报文可以携带客户到服务端的数据之后客户端处于 ESTABLISHED 状态。服务端收到后也进入 ESTABLISHED 状态。 记住第三次握手是可以携带数据的前两次握手是不可以携带数据的 在 Linux 可以通过 netstat -napt 命令查看 TCP 连接状态。 三次握手的原因 三次握手的原因为什么一定是三次 阻止重复历史连接的初始化主要原因 首要原因是为了防止旧的重复连接初始化造成混乱。如果客户端发了旧的 SYN 报文然后网络拥堵或者宕机然后恢复后发了新的 SYN 报文此时服务端回发的 SYN ACK 报文就会对不上客户端期望的确认应答号就会回发 RST 报文服务端收到后就会释放连接在之后新的 SYN 报文就到了会重新三次握手。 两次握手就无法阻止历史连接因为此时服务端没有中间状态给客户端来阻止历史连接导致服务端可能建立一个历史连接造成资源浪费。要解决这种现象最好就是在服务端发送数据前也就是建立连接之前要阻止掉历史连接这样就不会造成资源浪费而要实现这个功能就需要三次握手。 同步双方初始序列号 TCP 协议的通信双方 都必须维护一个「序列号」因为依靠序列号接收方可以去除重复数据接收方可以以此按序接收通过序列号来标识已经被接收到的报文通过 ACK 报文序列号。 因此客户端发送带「初始序列号」的 SYN 报文的时候需要服务端回一个 ACK 应答报文表示客户端的 SYN 报文已被服务端成功接收同理服务端发送「初始序列号」给客户端的时候依然也要得到客户端的应答回应这样一来一回才能确保双方的初始序列号能被可靠的同步。通过四次握手也就是两次互相发 SYN 和 ACK 信号就能完成序列号同步。那么可以将服务端回复 SYN 和发送 ACK 合并到一起就变成了同时发 SYN ACK也就变成了三次握手。 避免浪费资源 如之前所说只有两次握手那么就会有历史连接因为服务端不确定客户端是否收到 ACK 报文就只能每收到一个 SYN 报文都先主动建立一个连接。这样就会造成资源浪费。 初始序列号不同的原因 每次建立 TCP 连接初始化的序列号不一样是因为 防止历史报文被下一个相同四元组连接接收主要安全性防止相同序列号 TCP 报文被接受。 针对第一个方面例如双方已经都 ESTABLISHED 之后客户端传资源超时服务端断电重启那么久会在重启的时候建立失效进而导致服务端回发 RST 让客户端进入 CLOSED。然后如果再次建立连接此时序列号是一样的而上一次连接发送的那个报文正好抵达服务端那么现在的序列号没变就会导致数据传递是有效的那么服务端就会正常接收导致数据产生混乱。如果序列号不一样那么第二次连接生效的序列号就发生了改变也就不会导致接收成功了会把该报文直接丢弃。 初始序列号随机产生 初始序列号 ISN 是基于时钟每 4 微秒 1转一圈要 4.55 个小时。随机生成算法ISN M F(localhost, localport, remotehost, remoteport)。所以基本不会有一样的初始化序列号 ISN。 TCP 的 MSS存在意义 IP 层会分片那为什么还要在 TCP 层设置 MSS IP 层的分片就是把数据TCP 头部 TCP 数据发送出去如果超过 MTU大小就进行分片重组再交给 TCP 传输层。如果 IP 分片有一个丢失了那么整个 IP 报文所有分片都得重传。IP 本身没有超时重传机制那么就会造成传输层 TCP 来负责超时和重传。这样就会导致整个 TCP 的报文都得重发。所以为了达到最佳的传输效能 TCP 协议在建立连接的时候通常要协商双方的 MSS 值当 TCP 层发现数据超过 MSS 时则就先会进行分片当然由它形成的 IP 包的长度也就不会大于 MTU 自然也就不用 IP 分片了。这样如果一个 TCP 分片丢失重发也是以 MSS 为单位不用重传所有分片大大提升效率。 第一次握手丢失 当客户端想和服务端建立 TCP 连接的时候首先第一个发的就是 SYN 报文然后进入到 SYN_SENT 状态。在这之后如果客户端迟迟收不到服务端的 SYN-ACK 报文第二次握手就会触发「超时重传」机制重传 SYN 报文而且重传的 SYN 报文的序列号都是一样的。 每次重传判断超时的时间是写在内核里的最大重传次数由 tcp_syn_retries内核参数控制这个参数是可以自定义的默认值一般是 5。每次超时重传的时间是上一次的 2 倍。也就是说会等到 tcp_syn_retries 次客户端直接断开连接。 第二次握手丢失 当服务端收到客户端的第一次握手后就会回 SYN-ACK 报文给客户端这个就是第二次握手此时服务端会进入 SYN_RCVD 状态。相当于发了针对第一次握手的确认 ACK 报文同时发了服务端建立 TCP 连接的 SYN 报文。 所以如果第二次丢失对第一次握手的确认报文就没有了客户端就会触发超时重传机制重传 SYN 报文。而服务端发送的 SYN 报文也就没有回复的确认报文服务端这边会触发超时重传机制重传 SYN-ACK 报文。SYN-ACK 报文的最大重传次数由 tcp_synack_retries内核参数决定默认值是 5。 第三次握手丢失 客户端收到服务端的 SYN-ACK 报文后就会给服务端回一个 ACK 报文也就是第三次握手此时客户端状态进入到 ESTABLISH 状态。因为这个第三次握手的 ACK 是对第二次握手的 SYN 的确认报文所以当第三次握手丢失了如果服务端那一方迟迟收不到这个确认报文就会触发超时重传机制重传 SYN-ACK 报文直到收到第三次握手或者达到最大重传次数。因为 ACK 报文是没有重传的所以只能有对方重传相应的 SYN 报文。 SYN 攻击 攻击者短时间伪造不同 IP 地址的 SYN 报文服务端每接收到一个 SYN 报文就进入SYN_RCVD 状态但服务端发送出去的 ACK SYN 报文无法得到未知 IP 主机的 ACK 应答久而久之就会占满服务端的半连接队列使得服务端不能为正常用户服务。 半连接队列就是 SYN 队列全连接队列就是 accept 队列。 SYN 攻击方式最直接的表现就会把 TCP 半连接队列打满这样当 TCP 半连接队列满了后续再在收到 SYN 报文就会丢弃导致客户端无法和服务端建立连接。 解决方法调大 netdev_max_backlog增大 TCP 半连接队列开启 tcp_syncookiesecho 把 1 写入减少 SYNACK 重传次数。 TCP 连接断开 四次挥手 客户端打算关闭连接此时会发送一个 TCP 首部 FIN 标志位被置为 1 的报文也即 FIN 报文之后客户端进入 FIN_WAIT_1 状态。服务端收到该报文后就向客户端发送 ACK 应答报文接着服务端进入 CLOSE_WAIT 状态。客户端收到服务端的 ACK 应答报文后之后进入 FIN_WAIT_2 状态。等待服务端处理完数据后也向客户端发送 FIN 报文之后服务端进入 LAST_ACK 状态。客户端收到服务端的 FIN 报文后回一个 ACK 应答报文之后进入 TIME_WAIT 状态服务端收到了 ACK 应答报文后就进入了 CLOSE 状态至此服务端已经完成连接的关闭。客户端在经过 2MSL 一段时间后自动进入 CLOSE 状态至此客户端也完成连接的关闭。 每个方向都需要一个 FIN 和一个 ACK因此通常被称为四次挥手。这里一点需要注意是主动关闭连接的才有 TIME_WAIT 状态。 挥手四次的原因 关闭连接时客户端向服务端发送 FIN 时仅仅表示客户端不再发送数据了但是还能接收数据。服务端收到客户端的 FIN 报文时先回一个 ACK 应答报文而服务端可能还有数据需要处理和发送等服务端不再发送数据时才发送 FIN 报文给客户端来表示同意现在关闭连接。 所以服务端通常需要等待完成数据的发送处理所以 ACK 和 FIN 包是分开发送的。当然特殊情况下可以变成三次挥手之后会学习。 第一次挥手丢失 当客户端主动关闭方调用 close 函数后就会向服务端发送 FIN 报文试图与服务端断开连接此时客户端的连接进入到 FIN_WAIT_1 状态。正常情况下如果能及时收到服务端被动关闭方的 ACK则会很快变为 FIN_WAIT2状态。 如果第一次丢失那相当于客户端一直等不到服务端 ACK就会触发超时重传重传 FIN 报文重发次数由 tcp_orphan_retries 参数控制。每一次的等待时间都是上一次的 2 倍。如果等到 tcp_orphan_retries 1 次就会进入 close 状态。 第二次挥手丢失 当服务端收到客户端的第一次挥手后就会先回一个 ACK 确认报文此时服务端的连接进入到 CLOSE_WAIT 状态。 因为 ACK 不会重传所以第二次挥手丢失那就会等到客户端触发超时重传重传 FIN 报文直到收到服务端的第二次挥手或者达到最大的重传次数。 当客户端收到第二次挥手也就是收到服务端发送的 ACK 报文后客户端就会处于 FIN_WAIT2 状态在这个状态需要等服务端发送第三次挥手也就是服务端的 FIN 报文。对于 close 函数关闭的连接由于无法再发送和接收数据所以 FIN_WAIT2 状态不可以持续太久而 tcp_fin_timeout 控制了这个状态下连接的持续时长默认值是 60 秒。 这意味着对于调用 close 关闭的连接如果在 60 秒后还没有收到 FIN 报文客户端主动关闭方的连接就会直接关闭。 但是注意如果主动关闭方使用 shutdown 函数关闭连接指定了只关闭发送方向而接收方向并没有关闭那么意味着主动关闭方还是可以接收数据的。此时如果主动关闭方一直没收到第三次挥手那么主动关闭方的连接将会一直处于 FIN_WAIT2 状态tcp_fin_timeout 无法控制 shutdown 关闭的连接。 第三次挥手丢失 当服务端被动关闭方收到客户端主动关闭方的 FIN 报文后内核会自动回复 ACK同时连接处于 CLOSE_WAIT 状态顾名思义它表示等待应用进程调用 close 函数关闭连接。此时内核是没有权利替代进程关闭连接必须由进程主动调用 close 函数来触发服务端发送 FIN 报文。 服务端处于 CLOSE_WAIT 状态时调用了 close 函数内核就会发出 FIN 报文同时连接进入 LAST_ACK 状态等待客户端返回 ACK 来确认连接关闭。如果没有 ACK服务端会重发 FIN 报文重发次数仍然由 tcp_orphan_retries 参数控制这与客户端重发 FIN 报文的重传次数控制方式是一样的。 第四次挥手 当客户端收到服务端的第三次挥手的 FIN 报文后就会回 ACK 报文也就是第四次挥手此时客户端连接进入 TIME_WAIT 状态。在 Linux 系统TIME_WAIT 状态会持续 2MSL 后才会进入关闭状态。 如果第四次挥手的 ACK 报文没有到达服务端服务端就会重发 FIN 报文重发次数仍然由前面介绍过的 tcp_orphan_retries 参数控制。 TIME_WAIT 2MSL 原因 MSL 是 Maximum Segment Lifetime报文最大生存时间它是任何报文在网络上存在的最长时间超过这个时间报文将被丢弃。因为 TCP 报文基于是 IP 协议的而 IP 头中有一个 TTL 字段是 IP 数据报可以经过的最大路由数每经过一个处理他的路由器此值就减 1当此值为 0 则数据报将被丢弃同时发送 ICMP 报文通知源主机。 MSL 与 TTL 的区别 MSL 的单位是时间而 TTL 是经过路由跳数。所以 MSL 应该要大于等于 TTL 消耗为 0 的时间以确保报文已被自然消亡。TTL 的值一般是 64Linux 将 MSL 设置为 30 秒意味着 Linux 认为数据报文经过 64 个路由器的时间不会超过 30 秒如果超过了就认为报文已经消失在网络中了。 TIME_WAIT 等待 2 倍的 MSL比较合理的解释是 网络中可能存在来自发送方的数据包当这些发送方的数据包被接收方处理后又会向对方发送响应所以一来一回需要等待 2 倍的时间。 2MSL时长 这其实是相当于至少允许报文丢失一次。而起始计算的时间是从客户端接收到 FIN 后发送 ACK 开始计时如果在 TIME-WAIT 时间内客户端接收到重发的 FIN 报文2MSL 会重新计时。 Linux 系统停留在 TIME_WAIT 的时间为固定的 60 秒。 为什么要 TIME_WAIT 状态 主动发起关闭连接的一方才会有 TIME-WAIT 状态。 主要是两个原因 防止历史连接中的数据被后面相同四元组的连接错误的接收保证「被动关闭连接」的一方能被正确的关闭。 序列号是 TCP 一个头部字段标识了 TCP 发送端到接收端的数据流的一个字节是一个 32 位的无符号数因此在到达 4G 之后再循环回到 0。初始序列号是客户端和服务端会各自生成的一个随机数可被视为一个 32 位的计数器该计数器的数值每 4 微秒加 1循环一次需要 4.55 小时。两者并不是无限递增的会发生回绕为初始值的情况这意味着无法根据序列号来判断新老数据。 如果服务端关闭连接前发送了 SEQ 的报文然后服务端以相同四元组重新打开新连接则之前的 SEQ 报文就会抵达客户端该序列号又刚好落在客户端接收窗口内就会使得客户端正常接收导致数据错乱。 因此 TCP 设计了 TIME_WAIT 状态状态会持续 2MSL 时长这个时间足以让两个方向上的数据包都被丢弃使得原来连接的数据包在网络中都自然消失再出现的数据包一定都是新建立连接所产生的。 TIME-WAIT 还有一个作用是等待足够的时间以确保最后的 ACK 能让被动关闭方接收从而帮助其正常关闭。如果客户端主动关闭方最后一次 ACK 报文第四次挥手在网络中丢失了那么按照 TCP 可靠性原则服务端被动关闭方会重发 FIN 报文。假设客户端没有 TIME_WAIT 状态而是在发完最后一次回 ACK 报文就直接进入 CLOSE 状态如果该 ACK 报文丢失了服务端则重传的 FIN 报文而这时客户端已经进入到关闭状态了在收到服务端重传的 FIN 报文后就会回 RST 报文。此时就发生了异常情况这对于可靠协议而言还是有问题的。 TIME_WAIT 过多的危害 第一是占用系统资源比如文件描述符、内存资源、CPU 资源、线程资源等第二是占用端口资源端口资源也是有限的一般可以开启的端口为 3276861000也可以通过 net.ipv4.ip_local_port_range 参数指定范围。 如果客户端主动发起关闭连接方的 TIME_WAIT 状态过多占满了所有端口资源就无法对「目的 IP 目的 PORT」都一样的服务端发起连接了但已经被使用的端口还是可以继续对另一个服务端发起连接的。四元组来定位客户端端口一样并不影响连接 如果服务端主动发起关闭连接方的 TIME_WAIT 状态过多并不会导致端口资源受限因为服务端只监听一个端口但是 TCP 连接过多会占用系统资源比如文件描述符、内存资源、CPU 资源、线程资源等。 优化 TIME_WAIT 打开 net.ipv4.tcp_tw_reuse 和 net.ipv4.tcp_timestamps 选项复用处于 TIME_WAIT 的 socket 为新的连接所用。tcp_tw_reuse 功能只能用客户端连接发起方因为开启了该功能在调用 connect() 函数时内核会随机找一个 time_wait 状态超过 1 秒的连接给新的连接复用。这个值置 1还有一个前提需要打开对 TCP 时间戳的支持默认开启。在 TCP 头部的「选项」里它由一共 8 个字节表示时间戳其中第一个 4 字节字段用来保存发送该数据包的时间第二个 4 字节字段用来保存最近一次接收对方发送到达数据的时间。由于引入了时间戳我们在前面提到的 2MSL 问题就不复存在了因为重复的数据包会因为时间戳过期被自然丢弃。 net.ipv4.tcp_max_tw_buckets这个值默认为 18000当系统中处于 TIME_WAIT 的连接一旦超过这个值时系统就会将后面的 TIME_WAIT 连接状态重置这个方法比较暴力。 可以通过设置 socket 选项来设置调用 close 关闭连接行为。如下所示l_onoff 非 0l_linger 为 0那么 close 后会直接发送 RST 给对面跳过四次挥手。这个并不提倡。 struct linger so_linger; so_linger.l_onoff 1; so_linger.l_linger 0; setsockopt(s, SOL_SOCKET, SO_LINGER, so_linger,sizeof(so_linger));如果服务端要避免过多的 TIME_WAIT 状态的连接就永远不要主动断开连接让客户端去断开由分布在各处的客户端去承受 TIME_WAIT。 服务器出现大量 TIME_WAIT 首先要知道 TIME_WAIT 状态是主动关闭连接方才会出现的状态也就是说现在是服务器主动断开很多 TCP 连接。 会有如下三个场景 HTTP 没有使用长连接 从 HTTP/1.1 开始 就默认是开启了 Keep-Alive如果要关闭 HTTP Keep-Alive需要在 HTTP 请求或者响应的 header 里添加 Connection:close 信息也就是说只要客户端和服务端任意一方的 HTTP header 中有 Connection:close 信息那么就无法使用 HTTP 长连接的机制。 虽然 RFC 文档中请求和响应的双方都可以主动关闭 TCP 连接。不过根据大多数 Web 服务的实现不管哪一方禁用了 HTTP Keep-Alive都是由服务端主动关闭连接那么此时服务端上就会出现 TIME_WAIT 状态的连接。 客户端禁用了 HTTP Keep-Alive服务端开启 HTTP Keep-Alive服务端是主动关闭方。HTTP 是请求-响应模型发起方一直是客户端HTTP Keep-Alive 的初衷是为客户端后续的请求重用连接如果我们在某次 HTTP 请求-响应模型中请求的 header 定义了 connectionclose 信息那不再重用这个连接的时机就只有在服务端了所以我们在 HTTP 请求-响应这个周期的「末端」关闭连接是合理的。 当客户端开启了 HTTP Keep-Alive而服务端禁用了 HTTP Keep-Alive这时服务端在发完 HTTP 响应后服务端也会主动关闭连接。在服务端主动关闭连接的情况下只要调用一次 close() 就可以释放连接剩下的工作由内核 TCP 栈直接进行了处理整个过程只有一次 syscall如果是要求 客户端关闭则服务端在写完最后一个 response 之后需要把这个 socket 放入 readable 队列调用 select / epoll 去等待事件然后调用一次 read() 才能知道连接已经被关闭这其中是两次 syscall多一次用户态程序被激活执行而且 socket 保持时间也会更长。 **当服务端出现大量的 TIME_WAIT 状态连接的时候可以排查下是否客户端和服务端都开启了 HTTP Keep-Alive。**任意一方没开都会导致服务端在处理完一个 HTTP 请求后就主动关闭连接此时服务端上就会出现大量的 TIME_WAIT 状态的连接。 解决办法也很简单让客户端和服务端都开启 HTTP Keep-Alive 机制。 HTTP 长连接超时 HTTP 长连接的特点是只要任意一端没有明确提出断开连接则保持 TCP 连接状态。 假设设置了 HTTP 长连接的超时时间是 60 秒nginx 就会启动一个「定时器」如果客户端在完后一个 HTTP 请求后在 60 秒内都没有再发起新的请求定时器的时间一到nginx 就会触发回调函数来关闭该连接那么此时服务端上就会出现 TIME_WAIT 状态的连接。 当服务端出现大量 TIME_WAIT 状态的连接时如果现象是有大量的客户端建立完 TCP 连接后很长一段时间没有发送数据那么大概率就是因为 HTTP 长连接超时导致服务端主动关闭连接产生大量处于 TIME_WAIT 状态的连接。 解决方法排查网络问题。 HTTP 长连接请求数量达到上限 Web 服务端通常会有个参数来定义一条 HTTP 长连接上最大能处理的请求数量当超过最大限制时就会主动关闭连接。如果达到这个参数设置的最大值时则 nginx 会主动关闭这个长连接。 对于一些 QPS 比较高的场景比如超过 10000 QPS甚至达到 30000 , 50000 甚至更高如果 keepalive_requests 参数值是 100这时候就 nginx 就会很频繁地关闭连接那么此时服务端上就会出大量的 TIME_WAIT 状态。 解决方法调大对应参数。 服务器出现大量 CLOSE_WAIT 状态 CLOSE_WAIT 状态是「被动关闭方」才会有的状态而且如果「被动关闭方」没有调用 close 函数关闭连接那么就无法发出 FIN 报文从而无法使得 CLOSE_WAIT 状态的连接转变为 LAST_ACK 状态。 当服务端出现大量 CLOSE_WAIT 状态的连接的时候说明服务端的程序没有调用 close 函数关闭连接。 一个普通 TCP 连接的流程 创建服务端 socketbind 绑定端口、listen 监听端口将服务端 socket 注册到 epollepoll_wait 等待连接到来连接到来时调用 accpet 获取已连接的 socket将已连接的 socket 注册到 epollepoll_wait 等待事件发生对方连接关闭时我方调用 close 一般可能有 4 个原因 第 2 步没有做socket 没有注册到 epoll那么新连接到来服务端没法感知也就无法获取 socket无法调用 close第 3 步没做新连接来没有 accept 获取socket导致客户端主动断开连接所以服务端没机会调用 close第 4 步没做accept 获取后没有注册到 epoll后续收到 FIN 报文没法感知同理无法调用 close第 6 步没做客户端关闭连接服务端没有执行 close可能是代码没有执行例如发生死锁等。 当服务端出现大量 CLOSE_WAIT 状态的连接的时候通常都是代码的问题这时候我们需要针对具体的代码一步一步的进行排查和定位主要分析的方向就是服务端为什么没有调用 close。 建立连接后客户端故障 避免这种情况TCP 搞了个保活机制。这个机制的原理是这样的 定义一个时间段在这个时间段内如果没有任何连接相关的活动TCP 保活机制会开始作用每隔一个时间间隔发送一个探测报文该探测报文包含的数据非常少如果连续几个探测报文都没有得到响应则认为当前的 TCP 连接已经死亡系统内核将错误信息通知给上层应用程序。 注意应用程序若想使用 TCP 保活机制需要通过 socket 接口设置 SO_KEEPALIVE 选项才能够生效如果没有设置那么就无法使用 TCP 保活机制。 如果使用就要考虑三个情况 对端程序正常工作那么需要在正常发送探测报文后重置 TCP 保活时间对端主机宕机并重启此时对端可以响应但是连接已经失效了只会返回 RST对端主机宕机不是进程崩溃进程崩溃操作系统会回收并发送 FIN 报文此时进入真正的保活机制没有响应那么 TCP 会报告 TCP 连接死亡。 但这个时间是很长的所以可以自己定义一个时间来实现这个功能web 服务软件一般都会提供 keepalive_timeout 参数用来指定 HTTP 长连接的超时时间。如果设置了 HTTP 长连接的超时时间是 60 秒web 服务软件就会启动一个定时器如果客户端在完成一个 HTTP 请求后在 60 秒内都没有再发起新的请求定时器的时间一到就会触发回调函数来释放该连接。 已经建立连接服务端进程崩溃 服务端的进程崩溃后内核需要回收该进程的所有 TCP 连接资源于是内核会发送第一次挥手 FIN 报文后续的挥手过程也都是在内核完成并不需要进程的参与所以即使服务端的进程退出了还是能与客户端完成 TCP 四次挥手的过程。 Socket 编程 服务端和客户端初始化 socket得到文件描述符服务端调用 bind将 socket 绑定在指定的 IP 地址和端口;服务端调用 listen进行监听服务端调用 accept等待客户端连接客户端调用 connect向服务端的地址和端口发起连接请求服务端 accept 返回用于传输的 socket 的文件描述符客户端调用 write 写入数据服务端调用 read 读取数据客户端断开连接时会调用 close那么服务端 read 读取数据的时候就会读取到了 EOF待处理完数据后服务端调用 close表示连接关闭。 服务端调用 accept 时连接成功了会返回一个已完成连接的 socket后续用来传输数据。所以监听的 socket 和真正用来传送数据的 socket是「两个」 socket一个叫作监听 socket一个叫作已完成连接 socket。 listen 时的 backlog 一共维护两个队列一个是半连接队列SYN 队列一个是全连接队列Accept 队列之前有讲过。 int listen (int socketfd, int backlog)参数一就是文件描述符参数二在 Linux 内核 2.2 之后就是 accept 队列也就是已完成连接建立的队列长度。上限值是内核参数 somaxconn 的大小也就说 accpet 队列长度 min(backlog, somaxconn)。 accept 发生位置 客户端 connect 成功返回是在第二次握手服务端 accept 成功返回是在三次握手成功之后。 客户端调用 close 的断开流程 服务端接收到了 FIN 报文TCP 协议栈会为 FIN 包插入一个文件结束符 EOF 到接收缓冲区中应用程序可以通过 read 调用来感知这个 FIN 包。这个 EOF 会被放在已排队等候的其他已接收的数据之后这就意味着服务端需要处理这种异常情况因为 EOF 表示在该连接上再无额外数据到达。此时服务端进入 CLOSE_WAIT 状态接着当处理完数据后自然就会读到 EOF于是也调用 close 关闭它的套接字这会使得服务端发出一个 FIN 包之后处于 LAST_ACK 状态。 没有 accept 可以建立 TCP 连接 accpet 系统调用并不参与 TCP 三次握手过程它只是负责从 TCP 全连接队列取出一个已经建立连接的 socket用户层通过 accpet 系统调用拿到了已经建立连接的 socket就可以对该 socket 进行读写操作了。 没有 listen 可以建立 TCP 连接 客户端是可以自己连自己的形成连接TCP自连接也可以两个客户端同时向对方发出请求建立连接TCP同时打开这两个情况都有个共同点就是没有服务端参与也就是没有 listen就能 TCP 建立连接。 TCP 重传、滑动窗口、流量控制、拥塞控制 TCP 是通过序列号、确认应答、重发控制、连接管理以及窗口控制等机制实现可靠性传输的。 重传机制 TCP 针对数据包丢失的情况会用重传机制解决。 超时重传 在发送数据时设定一个定时器当超过指定的时间后没有收到对方的 ACK 确认应答报文就会重发该数据也就是我们常说的超时重传。 触发这种机制一般会有两种情况数据包丢失、确认应答丢失。 RTT 指的是数据发送时刻到接收到确认的时刻的差值也就是包的往返时间。超时重传时间是以 RTO Retransmission Timeout 超时重传时间表示。根据这个定义可以发现RTO 过长或过短会分别导致效率低和网络拥塞两种情况。 所以超时重传时间 RTO 的值应该略大于报文往返 RTT 的值。 不过由于 RTT 是经常变化的所以 RTO 也需要动态变化。 SRTT 是计算平滑的 RTTDevRTT 是平滑的 RTT 与现在 RTT 的差距。在 Linux 下α 0.125β 0.25 μ 1∂ 4。 如果已经重发再度出发超时重发这时候超时的时间就会加倍。两次超时就说明网络环境差不宜频繁反复发送。 快速重传 不以时间为驱动而是以数据驱动重传。 这个可以直接看图非常直观 快速重传的工作方式是当收到三个相同的 ACK 报文时会在定时器过期之前重传丢失的报文段。 但是虽然解决了超时时间的问题还是存在着重传时是重传一个还是重传所有的问题。为了解决不知道该重传哪些 TCP 报文于是就有 SACK 方法。 SACK 方法 SACK Selective Acknowledgment 选择性确认。 在 TCP 头部「选项」字段里加一个 SACK 的东西它可以将已收到的数据的信息发送给「发送方」这样发送方就可以知道哪些数据收到了哪些数据没收到知道了这些信息就可以只重传丢失的数据。 同样的三次 ACK 想通就会触发重传机制此时就通过 SACK 的头部信息接收到并缓存下来的接收缓冲区查看丢失的数据然后只重发丢失数据即可。 如果要支持 SACK必须双方都要支持。在 Linux 下可以通过 net.ipv4.tcp_sack 参数打开这个功能Linux 2.4 后默认打开。 Duplicate SACK Duplicate SACK 又称 D-SACK其主要使用了 SACK 来告诉「发送方」有哪些数据被重复接收了。 主要就是用来解决应答报文的丢失以及网络延时的问题。 ACK 丢失就是在接收方收到重复报文的时候返回一个 SACK 告诉发送方已经收到过了网络延时如果有数据没收到就会一直回复 ACK SACK三次 ACK 相同会触发快速重传如果之后收到了延时的包此时就会再回一个这一时刻收到的那个重复的包SACK通知发送方这个是网络延时问题。 在 Linux 下可以通过 net.ipv4.tcp_dsack 参数开启/关闭这个功能Linux 2.4 后默认打开。 滑动窗口 TCP 引入了窗口这个概念。即使在往返时间较长的情况下它也不会降低网络通信的效率。那么有了窗口就可以指定窗口大小窗口大小就是指无需等待确认应答而可以继续发送数据的最大值。 这样的话即使有 ACK 丢失也可以通过下一个 ACK 来确认应答判断是否收到数据。这个模式就叫累计确认或者累计应答。 TCP 头里有一个字段叫 Window也就是窗口大小。这个字段是接收端告诉发送端自己还有多少缓冲区可以接收数据。于是发送端就可以根据这个接收端的处理能力来发送数据而不会导致接收端处理不过来。所以通常窗口大小是由接收方决定的。 只有在窗口还有剩余的情况下发送方才可以继续发送如果收到了应答那么就会滑动窗口也就是又有可以发送的数据了这时就会继续发送直到窗口再度耗尽。 TCP 滑动窗口方案使用三个指针来跟踪在四个传输类别中的每一个类别中的字节。其中两个指针是绝对指针指特定的序列号一个是相对指针需要做偏移。 由图可看出可用窗口大小 SND.WND -SND.NXT - SND.UNA。 接收方就两个指针一个是窗口大小一个是期望发来的序列号。 同时注意接收窗口的大小是约等于发送窗口的大小的。滑动窗口并不是一成不变的。 流量控制 TCP 提供一种机制可以让「发送方」根据「接收方」的实际接收能力控制发送的数据量这就是所谓的流量控制。 一个具体的例子可以直接看这张图 操作系统缓冲区与滑动窗口 实际上发送窗口和接收窗口中所存放的字节数都是放在操作系统内存缓冲区中的而操作系统的缓冲区会被操作系统调整。 这种调整可以看下面两个图的例子来感受一下就不配说明性的文字了图已经比较清晰的展示出来了 当发送方可用窗口变为 0 时发送方实际上会定时发送窗口探测报文以便知道接收方的窗口是否发生了改变这个内容后面会进一步学习。 上图的情况就会出现最后发送端的窗口出现负值。为了规避这种情况TCP 规定是不允许同时减少缓存又收缩窗口的而是采用先收缩窗口过段时间再减少缓存这样就可以避免了丢包情况。 窗口关闭 如果窗口大小为 0 时就会阻止发送方给接收方传递数据直到窗口变为非 0 为止这就是窗口关闭。 接收方是通过 ACK 报文来告知发送方窗口大小的如果窗口变为 0 然后发送 ACK 报文之后恢复了窗口在通过 ACK 告知但是报文丢失就会造成死锁两边都在等对方的通知。 为了解决死锁TCP 为每个连接设有一个持续定时器只要 TCP 连接一方收到对方的零窗口通知就启动持续计时器。 如果持续计时器超时就会发送窗口探测 ( Window probe ) 报文而对方在确认这个探测报文时给出自己现在的接收窗口大小。 该过程流程图如下 糊涂窗口综合征 如果接收方来不及取走窗口数据那么就会导致窗口的减小如果接收方腾出几个字节并告诉发送方现在有几个字节的窗口而发送方会义无反顾地发送这几个字节这就是糊涂窗口综合症。 也就是说在接收方来不及取出数据的时候就会告知窗口大小然后每次循环就会导致最后窗口越来越小每次只能发送一点点数据。那么为了解决这个问题一般就是同时解决两个方面接收方不通告小窗口发送方避免发送小数据。 接收方的通常策略 当**「窗口大小」小于 min( MSS缓存空间/2 )** 也就是小于 MSS 与 1/2 缓存大小中的最小值时就会向发送方通告窗口为 0也就阻止了发送方再发数据过来。当窗口大小 MSS或者接收方缓存空间超过一半可用再打开窗口让发送方发送数据。 发送方策略 使用 Nagle 算法该算法的思路是延时处理只有满足下面两个条件中的任意一个条件才可以发送数据 条件一要等到窗口大小 MSS 并且 数据大小 MSS条件二收到之前发送数据的 ack 回包 也就是说接收方得满足「不通告小窗口给发送方」 发送方开启 Nagle 算法才能避免糊涂窗口综合症。一般 Nagle 算法是默认开启的除非就是一些小数据交换的应用场景才会关闭 Nagle 算法。 拥塞控制 流量控制是避免「发送方」的数据填满「接收方」的缓存但是并不知道网络的中发生了什么。 但是其他主机通信也可能导致网络拥堵不只是当前的一对 TCP 连接的问题。 在网络出现拥堵时如果继续发送大量数据包可能会导致数据包时延、丢失等这时 TCP 就会重传数据但是一重传就会导致网络的负担更重于是会导致更大的延迟以及更多的丢包这个情况就会进入恶性循环被不断地放大… 拥塞控制控制的目的就是避免「发送方」的数据填满整个网络。 所以为了在「发送方」调节所要发送数据的量定义了一个叫做「拥塞窗口」的概念。拥塞窗口 cwnd 是发送方维护的一个的状态变量它会根据网络的拥塞程度动态变化的。 发送窗口 swnd 和接收窗口 rwnd 是约等于的关系那么由于加入了拥塞窗口的概念后此时发送窗口的值是swnd min(cwnd, rwnd)也就是拥塞窗口和接收窗口中的最小值。 拥塞窗口 cwnd 的变化规则如下 网络中没有出现拥塞cwnd 会增大网络出现拥塞cwnd 会减小。 网络拥塞的判断条件只要「发送方」没有在规定时间内接收到 ACK 应答报文也就是发生了超时重传就会认为网络出现了拥塞。 慢启动 慢启动的算法记住一个规则就行当发送方每收到一个 ACK拥塞窗口 cwnd 的大小就会加 1。也就是说慢启动的发包个数是指数增长的如下图所示 当然慢启动到了一定情况就会停止有一个慢启动门限 ssthresh slow start threshold状态变量。一般来说 ssthresh 的大小是 65535 字节。 当 cwnd ssthresh 时使用慢启动算法。当 cwnd ssthresh 时就会使用「拥塞避免算法」。 拥塞避免算法 进入拥塞避免算法后慢启动门限 ssthresh 的规则是每当收到一个 ACK 时cwnd 增加 1/cwnd。也就是说进入拥塞避免算法发包的个数变成了线性增长。 拥塞发生 拥塞发生后就会触发重传机制一般而言有两个一个是超时重传一个是快速重传。两者的拥塞发送算法不同。 超时重传 ssthresh 设为 cwnd/2cwnd 重置为 1 是恢复为 cwnd 初始化值这里假定 cwnd 初始化值 1可以用 ss -nli 命令查看每一个 TCP 连接的 cwnd 初始化值。这时的拥塞发生算法如下所示 于是该算法触发后就会再次回到慢启动。该算法较为激进。 快速重传 当接收方发现丢了一个中间包的时候发送三次前一个包的 ACK于是发送端就会快速地重传不必等待超时再重传。这时相当于只丢了一小部分的包所以并不严重。 cwnd cwnd/2 也就是设置为原来的一半; ssthresh cwnd; 进入快速恢复算法。 快速恢复 拥塞窗口 cwnd ssthresh 3 3 的意思是确认有 3 个数据包被收到了重传丢失的数据包如果再收到重复的 ACK那么 cwnd 增加 1如果收到新数据的 ACK 后把 cwnd 设置为第一步中的 ssthresh 的值原因是该 ACK 确认了新的数据说明从 duplicated ACK 时的数据都已收到该恢复过程已经结束可以回到恢复之前的状态了也即再次进入拥塞避免状态 TCP 实战抓包 如何抓包 分析网络的利器tcpdump 和 Wireshark。 tcpdump 仅支持命令行格式使用常用在 Linux 服务器中抓取和分析网络包。Wireshark 除了可以抓包外还提供了可视化分析网络包的图形页面。 一般两者可搭配使用tcpdump 在 Linux 服务器抓包然后文件放到 Windows 中用 Wireshark 可视化分析。 如果是抓取 PING 命令包就需要先知道 PING 命令是 icmp 协议那么就可以只抓 icmp 协议的数据包。抓包的输出格式就是 时间戳协议源地址.源端口 目的地址.目的端口 网络包信息 这么一个格式如下所示 一般 tcpdump 的常用选项以及过滤表达式如下两张表格所示 抓包完成后把 tcpdump 抓取的数据包保存成 pcap 后缀的文件接着用 Wireshark 工具进行数据包分析。 保存之后直接用 Wireshark 打开就可以直观的来分析数据。点开每一个数据包还可以看到各个协议栈各层的信息如下 抓包分析 TCP 三次握手和四次挥手 这里具体的可以看这篇这里我就自己记一下笔记解密 TCP Wireshark 可以用时序图直接看数据包的交互同时序列号 Seq 或默认用相对值来显示。 这里注意会出现三次挥手的原因当被动关闭方上图的服务端在 TCP 挥手过程中「没有数据要发送」并且「开启了 TCP 延迟确认机制」那么第二和第三次挥手就会合并传输这样就出现了三次挥手。 三次握手异常 这里也是看的图解具体见链接三次握手异常的实验 第一次握手 SYN 丢包 模拟方法就是拔掉网线来模拟。 实验可知超时重传 SYN 数据包每次超时重传的 RTO 是翻倍上涨的直到 SYN 包的重传次数到达 tcp_syn_retries 值后客户端不再发送 SYN 包。 第二次握手 SYN ACK 丢包 在客户端加上防火墙限制直接粗暴的把来自服务端的数据都丢弃。 当第二次握手的 SYN、ACK 丢包时客户端会超时重发 SYN 包服务端也会超时重传 SYN、ACK 包。 客户端 SYN 包超时重传的最大次数是由 tcp_syn_retries 决定的默认值是 5 次服务端 SYN、ACK 包时重传的最大次数是由 tcp_synack_retries 决定的默认值是 5 次。 第三次握手 ACK 包丢失 模拟方法是在服务端配置防火墙屏蔽客户端 TCP 报文中标志位是 ACK 的包。 在建立 TCP 连接时如果第三次握手的 ACK服务端无法收到则服务端就会短暂处于 SYN_RECV 状态而客户端会处于 ESTABLISHED 状态。 由于服务端一直收不到 TCP 第三次握手的 ACK则会一直重传 SYN、ACK 包直到重传次数超过 tcp_synack_retries 值默认值 5 次后服务端就会断开 TCP 连接。 客户端则会有两种情况 如果客户端没发送数据包一直处于 ESTABLISHED 状态然后经过 2 小时 11 分 15 秒才可以发现一个「死亡」连接于是客户端连接就会断开连接。如果客户端发送了数据包一直没有收到服务端对该数据包的确认报文则会一直重传该数据包直到重传次数超过 tcp_retries2 值默认值 15 次后客户端就会断开 TCP 连接。 TCP 快速建立连接 在 Linux 3.7 内核版本中提供了 TCP Fast Open 功能这个功能可以减少 TCP 连接建立的时延。 在第一次建立连接的时候服务端在第二次握手产生一个 Cookie 已加密并通过 SYN、ACK 包一起发给客户端于是客户端就会缓存这个 Cookie所以第一次发起 HTTP Get 请求的时候还是需要 2 个 RTT 的时延在下次请求的时候客户端在 SYN 包带上 Cookie 发给服务端就提前可以跳过三次握手的过程因为 Cookie 中维护了一些信息服务端可以从 Cookie 获取 TCP 相关的信息这时发起的 HTTP GET 请求就只需要 1 个 RTT 的时延 通过设置 net.ipv4.tcp_fastopen 内核参数来打开 Fast Open 功能。1 就是客户端打开2 是服务端打开3 是都打开。 重复确认和快速重传 当发送方收到 3 个重复 ACK 时就会触发快速重传立刻重发丢失数据包。 流量控制 服务器会出现繁忙的情况当应用程序读取速度慢那么缓存空间会慢慢被占满于是为了保证发送方发送的数据不会超过缓冲区大小服务器则会调整窗口大小的值接着通过 ACK 报文通知给对方告知现在的接收窗口大小从而控制发送方发送的数据大小。 如果窗口收缩至 0发送方会定时发送窗口大小探测报文以便及时知道接收方窗口大小的变化。超时时间会翻倍递增。 TCP 延迟确认和 Nagle 算法 Nagle 算法一定会有一个小报文也就是在最开始的时候。 解决 ACK 传输效率低问题所以就衍生出了 TCP 延迟确认。 当有响应数据要发送时ACK 会随着响应数据一起立刻发送给对方当没有响应数据要发送时ACK 将会延迟一段时间以等待是否有响应数据可以一起发送如果在延迟等待发送 ACK 期间对方的第二个数据报文又到达了这时就会立刻发送 ACK 这两者如果一起使用就会造成额外的延时所以要么发送方关闭 Nagle要么接收方关闭 TCP 延迟确认。 TCP 半连接队列和全连接队列 服务端收到客户端发起的 SYN 请求后内核会把该连接存储到半连接队列并向客户端响应 SYNACK接着客户端会返回 ACK服务端收到第三次握手的 ACK 后内核会把连接从半连接队列移除然后创建新的完全的连接并将其添加到 accept 队列等待进程调用 accept 函数时把连接取出来。 两者都有最大长度限制超过限制时内核会直接丢弃或返回 RST 包。 全连接队列溢出 首先可以通过ss命令来查看 TCP 全连接队列的情况。获取到的 Recv-Q/Send-Q 在「LISTEN 状态」和「非 LISTEN 状态」所表达的含义是不同的。 「LISTEN 状态」 Recv-Q当前全连接队列的大小也就是当前已完成三次握手并等待服务端 accept() 的 TCP 连接Send-Q当前全连接最大队列长度上面的输出结果说明监听 8088 端口的 TCP 服务最大全连接长度为 128 「非 LISTEN 状态」 Recv-Q已收到但未被应用进程读取的字节数Send-Q已发送但未收到确认的字节数 可以使用 netstat -s 命令来查看 TCP 连接丢掉的个数 当服务端并发处理大量请求时如果 TCP 全连接队列过小就容易溢出。发生 TCP 全连接队溢出的时候后续的请求就会被丢弃这样就会出现服务端请求数量上不去的现象。当然也可以修改变成向客户端发送 RST 复位报文。修改 tcp_abort_on_overflow 为 1 即可默认是 0。 默认丢弃的原因只要服务器没有为请求回复 ACK请求就会被多次重发。如果服务器上的进程只是短暂的繁忙造成 accept 队列满那么当 TCP 全连接队列有空位时再次接收到的请求报文由于含有 ACK仍然会触发服务器端成功建立连接。所以tcp_abort_on_overflow 设为 0 可以提高连接建立的成功率只有你非常肯定 TCP 全连接队列会长期溢出时才能设置为 1 以尽快通知客户端。 TCP 全连接队列的最大值取决于 somaxconn 和 backlog 之间的最小值也就是 min(somaxconn, backlog)。也就是说如果持续不断地有连接因为 TCP 全连接队列溢出被丢弃就应该调大 backlog 以及 somaxconn 参数。 半连接队列溢出 可以抓住 TCP 半连接的特点就是服务端处于 SYN_RECV 状态的 TCP 连接就是 TCP 半连接队列。所以可以通过如下命令来查看当前 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; 当然以上只是理论值实际的丢弃逻辑如下 如果半连接队列满了并且没有开启 tcp_syncookies则会丢弃若全连接队列满了且没有重传 SYNACK 包的连接请求多于 1 个则会丢弃如果没有开启 tcp_syncookies并且 max_syn_backlog 减去 当前半连接队列长度小于 (max_syn_backlog 2)则会丢弃 如果当前半连接队列的长度 「没有超过」理论半连接队列最大值 max_qlen_log那么如果条件 3 成立则依然会丢弃 SYN 包也就会使得服务端处于 SYN_RECV 状态的最大个数不会是理论值 max_qlen_log。 所以服务端处于 SYN_RECV 状态的最大个数分为如下两种情况 如果「当前半连接队列」没超过「理论半连接队列最大值」但是超过 max_syn_backlog - (max_syn_backlog 2)那么处于 SYN_RECV 状态的最大个数就是 max_syn_backlog - (max_syn_backlog 2)如果「当前半连接队列」超过「理论半连接队列最大值」那么处于 SYN_RECV 状态的最大个数就是「理论半连接队列最大值」 当然Linux 内核版本不一样算法就会不一样。以上针对的是 Linux 2.6.32。 跟全连接队列一样并不是说半连接队列满了就只能丢弃。开启 syncookies 功能就可以在不使用 SYN 半连接队列的情况下成功建立连接在前面我们源码分析也可以看到这点当开启了 syncookies 功能就不会丢弃连接。 syncookies 是这么做的服务器根据当前状态计算出一个值放在己方发出的 SYNACK 报文中发出当客户端返回 ACK 报文时取出该值验证如果合法就认为连接建立成功如下图所示。 那么如果要应对造成半连接队列溢出的 SYN 攻击可以把 tcp_syncookies 的参数设置为 1。 半连接队列相关的最容易问的就是 如何防御 SYN 攻击 增大半连接队列开启 tcp_syncookies 功能减少 SYNACK 重传次数 要想增大半连接队列我们得知不能只单纯增大 tcp_max_syn_backlog 的值还需一同增大 somaxconn 和 backlog也就是增大全连接队列。 优化 TCP 三次握手性能提升 客户端优化 三次握手建立连接的首要目的是「同步序列号」。序列号就是在 TCP 头部中的一个 32 位的数据。 客户端发了 SYN 报文之后就会进入 SYN_SENT 状态如果没收到服务端返回的 SYNACK 报文就会进入 SYN 包的重传这里是有 tcp_syn_retries 参数控制每次超时时间是上一次的两倍默认重传 5 次大致是 63 秒之后停止重传。 这里的优化就是可以适当修改 tcp_syn_retries 参数控制 SYN 包的重传次数。 服务端优化 服务端收到 SYN 包后服务端会立马回复 SYNACK 包表明确认收到了客户端的序列号同时也把自己的序列号发给对方并将自己的状态切换至 SYN_RECV这时候内核就会构建「半连接队列」来维护「未完成」的握手信息当半连接队列溢出后服务端就无法再建立新的连接。 可以通过该 netstat -s 命令给出的统计结果中 可以得到由于半连接队列已满引发的失败次数 上面输出的数值是累计值表示共有多少个 TCP 连接因为半连接队列溢出而被丢弃。隔几秒执行几次如果有上升的趋势说明当前存在半连接队列溢出的现象。 增大半连接队列大小不能只单纯增大 tcp_max_syn_backlog 的值还需一同增大 somaxconn 和 backlog也就是增大 accept 队列。否则只单纯增大 tcp_max_syn_backlog 是无效的。 同时半连接队列已满只要开启 syncookies 功能就可以在不使用 SYN 半连接队列的情况下成功建立连接。把 tcp_syncookies 设置为 1 即可默认就是开启的。 服务端发送 SYNACK 报文后客户端收到就会回复 ACK 报文如果服务端没收到那就会重传 SYNACK 报文同时一直处于 SYN_RECV 状态。修改重发次数的方法是调整 tcp_synack_retries 参数。 直到服务端收到 ACK 报文建立成功内核会将连接从半连接队列中移除并添加到 accept 队列等待进程调用 accept 函数时把连接取出来。如果进程不能及时地调用 accept 函数就会造成 accept 队列也称全连接队列溢出最终导致建立好的 TCP 连接被丢弃。 全连接队列也是同理并不一定是丢弃可以设置 tcp_abort_on_overflow 为 1这样在溢出后就会返回客户端一个 RST 包。一般默认是 0直接丢弃这样处理更高效 accept 队列的长度取决于 somaxconn 和 backlog 之间的最小值也就是 min(somaxconn, backlog)。 绕过三次握手 如果是三次握手那至少需要一个 RTT 后才能发送数据在第三次握手的 ACK 包中可以携带数据。 绕过三次握手这个之前也有讲过就是在 Linux 3.7 之后提供了 TCP Fast Open 功能。 第一次握手就是正常的三次握手流程。但如果之后再次建立连接这时客户端发送 SYN 报文就会携带数据以及之前记录的 Cookie然后服务器就会校验该 Cookie有效就会在返回的 SYNACK 包中对 SYN 和数据确认并将数据进一步给应用进程处理无效则会丢弃只发送正常的 SYNACK 包之后客户端会发送 ACK 确认服务器的 SYN 和数据。 相当于绕过了三次握手减少了 1 个 RTT 的时间消耗。 开启了 TFO 功能这个 cookie 是放在 TCP 头部中的 设置 tcp_fastopn 内核参数为 3来打开 Fast Open 功能服务端和客户端均需打开。 四次挥手性能提升 通常先关闭连接的一方称为主动方后关闭连接的一方称为被动方。 当主动方关闭连接时会发送 FIN 报文此时发送方的 TCP 连接将从 ESTABLISHED 变成 FIN_WAIT1。当被动方收到 FIN 报文后内核会自动回复 ACK 报文连接状态将从 ESTABLISHED 变成 CLOSE_WAIT表示被动方在等待进程调用 close 函数关闭连接。当主动方收到这个 ACK 后连接状态由 FIN_WAIT1 变为 FIN_WAIT2也就是表示主动方的发送通道就关闭了。当被动方进入 CLOSE_WAIT 时被动方还会继续处理数据等到进程的 read 函数返回 0 后应用程序就会调用 close 函数进而触发内核发送 FIN 报文此时被动方的连接状态变为 LAST_ACK。当主动方收到这个 FIN 报文后内核会回复 ACK 报文给被动方同时主动方的连接状态由 FIN_WAIT2 变为 TIME_WAIT在 Linux 系统下大约等待 1 分钟后TIME_WAIT 状态的连接才会彻底关闭。当被动方收到最后的 ACK 报文后被动方的连接就会关闭。 主动关闭连接的才有 TIME_WAIT 状态。 主动方优化 关闭连接的方式通常有两种分别是 RST 报文关闭和 FIN 报文关闭。如果进程收到 RST 报文就直接关闭连接了不需要走四次挥手流程是一个暴力关闭连接的方式。 四次挥手由进程调用 close 和 shutdown 函数发起 FIN 报文shutdown 参数须传入 SHUT_WR 或者 SHUT_RDWR 才会发送 FIN。调用了 close 函数意味着完全断开连接完全断开不仅指无法传输数据而且也不能发送数据。 此时调用了 close 函数的一方的连接叫做「孤儿连接」如果你用 netstat -p 命令会发现连接对应的进程名为空。使用 close 函数关闭连接是不优雅的。于是就出现了一种优雅关闭连接的 shutdown 函数它可以控制只关闭一个方向的连接。第一个参数就是 sock第二个参数决定了关闭的方向0 就是关闭连接的「读」这个方向1 就是关闭连接的「写」这个方向2 相当于关闭套接字的读和写两个方向。 主动方发送 FIN 报文后会等待 ACK 报文连接就处于 FIN_WAIT1 状态如果收不到就会重发 FIN 报文重发次数由 tcp_orphan_retries 参数控制默认为 0实际就是重发 8 次。如果 FIN_WAIT1 状态连接很多我们就需要考虑降低 tcp_orphan_retries 的值当重传次数超过 tcp_orphan_retries 时连接就会直接关闭掉。 如果遇到恶意攻击FIN 报文无法发出那就调整 tcp_max_orphans 参数它定义了「孤儿连接」的最大数量。当进程调用了 close 函数关闭连接此时连接就会是「孤儿连接」因为它无法再发送和接收数据。如果孤儿连接数量大于它新增的孤儿连接将不再走四次挥手而是直接发送 RST 复位报文强制关闭。 主动方收到 ACK 报文后会处于 FIN_WAIT2 状态就表示主动方的发送通道已经关闭接下来将等待对方发送 FIN 报文关闭对方的发送通道。如果连接是用 shutdown 函数关闭的连接可以一直处于 FIN_WAIT2 状态因为它可能还可以发送或接收数据。但对于 close 函数关闭的孤儿连接由于无法再发送和接收数据所以这个状态不可以持续太久而 tcp_fin_timeout 控制了这个状态下连接的持续时长默认值是 60表示能在 FIN_WAIT2 持续 60s。与 TIME_WAIT 状态持续的时间是相同的 TIME_WAIT 是主动方四次挥手的最后一个状态也是最常遇见的状态。当收到被动方发来的 FIN 报文后主动方会立刻回复 ACK表示确认对方的发送通道已经关闭接着就处于 TIME_WAIT 状态。在 Linux 系统TIME_WAIT 状态会持续 60 秒后才会进入关闭状态。 **TIME_WAIT 状态存在的必要之前已经学过了可以往上翻到最前面刚学四次挥手的地方。**主要是两点 防止历史连接中数据被后面的四元组连接错误接受保证「被动关闭连接」的一方能被正确的关闭 Linux 提供了 tcp_max_tw_buckets 参数当 TIME_WAIT 的连接数量超过该参数时新关闭的连接就不再经历 TIME_WAIT 而直接关闭。当服务器的并发连接增多时相应地同时处于 TIME_WAIT 状态的连接数量也会变多此时就应当调大 tcp_max_tw_buckets 参数减少不同连接间数据错乱的概率。tcp_max_tw_buckets 也不是越大越好毕竟系统资源是有限的。 有一种方式可以在建立新连接时复用处于 TIME_WAIT 状态的连接那就是打开 tcp_tw_reuse 参数。但是需要注意该参数是只用于客户端建立连接的发起方因为是在调用 connect() 时起作用的而对于服务端被动连接方是没有用的。同时还有一个前提是双方都需要打开时间戳tcp_timestamps 设置为 1。时间戳一开TIME_WAIT 的 2MSL 就会因为重复数据包的时间戳过期直接舍弃还可以防止序列号的绕回。 还可以直接在 socket 选项中设置调用 close 关闭连接行为。 l_onoff 为非 0 且 l_linger 值为 0那么调用 close 后会立该发送一个 RST 标志给对端该 TCP 连接将跳过四次挥手也就跳过了 TIME_WAIT 状态直接关闭。只推荐在客户端使用 被动方优化 当被动方收到 FIN 报文时内核会自动回复 ACK同时连接处于 CLOSE_WAIT 状态顾名思义它表示等待应用进程调用 close 函数关闭连接。当你用 netstat 命令发现大量 CLOSE_WAIT 状态就需要排查你的应用程序因为可能因为应用程序出现了 Bugread 函数返回 0 时没有调用 close 函数。 处于 CLOSE_WAIT 状态时调用了 close 函数内核就会发出 FIN 报文关闭发送通道同时连接进入 LAST_ACK 状态等待主动方返回 ACK 来确认连接关闭。如果迟迟收不到这个 ACK内核就会重发 FIN 报文重发次数仍然由 tcp_orphan_retries 参数控制这与主动方重发 FIN 报文的优化策略一致。 还有一种特殊情况因为 TCP 是全双工连接就有可能出现客户端和服务端同时关闭连接两者都认为自己是主动方发送 FIN 报文后进入 FIN_WAIT1 状态重发同样是 tcp_orphan_retries 控制之后双方在等待 ACK 报文的过程中都等来了 FIN 报文。这是一种新情况所以连接会进入一种叫做 CLOSING 的新状态它替代了 FIN_WAIT2 状态。 传输数据性能提升 TCP 连接是由内核维护的内核会为每个连接建立内存缓冲区内存过小就会无法充分利用网络带宽内存过大会导致服务器资源耗尽无法建立新连接。 滑动窗口 TCP 会保证每一个报文都能够抵达对方它的机制是这样报文发出去后必须接收到对方返回的确认报文 ACK如果迟迟未收到就会超时重发该报文直到收到对方的 ACK 为止。所以TCP 报文发出去后并不会立马从内存中删除因为重传时还需要用到它。 TCP 提供一种机制可以让「发送方」根据「接收方」的实际接收能力控制发送的数据量这就是滑动窗口的由来。同时要注意发送方的窗口和接收方的窗口都是动态变化的如果不考虑拥塞控制那么两者是约等于的关系。 这里的窗口是 2 个字节也就是最大值为 65535 字节64 KB。后续有了扩充窗口的方法在 TCP 选项字段定义了窗口扩大因子用于扩大 TCP 通告窗口其值大小是 2^14这样就使 TCP 的窗口大小从 16 位扩大为 30 位2^16 * 2^ 14 2^30所以此时窗口的最大值可以达到 1GB。 该功能需要配置 tcp_window_scaling 设为 1默认打开。同时要使用窗口扩大选项通讯双方必须在各自的 SYN 报文中发送这个选项。 但是因为网络的传输能力是有限的当发送方依据发送窗口发送超过网络处理能力的报文时路由器会直接丢弃这些报文。因此缓冲区的内存并不是越大越好。 确定最大传输速度 网络是有「带宽」限制的带宽描述的是网络传输能力它与内核缓冲区的计量单位不同: 带宽是单位时间内的流量表达是「速度」比如常见的带宽 100 MB/s缓冲区单位是字节当网络速度乘以时间才能得到字节数 带宽时延积它决定网络中飞行报文的大小由于发送缓冲区大小决定了发送窗口的上限而发送窗口又决定了「已发送未确认」的飞行报文的上限。因此发送缓冲区不能超过「带宽时延积」。 发送缓冲区的大小最好是往带宽时延积靠近。 调整缓冲区大小 发送缓冲区它的范围通过 tcp_wmem 参数配置。 第一个数值是动态范围的最小值4096 byte 4K第二个数值是初始默认值16384 byte ≈ 16K第三个数值是动态范围的最大值4194304 byte 4096K4M 发送缓冲区是自行调节的。 接收缓冲区的调整就比较复杂一些先来看看设置接收缓冲区范围的 tcp_rmem 参数。 第一个数值是动态范围的最小值表示即使在内存压力下也可以保证的最小接收缓冲区大小4096 byte 4K第二个数值是初始默认值87380 byte ≈ 86K第三个数值是动态范围的最大值6291456 byte 6144K6M 接收缓冲区可以根据系统空闲内存的大小来调节接收窗口但是不是自动开启的需要配置 tcp_moderate_rcvbuf 为 1 来开启调节功能。 同时引入了新的问题接收缓冲区调节时怎么知道当前内存是否紧张或充分呢这是通过 tcp_mem 配置完成的。 当 TCP 内存小于第 1 个值时不需要进行自动调节在第 1 和第 2 个值之间时内核开始调节接收缓冲区的大小大于第 3 个值时内核不再为 TCP 分配新内存此时新连接是无法建立的 在高并发服务器中为了兼顾网速与大量的并发连接我们应当保证缓冲区的动态调整的最大值达到带宽时延积而最小值保持默认的 4K 不变即可。而对于内存紧张的服务而言调低默认值是提高并发的有效手段。 同时如果这是网络 IO 型服务器那么调大 tcp_mem 的上限可以让 TCP 连接使用更多的系统内存这有利于提升并发能力。需要注意的是tcp_wmem 和 tcp_rmem 的单位是字节而 tcp_mem 的单位是页面大小。而且千万不要在 socket 上直接设置 SO_SNDBUF 或者 SO_RCVBUF这样会关闭缓冲区的动态调整功能。 TCP是面向字节流协议 理解字节流 UDP 是面向报文的TCP 是面向字节流的两者的发送方机制不同。 UDP UDP 协议传输时操作系统不会对消息进行拆分在组装好 UDP 头部后就交给网络层来处理所以发出去的 UDP 报文中的数据部分就是完整的用户消息也就是每个 UDP 报文就是一个用户消息的边界这样接收方在接收到 UDP 报文后读一个 UDP 报文就能读取到完整的用户消息。 对操作系统而言收到 UDP 报文会插入一个队列队列的每个元素就是一个 UDP 报文用户调用 recvfrom() 读取就会从队列读取数据然后从内核拷贝到用户缓冲区。 TCP TCP 协议传输时消息可能会被操作系统分组成多个的 TCP 报文也就是一个完整的用户消息被拆分成多个 TCP 报文进行传输。 所以接收方一定要知道发送方的消息长度不然无法解析成一个完成的用户消息。 在发送端当我们调用 send 函数完成数据“发送”以后数据并没有被真正从网络上发送出去只是从应用程序拷贝到了操作系统内核协议栈中。至于什么时候真正被发送取决于发送窗口、拥塞窗口以及当前发送缓冲区的大小等条件。 所以不能认为一个用户消息对应一个 TCP 报文正因为这样所以 TCP 是面向字节流的协议。 解决粘包 一般有三种方式分包的方式 固定长度的消息特殊字符作为边界自定义消息结构。 固定长度的消息 最简单的方法直接规定每一个消息的长度是相通的。但是使用很不灵活很少使用。 特殊字符作为边界 可以在两个用户消息之间插入特殊的字符接收数据时读到这个字符相当于读完了一个完整消息。 HTTP 就是用了这种方法 通过设置回车符、换行符作为 HTTP 报文协议的边界。 自定义消息结构 可以自定义一个消息结构由包头和数据组成其中包头包是固定大小的而且包头里有一个字段来说明紧随其后的数据有多大。 TCP 建立初始化序列号均不相同 主要目的防止历史报文被像一个相同四元组的连接接收。 TIME_WAIT 会使得历史报文消失 虽然 TCP 连接中 TIME_WAIT 持续 2 MSL 时长后历史报文会自动消失但前提是能正常四次挥手。例如如下的情况就能很好的解释为什么可能有历史报文 初始化序列号不同是否解决历史报文 当然即使客户端和服务端的初始化序列号不一样也会存在收到历史报文的可能。但是历史报文是否接收还要看序列号是否在接收窗口只有在窗口才会接收。如果每次连接客户端和服务端序列号均不相同那么大概率历史报文序列号「不在」对方接收窗口从而很大程度上避免了历史报文。 初始化序列号随机化 RFC793 采用了 ISN 随机生成算法基本不会有一样的初始化序列号。 先来了解序列号SEQ和初始序列号ISN。 序列号是 TCP 一个头部字段标识了 TCP 发送端到 TCP 接收端的数据流的一个字节因为 TCP 是面向字节流的可靠协议为了保证消息的顺序性和可靠性TCP 为每个传输方向上的每个字节都赋予了一个编号以便于传输成功后确认、丢失后重传以及在接收端保证不会乱序。序列号是一个 32 位的无符号数因此在到达 4G 之后再循环回到 0。初始序列号在 TCP 建立连接的时候客户端和服务端都会各自生成一个初始序列号它是基于时钟生成的一个随机数来保证每个连接都拥有不同的初始序列号。初始化序列号可被视为一个 32 位的计数器该计数器的数值每 4 微秒加 1循环一次需要 4.55 小时。 也就是说序列号和初始化序列号均会发生回绕无法根据序列号来直接判断新老数据。为了解决这个问题就需要有 TCP 时间戳。 tcp_timestamps 参数是默认开启的开启了 tcp_timestamps 参数TCP 头部就会使用时间戳选项它有两个好处一个是便于精确计算 RTT 另一个是能防止序列号回绕PAWS。如果发现收到的数据包中时间戳不是递增的则表示该数据包是过期的就会直接丢弃这个数据包。 当然时间戳也可能回绕。Linux 在 PAWS 检查做了一个特殊处理如果一个 TCP 连接连续 24 天不收发数据则在接收第一个包时基于时间戳的 PAWS 会失效也就是可以 PAWS 函数会放过这个特殊的情况认为是合法的可以接收该数据包。所以解决这个问题可以如下出发 增加时间戳大小从原先的 32 bit 扩大到 64 bit。会造成兼容问题。将与时钟频率无关的值作为时间戳时钟频率可以增加但时间戳增速不变。这样报文太多一样会导致时间戳失去意义。 SYN 报文被丢弃情况 可能的 SYN 报文被丢弃情况 开启 tcp_tw_recycle 参数并且在 NAT 环境下造成 SYN 报文被丢弃TCP 两个队列满了半连接队列和全连接队列造成 SYN 报文被丢弃 tcp_tw_recycle TCP 四次挥手过程中主动断开连接方会有一个 TIME_WAIT 的状态这个状态会持续 2 MSL 后才会转变为 CLOSED 状态。默认 TIME_WAIT 在 Linux 中是 60 秒。 如果客户端发起连接方的 TIME_WAIT 状态过多占满了所有端口资源就无法对「目的 IP 目的 PORT」都一样的服务器发起连接了。当然TIME_WAIT 也是有作用的 防止具有相同四元组的旧数据包被收到也就是防止历史连接中的数据被后面的连接接受否则就会导致后面的连接收到一个无效的数据保证「被动关闭连接」的一方能被正确的关闭即保证最后的 ACK 能让被动关闭方接收从而帮助其正常关闭。 Linux 操作系统提供了两个可以系统参数来快速回收处于 TIME_WAIT 状态的连接这两个参数都是默认关闭的 net.ipv4.tcp_tw_reuse如果开启该选项的话客户端连接发起方 在调用 connect() 函数时如果内核选择到的端口已经被相同四元组的连接占用的时候就会判断该连接是否处于 TIME_WAIT 状态如果该连接处于 TIME_WAIT 状态并且 TIME_WAIT 状态持续的时间超过了 1 秒那么就会重用这个连接然后就可以正常使用该端口了。所以该选项只适用于连接发起方。net.ipv4.tcp_tw_recycle如果开启该选项的话允许处于 TIME_WAIT 状态的连接被快速回收。 要使得这两个选项生效有一个前提条件就是要打开 TCP 时间戳即 net.ipv4.tcp_timestamps1默认即为 1)。 tcp_tw_recycle 在使用了 NAT 的网络下是不安全的 对于服务器如果同时开启了 recycle 和timestamps 则会开启一种称之为「 per-host 的 PAWS 机制」。 PAWS 就是上一篇中的防回绕的机制。per-host 是对「对端 IP 做 PAWS 检查」而非对「IP 端口」四元组做 PAWS 检查。 Per-host PAWS 机制利用TCP option里的 timestamp 字段的增长来判断串扰数据而 timestamp 是根据客户端各自的 CPU tick 得出的值。如果通过 NAT 网关建立 TCP 连接都会得到相同的 IP 地址那么如果有两个客户端就存在可能 timestamps 的大小来触发 PAWS 进而导致第二个客户端的 SYN 包被丢掉。 tcp_tw_recycle 在 Linux 4.12 版本后直接取消了这一参数。 accept 队列满 TCP 三次握手内核会维护两个队列 半连接队列 SYN 队列全连接队列accept 队列。 服务端收到客户端发起的 SYN 请求后内核会把该连接存储到半连接队列并向客户端响应 SYNACK接着客户端会返回 ACK服务端收到第三次握手的 ACK 后内核会把连接从半连接队列移除然后创建新的完全的连接并将其添加到 accept 队列等待进程调用 accept 函数时把连接取出来。 在服务端并发处理大量请求时如果 TCP accpet 队列过小或者应用程序调用 accept() 不及时就会造成 accpet 队列满了 这时后续的连接就会被丢弃这样就会出现服务端请求数量上不去的现象。 半连接队列满 例如服务器遭受 syn 攻击就会导致半连接队列满了之后的 SYN 包就会被丢弃。当然开启 syncookies 功能即使半连接队列满了也不会丢弃 SYN 包。 syncookies 是这么做的服务器根据当前状态计算出一个值放在己方发出的 SYNACK 报文中发出当客户端返回 ACK 报文时取出该值验证如果合法就认为连接建立成功。如果要开启只需要设置 syncookies 参数为 1 就可以了。 以下有三种应对 SYN 攻击的方法来解决 SYN 丢包 增大半连接队列 要想增大半连接队列我们得知不能只单纯增大 tcp_max_syn_backlog 的值还需一同增大 somaxconn 和 backlog也就是增大全连接队列。 开启 tcp_syncookies 参数减少 SYNACK 重传次数 已经建立 TCP 连接收到 SYN 客户端中途宕机服务端没有数据发送就一直处于 Established 状态客户端恢复连接服务端会如何呢 此时客户端 IP、服务端 IP、目的端口均无变化所以主要看 SYN 包的源端口与上一次源端口是否相同。 不相同 那么就会三次握手建立新的连接 旧连接如果服务端发数据就会有 RST 报文然后服务端释放连接如果一直没有数据那么超过一定时间 TCP 保活机制出发就会释放。 相同 处于 Established 状态的服务端如果收到了客户端的 SYN 报文注意此时的 SYN 报文其实是乱序的因为 SYN 报文的初始化序列号其实是一个随机数会回复一个携带了正确序列号和确认号的 ACK 报文这个 ACK 被称之为 Challenge ACK。 接着客户端收到这个 Challenge ACK发现确认号ack num并不是自己期望收到的于是就会回 RST 报文服务端收到后就会释放掉该连接。 关闭 TCP 连接 直接杀掉进程会造成不同影响 客户端杀掉会发送 FIN 报文断开客户端进程与服务端建立的所有 TCP 连接其余客户端不影响服务端直接杀掉所有 TCP 连接均关闭。 可以通过伪造一个 RST 报文来完成关闭要注意的是必须同时满足「四元组相同」和「序列号是对方期望的」这两个条件。 这件事情可以通过 killcx 工具完成直接发 SYN 报文服务端会回复一个 Challenge ACK他的报文确认号就是服务端下一次想要接收的序列号。用这个序列号作为 RST 的序列号就会释放连接。 killcx 工具则是属于主动获取它是主动发送一个 SYN 报文通过对方回复的 Challenge ACK 来获取正确的序列号所以这种方式无论 TCP 连接是否活跃都可以关闭。 tcpkill 工具也可以关闭连接其在双方 TCP 通信中拿到下一次期望的序列号从而伪造 RST 报文。这是被动获取的方式无法关闭非活跃 TCP 连接。 四次挥手收到乱序 FIN 包 在 FIN_WAIT_2 状态下是如何处理收到的乱序到 FIN 报文然后 TCP 连接又是什么时候才进入到 TIME_WAIT 状态 这里直接上结论 在 FIN_WAIT_2 状态时如果收到乱序的 FIN 报文那么就被会加入到「乱序队列」并不会进入到 TIME_WAIT 状态。 等再次收到前面被网络延迟的数据包时会判断乱序队列有没有数据然后会检测乱序队列中是否有可用的数据如果能在乱序队列中找到与当前报文的序列号保持的顺序的报文就会看该报文是否有 FIN 标志如果发现有 FIN 标志这时才会进入 TIME_WAIT 状态。 可以看下图 TIME_WAIT 状态 TCP 连接收到 SYN 这个状态就是下图这样 这个问题关键是要看 SYN 的「序列号和时间戳」是否合法因为处于 TIME_WAIT 状态的连接收到 SYN 后会判断 SYN 的「序列号和时间戳」是否合法然后根据判断结果的不同做不同的处理。 什么是「合法」的 SYN 合法 SYN客户端的 SYN 的「序列号」比服务端「期望下一个收到的序列号」要大并且 SYN 的「时间戳」比服务端「最后收到的报文的时间戳」要大。非法 SYN客户端的 SYN 的「序列号」比服务端「期望下一个收到的序列号」要小或者 SYN 的「时间戳」比服务端「最后收到的报文的时间戳」要小。 如果没有时间戳就可以简化成以下形式 合法 SYN客户端的 SYN 的「序列号」比服务端「期望下一个收到的序列号」要大。非法 SYN客户端的 SYN 的「序列号」比服务端「期望下一个收到的序列号」要小。 收到合法 SYN 会直接重用此四元组跳过 2 MSL 而转变为 SYN_RECV 状态接着就能进行建立连接的过程。 这个过程如下所示 在收到第三次挥手的 FIN 报文时会记录该报文的 TSval 21用 ts_recent 变量保存。然后会计算下一次期望收到的序列号本次例子下一次期望收到的序列号就是 301用 rcv_nxt 变量保存。 之后接到 SYN因为符合判断逻辑所以是一个「合法的 SYN」于是就会重用此四元组连接跳过 2MSL 而转变为 SYN_RECV 状态接着就能进行建立连接过程。 收到非法 SYN 再回复一个第四次挥手的 ACK 报文客户端收到后发现并不是自己期望收到确认号ack num就回 RST 报文给服务端。 这个过程如下所示 TIME_WAIT 状态收到 RST 收到 RST 之后是否会断开 会不会断开关键看 net.ipv4.tcp_rfc1337 这个内核参数默认情况是为 0 如果这个参数设置为 0 收到 RST 报文会提前结束 TIME_WAIT 状态释放连接。如果这个参数设置为 1 就会丢掉 RST 报文。 这里要注意如果收到 RST 默认参数就会直接释放连接跳过了 2MSL 时间这是有一定风险的。2MSL的主要目的是 防止历史连接中的数据被后面相同四元组的连接错误的接收保证「被动关闭连接」的一方能被正确的关闭 TCP 连接一端断电与进程崩溃 这是 TCP 异常断开连接的一个场景。 首先没有打开 TCP keepalive也就是关闭了保活机制不会发送探测报文。这个机制如下 如果对端程序是正常工作的。当 TCP 保活的探测报文发送给对端, 对端会正常响应这样 TCP 保活时间会被重置等待下一个 TCP 保活时间的到来。如果对端主机崩溃或对端由于其他原因导致报文不可达。当 TCP 保活的探测报文发送给对端后石沉大海没有响应连续几次达到保活探测次数后TCP 会报告该 TCP 连接已经死亡。 主机崩溃 客户端主机崩溃了服务端是无法感知到的在加上服务端没有开启 TCP keepalive又没有数据交互的情况下服务端的 TCP 连接将会一直处于 ESTABLISHED 连接状态直到服务端重启进程。 进程崩溃 当服务端的进程崩溃后内核需要回收该进程的所有 TCP 连接资源于是内核会发送第一次挥手 FIN 报文后续的挥手过程也都是在内核完成并不需要进程的参与所以即使服务端的进程退出了还是能与客户端完成 TCP四次挥手的过程。 有数据传输 客户端主机宕机迅速重启 在客户端主机宕机后服务端向客户端发送的报文会得不到任何的响应在一定时长后服务端就会触发超时重传机制重传未得到响应的报文。 客户端重启就会收到这个重传报文 如果客户端主机上没有进程绑定该 TCP 报文的目标端口号那么客户端内核就会回复 RST 报文重置该 TCP 连接如果客户端主机上有进程绑定该 TCP 报文的目标端口号由于客户端主机重启后之前的 TCP 连接的数据结构已经丢失了客户端内核里协议栈会发现找不到该 TCP 连接的 socket 结构体于是就会回复 RST 报文重置该 TCP 连接。 总结只要有一方重启完成后收到之前 TCP 连接的报文都会回复 RST 报文以断开连接。 客户端主机宕机一直没有重启 服务端多次重传达到阈值就会调用 Socket 接口断开 TCP 连接。 重传次数 由 Linux 的参数 tcp_retries2 决定默认为 15 内核会根据 tcp_retries2 设置的值计算出一个 timeout如果 tcp_retries2 15那么计算得到的 timeout 924600 ms如果重传间隔超过这个 timeout则认为超过了阈值就会停止重传然后就会断开 TCP 连接。并且重传的超时时间RTO是倍数增长的。而 RTO 是根据 RTT一个包往返时间计算。 在默认情况下RTT 较小那么 RTO 就是大致为下限 200 ms大致就是重传 15 次。 拔掉网线后 TCP 连接 直接说结论不会影响仍然处于 ESTABLISHED 状态。 拔掉后有数据传输 服务端传输给客户端无响应那么一段时间后就会触发服务端的超时重传。 如果在服务端重传报文的过程中客户端刚好把网线插回去了由于拔掉网线并不会改变客户端的 TCP 连接状态并且还是处于 ESTABLISHED 状态所以这时客户端是可以正常接收服务端发来的数据报文的然后客户端就会回 ACK 响应报文。 如果如果在服务端重传报文的过程中客户端一直没有将网线插回去服务端超时重传报文的次数达到一定阈值后内核就会判定出该 TCP 有问题然后通过 Socket 接口告诉应用程序该 TCP 连接出问题了于是服务端的 TCP 连接就会断开。而等客户端插回网线后如果客户端向服务端发送了数据由于服务端已经没有与客户端相同四元祖的 TCP 连接了因此服务端内核就会回复 RST 报文客户端收到后就会释放该 TCP 连接。 拔掉后没有数据传输 如果没有开启 TCP keepalive 机制在客户端拔掉网线后并且双方都没有进行数据传输那么客户端和服务端的 TCP 连接将会一直保持存在。 而如果开启了 TCP keepalive 机制在客户端拔掉网线后即使双方都没有进行数据传输在持续一段时间后TCP 就会发送探测报文 如果对端是正常工作的。当 TCP 保活的探测报文发送给对端, 对端会正常响应这样 TCP 保活时间会被重置等待下一个 TCP 保活时间的到来。如果对端主机宕机注意不是进程崩溃进程崩溃后操作系统在回收进程资源的时候会发送 FIN 报文而主机宕机则是无法感知的所以需要 TCP 保活机制来探测对方是不是发生了主机宕机或对端由于其他原因导致报文不可达。当 TCP 保活的探测报文发送给对端后石沉大海没有响应连续几次达到保活探测次数后TCP 会报告该 TCP 连接已经死亡。 TCP keepalive 机制之前已经介绍过就不赘述了。 tcp_tw_reuse 为什么默认关闭 这个参数打开就可以快速复用处于 TIME_WAIT 状态的 TCP 连接但是默认是关闭的。相当于在问如果 TIME_WAIT 状态持续时间过短会怎么样。 TIME_WAIT 状态 四次挥手的过程简图如下 可以看到主动关闭连接的才有 TIME_WAIT 状态。且该状态会持续 2MSL 之后进入 CLOSED 状态。 MSL 指的是 TCP 协议中任何报文在网络上最大的生存时间MSL 是由网络层的 IP 包中的 TTL 来保证的TTL 是 IP 头部的一个字段用于设置一个数据报可经过的路由器的数量上限 MSL 应该要大于等于 TTL 消耗为 0 的时间以确保报文已被自然消亡。 TTL 的值一般是 64Linux 将 MSL 设置为 30 秒意味着 Linux 认为数据报文经过 64 个路由器的时间不会超过 30 秒如果超过了就认为报文已经消失在网络中了。 TIME_WAIT 状态意义 老生常谈两个原因 防止历史连接中的数据被后面相同四元组的连接错误的接收保证「被动关闭连接」的一方能被正确的关闭 针对第二个情况比如被动关闭发送 FIN 包丢失那么会重传 FIN此时主动关闭端处于 TIME_WAIT 状态就会返回 ACK 包同时 TIME_WAIT 会重置计时器进入新的 TIME_WAIT 时间。 tcp_tw_reuse 是什么 如果客户端主动关闭连接方的 TIME_WAIT 状态过多占满了所有端口资源那么就无法对「目的 IP 目的 PORT」都一样的服务器发起连接了但是被使用的端口还是可以继续对另外一个服务器发起连接的。不过即使是在这种场景下只要连接的是不同的服务器端口是可以重复使用的所以客户端还是可以向其他服务器发起连接的这是因为内核在定位一个连接的时候是通过四元组源IP、源端口、目的IP、目的端口信息来定位的并不会因为客户端的端口一样而导致连接冲突。 Linux 是有两个参数可以快速回收 TIME_WAIT 状态的连接的不过都是默认关闭 net.ipv4.tcp_tw_reuse如果开启该选项的话客户端连接发起方 在调用 connect() 函数时如果内核选择到的端口已经被相同四元组的连接占用的时候就会判断该连接是否处于 TIME_WAIT 状态如果该连接处于 TIME_WAIT 状态并且 TIME_WAIT 状态持续的时间超过了 1 秒那么就会重用这个连接然后就可以正常使用该端口了。所以该选项只适用于连接发起方。net.ipv4.tcp_tw_recycle如果开启该选项的话允许处于 TIME_WAIT 状态的连接被快速回收该参数在 NAT 的网络下是不安全的 要使得上面这两个参数生效有一个前提条件就是要打开 TCP 时间戳即 net.ipv4.tcp_timestamps1默认即为 1。 tcp_tw_reuse 默认关闭原因 有两个原因 第一个问题 时间戳此时打开就可以有效判断回绕的序列号。但是对于 RST 报文的时间戳即使过期了只要 RST 报文的序列号在对方的接收窗口内也是能被接受的。 就有可能发生如下情况 第二个问题 如果第四次挥手的 ACK 报文丢失服务端就会重传 FIN 包此时处于 SYN_SENT 状态的客户端收到第三次挥手报文就会回 RST 报文。 过程如下图所示 处于 last_ack 状态的服务端收到了 SYN 报文后会回复确认号与服务端上一次发送 ACK 报文一样的 ACK 报文这个 ACK 报文称为 Challenge ACK (opens new window)并不是确认收到 SYN 报文。 处于 syn_sent 状态的客户端收到服务端的 Challenge ACK (opens new window)后发现不是自己期望收到的确认号于是就会回复 RST 报文服务端收到后就会断开连接。 HTTPS 中 TLS 和 TCP 可以同时握手 TLS 是 HTTPS 协议中的内容。 一般情况下不管 TLS 握手次数如何都得先经过 TCP 三次握手后才能进行。当然也有情况是可以同时握手的需要以下两个条件同时满足 客户端和服务端都开启了 TCP Fast Open 功能且 TLS 版本是 1.3客户端和服务端已经完成过一次通信。 TCP Fast Open TCP 的第一次和第二次握手是不能够携带数据的而 TCP 的第三次握手是可以携带数据的因为这时候客户端的 TCP 连接状态已经是 ESTABLISHED表明客户端这一方已经完成了 TCP 连接建立。 TCP Fast Open 是为了绕过 TCP 三次握手发送数据在 Linux 3.7 内核版本之后提供了 TCP Fast Open 功能这个功能可以减少 TCP 连接建立的时延。要使用该功能客户端和服务端需要同时支持该功能。 开启了 TCP Fast Open 功能想要绕过 TCP 三次握手发送数据得建立第二次以后的通信过程。 客户端首次建立连接 具体介绍 客户端发送 SYN 报文该报文包含 Fast Open 选项且该选项的 Cookie 为空这表明客户端请求 Fast Open Cookie支持 TCP Fast Open 的服务器生成 Cookie并将其置于 SYN-ACK 报文中的 Fast Open 选项以发回客户端客户端收到 SYN-ACK 后本地缓存 Fast Open 选项中的 Cookie。 后续通信客户端第一次握手就可以携带数据从而绕过三次握手发送数据 具体情况如下 客户端发送 SYN 报文该报文可以携带「应用数据」以及此前记录的 Cookie支持 TCP Fast Open 的服务器会对收到 Cookie 进行校验如果 Cookie 有效服务器将在 SYN-ACK 报文中对 SYN 和「数据」进行确认服务器随后将「应用数据」递送给对应的应用程序如果 Cookie 无效服务器将丢弃 SYN 报文中包含的「应用数据」且其随后发出的 SYN-ACK 报文将只确认 SYN 的对应序列号如果服务器接受了 SYN 报文中的「应用数据」服务器可在握手完成之前发送「响应数据」这就减少了握手带来的 1 个 RTT 的时间消耗客户端将发送 ACK 确认服务器发回的 SYN 以及「应用数据」但如果客户端在初始的 SYN 报文中发送的「应用数据」没有被确认则客户端将重新发送「应用数据」此后的 TCP 连接的数据传输过程和非 TCP Fast Open 的正常情况一致。 TLSv1.3 过程如下 TCP 连接的第三次握手是可以携带数据的如果客户端在第三次握手发送了 TLSv1.3 第一次握手数据是不是就表示「HTTPS 中的 TLS 握手过程可以同时进行三次握手」。并没有服务端只有收到客户端的 TCP 第三次握手后才能进行后续 TLS 握手。 TLSv1.3 还有个更厉害到地方在于会话恢复机制在重连 TLvS1.3 只需要 0-RTT用“pre_shared_key”和“early_data”扩展在 TCP 连接后立即就建立安全连接发送加密消息如下所示 TCP Fast Open TLSv1.3 如果「TCP Fast Open TLSv1.3」情况下在第二次以后的通信过程中TLS 和 TCP 的握手过程是可以同时进行的。 如果基于 TCP Fast Open 场景下的 TLSv1.3 0-RTT 会话恢复过程不仅 TLS 和 TCP 的握手过程是可以同时进行的而且 HTTP 请求也可以在这期间内一同完成。 TCP Keepalive 和 HTTP Keep-Alive 两者是有区别的实现也不同 HTTP 的 Keep-Alive是由应用层用户态 实现的称为 HTTP 长连接TCP 的 Keepalive是由 TCP 层内核态 实现的称为 TCP 保活机制 HTTP Keep-Alive HTTP 是基于 TCP 传输协议实现的客户端与服务端要进行 HTTP 通信前需要先建立 TCP 连接然后客户端发送 HTTP 请求服务端收到后就返回响应至此「请求-应答」的模式就完成了随后就会释放 TCP 连接。整体流程如下所示 如果每一次都重复如上过程那就是短连接。如果第一次请求完先不断开 TCP后续复用这个连接那就是 Keep-Alive 实现的功能可以使用同一个 TCP 连接来发送和接收多个 HTTP 请求/应答避免了连接建立和释放的开销这个方法称为 HTTP 长连接。 在 HTTP 1.0 中默认是关闭的如果浏览器要开启 Keep-Alive它必须在请求的包头中添加对应字段然后服务器收到请求同样也要添加这个响应字段。从 HTTP 1,1 开始就是默认开启了 Kepp-Alive。 HTTP 长连接不仅仅减少了 TCP 连接资源的开销而且这给 HTTP 流水线技术提供了可实现的基础。所谓的 HTTP 流水线是客户端可以先一次性发送多个请求而在发送过程中不需先等待服务器的回应可以减少整体的响应时间。当然服务器还是按照顺序响应。 当然为了避免 TCP 长连接浪费资源web 服务软件一般都会提供 keepalive_timeout 参数用来指定 HTTP 长连接的超时时间。这就相当于给定时器计时超时就会触发回调函数释放该连接。 TCP Keepalive TCP 的 Keepalive 这东西其实就是 TCP 的保活机制。之前有讲过。直接看一个图示复习一下 TCP 协议缺陷 主要有如下四个方面 升级 TCP 的工作很困难TCP 建立连接的延迟TCP 存在队头阻塞问题网络迁移需要重新建立 TCP 连接 升级 TCP 困难 TCP 协议是在内核中实现的应用程序只能使用不能修改如果要想升级 TCP 协议那么只能升级内核。 TCP 连接延迟 现在大多数网站都是使用 HTTPS 的这意味着在 TCP 三次握手之后还需要经过 TLS 四次握手后才能进行 HTTP 数据的传输这在一定程序上增加了数据传输的延迟。 整个上网流程如下所示 TCP 三次握手可以通过 TCP Fast Open 解决在「第二次建立连接」时减少 TCP 连接建立的时延。优化如下 TCP Fast Open 这个特性是不错但是它需要服务端和客户端的操作系统同时支持才能体验到而 TCP Fast Open 是在 2013 年提出的所以市面上依然有很多老式的操作系统不支持而升级操作系统是很麻烦的事情因此 TCP Fast Open 很难被普及开来。 并且TCP 在内核实现而 HTTPS 的 TLS 是在应用层握手这两个握手无法结合同时TLS 无法对 TCP 头部加密所以 TCP 序列号都是明文传输有安全隐患。 TCP 存在队头阻塞 TCP 是字节流协议TCP 层必须保证收到的字节数据是完整且有序的如果序列号较低的 TCP 段在网络传输中丢失了即使序列号较高的 TCP 段已经被接收了应用层也无法从内核中读取到这部分数据。如下所示 这就是队头阻塞如果丢包那么整个 TCP 都要重传那就会阻塞该 TCP 连接中的所有请求。 网络迁移需要重建 TCP 因为 TCP 协议是通过四元组源 IP、源端口、目的 IP、目的端口那么当移动设备的网络从 4G 切换到 WIFI 时意味着 IP 地址变化了那么就必须要断开连接然后重新建立 TCP 连接。 基于 UDP 实现可靠传输 现在市面上已经有基于 UDP 协议实现的可靠传输协议的成熟方案了那就是 QUIC 协议已经应用在了 HTTP/3。 如何实现可靠传输 相当于在应用层修改也就是设计好协议头部字段。在 HTTP/3 中头部如下 整体结构如下 Packet Header Packet Header 首次建立连接时和日常传输数据时使用的 Header 是不同的。如下 左侧的用于首次连接右侧用于日常传输数据。 QUIC 也是需要三次握手来建立连接的主要目的是为了协商连接 ID。协商出连接 ID 后后续传输时双方只需要固定住连接 ID从而实现连接迁移功能。 Short Packet Header 中的 Packet Number 是每个报文独一无二的编号它是严格递增的 。这就解决了 TCP 重传时序列号不变而可能造成的 TCP 重传歧义。 QUIC 报文中Packet Number 严格递增即使重传报文也会递增可以更加精确的计算报文的 RTT。 还有一个好处QUIC 使用的 Packet Number 单调递增的设计可以让数据包不再像 TCP 那样必须有序确认QUIC 支持乱序确认当数据包Packet N 丢失后只要有新的已接收数据包确认当前窗口就会继续向右滑动。 QUIC Frame Header 一个 Packet 报文可以有多个 Frame 每个 Frame 都有明确类型类型不同功能不同格式也会不同。例如 Stream 类型的 Frame就可以认为是一条 HTTP 请求 具体如下 Stream ID 作用多个并发传输的 HTTP 消息通过不同的 Stream ID 加以区别类似于 HTTP2 的 Stream IDOffset 作用类似于 TCP 协议中的 Seq 序号保证数据的顺序性和可靠性Length 作用指明了 Frame 数据的长度。 引入 Frame Header 这一层通过 Stream ID Offset 字段信息实现数据的有序性丢失的数据包和重传的数据包 Stream ID 与 Offset 都一致说明这两个数据包的内容一致。 总结来说QUIC 通过单向递增的 Packet Number配合 Stream ID 与 Offset 字段信息可以支持乱序确认而不影响数据包的正确组装。 QUIC 解决队头阻塞 之前讲过TCP 是有队头阻塞的 TCP 必须按序处理数据也就是 TCP 层为了保证数据的有序性只有在处理完有序的数据后滑动窗口才能往前滑动否则就停留。 HTTP/2 的队头阻塞 HTTP/2 中抽象出 Stream实现并发传输一个 Stream 相当于一次请求和响应。不同 Stream 是可以乱序发送的但是 HTTP/2 多个 Stream 请求都是在一条 TCP 连接上传输这意味着多个 Stream 共用同一个 TCP 滑动窗口那么当发生数据丢失滑动窗口是无法往前移动的此时就会阻塞住所有的 HTTP 请求这属于 TCP 层队头阻塞。 没有队头阻塞的 QUIC QUIC 给每一个 Stream 都分配了一个独立的滑动窗口这样使得一个连接上的多个 Stream 之间没有依赖关系都是相互独立的各自控制的滑动窗口。 QUIC 流量控制 QUIC 实现流量控制的方式 通过 window_update 帧告诉对端自己可以接收的字节数这样发送方就不会发送超过这个数量的数据。通过 BlockFrame 告诉对端由于流量控制被阻塞了无法发送数据。 这里注意QUIC 的滑动窗口滑动的条件跟 TCP 有一点差别但是同一个 Stream 的数据也是要保证顺序的不然无法实现可靠传输因此同一个 Stream 的数据包丢失了也会造成窗口无法滑动。 QUIC 的 每个 Stream 都有各自的滑动窗口不同 Stream 互相独立队头的 Stream A 被阻塞后不妨碍 StreamB、C的读取。 QUIC 实现了两种级别的流量控制分别为 Stream 和 Connection 两种级别 Stream 级别的流量控制Stream 可以认为就是一条 HTTP 请求每个 Stream 都有独立的滑动窗口所以每个 Stream 都可以做流量控制防止单个 Stream 消耗连接Connection的全部接收缓冲。Connection 流量控制限制连接中所有 Stream 相加起来的总字节数防止发送方超过连接的缓冲容量。 Stream 级别流量控制 比较复杂直接上链接 Stream 流量控制 Connection 流量控制 其接收窗口大小就是各个 Stream 接收窗口大小之和。如下所示 QUIC 对拥塞控制改进 QUIC 协议当前默认使用了 TCP 的 Cubic 拥塞控制算法我们熟知的慢开始、拥塞避免、快重传、快恢复策略同时也支持 CubicBytes、Reno、RenoBytes、BBR、PCC 等拥塞控制算法相当于将 TCP 的拥塞控制算法照搬过来了。 QUIC 是处于应用层的应用程序层面就能实现不同的拥塞控制算法不需要操作系统不需要内核支持。这是一个飞跃因为传统的 TCP 拥塞控制必须要端到端的网络协议栈支持才能实现控制效果。而内核和操作系统的部署成本非常高升级周期很长所以 TCP 拥塞控制算法迭代速度是很慢的。而 QUIC 可以随浏览器更新QUIC 的拥塞控制算法就可以有较快的迭代速度。可以针对不同应用场景设置不同的拥塞控制算法。 QUIC 更快的连接建立 HTTP/3 的 QUIC 协议并不是与 TLS 分层而是QUIC 内部包含了 TLS它在自己的帧会携带 TLS 里的“记录”再加上 QUIC 使用的是 TLS1.3因此仅需 1 个 RTT 就可以「同时」完成建立连接与密钥协商甚至在第二次连接的时候应用数据包可以和 QUIC 握手信息连接信息 TLS 信息一起发送达到 0-RTT 的效果。 如下图所示 QUIC 迁移连接 QUIC 协议没有用四元组的方式来“绑定”连接而是通过连接 ID来标记通信的两个端点客户端和服务器可以各自选择一组 ID 来标记自己因此即使移动设备的网络变化后导致 IP 地址变化了只要仍保有上下文信息比如连接 ID、TLS 密钥等就可以“无缝”地复用原连接消除重连的成本没有丝毫卡顿感达到了连接迁移的功能。 TCP 和 UDP 可以同时使用同一个端口 可以同时绑定相同端口 TCP 和 UDP 服务端网络相似的一个地方就是会调用 bind 绑定端口。 TCP 会监听端口但是 UDP 是没有这个操作的分别如下方两张图所示 两者是可以同时绑定相同端口的 在数据链路层中通过 MAC 地址来寻找局域网中的主机。在网际层中通过 IP 地址来寻找网络中互连的主机或路由器。在传输层中需要通过端口进行寻址来识别同一计算机中同时通信的不同应用程序。 所以传输层的「端口号」的作用是为了区分同一个主机上不同应用程序的数据包。传输层有两个传输协议分别是 TCP 和 UDP在内核中是两个完全独立的软件模块。收到数据包根据 IP 包头的「协议号」字段知道该数据包是 TCP/UDP所以可以根据这个信息确定送给哪个模块TCP/UDP处理送给 TCP/UDP 模块的报文根据「端口号」确定送给哪个应用程序处理。所以这两者共用端口并不会造成冲突过程如下 多个 TCP 可以绑定同一端口 如果两个 TCP 服务进程绑定的 IP 地址不同而端口相同的话也是可以绑定成功的。 如果两个 TCP 服务进程同时绑定的 IP 地址和端口都相同那么执行 bind() 时候就会出错错误是“Address already in use”。 这里注意重启 TCP 进程也会有这个报错这是因为重启的时候相当于发生了四次挥手那么主动关闭方就会处于 TIME_WAIT 状态TIME_WAIT 状态的连接使用的 IPPORT 仍然被认为是一个有效的 IPPORT 组合相同机器上不能够在该 IPPORT 组合上进行绑定那么执行 bind() 函数的时候就会返回了 Address already in use 的错误。 避免以上问题可以在调用 bind 之前对 socket 设置 SO_REUSEADDR 属性。并不会造成危害。 客户端端口可以重复使用 整体 TCP 连接如下所示 客户端选择端口是在 connect 函数内核在选择端口的时候会从 net.ipv4.ip_local_port_range 这个内核参数指定的范围来选取一个端口作为客户端端口。 不过这里端口是可以重复用的因为 TCP 连接是由四元组源IP地址源端口目的IP地址目的端口唯一确认的那么只要四元组中其中一个元素发生了变化那么就表示不同的 TCP 连接的。 如果想自己指定端口那就要在客户端调用 bind 函数绑定端口那么再调用 connect 就会跳过端口选择。这里同样只要绑定的 IP PORT 是否都相同如果都是相同的那么在执行 bind() 时候就会出错错误是“Address already in use”。 不过一般不会在客户端 bind。 客户端 TIME_WAIT 过多会导致端口耗尽 只要客户端连接的服务器不同端口资源可以重复使用的。 客户端 TIME_WAIT 过多无法与同一服务器建立 TCP 打开 net.ipv4.tcp_tw_reuse 这个内核参数。 开启了这个内核参数后客户端调用 connect 函数时如果选择到的端口已经被相同四元组的连接占用的时候就会判断该连接是否处于 TIME_WAIT 状态如果该连接处于 TIME_WAIT 状态并且 TIME_WAIT 状态持续的时间超过了 1 秒那么就会重用这个连接然后就可以正常使用该端口了。 总结 端口选择有一张总结的流程图可以看一下 服务端没有 listen客户端建立连接 服务端如果只 bind 了 IP 地址和端口而没有调用 listen 的话然后客户端对服务端发起了连接建立服务端会回 RST 报文。 没有 listen可以建立 TCP 可以的客户端是可以自己连自己的形成连接TCP自连接也可以两个客户端同时向对方发出请求建立连接TCP同时打开这两个情况都有个共同点就是没有服务端参与也就是没有listen就能建立连接。 虽然没有 listen 就没有半连接队列和全连接队列来存 IP 端口但是在 TCP 自连接的情况中客户端在 connect 方法时最后会将自己的连接信息放入到这个全局 hash 表中然后将信息发出消息在经过回环地址重新回到 TCP 传输层的时候就会根据 IP 端口信息再一次从这个全局 hash 中取出信息。于是握手包一来一回最后成功建立连接。 没有 accept能建立 TCP 这里可以看一下简单的服务端伪代码 int main() {/*Step 1: 创建服务器端监听socket描述符listen_fd*/ listen_fd socket(AF_INET, SOCK_STREAM, 0);/*Step 2: bind绑定服务器端的IP和端口所有客户端都向这个IP和端口发送和请求数据*/ bind(listen_fd, xxx);/*Step 3: 服务端开启监听*/ listen(listen_fd, 128);/*Step 4: 服务器等待客户端的链接返回值cfd为客户端的socket描述符*/ cfd accept(listen_fd, xxx);/*Step 5: 读取客户端发来的数据*/n read(cfd, buf, sizeof(buf)); }也就是说listen 之后会执行 accept一般而言启动服务器最后程序会阻塞在 accept。 再看看客户端简化伪代码 int main() {/*Step 1: 创建客户端端socket描述符cfd*/ cfd socket(AF_INET, SOCK_STREAM, 0);/*Step 2: connect方法,对服务器端的IP和端口号发起连接*/ ret connect(cfd, xxxx);/*Step 4: 向服务器端写数据*/write(cfd, buf, strlen(buf)); }客户端创建好 socket 之后就会直接 connect这时服务端阻塞的 accept 就会返回结果了。 实验抓包得到结果**就算不执行accept()方法三次握手照常进行并顺利建立连接。**而且在服务端执行accept()前如果客户端发送消息给服务端服务端是能够正常回复ack确认包的。 这里就回到了之前的半连接队列和全连接队列全连接队列就是第三次握手之后会从半连接队列中取出 sock 存到自己这里这里的所有连接全部是 ESTABLISHED 状态只等着服务端执行 accept 取出。可以看下面这张图 半连接队列是哈希表 虽然叫队列但其实半连接队列是哈希表全连接队列是链表 半连接队列却不太一样因为队列里的都是不完整的连接嗷嗷等待着第三次握手的到来。那么现在有一个第三次握手来了则需要从队列里把相应IP端口的连接取出如果半连接队列还是个链表那我们就需要依次遍历才能拿到我们想要的那个连接算法复杂度就是O(n)。 全连接队列满 如果队列满了服务端还收到客户端的第三次握手ACK默认当然会丢弃这个ACK。 但除了丢弃之外还有一些附带行为这会受 tcp_abort_on_overflow 参数的影响。 tcp_abort_on_overflow设置为 0 会丢弃这个第三次握手ACK包并且开启定时器重传第二次握手的SYNACK如果重传超过一定限制次数还会把对应的半连接队列里的连接给删掉。 tcp_abort_on_overflow设置为 1 全连接队列满了之后就直接发RST给客户端效果上看就是连接断了。 这里注意服务端端口未监听时客户端尝试去连接服务端也会回一个 RST。这两个情况长一样所以客户端这时候收到 RST 之后其实无法区分到底是端口未监听还是全连接队列满了。 半连接队列满 一般是丢弃但这个行为可以通过 tcp_syncookies 参数去控制。 一般满了之前提到过就是 SYN Flood 攻击。 遇到这种情况可以将 tcp_syncookies 参数设置为 1此时 SYN 发来服务端不会将其放入半连接队列中而是直接生成一个cookies这个cookies会跟着第二次握手发回客户端。客户端在发第三次握手的时候带上这个cookies服务端验证到它就是当初发出去的那个就会建立连接并放入到全连接队列中。可以看出整个过程不再需要半连接队列的参与。 注意cookies并不会有一个专门的队列保存它是通过通信双方的IP地址端口、时间戳、MSS等信息进行实时计算的保存在TCP报头的seq里。 cookies 直接取代半连接队列 服务端并不会保存连接信息所以如果传输过程中数据包丢了也不会重发第二次握手的信息。 编码解码 cookies都是比较耗 CPU 的利用这一点如果此时攻击者构造大量的第三次握手包ACK包同时带上各种瞎编的cookies信息就会消耗很多资源。 TCP 能保证数据不丢 假设有两个用户把中间的服务器简化掉变成一个端到端的通信 会首先三次握手建立连接。 一个数据包从聊天框里发出消息会从聊天软件所在的用户空间拷贝到内核空间的发送缓冲区send buffer数据包就这样顺着传输层、网络层进入到数据链路层在这里数据包会经过流控qdisc再通过RingBuffer发到物理层的网卡。数据就这样顺着网卡发到了纷繁复杂的网络世界里。这里头数据会经过n多个路由器和交换机之间的跳转最后到达目的机器的网卡处。 此时目的机器的网卡会通知DMA将数据包信息放到RingBuffer中再触发一个硬中断给CPUCPU触发软中断让ksoftirqd去RingBuffer收包于是一个数据包就这样顺着物理层数据链路层网络层传输层最后从内核空间拷贝到用户空间里的聊天软件里。 建立连接丢包 半连接队列和全连接队列如果参数没有设置队列满了就会丢包。 流量控制丢包 让数据按一定的规则排个队依次处理也就是所谓的qdisc(Queueing Disciplines排队规则)这也是我们常说的流量控制机制。 可以通过下面的ifconfig命令查看到里面涉及到的txqueuelen后面的数字1000其实就是流控队列的长度。 当发送数据过快流控队列长度txqueuelen又不够大时就容易出现丢包现象。 网卡丢包 RingBuffer 过小丢包 接收数据时会将数据暂存到RingBuffer接收缓冲区中然后等着内核触发软中断慢慢收走。如果这个缓冲区过小而这时候发送的数据又过快就有可能发生溢出此时也会产生丢包。 一个网卡里是可以有多个RingBuffer的所以上面的rx_queue_0_drops里的0代表的是第0个RingBuffer的丢包数对于多队列的网卡这个0还可以改成其他数字。 网卡性能不足 网卡作为硬件传输速度是有上限的。当网络传输速度过大达到网卡上限时就会发生丢包。 接收缓冲区丢包 一般使用TCP socket进行网络编程的时候内核都会分配一个发送缓冲区和一个接收缓冲区。 发送时将数据拷贝到内核发送缓冲区就完事返回了至于什么时候发数据发多少数据这个后续由内核自己做决定。 接收缓冲区作用也类似从外部网络收到的数据包就暂存在这个地方然后坐等用户空间的应用程序将数据包取走。 # 查看接收缓冲区 # sysctl net.ipv4.tcp_rmem net.ipv4.tcp_rmem 4096 87380 6291456# 查看发送缓冲区 # sysctl net.ipv4.tcp_wmem net.ipv4.tcp_wmem 4096 16384 4194304不管是接收缓冲区还是发送缓冲区都能看到三个数值分别对应缓冲区的最小值默认值和最大值 min、default、max。缓冲区会在min和max之间动态调整。 对于发送缓冲区执行send的时候如果是阻塞调用那就会等等到缓冲区有空位可以发数据。如果是非阻塞调用就会立刻返回一个 EAGAIN 错误信息意思是 Try again。让应用程序下次再重试。这种情况下一般不会发生丢包。 当接受缓冲区满了事情就不一样了它的TCP接收窗口会变为0也就是所谓的零窗口并且会通过数据包里的win0告诉发送端不要发消息了。一般这种情况下发送端就该停止发消息了但如果这时候确实还有数据发来就会发生丢包。 两端间网络丢包 只能通过一些命令来观察整个链路情况。 ping 查看 需要知道目的地的域名。想知道你的机器到服务器之间有没有产生丢包行为。可以使用ping命令。 ping之后查看 packet loss就知道有没有丢包了。 mtr 命令 mtr命令可以查看到你的机器和目的机器之间的每个节点的丢包情况。 可以看到Host那一列出现的都是链路中间每一跳的机器Loss的那一列就是指这一跳对应的丢包率。 同时因为mtr默认用的是ICMP包有些节点限制了ICMP包导致不能正常展示。可以加一个参数使用 udp 包来展示 把ICMP包和UDP包的结果拼在一起看就是比较完整的链路图了。有个小细节Loss那一列我们在icmp的场景下关注最后一行如果是0%那不管前面loss是100%还是80%都无所谓那些都是节点限制导致的虚报。 如何解决丢包 建立了TCP连接的两端发送端在发出数据后会等待接收端回复ack包ack包的目的是为了告诉对方自己确实收到了数据但如果中间链路发生了丢包那发送端会迟迟收不到确认ack于是就会进行重传。以此来保证每个数据包都确确实实到达了接收端。 TCP 一定不会丢包 TCP位于传输层在它的上面还有各种应用层协议比如常见的HTTP或者各类RPC协议。 TCP保证的可靠性是传输层的可靠性。也就是说TCP只保证数据从A机器的传输层可靠地发到B机器的传输层。 这时就有可能在完成了传输层的正常传输之后应用层需要从接收缓冲区取出数据这时就可能导致丢包 解决丢包 不再简化模型重新加入服务器 服务器可能记录了我们最近发过什么数据假设每条消息都有个id服务器和聊天软件每次都拿最新消息的id进行对比就能知道两端消息是否一致就像对账一样。 对于发送方只要定时跟服务端的内容对账一下就知道哪条消息没发送成功直接重发就好了。 如果接收方的聊天软件崩溃了重启后跟服务器稍微通信一下就知道少了哪条数据同步上来就是了所以也不存在上面提到的丢包情况。 可以看出TCP只保证传输层的消息可靠性并不保证应用层的消息可靠性。如果我们还想保证应用层的消息可靠性就需要应用层自己去实现逻辑做保证。 两端通信的时候也能对账为什么还要引入第三端服务器 第一如果是两端通信你聊天软件里有1000个好友你就得建立1000个连接。但如果引入服务端你只需要跟服务器建立1个连接就够了聊天软件消耗的资源越少手机就越省电。第二就是安全问题如果还是两端通信随便一个人找你对账一下你就把聊天记录给同步过去了这并不合适吧。如果对方别有用心信息就泄露了。引入第三方服务端就可以很方便的做各种鉴权校验。第三是软件版本问题。软件装到用户手机之后软件更不更新就是由用户说了算了。如果还是两端通信且两端的软件版本跨度太大很容易产生各种兼容性问题但引入第三端服务器就可以强制部分过低版本升级否则不能使用软件。但对于大部分兼容性问题给服务端加兼容逻辑就好了不需要强制用户更新软件。 TCP 四次挥手可以变成三次 在一些情况下 TCP 四次挥手是可以变成 TCP 三次挥手的。 TCP 四次挥手 具体过程 客户端主动调用关闭连接的函数于是就会发送 FIN 报文这个 FIN 报文代表客户端不会再发送数据了进入 FIN_WAIT_1 状态服务端收到了 FIN 报文然后马上回复一个 ACK 确认报文此时服务端进入 CLOSE_WAIT 状态。在收到 FIN 报文的时候TCP 协议栈会为 FIN 包插入一个文件结束符 EOF 到接收缓冲区中服务端应用程序可以通过 read 调用来感知这个 FIN 包这个 EOF 会被放在已排队等候的其他已接收的数据之后所以必须要得继续 read 接收缓冲区已接收的数据接着当服务端在 read 数据的时候最后自然就会读到 EOF接着 read() 就会返回 0这时服务端应用程序如果有数据要发送的话就发完数据后才调用关闭连接的函数如果服务端应用程序没有数据要发送的话可以直接调用关闭连接的函数这时服务端就会发一个 FIN 包这个 FIN 报文代表服务端不会再发送数据了之后处于 LAST_ACK 状态客户端接收到服务端的 FIN 包并发送 ACK 确认包给服务端此时客户端将进入 TIME_WAIT 状态服务端收到 ACK 确认包后就进入了最后的 CLOSE 状态客户端经过 2MSL 时间之后也进入 CLOSE 状态 四次挥手原因 服务器收到客户端的 FIN 报文时内核会马上回一个 ACK 应答报文但是服务端应用程序可能还有数据要发送所以并不能马上发送 FIN 报文而是将发送 FIN 报文的控制权交给服务端应用程序 如果服务端应用程序有数据要发送的话就发完数据后才调用关闭连接的函数如果服务端应用程序没有数据要发送的话可以直接调用关闭连接的函数 如何关闭 关闭的连接的函数有两种函数 close 函数同时 socket 关闭发送方向和读取方向也就是 socket 不再有发送和接收数据的能力。如果有多进程/多线程共享同一个 socket如果有一个进程调用了 close 关闭只是让 socket 引用计数 -1并不会导致 socket 不可用同时也不会发出 FIN 报文其他进程还是可以正常读写该 socket直到引用计数变为 0才会发出 FIN 报文。shutdown 函数可以指定 socket 只关闭发送方向而不关闭读取方向也就是 socket 不再有发送数据的能力但是还是具有接收数据的能力。如果有多进程/多线程共享同一个 socketshutdown 则不管引用计数直接使得该 socket 不可用然后发出 FIN 报文如果有别的进程企图使用该 socket将会受到影响。 如果调用 close 函数这时如果挥手的时候客户端收到了服务端数据因为客户端无法收发数据就会直接回 RST 报文然后内核释放连接。此时就不会经历完整四次挥手是一种粗暴的关闭。当服务端收到 RST 后内核就会释放连接当服务端应用程序再次发起读操作或者写操作时就能感知到连接已经被释放了 如果是读操作则会返回 RST 的报错也就是我们常见的Connection reset by peer。如果是写操作那么程序会产生 SIGPIPE 信号应用层代码可以捕获并处理信号如果不处理则默认情况下进程会终止异常退出。 shutdown 函数因为可以指定只关闭发送方向而不关闭读取方向所以即使在 TCP 四次挥手过程中如果收到了服务端发送的数据客户端也是可以正常读取到该数据的然后就会经历完整的 TCP 四次挥手所以调用 shutdown 是优雅的关闭。 出现三次挥手 当被动关闭方上图的服务端在 TCP 挥手过程中「没有数据要发送」并且「开启了 TCP 延迟确认机制」那么第二和第三次挥手就会合并传输这样就出现了三次挥手。 TCP 延迟确认机制是默认开启的所以导致我们抓包时看见三次挥手的次数比四次挥手还多。TCP 延迟确认的策略 当有响应数据要发送时ACK 会随着响应数据一起立刻发送给对方当没有响应数据要发送时ACK 将会延迟一段时间以等待是否有响应数据可以一起发送如果在延迟等待发送 ACK 期间对方的第二个数据报文又到达了这时就会立刻发送 ACK TCP 序列号和确认号的变化 万能公式 发送的 TCP 报文 公式一序列号 上一次发送的序列号 len数据长度。特殊情况如果上一次发送的报文是 SYN 报文或者 FIN 报文则改为 上一次发送的序列号 1。公式二确认号 上一次收到的报文中的序列号 len数据长度。特殊情况如果收到的是 SYN 报文或者 FIN 报文则改为上一次收到的报文中的序列号 1。 重点关注这三个字段的作用 序列号在建立连接时由内核生成的随机数作为其初始值通过 SYN 报文传给接收端主机每发送一次数据就「累加」一次该「数据字节数」的大小。用来解决网络包乱序问题。确认号指下一次「期望」收到的数据的序列号发送端收到接收方发来的 ACK 确认报文以后就可以认为在这个序号以前的数据都已经被正常接收。用来解决丢包的问题。控制位用来标识 TCP 报文是什么类型的报文比如是 SYN 报文、数据报文、ACK 报文FIN 报文等。 三次握手 握手建立连接的过程就是上图所示应该已经很熟悉了。 数据传输阶段 客户端发送 10 字节的数据通常 TCP 数据报文的控制位是 [PSH, ACK]此时该 TCP 数据报文的序列号和确认号分别设置为 序列号设置为 client_isn 1。客户端上一次发送报文是 ACK 报文第三次握手该报文的 seq client_isn 1由于是一个单纯的 ACK 报文没有携带用户数据所以 len 0。根据公式 1序列号 上一次发送的序列号 len可以得出当前的序列号为 client_isn 1 0即 client_isn 1。确认号设置为 server_isn 1。没错还是和第三次握手的 ACK 报文的确认号一样这是因为客户端三次握手之后发送 TCP 数据报文 之前如果没有收到服务端的 TCP 数据报文确认号还是延用上一次的其实根据公式 2 你也能得到这个结论。 接着当服务端收到客户端 10 字节的 TCP 数据报文后就需要回复一个 ACK 报文此时该报文的序列号和确认号分别设置为 序列号设置为 server_isn 1。服务端上一次发送报文是 SYN-ACK 报文序列号为 server_isn根据公式 1序列号 上一次发送的序列号 len。特殊情况如果上一次发送的报文是 SYN 报文或者 FIN 报文则改为 1所以当前的序列号为 server_isn 1。确认号设置为 client_isn 11 。服务端上一次收到的报文是客户端发来的 10 字节 TCP 数据报文该报文的 seq client_isn 1len 10。根据公式 2确认号 上一次收到的报文中的序列号 len也就是将「收到的 TCP 数据报文中的序列号 client_isn 1再加上 10len 10 」的值作为了确认号表示自己收到了该 10 字节的数据报文。 这里注意特殊情况如果第三次握手的 ACK 丢失处于 SYN_RCVD 的服务端收到客户端第一个 TCP 数据仍然是可以正常完成连接的。 四次挥手 这个应该也很熟悉了。
http://www.dnsts.com.cn/news/212878.html

相关文章:

  • 用花生壳怎么做网站的服务器西安做网站 怎样备案
  • 制作一个简单网站网站设计主要包含3个方面
  • 有哪些网站有做网页用的小图片做网页原型图一张多少钱
  • 少主网络建站推荐手机网址
  • 西安公司网站设计网络加速器手机版
  • 团购网站模板做网站内容图片多大
  • 美食健康网站的建设怎样通过网络销售自己的产品
  • 有做soho网站的吗百度竞价推广的技巧
  • 域名网站购买临淄58同城招聘信息网
  • 太原网站域名开发外网图片素材网站
  • 给网站做插画分辨率网络营销的流程
  • asp网站发布ftp设计得到app下载
  • 东莞常平做网站公司西安做网站优化
  • 互联网推广网站建设用六类网站做电话可以吗
  • 公司想制作网站吗怎么做网站页面代码搜索
  • 网络及建设公司网站做网站用php如何学习
  • wordpress个人站无法升级有关西安的网页设计
  • 多语言外贸网站建设详情页模板图
  • app开发网站建设及开发广告推广有哪些平台
  • 建立一个网站 优帮云wordpress时间轴源码
  • 无锡网站建设优化公司论坛类型的网站怎么做
  • 网站公司维护建网站系统能换吗
  • 一个网站的渠道网络建设广宏建设集团有限公司网站
  • 高埗做网站哪里有做杂志的免费模板下载网站
  • 太原网站开发网站建设市区
  • 专注做一家男人最爱的网站去加网 wordpress
  • 网站建设企业公司wordpress产品系统
  • 桂林建设网站公司品牌推广与传播怎么写
  • php实战做网站视频教程一流的常州网站优化
  • c 购物网站开发流程深圳网站系统建设