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

不想花钱做网站推广现代简约装修三室两厅两卫样

不想花钱做网站推广,现代简约装修三室两厅两卫样,大型网站建设地址,网站建设与管理用什么软件有哪些TCP 三次握手与四次挥手面试题 任 TCP 虐我千百遍#xff0c;我仍待 TCP 如初恋。 巨巨巨巨长的提纲#xff0c;发车#xff01;发车#xff01; PS#xff1a;本次文章不涉及 TCP 流量控制、拥塞控制、可靠性传输等方面知识#xff0c;这些知识在这篇#xff1a; TCP …TCP 三次握手与四次挥手面试题 任 TCP 虐我千百遍我仍待 TCP 如初恋。 巨巨巨巨长的提纲发车发车 PS本次文章不涉及 TCP 流量控制、拥塞控制、可靠性传输等方面知识这些知识在这篇 TCP 基本认识 TCP 头格式有哪些 我们先来看看 TCP 头的格式标注颜色的表示与本文关联比较大的字段其他字段不做详细阐述。 序列号在建立连接时由计算机生成的随机数作为其初始值通过 SYN 包传给接收端主机每发送一次数据就「累加」一次该「数据字节数」的大小。用来解决网络包乱序问题。 确认应答号指下一次「期望」收到的数据的序列号发送端收到这个确认应答以后可以认为在这个序号以前的数据都已经被正常接收。用来解决丢包的问题。 控制位 ACK该位为 1 时「确认应答」的字段变为有效TCP 规定除了最初建立连接时的 SYN 包之外该位必须设置为 1 。RST该位为 1 时表示 TCP 连接中出现异常必须强制断开连接。SYN该位为 1 时表示希望建立连接并在其「序列号」的字段进行序列号初始值的设定。FIN该位为 1 时表示今后不会再有数据发送希望断开连接。当通信结束希望断开连接时通信双方的主机之间就可以相互交换 FIN 位为 1 的 TCP 段。 为什么需要 TCP 协议 TCP 工作在哪一层 IP 层是「不可靠」的它不保证网络包的交付、不保证网络包的按序交付、也不保证网络包中的数据的完整性。 如果需要保障网络数据包的可靠性那么就需要由上层传输层的 TCP 协议来负责。 因为 TCP 是一个工作在传输层的可靠数据传输的服务它能确保接收端接收的网络包是无损坏、无间隔、非冗余和按序的。 什么是 TCP TCP 是面向连接的、可靠的、基于字节流的传输层通信协议。 面向连接一定是「一对一」才能连接不能像 UDP 协议可以一个主机同时向多个主机发送消息也就是一对多是无法做到的 可靠的无论的网络链路中出现了怎样的链路变化TCP 都可以保证一个报文一定能够到达接收端 字节流用户消息通过 TCP 协议传输时消息可能会被操作系统「分组」成多个的 TCP 报文如果接收方的程序如果不知道「消息的边界」是无法读出一个有效的用户消息的。并且 TCP 报文是「有序的」当「前一个」TCP 报文没有收到的时候即使它先收到了后面的 TCP 报文那么也不能扔给应用层去处理同时对「重复」的 TCP 报文会自动丢弃。 什么是 TCP 连接 我们来看看 RFC 793 是如何定义「连接」的 Connections: The reliability and flow control mechanisms described above require that TCPs initialize and maintain certain status information for each data stream. The combination of this information, including sockets, sequence numbers, and window sizes, is called a connection. 简单来说就是用于保证可靠性和流量控制维护的某些状态信息这些信息的组合包括 Socket、序列号和窗口大小称为连接。 所以我们可以知道建立一个 TCP 连接是需要客户端与服务端达成上述三个信息的共识。 Socket由 IP 地址和端口号组成序列号用来解决乱序问题等窗口大小用来做流量控制 如何唯一确定一个 TCP 连接呢 TCP 四元组可以唯一的确定一个连接四元组包括如下 源地址源端口目的地址目的端口 ​ 源地址和目的地址的字段32 位是在 IP 头部中作用是通过 IP 协议发送报文给对方主机。 源端口和目的端口的字段16 位是在 TCP 头部中作用是告诉 TCP 协议应该把报文发给哪个进程。 有一个 IP 的服务端监听了一个端口它的 TCP 的最大连接数是多少 服务端通常固定在某个本地端口上监听等待客户端的连接请求。 因此客户端 IP 和端口是可变的其理论值计算公式如下: ​ 对 IPv4客户端的 IP 数最多为 2 的 32 次方客户端的端口数最多为 2 的 16 次方也就是服务端单机最大 TCP 连接数约为 2 的 48 次方。 当然服务端最大并发 TCP 连接数远不能达到理论上限会受以下因素影响 文件描述符限制每个 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 不提供复杂的控制机制利用 IP 提供面向「无连接」的通信服务。 UDP 协议真的非常简头部只有 8 个字节64 位UDP 的头部格式如下 ​ 目标和源端口主要是告诉 UDP 协议应该把报文发给哪个进程。包长度该字段保存了 UDP 首部的长度跟数据的长度之和。校验和校验和是为了提供可靠的 UDP 首部和数据而设计防止收到在网络传输中受损的 UDP 包。 TCP 和 UDP 区别 1. 连接 TCP 是面向连接的传输层协议传输数据前先要建立连接。UDP 是不需要连接即刻传输数据。 2. 服务对象 TCP 是一对一的两点服务即一条连接只有两个端点。UDP 支持一对一、一对多、多对多的交互通信 3. 可靠性 TCP 是可靠交付数据的数据可以无差错、不丢失、不重复、按序到达。UDP 是尽最大努力交付不保证可靠交付数据。但是我们可以基于 UDP 传输协议实现一个可靠的传输协议比如 QUIC 协议具体可以参见这篇文章如何基于 UDP 协议实现可靠传输(opens new window) 4. 拥塞控制、流量控制 TCP 有拥塞控制和流量控制机制保证数据传输的安全性。UDP 则没有即使网络非常拥堵了也不会影响 UDP 的发送速率。 5. 首部开销 TCP 首部长度较长会有一定的开销首部在没有使用「选项」字段时是 20 个字节如果使用了「选项」字段则会变长的。UDP 首部只有 8 个字节并且是固定不变的开销较小。 6. 传输方式 TCP 是流式传输没有边界但保证顺序和可靠。UDP 是一个包一个包的发送是有边界的但可能会丢包和乱序。 7. 分片不同 TCP 的数据大小如果大于 MSS 大小则会在传输层进行分片目标主机收到后也同样在传输层组装 TCP 数据包如果中途丢失了一个分片只需要传输丢失的这个分片。UDP 的数据大小如果大于 MTU 大小则会在 IP 层进行分片目标主机收到后在 IP 层组装完数据接着再传给传输层。 TCP 和 UDP 应用场景 由于 TCP 是面向连接能保证数据的可靠性交付因此经常用于 FTP 文件传输HTTP / HTTPS 由于 UDP 面向无连接它可以随时发送数据再加上 UDP 本身的处理既简单又高效因此经常用于 包总量较少的通信如 DNS 、SNMP 等视频、音频等多媒体通信广播通信 为什么 UDP 头部没有「首部长度」字段而 TCP 头部有「首部长度」字段呢 原因是 TCP 有可变长的「选项」字段而 UDP 头部长度则是不会变化的无需多一个字段去记录 UDP 的首部长度。 为什么 UDP 头部有「包长度」字段而 TCP 头部则没有「包长度」字段呢 先说说 TCP 是如何计算负载数据长度 ​ 其中 IP 总长度 和 IP 首部长度在 IP 首部格式是已知的。TCP 首部长度则是在 TCP 首部格式已知的所以就可以求得 TCP 数据的长度。 大家这时就奇怪了问“UDP 也是基于 IP 层的呀那 UDP 的数据长度也可以通过这个公式计算呀 为何还要有「包长度」呢” 这么一问确实感觉 UDP 的「包长度」是冗余的。 我查阅了很多资料我觉得有两个比较靠谱的说法 第一种说法因为为了网络设备硬件设计和处理方便首部长度需要是 4 字节的整数倍。如果去掉 UDP 的「包长度」字段那 UDP 首部长度就不是 4 字节的整数倍了所以我觉得这可能是为了补全 UDP 首部长度是 4 字节的整数倍才补充了「包长度」字段。第二种说法如今的 UDP 协议是基于 IP 协议发展的而当年可能并非如此依赖的可能是别的不提供自身报文长度或首部长度的网络层协议因此 UDP 报文首部需要有长度字段以供计算。 TCP 和 UDP 可以使用同一个端口吗 答案可以的。 在数据链路层中通过 MAC 地址来寻找局域网中的主机。在网际层中通过 IP 地址来寻找网络中互连的主机或路由器。在传输层中需要通过端口进行寻址来识别同一计算机中同时通信的不同应用程序。 所以传输层的「端口号」的作用是为了区分同一个主机上不同应用程序的数据包。 传输层有两个传输协议分别是 TCP 和 UDP在内核中是两个完全独立的软件模块。 当主机收到数据包后可以在 IP 包头的「协议号」字段知道该数据包是 TCP/UDP所以可以根据这个信息确定送给哪个模块TCP/UDP处理送给 TCP/UDP 模块的报文根据「端口号」确定送给哪个应用程序处理。 ​ 因此TCP/UDP 各自的端口号也相互独立如 TCP 有一个 80 号端口UDP 也可以有一个 80 号端口二者并不冲突。 关于端口的知识点还是挺多可以讲的比如还可以牵扯到这几个问题 多个 TCP 服务进程可以同时绑定同一个端口吗重启 TCP 服务进程时为什么会出现“Address in use”的报错信息又该怎么避免客户端的端口可以重复使用吗客户端 TCP 连接 TIME_WAIT 状态过多会导致端口资源耗尽而无法建立新的连接吗 上面这些问题可以看这篇文章TCP 和 UDP 可以使用同一个端口吗(opens new window) 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 状态。 从上面的过程可以发现第三次握手是可以携带数据的前两次握手是不可以携带数据的这也是面试常问的题。 一旦完成三次握手双方都处于 ESTABLISHED 状态此时连接就已建立完成客户端和服务端就可以相互发送数据了。 如何在 Linux 系统中查看 TCP 状态 TCP 的连接状态查看在 Linux 可以通过 netstat -napt 命令查看。 ​ 为什么是三次握手不是两次、四次 相信大家比较常回答的是“因为三次握手才能保证双方具有接收和发送的能力。” 这回答是没问题但这回答是片面的并没有说出主要的原因。 在前面我们知道了什么是 TCP 连接 用于保证可靠性和流量控制维护的某些状态信息这些信息的组合包括 Socket、序列号和窗口大小称为连接。 所以重要的是为什么三次握手才可以初始化 Socket、序列号和窗口大小并建立 TCP 连接。 接下来以三个方面分析三次握手的原因 三次握手才可以阻止重复历史连接的初始化主要原因三次握手才可以同步双方的初始序列号三次握手才可以避免资源浪费 原因一避免历史连接 我们来看看 RFC 793 指出的 TCP 连接使用三次握手的首要原因 The principle reason for the three-way handshake is to prevent old duplicate connection initiations from causing confusion. 简单来说三次握手的首要原因是为了防止旧的重复连接初始化造成混乱。 我们考虑一个场景客户端先发送了 SYNseq 90报文然后客户端宕机了而且这个 SYN 报文还被网络阻塞了服务端并没有收到接着客户端重启后又重新向服务端建立连接发送了 SYNseq 100报文注意不是重传 SYN重传的 SYN 的序列号是一样的。 看看三次握手是如何阻止历史连接的 ​ 客户端连续发送多次 SYN都是同一个四元组建立连接的报文在网络拥堵情况下 一个「旧 SYN 报文」比「最新的 SYN」 报文早到达了服务端那么此时服务端就会回一个 SYN ACK 报文给客户端此报文中的确认号是 91901。客户端收到后发现自己期望收到的确认号应该是 100 1而不是 90 1于是就会回 RST 报文。服务端收到 RST 报文后就会释放连接。后续最新的 SYN 抵达了服务端后客户端与服务端就可以正常的完成三次握手了。 上述中的「旧 SYN 报文」称为历史连接TCP 使用三次握手建立连接的最主要原因就是防止「历史连接」初始化了连接。 TIP 有很多人问如果服务端在收到 RST 报文之前先收到了「新 SYN 报文」也就是服务端收到客户端报文的顺序是「旧 SYN 报文」-「新 SYN 报文」此时会发生什么? 当服务端第一次收到 SYN 报文也就是收到 「旧 SYN 报文」时就会回复 SYN ACK 报文给客户端此报文中的确认号是 91901。 然后这时再收到「新 SYN 报文」时就会回 Challenge Ack (opens new window)报文给客户端这个 ack 报文并不是确认收到「新 SYN 报文」的而是上一次的 ack 确认号也就是91901。所以客户端收到此 ACK 报文时发现自己期望收到的确认号应该是 101而不是 91于是就会回 RST 报文。 如果是两次握手连接就无法阻止历史连接那为什么 TCP 两次握手为什么无法阻止历史连接呢 我先直接说结论主要是因为在两次握手的情况下服务端没有中间状态给客户端来阻止历史连接导致服务端可能建立一个历史连接造成资源浪费。 你想想在两次握手的情况下服务端在收到 SYN 报文后就进入 ESTABLISHED 状态意味着这时可以给对方发送数据但是客户端此时还没有进入 ESTABLISHED 状态假设这次是历史连接客户端判断到此次连接为历史连接那么就会回 RST 报文来断开连接而服务端在第一次握手的时候就进入 ESTABLISHED 状态所以它可以发送数据的但是它并不知道这个是历史连接它只有在收到 RST 报文后才会断开连接。 ​ 可以看到如果采用两次握手建立 TCP 连接的场景下服务端在向客户端发送数据前并没有阻止掉历史连接导致服务端建立了一个历史连接又白白发送了数据妥妥地浪费了服务端的资源。 因此要解决这种现象最好就是在服务端发送数据前也就是建立连接之前要阻止掉历史连接这样就不会造成资源浪费而要实现这个功能就需要三次握手。 所以TCP 使用三次握手建立连接的最主要原因是防止「历史连接」初始化了连接。 TIP 有人问客户端发送三次握手ack 报文后就可以发送数据了而被动方此时还是 syn_received 状态如果 ack 丢了那客户端发的数据是不是也白白浪费了 不是的即使服务端还是在 syn_received 状态收到了客户端发送的数据还是可以建立连接的并且还可以正常收到这个数据包。这是因为数据报文中是有 ack 标识位也有确认号这个确认号就是确认收到了第二次握手。如下图 ​ 所以服务端收到这个数据报文是可以正常建立连接的然后就可以正常接收这个数据包了。 原因二同步双方初始序列号 TCP 协议的通信双方 都必须维护一个「序列号」 序列号是可靠传输的一个关键因素它的作用 接收方可以去除重复的数据接收方可以根据数据包的序列号按序接收可以标识发送出去的数据包中 哪些是已经被对方收到的通过 ACK 报文中的序列号知道 可见序列号在 TCP 连接中占据着非常重要的作用所以当客户端发送携带「初始序列号」的 SYN 报文的时候需要服务端回一个 ACK 应答报文表示客户端的 SYN 报文已被服务端成功接收那当服务端发送「初始序列号」给客户端的时候依然也要得到客户端的应答回应这样一来一回才能确保双方的初始序列号能被可靠的同步。 ​ 四次握手其实也能够可靠的同步双方的初始化序号但由于第二步和第三步可以优化成一步所以就成了「三次握手」。 而两次握手只保证了一方的初始序列号能被对方成功接收没办法保证双方的初始序列号都能被确认接收。 原因三避免资源浪费 如果只有「两次握手」当客户端发生的 SYN 报文在网络中阻塞客户端没有接收到 ACK 报文就会重新发送 SYN 由于没有第三次握手服务端不清楚客户端是否收到了自己回复的 ACK 报文所以服务端每收到一个 SYN 就只能先主动建立一个连接这会造成什么情况呢 如果客户端发送的 SYN 报文在网络中阻塞了重复发送多次 SYN 报文那么服务端在收到请求后就会建立多个冗余的无效链接造成不必要的资源浪费。 ​ 即两次握手会造成消息滞留情况下服务端重复接受无用的连接请求 SYN 报文而造成重复分配资源。 TIP 很多人问两次握手不是也可以根据上下文信息丢弃 syn 历史报文吗 我这里两次握手是假设「由于没有第三次握手服务端不清楚客户端是否收到了自己发送的建立连接的 ACK 确认报文所以每收到一个 SYN 就只能先主动建立一个连接」这个场景。 当然你要实现成类似三次握手那样根据上下文丢弃 syn 历史报文也是可以的两次握手没有具体的实现怎么假设都行。 小结 TCP 建立连接时通过三次握手能防止历史连接的建立能减少双方不必要的资源开销能帮助双方同步初始化序列号。序列号能够保证数据包不重复、不丢弃和按序传输。 不使用「两次握手」和「四次握手」的原因 「两次握手」无法防止历史连接的建立会造成双方资源的浪费也无法可靠的同步双方序列号「四次握手」三次握手就已经理论上最少可靠连接建立所以不需要使用更多的通信次数。 为什么每次建立 TCP 连接时初始化的序列号都要求不一样呢 主要原因有两个方面 为了防止历史报文被下一个相同四元组的连接接收主要方面为了安全性防止黑客伪造的相同序列号的 TCP 报文被对方接收 接下来详细说说第一点。 假设每次建立连接客户端和服务端的初始化序列号都是从 0 开始 ​ 过程如下 客户端和服务端建立一个 TCP 连接在客户端发送数据包被网络阻塞了然后超时重传了这个数据包而此时服务端设备断电重启了之前与客户端建立的连接就消失了于是在收到客户端的数据包的时候就会发送 RST 报文。紧接着客户端又与服务端建立了与上一个连接相同四元组的连接在新连接建立完成后上一个连接中被网络阻塞的数据包正好抵达了服务端刚好该数据包的序列号正好是在服务端的接收窗口内所以该数据包会被服务端正常接收就会造成数据错乱。 可以看到如果每次建立连接客户端和服务端的初始化序列号都是一样的话很容易出现历史报文被下一个相同四元组的连接接收的问题。 如果每次建立连接客户端和服务端的初始化序列号都「不一样」就有大概率因为历史报文的序列号「不在」对方接收窗口从而很大程度上避免了历史报文比如下图 ​ 相反如果每次建立连接客户端和服务端的初始化序列号都「一样」就有大概率遇到历史报文的序列号刚「好在」对方的接收窗口内从而导致历史报文被新连接成功接收。 所以每次初始化序列号不一样很大程度上能够避免历史报文被下一个相同四元组的连接接收注意是很大程度上并不是完全避免了因为序列号会有回绕的问题所以需要用时间戳的机制来判断历史报文详细看篇TCP 是如何避免历史报文的 (opens new window)。 初始序列号 ISN 是如何随机产生的 起始 ISN 是基于时钟的每 4 微秒 1转一圈要 4.55 个小时。 RFC793 提到初始化序列号 ISN 随机生成算法ISN M F(localhost, localport, remotehost, remoteport)。 M 是一个计时器这个计时器每隔 4 微秒加 1。F 是一个 Hash 算法根据源 IP、目的 IP、源端口、目的端口生成一个随机数值。要保证 Hash 算法不能被外部轻易推算得出用 MD5 算法是一个比较好的选择。 可以看到随机数是会基于时钟计时器递增的基本不可能会随机成一样的初始化序列号。 既然 IP 层会分片为什么 TCP 层还需要 MSS 呢 我们先来认识下 MTU 和 MSS ​ MTU一个网络包的最大长度以太网中一般为 1500 字节MSS除去 IP 和 TCP 头部之后一个网络包所能容纳的 TCP 数据的最大长度 如果在 TCP 的整个报文头部 数据交给 IP 层进行分片会有什么异常呢 当 IP 层有一个超过 MTU 大小的数据TCP 头部 TCP 数据要发送那么 IP 层就要进行分片把数据分片成若干片保证每一个分片都小于 MTU。把一份 IP 数据报进行分片以后由目标主机的 IP 层来进行重新组装后再交给上一层 TCP 传输层。 这看起来井然有序但这存在隐患的那么当如果一个 IP 分片丢失整个 IP 报文的所有分片都得重传。 因为 IP 层本身没有超时重传机制它由传输层的 TCP 来负责超时和重传。 当某一个 IP 分片丢失后接收方的 IP 层就无法组装成一个完整的 TCP 报文头部 数据也就无法将数据报文送到 TCP 层所以接收方不会响应 ACK 给发送方因为发送方迟迟收不到 ACK 确认报文所以会触发超时重传就会重发「整个 TCP 报文头部 数据」。 因此可以得知由 IP 层进行分片传输是非常没有效率的。 所以为了达到最佳的传输效能 TCP 协议在建立连接的时候通常要协商双方的 MSS 值当 TCP 层发现数据超过 MSS 时则就先会进行分片当然由它形成的 IP 包的长度也就不会大于 MTU 自然也就不用 IP 分片了。 ​ 经过 TCP 层分片后如果一个 TCP 分片丢失后进行重发时也是以 MSS 为单位而不用重传所有的分片大大增加了重传的效率。 第一次握手丢失了会发生什么 当客户端想和服务端建立 TCP 连接的时候首先第一个发的就是 SYN 报文然后进入到 SYN_SENT 状态。 在这之后如果客户端迟迟收不到服务端的 SYN-ACK 报文第二次握手就会触发「超时重传」机制重传 SYN 报文而且重传的 SYN 报文的序列号都是一样的。 不同版本的操作系统可能超时时间不同有的 1 秒的也有 3 秒的这个超时时间是写死在内核里的如果想要更改则需要重新编译内核比较麻烦。 当客户端在 1 秒后没收到服务端的 SYN-ACK 报文后客户端就会重发 SYN 报文那到底重发几次呢 在 Linux 里客户端的 SYN 报文最大重传次数由 tcp_syn_retries内核参数控制这个参数是可以自定义的默认值一般是 5。 # cat /proc/sys/net/ipv4/tcp_syn_retries 5通常第一次超时重传是在 1 秒后第二次超时重传是在 2 秒第三次超时重传是在 4 秒后第四次超时重传是在 8 秒后第五次是在超时重传 16 秒后。没错每次超时的时间是上一次的 2 倍。 当第五次超时重传后会继续等待 32 秒如果服务端仍然没有回应 ACK客户端就不再发送 SYN 包然后断开 TCP 连接。 所以总耗时是 1248163263 秒大约 1 分钟左右。 举个例子假设 tcp_syn_retries 参数值为 3那么当客户端的 SYN 报文一直在网络中丢失时会发生下图的过程 ​ 具体过程 当客户端超时重传 3 次 SYN 报文后由于 tcp_syn_retries 为 3已达到最大重传次数于是再等待一段时间时间为上一次超时时间的 2 倍如果还是没能收到服务端的第二次握手SYN-ACK 报文那么客户端就会断开连接。 第二次握手丢失了会发生什么 当服务端收到客户端的第一次握手后就会回 SYN-ACK 报文给客户端这个就是第二次握手此时服务端会进入 SYN_RCVD 状态。 第二次握手的 SYN-ACK 报文其实有两个目的 第二次握手里的 ACK 是对第一次握手的确认报文第二次握手里的 SYN是服务端发起建立 TCP 连接的报文 所以如果第二次握手丢了就会发生比较有意思的事情具体会怎么样呢 因为第二次握手报文里是包含对客户端的第一次握手的 ACK 确认报文所以如果客户端迟迟没有收到第二次握手那么客户端就觉得可能自己的 SYN 报文第一次握手丢失了于是客户端就会触发超时重传机制重传 SYN 报文。 然后因为第二次握手中包含服务端的 SYN 报文所以当客户端收到后需要给服务端发送 ACK 确认报文第三次握手服务端才会认为该 SYN 报文被客户端收到了。 那么如果第二次握手丢失了服务端就收不到第三次握手于是服务端这边会触发超时重传机制重传 SYN-ACK 报文。 在 Linux 下SYN-ACK 报文的最大重传次数由 tcp_synack_retries内核参数决定默认值是 5。 # cat /proc/sys/net/ipv4/tcp_synack_retries 5因此当第二次握手丢失了客户端和服务端都会重传 客户端会重传 SYN 报文也就是第一次握手最大重传次数由 tcp_syn_retries内核参数决定服务端会重传 SYN-ACK 报文也就是第二次握手最大重传次数由 tcp_synack_retries 内核参数决定。 举个例子假设 tcp_syn_retries 参数值为 1tcp_synack_retries 参数值为 2那么当第二次握手一直丢失时发生的过程如下图 ​ 具体过程 当客户端超时重传 1 次 SYN 报文后由于 tcp_syn_retries 为 1已达到最大重传次数于是再等待一段时间时间为上一次超时时间的 2 倍如果还是没能收到服务端的第二次握手SYN-ACK 报文那么客户端就会断开连接。当服务端超时重传 2 次 SYN-ACK 报文后由于 tcp_synack_retries 为 2已达到最大重传次数于是再等待一段时间时间为上一次超时时间的 2 倍如果还是没能收到客户端的第三次握手ACK 报文那么服务端就会断开连接。 第三次握手丢失了会发生什么 客户端收到服务端的 SYN-ACK 报文后就会给服务端回一个 ACK 报文也就是第三次握手此时客户端状态进入到 ESTABLISH 状态。 因为这个第三次握手的 ACK 是对第二次握手的 SYN 的确认报文所以当第三次握手丢失了如果服务端那一方迟迟收不到这个确认报文就会触发超时重传机制重传 SYN-ACK 报文直到收到第三次握手或者达到最大重传次数。 注意ACK 报文是不会有重传的当 ACK 丢失了就由对方重传对应的报文。 举个例子假设 tcp_synack_retries 参数值为 2那么当第三次握手一直丢失时发生的过程如下图 ​ 具体过程 当服务端超时重传 2 次 SYN-ACK 报文后由于 tcp_synack_retries 为 2已达到最大重传次数于是再等待一段时间时间为上一次超时时间的 2 倍如果还是没能收到客户端的第三次握手ACK 报文那么服务端就会断开连接。 什么是 SYN 攻击如何避免 SYN 攻击 我们都知道 TCP 连接建立是需要三次握手假设攻击者短时间伪造不同 IP 地址的 SYN 报文服务端每接收到一个 SYN 报文就进入SYN_RCVD 状态但服务端发送出去的 ACK SYN 报文无法得到未知 IP 主机的 ACK 应答久而久之就会占满服务端的半连接队列使得服务端不能为正常用户服务。 ​ 先跟大家说一下什么是 TCP 半连接和全连接队列。 在 TCP 三次握手的时候Linux 内核会维护两个队列分别是 半连接队列也称 SYN 队列全连接队列也称 accept 队列 我们先来看下 Linux 内核的 SYN 队列半连接队列与 Accpet 队列全连接队列是如何工作的 ​ 正常流程 当服务端接收到客户端的 SYN 报文时会创建一个半连接的对象然后将其加入到内核的「 SYN 队列」接着发送 SYN ACK 给客户端等待客户端回应 ACK 报文服务端接收到 ACK 报文后从「 SYN 队列」取出一个半连接对象然后创建一个新的连接对象放入到「 Accept 队列」应用通过调用 accpet() socket 接口从「 Accept 队列」取出连接对象。 不管是半连接队列还是全连接队列都有最大长度限制超过限制时默认情况都会丢弃报文。 SYN 攻击方式最直接的表现就会把 TCP 半连接队列打满这样当 TCP 半连接队列满了后续再在收到 SYN 报文就会丢弃导致客户端无法和服务端建立连接。 避免 SYN 攻击方式可以有以下四种方法 调大 netdev_max_backlog增大 TCP 半连接队列开启 tcp_syncookies减少 SYNACK 重传次数 方式一调大 netdev_max_backlog 当网卡接收数据包的速度大于内核处理的速度时会有一个队列保存这些数据包。控制该队列的最大值如下参数默认值是 1000我们要适当调大该参数的值比如设置为 10000 net.core.netdev_max_backlog 10000方式二增大 TCP 半连接队列 增大 TCP 半连接队列要同时增大下面这三个参数 增大 net.ipv4.tcp_max_syn_backlog增大 listen() 函数中的 backlog增大 net.core.somaxconn 具体为什么是三个参数决定 TCP 半连接队列的大小可以看这篇可以看这篇TCP 半连接队列和全连接队列满了会发生什么又该如何应对(opens new window) 方式三开启 net.ipv4.tcp_syncookies 开启 syncookies 功能就可以在不使用 SYN 半连接队列的情况下成功建立连接相当于绕过了 SYN 半连接来建立连接。 ​ 具体过程 当 「 SYN 队列」满之后后续服务端收到 SYN 包不会丢弃而是根据算法计算出一个 cookie 值将 cookie 值放到第二次握手报文的「序列号」里然后服务端回第二次握手给客户端服务端接收到客户端的应答报文时服务端会检查这个 ACK 包的合法性。如果合法将该连接对象放入到「 Accept 队列」。最后应用程序通过调用 accpet() 接口从「 Accept 队列」取出的连接。 可以看到当开启了 tcp_syncookies 了即使受到 SYN 攻击而导致 SYN 队列满时也能保证正常的连接成功建立。 net.ipv4.tcp_syncookies 参数主要有以下三个值 0 值表示关闭该功能1 值表示仅当 SYN 半连接队列放不下时再启用它2 值表示无条件开启功能 那么在应对 SYN 攻击时只需要设置为 1 即可。 $ echo 1 /proc/sys/net/ipv4/tcp_syncookies方式四减少 SYNACK 重传次数 当服务端受到 SYN 攻击时就会有大量处于 SYN_REVC 状态的 TCP 连接处于这个状态的 TCP 会重传 SYNACK 当重传超过次数达到上限后就会断开连接。 那么针对 SYN 攻击的场景我们可以减少 SYN-ACK 的重传次数以加快处于 SYN_REVC 状态的 TCP 连接断开。 SYN-ACK 报文的最大重传次数由 tcp_synack_retries内核参数决定默认值是 5 次比如将 tcp_synack_retries 减少到 2 次 $ echo 2 /proc/sys/net/ipv4/tcp_synack_retriesTCP 连接断开 TCP 四次挥手过程是怎样的 天下没有不散的宴席对于 TCP 连接也是这样 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 时仅仅表示客户端不再发送数据了但是还能接收数据。服务端收到客户端的 FIN 报文时先回一个 ACK 应答报文而服务端可能还有数据需要处理和发送等服务端不再发送数据时才发送 FIN 报文给客户端来表示同意现在关闭连接。 从上面过程可知服务端通常需要等待完成数据的发送和处理所以服务端的 ACK 和 FIN 一般都会分开发送因此是需要四次挥手。 但是在特定情况下四次挥手是可以变成三次挥手的具体情况可以看这篇TCP 四次挥手可以变成三次吗(opens new window) 第一次挥手丢失了会发生什么 当客户端主动关闭方调用 close 函数后就会向服务端发送 FIN 报文试图与服务端断开连接此时客户端的连接进入到 FIN_WAIT_1 状态。 正常情况下如果能及时收到服务端被动关闭方的 ACK则会很快变为 FIN_WAIT2状态。 如果第一次挥手丢失了那么客户端迟迟收不到被动方的 ACK 的话也就会触发超时重传机制重传 FIN 报文重发次数由 tcp_orphan_retries 参数控制。 当客户端重传 FIN 报文的次数超过 tcp_orphan_retries 后就不再发送 FIN 报文则会在等待一段时间时间为上一次超时时间的 2 倍如果还是没能收到第二次挥手那么直接进入到 close 状态。 举个例子假设 tcp_orphan_retries 参数值为 3当第一次挥手一直丢失时发生的过程如下图 ​ 具体过程 当客户端超时重传 3 次 FIN 报文后由于 tcp_orphan_retries 为 3已达到最大重传次数于是再等待一段时间时间为上一次超时时间的 2 倍如果还是没能收到服务端的第二次挥手ACK报文那么客户端就会断开连接。 第二次挥手丢失了会发生什么 当服务端收到客户端的第一次挥手后就会先回一个 ACK 确认报文此时服务端的连接进入到 CLOSE_WAIT 状态。 在前面我们也提了ACK 报文是不会重传的所以如果服务端的第二次挥手丢失了客户端就会触发超时重传机制重传 FIN 报文直到收到服务端的第二次挥手或者达到最大的重传次数。 举个例子假设 tcp_orphan_retries 参数值为 2当第二次挥手一直丢失时发生的过程如下图 ​ 具体过程 当客户端超时重传 2 次 FIN 报文后由于 tcp_orphan_retries 为 2已达到最大重传次数于是再等待一段时间时间为上一次超时时间的 2 倍如果还是没能收到服务端的第二次挥手ACK 报文那么客户端就会断开连接。 这里提一下当客户端收到第二次挥手也就是收到服务端发送的 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 报文的重传次数控制方式是一样的。 举个例子假设 tcp_orphan_retries 3当第三次挥手一直丢失时发生的过程如下图 ​ 具体过程 当服务端重传第三次挥手报文的次数达到了 3 次后由于 tcp_orphan_retries 为 3达到了重传最大次数于是再等待一段时间时间为上一次超时时间的 2 倍如果还是没能收到客户端的第四次挥手ACK报文那么服务端就会断开连接。客户端因为是通过 close 函数关闭连接的处于 FIN_WAIT_2 状态是有时长限制的如果 tcp_fin_timeout 时间内还是没能收到服务端的第三次挥手FIN 报文那么客户端就会断开连接。 第四次挥手丢失了会发生什么 当客户端收到服务端的第三次挥手的 FIN 报文后就会回 ACK 报文也就是第四次挥手此时客户端连接进入 TIME_WAIT 状态。 在 Linux 系统TIME_WAIT 状态会持续 2MSL 后才会进入关闭状态。 然后服务端被动关闭方没有收到 ACK 报文前还是处于 LAST_ACK 状态。 如果第四次挥手的 ACK 报文没有到达服务端服务端就会重发 FIN 报文重发次数仍然由前面介绍过的 tcp_orphan_retries 参数控制。 举个例子假设 tcp_orphan_retries 为 2当第四次挥手一直丢失时发生的过程如下 ​ 具体过程 当服务端重传第三次挥手报文达到 2 时由于 tcp_orphan_retries 为 2 达到了最大重传次数于是再等待一段时间时间为上一次超时时间的 2 倍如果还是没能收到客户端的第四次挥手ACK 报文那么服务端就会断开连接。客户端在收到第三次挥手后就会进入 TIME_WAIT 状态开启时长为 2MSL 的定时器如果途中再次收到第三次挥手FIN 报文后就会重置定时器当等待 2MSL 时长后客户端就会断开连接。 为什么 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 倍的时间。 比如如果被动关闭方没有收到断开连接的最后的 ACK 报文就会触发超时重发 FIN 报文另一方接收到 FIN 后会重发 ACK 给被动关闭方 一来一去正好 2 个 MSL。 可以看到 2MSL时长 这其实是相当于至少允许报文丢失一次。比如若 ACK 在一个 MSL 内丢失这样被动方重发的 FIN 会在第 2 个 MSL 内到达TIME_WAIT 状态的连接可以应对。 为什么不是 4 或者 8 MSL 的时长呢你可以想象一个丢包率达到百分之一的糟糕网络连续两次丢包的概率只有万分之一这个概率实在是太小了忽略它比解决它更具性价比。 2MSL 的时间是从客户端接收到 FIN 后发送 ACK 开始计时的。如果在 TIME-WAIT 时间内因为客户端的 ACK 没有传输到服务端客户端又接收到了服务端重发的 FIN 报文那么 2MSL 时间将重新计时。 在 Linux 系统里 2MSL 默认是 60 秒那么一个 MSL 也就是 30 秒。Linux 系统停留在 TIME_WAIT 的时间为固定的 60 秒。 其定义在 Linux 内核代码里的名称为 TCP_TIMEWAIT_LEN #define TCP_TIMEWAIT_LEN (60*HZ) /* how long to wait to destroy TIME-WAIT state, about 60 seconds */如果要修改 TIME_WAIT 的时间长度只能修改 Linux 内核代码里 TCP_TIMEWAIT_LEN 的值并重新编译 Linux 内核。 为什么需要 TIME_WAIT 状态 主动发起关闭连接的一方才会有 TIME-WAIT 状态。 需要 TIME-WAIT 状态主要是两个原因 防止历史连接中的数据被后面相同四元组的连接错误的接收保证「被动关闭连接」的一方能被正确的关闭 原因一防止历史连接中的数据被后面相同四元组的连接错误的接收 为了能更好的理解这个原因我们先来了解序列号SEQ和初始序列号ISN。 序列号是 TCP 一个头部字段标识了 TCP 发送端到 TCP 接收端的数据流的一个字节因为 TCP 是面向字节流的可靠协议为了保证消息的顺序性和可靠性TCP 为每个传输方向上的每个字节都赋予了一个编号以便于传输成功后确认、丢失后重传以及在接收端保证不会乱序。序列号是一个 32 位的无符号数因此在到达 4G 之后再循环回到 0。初始序列号在 TCP 建立连接的时候客户端和服务端都会各自生成一个初始序列号它是基于时钟生成的一个随机数来保证每个连接都拥有不同的初始序列号。初始化序列号可被视为一个 32 位的计数器该计数器的数值每 4 微秒加 1循环一次需要 4.55 小时。 给大家抓了一个包下图中的 Seq 就是序列号其中红色框住的分别是客户端和服务端各自生成的初始序列号。 ​ 通过前面我们知道序列号和初始化序列号并不是无限递增的会发生回绕为初始值的情况这意味着无法根据序列号来判断新老数据。 假设 TIME-WAIT 没有等待时间或时间过短被延迟的数据包抵达后会发生什么呢 ​ 如上图 服务端在关闭连接之前发送的 SEQ 301 报文被网络延迟了。接着服务端以相同的四元组重新打开了新连接前面被延迟的 SEQ 301 这时抵达了客户端而且该数据报文的序列号刚好在客户端接收窗口内因此客户端会正常接收这个数据报文但是这个数据报文是上一个连接残留下来的这样就产生数据错乱等严重的问题。 为了防止历史连接中的数据被后面相同四元组的连接错误的接收因此 TCP 设计了 TIME_WAIT 状态状态会持续 2MSL 时长这个时间足以让两个方向上的数据包都被丢弃使得原来连接的数据包在网络中都自然消失再出现的数据包一定都是新建立连接所产生的。 原因二保证「被动关闭连接」的一方能被正确的关闭 在 RFC 793 指出 TIME-WAIT 另一个重要的作用是 TIME-WAIT - represents waiting for enough time to pass to be sure the remote TCP received the acknowledgment of its connection termination request. 也就是说TIME-WAIT 作用是等待足够的时间以确保最后的 ACK 能让被动关闭方接收从而帮助其正常关闭。 如果客户端主动关闭方最后一次 ACK 报文第四次挥手在网络中丢失了那么按照 TCP 可靠性原则服务端被动关闭方会重发 FIN 报文。 假设客户端没有 TIME_WAIT 状态而是在发完最后一次回 ACK 报文就直接进入 CLOSE 状态如果该 ACK 报文丢失了服务端则重传的 FIN 报文而这时客户端已经进入到关闭状态了在收到服务端重传的 FIN 报文后就会回 RST 报文。 ​ 服务端收到这个 RST 并将其解释为一个错误Connection reset by peer这对于一个可靠的协议来说不是一个优雅的终止方式。 为了防止这种情况出现客户端必须等待足够长的时间确保服务端能够收到 ACK如果服务端没有收到 ACK那么就会触发 TCP 重传机制服务端会重新发送一个 FIN这样一去一来刚好两个 MSL 的时间。 ​ 客户端在收到服务端重传的 FIN 报文时TIME_WAIT 状态的等待时间会重置回 2MSL。 TIME_WAIT 过多有什么危害 过多的 TIME-WAIT 状态主要的危害有两种 第一是占用系统资源比如文件描述符、内存资源、CPU 资源、线程资源等第二是占用端口资源端口资源也是有限的一般可以开启的端口为 3276861000也可以通过 net.ipv4.ip_local_port_range参数指定范围。 客户端和服务端 TIME_WAIT 过多造成的影响是不同的。 如果客户端主动发起关闭连接方的 TIME_WAIT 状态过多占满了所有端口资源那么就无法对「目的 IP 目的 PORT」都一样的服务端发起连接了但是被使用的端口还是可以继续对另外一个服务端发起连接的。具体可以看我这篇文章客户端的端口可以重复使用吗(opens new window) 因此客户端发起连接方都是和「目的 IP 目的 PORT 」都一样的服务端建立连接的话当客户端的 TIME_WAIT 状态连接过多的话就会受端口资源限制如果占满了所有端口资源那么就无法再跟「目的 IP 目的 PORT」都一样的服务端建立连接了。 不过即使是在这种场景下只要连接的是不同的服务端端口是可以重复使用的所以客户端还是可以向其他服务端发起连接的这是因为内核在定位一个连接的时候是通过四元组源IP、源端口、目的IP、目的端口信息来定位的并不会因为客户端的端口一样而导致连接冲突。 如果服务端主动发起关闭连接方的 TIME_WAIT 状态过多并不会导致端口资源受限因为服务端只监听一个端口而且由于一个四元组唯一确定一个 TCP 连接因此理论上服务端可以建立很多连接但是 TCP 连接过多会占用系统资源比如文件描述符、内存资源、CPU 资源、线程资源等。 如何优化 TIME_WAIT 这里给出优化 TIME-WAIT 的几个方式都是有利有弊 打开 net.ipv4.tcp_tw_reuse 和 net.ipv4.tcp_timestamps 选项net.ipv4.tcp_max_tw_buckets程序中使用 SO_LINGER 应用强制使用 RST 关闭。 方式一net.ipv4.tcp_tw_reuse 和 tcp_timestamps 如下的 Linux 内核参数开启后则可以复用处于 TIME_WAIT 的 socket 为新的连接所用。 有一点需要注意的是tcp_tw_reuse 功能只能用客户端连接发起方因为开启了该功能在调用 connect() 函数时内核会随机找一个 time_wait 状态超过 1 秒的连接给新的连接复用。 net.ipv4.tcp_tw_reuse 1使用这个选项还有一个前提需要打开对 TCP 时间戳的支持即 net.ipv4.tcp_timestamps1默认即为 1这个时间戳的字段是在 TCP 头部的「选项」里它由一共 8 个字节表示时间戳其中第一个 4 字节字段用来保存发送该数据包的时间第二个 4 字节字段用来保存最近一次接收对方发送到达数据的时间。 由于引入了时间戳我们在前面提到的 2MSL 问题就不复存在了因为重复的数据包会因为时间戳过期被自然丢弃。 方式二net.ipv4.tcp_max_tw_buckets 这个值默认为 18000当系统中处于 TIME_WAIT 的连接一旦超过这个值时系统就会将后面的 TIME_WAIT 连接状态重置这个方法比较暴力。 方式三程序中使用 SO_LINGER 我们可以通过设置 socket 选项来设置调用 close 关闭连接行为。 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));如果l_onoff为非 0 且l_linger值为 0那么调用close后会立该发送一个RST标志给对端该 TCP 连接将跳过四次挥手也就跳过了TIME_WAIT状态直接关闭。 但这为跨越TIME_WAIT状态提供了一个可能不过是一个非常危险的行为不值得提倡。 前面介绍的方法都是试图越过 TIME_WAIT状态的这样其实不太好。虽然 TIME_WAIT 状态持续的时间是有一点长显得很不友好但是它被设计来就是用来避免发生乱七八糟的事情。 《UNIX网络编程》一书中却说道TIME_WAIT 是我们的朋友它是有助于我们的不要试图避免这个状态而是应该弄清楚它。 如果服务端要避免过多的 TIME_WAIT 状态的连接就永远不要主动断开连接让客户端去断开由分布在各处的客户端去承受 TIME_WAIT。 服务器出现大量 TIME_WAIT 状态的原因有哪些 首先要知道 TIME_WAIT 状态是主动关闭连接方才会出现的状态所以如果服务器出现大量的 TIME_WAIT 状态的 TCP 连接就是说明服务器主动断开了很多 TCP 连接。 问题来了什么场景下服务端会主动断开连接呢 第一个场景HTTP 没有使用长连接第二个场景HTTP 长连接超时第三个场景HTTP 长连接的请求数量达到上限 接下来分别介绍下。 第一个场景HTTP 没有使用长连接 我们先来看看 HTTP 长连接Keep-Alive机制是怎么开启的。 在 HTTP/1.0 中默认是关闭的如果浏览器要开启 Keep-Alive它必须在请求的 header 中添加 Connection: Keep-Alive然后当服务器收到请求作出回应的时候它也被添加到响应中 header 里 Connection: Keep-Alive这样做TCP 连接就不会中断而是保持连接。当客户端发送另一个请求时它会使用同一个 TCP 连接。这一直继续到客户端或服务器端提出断开连接。 从 HTTP/1.1 开始 就默认是开启了 Keep-Alive现在大多数浏览器都默认是使用 HTTP/1.1所以 Keep-Alive 都是默认打开的。一旦客户端和服务端达成协议那么长连接就建立好了。 如果要关闭 HTTP Keep-Alive需要在 HTTP 请求或者响应的 header 里添加 Connection:close 信息也就是说只要客户端和服务端任意一方的 HTTP header 中有 Connection:close 信息那么就无法使用 HTTP 长连接的机制。 关闭 HTTP 长连接机制后每次请求都要经历这样的过程建立 TCP - 请求资源 - 响应资源 - 释放连接那么此方式就是 HTTP 短连接如下图 ​ 在前面我们知道只要任意一方的 HTTP header 中有 Connection:close 信息就无法使用 HTTP 长连接机制这样在完成一次 HTTP 请求/处理后就会关闭连接。 问题来了这时候是客户端还是服务端主动关闭连接呢 在 RFC 文档中并没有明确由谁来关闭连接请求和响应的双方都可以主动关闭 TCP 连接。 不过根据大多数 Web 服务的实现不管哪一方禁用了 HTTP Keep-Alive都是由服务端主动关闭连接那么此时服务端上就会出现 TIME_WAIT 状态的连接。 客户端禁用了 HTTP Keep-Alive服务端开启 HTTP Keep-Alive谁是主动关闭方 当客户端禁用了 HTTP Keep-Alive这时候 HTTP 请求的 header 就会有 Connection:close 信息这时服务端在发完 HTTP 响应后就会主动关闭连接。 为什么要这么设计呢HTTP 是请求-响应模型发起方一直是客户端HTTP Keep-Alive 的初衷是为客户端后续的请求重用连接如果我们在某次 HTTP 请求-响应模型中请求的 header 定义了 connectionclose 信息那不再重用这个连接的时机就只有在服务端了所以我们在 HTTP 请求-响应这个周期的「末端」关闭连接是合理的。 客户端开启了 HTTP Keep-Alive服务端禁用了 HTTP Keep-Alive谁是主动关闭方 当客户端开启了 HTTP Keep-Alive而服务端禁用了 HTTP Keep-Alive这时服务端在发完 HTTP 响应后服务端也会主动关闭连接。 为什么要这么设计呢在服务端主动关闭连接的情况下只要调用一次 close() 就可以释放连接剩下的工作由内核 TCP 栈直接进行了处理整个过程只有一次 syscall如果是要求 客户端关闭则服务端在写完最后一个 response 之后需要把这个 socket 放入 readable 队列调用 select / epoll 去等待事件然后调用一次 read() 才能知道连接已经被关闭这其中是两次 syscall多一次用户态程序被激活执行而且 socket 保持时间也会更长。 因此当服务端出现大量的 TIME_WAIT 状态连接的时候可以排查下是否客户端和服务端都开启了 HTTP Keep-Alive因为任意一方没有开启 HTTP Keep-Alive都会导致服务端在处理完一个 HTTP 请求后就主动关闭连接此时服务端上就会出现大量的 TIME_WAIT 状态的连接。 针对这个场景下解决的方式也很简单让客户端和服务端都开启 HTTP Keep-Alive 机制。 第二个场景HTTP 长连接超时 HTTP 长连接的特点是只要任意一端没有明确提出断开连接则保持 TCP 连接状态。 HTTP 长连接可以在同一个 TCP 连接上接收和发送多个 HTTP 请求/应答避免了连接建立和释放的开销。 ​ 可能有的同学会问如果使用了 HTTP 长连接如果客户端完成一个 HTTP 请求后就不再发起新的请求此时这个 TCP 连接一直占用着不是挺浪费资源的吗 对没错所以为了避免资源浪费的情况web 服务软件一般都会提供一个参数用来指定 HTTP 长连接的超时时间比如 nginx 提供的 keepalive_timeout 参数。 假设设置了 HTTP 长连接的超时时间是 60 秒nginx 就会启动一个「定时器」如果客户端在完后一个 HTTP 请求后在 60 秒内都没有再发起新的请求定时器的时间一到nginx 就会触发回调函数来关闭该连接那么此时服务端上就会出现 TIME_WAIT 状态的连接。 ​ 当服务端出现大量 TIME_WAIT 状态的连接时如果现象是有大量的客户端建立完 TCP 连接后很长一段时间没有发送数据那么大概率就是因为 HTTP 长连接超时导致服务端主动关闭连接产生大量处于 TIME_WAIT 状态的连接。 可以往网络问题的方向排查比如是否是因为网络问题导致客户端发送的数据一直没有被服务端接收到以至于 HTTP 长连接超时。 第三个场景HTTP 长连接的请求数量达到上限 Web 服务端通常会有个参数来定义一条 HTTP 长连接上最大能处理的请求数量当超过最大限制时就会主动关闭连接。 比如 nginx 的 keepalive_requests 这个参数这个参数是指一个 HTTP 长连接建立之后nginx 就会为这个连接设置一个计数器记录这个 HTTP 长连接上已经接收并处理的客户端请求的数量。如果达到这个参数设置的最大值时则 nginx 会主动关闭这个长连接那么此时服务端上就会出现 TIME_WAIT 状态的连接。 keepalive_requests 参数的默认值是 100 意味着每个 HTTP 长连接最多只能跑 100 次请求这个参数往往被大多数人忽略因为当 QPS (每秒请求数) 不是很高时默认值 100 凑合够用。 但是对于一些 QPS 比较高的场景比如超过 10000 QPS甚至达到 30000 , 50000 甚至更高如果 keepalive_requests 参数值是 100这时候就 nginx 就会很频繁地关闭连接那么此时服务端上就会出大量的 TIME_WAIT 状态。 针对这个场景下解决的方式也很简单调大 nginx 的 keepalive_requests 参数就行。 服务器出现大量 CLOSE_WAIT 状态的原因有哪些 CLOSE_WAIT 状态是「被动关闭方」才会有的状态而且如果「被动关闭方」没有调用 close 函数关闭连接那么就无法发出 FIN 报文从而无法使得 CLOSE_WAIT 状态的连接转变为 LAST_ACK 状态。 所以当服务端出现大量 CLOSE_WAIT 状态的连接的时候说明服务端的程序没有调用 close 函数关闭连接。 那什么情况会导致服务端的程序没有调用 close 函数关闭连接这时候通常需要排查代码。 我们先来分析一个普通的 TCP 服务端的流程 创建服务端 socketbind 绑定端口、listen 监听端口将服务端 socket 注册到 epollepoll_wait 等待连接到来连接到来时调用 accpet 获取已连接的 socket将已连接的 socket 注册到 epollepoll_wait 等待事件发生对方连接关闭时我方调用 close 可能导致服务端没有调用 close 函数的原因如下。 第一个原因第 2 步没有做没有将服务端 socket 注册到 epoll这样有新连接到来时服务端没办法感知这个事件也就无法获取到已连接的 socket那服务端自然就没机会对 socket 调用 close 函数了。 不过这种原因发生的概率比较小这种属于明显的代码逻辑 bug在前期 read view 阶段就能发现的了。 第二个原因 第 3 步没有做有新连接到来时没有调用 accpet 获取该连接的 socket导致当有大量的客户端主动断开了连接而服务端没机会对这些 socket 调用 close 函数从而导致服务端出现大量 CLOSE_WAIT 状态的连接。 发生这种情况可能是因为服务端在执行 accpet 函数之前代码卡在某一个逻辑或者提前抛出了异常。 第三个原因第 4 步没有做通过 accpet 获取已连接的 socket 后没有将其注册到 epoll导致后续收到 FIN 报文的时候服务端没办法感知这个事件那服务端就没机会调用 close 函数了。 发生这种情况可能是因为服务端在将已连接的 socket 注册到 epoll 之前代码卡在某一个逻辑或者提前抛出了异常。之前看到过别人解决 close_wait 问题的实践文章感兴趣的可以看看一次 Netty 代码不健壮导致的大量 CLOSE_WAIT 连接原因分析(opens new window) 第四个原因第 6 步没有做当发现客户端关闭连接后服务端没有执行 close 函数可能是因为代码漏处理或者是在执行 close 函数之前代码卡在某一个逻辑比如发生死锁等等。 可以发现当服务端出现大量 CLOSE_WAIT 状态的连接的时候通常都是代码的问题这时候我们需要针对具体的代码一步一步的进行排查和定位主要分析的方向就是服务端为什么没有调用 close。 如果已经建立了连接但是客户端突然出现故障了怎么办 客户端出现故障指的是客户端的主机发生了宕机或者断电的场景。发生这种情况的时候如果服务端一直不会发送数据给客户端那么服务端是永远无法感知到客户端宕机这个事件的也就是服务端的 TCP 连接将一直处于 ESTABLISH 状态占用着系统资源。 为了避免这种情况TCP 搞了个保活机制。这个机制的原理是这样的 定义一个时间段在这个时间段内如果没有任何连接相关的活动TCP 保活机制会开始作用每隔一个时间间隔发送一个探测报文该探测报文包含的数据非常少如果连续几个探测报文都没有得到响应则认为当前的 TCP 连接已经死亡系统内核将错误信息通知给上层应用程序。 在 Linux 内核可以有对应的参数可以设置保活时间、保活探测的次数、保活探测的时间间隔以下都为默认值 net.ipv4.tcp_keepalive_time7200 net.ipv4.tcp_keepalive_intvl75 net.ipv4.tcp_keepalive_probes9tcp_keepalive_time7200表示保活时间是 7200 秒2小时也就 2 小时内如果没有任何连接相关的活动则会启动保活机制tcp_keepalive_intvl75表示每次检测间隔 75 秒tcp_keepalive_probes9表示检测 9 次无响应认为对方是不可达的从而中断本次的连接。 也就是说在 Linux 系统中最少需要经过 2 小时 11 分 15 秒才可以发现一个「死亡」连接。 ​ 注意应用程序若想使用 TCP 保活机制需要通过 socket 接口设置 SO_KEEPALIVE 选项才能够生效如果没有设置那么就无法使用 TCP 保活机制。 如果开启了 TCP 保活需要考虑以下几种情况 第一种对端程序是正常工作的。当 TCP 保活的探测报文发送给对端, 对端会正常响应这样 TCP 保活时间会被重置等待下一个 TCP 保活时间的到来。 第二种对端主机宕机并重启。当 TCP 保活的探测报文发送给对端后对端是可以响应的但由于没有该连接的有效信息会产生一个 RST 报文这样很快就会发现 TCP 连接已经被重置。 第三种是对端主机宕机注意不是进程崩溃进程崩溃后操作系统在回收进程资源的时候会发送 FIN 报文而主机宕机则是无法感知的所以需要 TCP 保活机制来探测对方是不是发生了主机宕机或对端由于其他原因导致报文不可达。当 TCP 保活的探测报文发送给对端后石沉大海没有响应连续几次达到保活探测次数后TCP 会报告该 TCP 连接已经死亡。 TCP 保活的这个机制检测的时间是有点长我们可以自己在应用层实现一个心跳机制。 比如web 服务软件一般都会提供 keepalive_timeout 参数用来指定 HTTP 长连接的超时时间。如果设置了 HTTP 长连接的超时时间是 60 秒web 服务软件就会启动一个定时器如果客户端在完成一个 HTTP 请求后在 60 秒内都没有再发起新的请求定时器的时间一到就会触发回调函数来释放该连接。 ​ 如果已经建立了连接但是服务端的进程崩溃会发生什么 TCP 的连接信息是由内核维护的所以当服务端的进程崩溃后内核需要回收该进程的所有 TCP 连接资源于是内核会发送第一次挥手 FIN 报文后续的挥手过程也都是在内核完成并不需要进程的参与所以即使服务端的进程退出了还是能与客户端完成 TCP 四次挥手的过程。 我自己做了个实验使用 kill -9 来模拟进程崩溃的情况发现在 kill 掉进程后服务端会发送 FIN 报文与客户端进行四次挥手。 TIP 关于进程崩溃和主机宕机的区别可以参考这篇TCP 连接一端断电和进程崩溃有什么区别(opens new window) 还有一个类似的问题「拔掉网线后 原本的 TCP 连接还存在吗」具体可以看这篇拔掉网线后 原本的 TCP 连接还存在吗(opens new window) Socket 编程 针对 TCP 应该如何 Socket 编程 ​ 服务端和客户端初始化 socket得到文件描述符服务端调用 bind将 socket 绑定在指定的 IP 地址和端口;服务端调用 listen进行监听服务端调用 accept等待客户端连接客户端调用 connect向服务端的地址和端口发起连接请求服务端 accept 返回用于传输的 socket 的文件描述符客户端调用 write 写入数据服务端调用 read 读取数据客户端断开连接时会调用 close那么服务端 read 读取数据的时候就会读取到了 EOF待处理完数据后服务端调用 close表示连接关闭。 这里需要注意的是服务端调用 accept 时连接成功了会返回一个已完成连接的 socket后续用来传输数据。 所以监听的 socket 和真正用来传送数据的 socket是「两个」 socket一个叫作监听 socket一个叫作已完成连接 socket。 成功连接建立之后双方开始通过 read 和 write 函数来读写数据就像往一个文件流里面写东西一样。 listen 时候参数 backlog 的意义 Linux内核中会维护两个队列 半连接队列SYN 队列接收到一个 SYN 建立连接请求处于 SYN_RCVD 状态全连接队列Accpet 队列已完成 TCP 三次握手过程处于 ESTABLISHED 状态 ​ int listen (int socketfd, int backlog)参数一 socketfd 为 socketfd 文件描述符参数二 backlog这参数在历史版本有一定的变化 在早期 Linux 内核 backlog 是 SYN 队列大小也就是未完成的队列大小。 在 Linux 内核 2.2 之后backlog 变成 accept 队列也就是已完成连接建立的队列长度所以现在通常认为 backlog 是 accept 队列。 但是上限值是内核参数 somaxconn 的大小也就说 accpet 队列长度 min(backlog, somaxconn)。 想详细了解 TCP 半连接队列和全连接队列可以看这篇TCP 半连接队列和全连接队列满了会发生什么又该如何应对(opens new window) accept 发生在三次握手的哪一步 我们先看看客户端连接服务端时发送了什么 ​ 客户端的协议栈向服务端发送了 SYN 包并告诉服务端当前发送序列号 client_isn客户端进入 SYN_SENT 状态服务端的协议栈收到这个包之后和客户端进行 ACK 应答应答的值为 client_isn1表示对 SYN 包 client_isn 的确认同时服务端也发送一个 SYN 包告诉客户端当前我的发送序列号为 server_isn服务端进入 SYN_RCVD 状态客户端协议栈收到 ACK 之后使得应用程序从 connect 调用返回表示客户端到服务端的单向连接建立成功客户端的状态为 ESTABLISHED同时客户端协议栈也会对服务端的 SYN 包进行应答应答数据为 server_isn1ACK 应答包到达服务端后服务端的 TCP 连接进入 ESTABLISHED 状态同时服务端协议栈使得 accept 阻塞调用返回这个时候服务端到客户端的单向连接也建立成功。至此客户端与服务端两个方向的连接都建立成功。 从上面的描述过程我们可以得知客户端 connect 成功返回是在第二次握手服务端 accept 成功返回是在三次握手成功之后。 客户端调用 close 了连接是断开的流程是什么 我们看看客户端主动调用了 close会发生什么 ​ 客户端调用 close表明客户端没有数据需要发送了则此时会向服务端发送 FIN 报文进入 FIN_WAIT_1 状态服务端接收到了 FIN 报文TCP 协议栈会为 FIN 包插入一个文件结束符 EOF 到接收缓冲区中应用程序可以通过 read 调用来感知这个 FIN 包。这个 EOF 会被放在已排队等候的其他已接收的数据之后这就意味着服务端需要处理这种异常情况因为 EOF 表示在该连接上再无额外数据到达。此时服务端进入 CLOSE_WAIT 状态接着当处理完数据后自然就会读到 EOF于是也调用 close 关闭它的套接字这会使得服务端发出一个 FIN 包之后处于 LAST_ACK 状态客户端接收到服务端的 FIN 包并发送 ACK 确认包给服务端此时客户端将进入 TIME_WAIT 状态服务端收到 ACK 确认包后就进入了最后的 CLOSE 状态客户端经过 2MSL 时间之后也进入 CLOSE 状态 没有 accept能建立 TCP 连接吗 答案可以的。 accpet 系统调用并不参与 TCP 三次握手过程它只是负责从 TCP 全连接队列取出一个已经建立连接的 socket用户层通过 accpet 系统调用拿到了已经建立连接的 socket就可以对该 socket 进行读写操作了。 ​ 更想了解这个问题可以参考这篇文章没有 accept能建立 TCP 连接吗(opens new window) 没有 listen能建立 TCP 连接吗 答案可以的。 客户端是可以自己连自己的形成连接TCP自连接也可以两个客户端同时向对方发出请求建立连接TCP同时打开这两个情况都有个共同点就是没有服务端参与也就是没有 listen就能 TCP 建立连接。 更想了解这个问题可以参考这篇文章服务端没有 listen客户端发起连接建立会发生什么
http://www.tj-hxxt.cn/news/232725.html

相关文章:

  • 万网如何建网站朔州seo网站建设
  • 自己开网站工作室林州建筑网
  • 网站建设捌金手指下拉十六长宁品牌网站建设
  • 汕头电商网站建设易店无忧官网
  • 凡科网站建设视频陕西最新消息
  • 州网站建设wordpress 菜单添加图标
  • 网站建设界面建议郴州网站建设服务
  • 在网上做游戏网站违法吗视频网站用什么做的好
  • 网站一般多少钱一年黑龙江网站建设工作室
  • 惠安网站建设江苏网站备案要多久
  • 个人怎样建设网站如何给网站做地图
  • 没有静态ip可以做网站服务器专业做面膜的网站
  • 视频 主题 wordpress烟台seo网站诊断
  • 建个网站用多少钱网站建设推广优化
  • seo网站营销php网站搭建教程
  • 怎么做网站建设作业科普网站栏目建设方案策划
  • 做网站为什么能赚钱做消费网站流程
  • c 教学网站开发网站如何提高转化率
  • 电子商务是建网站网站网站制作价格建站网站
  • 网站信息发布和内容建设自查报告如何看网站是用什么程序做的
  • 营销型集团网站怎么制作app平台
  • 网站与个人网站免费wordpress中文主题下载地址
  • 网站空间安装中天建设集团山西分公司网站
  • 网站搭建制作小吃网站怎么做
  • 彩票网站该怎么建设小型公司网站建设论文
  • 泉州专门做网站甘肃省住房城乡建设厅网站
  • 固原网站制作石家庄招聘网最新招聘
  • 做网站还需要买空间吗中国做视频网站有哪些
  • 广东华迪工程建设监理公司网站会员网站开发
  • 网站设置文件西安网站推广优化