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

化隆县公司网站建设一天挣5000块钱捕鱼

化隆县公司网站建设,一天挣5000块钱捕鱼,企业信息系统开发,黄山旅游最佳路线目录 一、如何理解 TCP 是面向字节流协议 先来说说为什么 UDP 是面向报文的协议#xff1f; 如果收到了两个 UDP 报文#xff0c;操作系统是如何区分开的呢#xff1f; 再说说为什么 TCP 是面向字节流的协议#xff1f; 二、如何解决粘包问题#xff1f; ①、固定消…目录 一、如何理解 TCP 是面向字节流协议 先来说说为什么 UDP 是面向报文的协议 如果收到了两个 UDP 报文操作系统是如何区分开的呢 再说说为什么 TCP 是面向字节流的协议 二、如何解决粘包问题 ①、固定消息的长度 ②、特殊字符作为边界 ③、自定义消息结构 三、SYN 报文什么情况下会被丢弃 什么是 PAWS 机制 那么什么是 per-host 的 PAWS 机制呢 四、已建立连接的 TCP收到 SYN 会发生什么 ①、客户端的 SYN 报文里的端口号与历史连接不同 ②、客户端的 SYN 报文里的端口号与历史连接相同 五、如何关闭一个 TCP 连接 killcx 的工具 tcpkill 的工具 六、四次挥手中收到乱序的 FIN 包会如何处理 七、在 TIME_WAIT 状态下 TCP 连接收到 SYN 后会发生什么 ①、收到合法的 SYN  ②、收到非法的 SYN 八、TCP 连接一端断电和进程崩溃有什么区别 无数据传输的场景 ①、主机崩溃情况 ②、进程崩溃 有数据传输的场景 ①、客户端主机宕机又迅速重启 ②、客户端主机宕机一直没有重启 九、拔掉网线后原本的 TCP 连接还存在吗 ①、拔掉网线后有数据传输 ②、拔掉网线后没有数据传输 十、tcp_tw_reuse 为什么默认是关闭的 第一个问题 第二个问题 十一、HTTPS 中 TLS 和 TCP 能同时握手吗 TCP Fast Open TLSv1.3 TCP Fast Open 十二、TCP Keepalive 和 HTTP Keep-Alive 是一个东西吗 十三、TCP 协议的缺陷 13.1、升级 TCP 的工作很困难 13.2、TCP 建立连接的延迟 13.3、TCP 存在队头阻塞的问题 13.4、网络迁移需要重新建立 TCP 连接 十四、如何基于 UDP 协议实现可靠传输 14.1、QUIC 是如何实现可靠传输的 14.2、QUIC 是如何解决 TCP  队头阻塞问题的 十五、TCP 和 UDP 可以使用同一个端口吗 TCP 和 UDP 可以同时绑定相同的端口吗 多个 TCP 服务进程可以绑定同一个端口吗 客户端的端口可以重复使用吗 多个客户端可以 bind 同一个端口吗 十六、没有 listen 能建立 TCP 连接吗 那没有 listen 为什么还能建立连接呢 那么客户端会有半连接队列吗 十七、没有 accept 能建立 TCP 连接吗 为什么半连接队列要设计成哈希表呢 会不会有一个 cookies 队列呢 cookies 方案为什么不直接取代半连接队列 十八、用了 TCP 协议数据一定不会丢失吗 18.1、建立连接时丢包 18.2、流量控制丢包 18.3、网卡丢包 18.3、接收缓冲区丢包 18.4、两端之间的网络丢包 十九、TCP 四次挥手可以变成三次吗 为什么需要四次挥手呢 什么情况会出现三次挥手 什么是 TCP 延迟确认机制 二十、TCP 序列号和确认号是如何变化的 一、如何理解 TCP 是面向字节流协议 之所以会说 TCP 是面向字节流的协议UDP 是面向报文的协议是因为操作系统对 TCP 和 UDP 协议的发送方的机制不同也就是问题在发送方。 先来说说为什么 UDP 是面向报文的协议 当用户消息通过 UDP 协议传输时操作系统不会对消息进行拆分在组装好 UDP 头部后就交给网络层来处理所以发出去的 UDP 报文中的数据部分就是完整的用户消息也就是每个 UDP 报文就是一个用户消息的边界这样接收在接收到 UDP 报文后读一个 UDP 报文就能够读取到完整的用户消息。 如果收到了两个 UDP 报文操作系统是如何区分开的呢 操作系统在收到 UDP 报文后会将其插入到队列中队列中的每一个元素就是一个 UDP 报文这样用户调用 recvfrom() 系统调用读数据的时候就会从队列里取出一个数据然后从内核里拷贝给用户缓冲区。如图 再说说为什么 TCP 是面向字节流的协议 当用户消息通过 TCP 协议传输时消息可能会被操作系统分组成多个的 TCP 报文也就是一个完整的用户消息被拆分成多个 TCP 报文进行传输。 这时接收方的程序如果不知道发送方发送的消息的长度也就是不知道消息的边界时是无法读出一个有效的用户消息的因为用户消息被拆分成多个 TCP 报文后并不能像 UDP 那样一个 UDP 报文就能代表一个完整的用户消息。因此我们不能认为一个用户消息对应一个 TCP 报文正因为如此所以 TCP 是面向字节流的协议。 二、如何解决粘包问题 粘包的问题出现是因为不知道一个用户消息的边界在哪如果知道了边界在哪接收方就可以通过边界来划分出有效的用户消息。一般有三种方式分包的方法 固定的消息长度特殊字符作为边界自定义消息结构 ①、固定消息的长度 这种是最简单的方法即每个用户消息都是固定长度的比如规定一个消息的长度为 64 字节当接收方接满 64 个字节就认为这个内容是一个完整且有效的消息但是这个方法灵活性不高实际中很少使用 ②、特殊字符作为边界 在两个用户消息之间插入一个特殊的字符串这样接收方在接收数据时读到了这个特殊字符就把认为已经读完一个完整的消息。HTTP 就是一个很好的通过设置回车符、换行符作为 HTTP 报文协议的边界。有一点注意这个作为边界点的特殊字符如果刚好消息内容里有这个特殊字符我们要对这个字符转义避免接收方当作消息的边界点而解析到无效的数据。 ③、自定义消息结构 我们可以自定义一个消息结构由包头和数据组成其中包头包是固定大小的而且包头里有一个字段来说明紧随其后的数据有多大比如这个消息结构体首先 4 个字节大小的变量来表示数据的大小真正的数据则在后面。当数据方接收到包头的大小后就解析包头的内容于是就可以直到数据的长度然后接下来就是继续读取数据直到读满数据的长度就可以组装成一个完整的用户消息来处理了。 三、SYN 报文什么情况下会被丢弃 SYN 报文会被丢弃的两种场景 开启 tcp_tw_recycle 参数并在 NAT 环境下造成 SYN 报文被丢弃TCP 两个队列满了半连接队列和全连接队列造成 SYN 报文被丢弃 损人的 tcp_tw_recycle 对于服务器来说如果同时开启了 recycle 和 timestamps 选项则会开启一种称之为【per-host 的 PAWS 机制】。 什么是 PAWS 机制 tcp_timestamps 选项开启后PAWS 机制会自动开启它的作用是防止 TCP 包中的序列号发生绕回。正常来说每个 TCP 包都会有自己唯一的 SEQ 出现 TCP 数据包重传的时候回复用 SEQ 号这样接收方能够通过 SEQ 号来判断数据包的唯一性也能在重复收到某个数据包的时候判断数据是不是重传的。但是 TCP 这个 SEQ 号是有限的一共 32 bitSEQ 开始递增溢出之后从 0 开始再次依次递增。所以当 SEQ 号出现溢出后单纯通过 SEQ 号无法标识数据包的唯一性某个数据包延迟或因重发而延迟时可能导致连接传递的数据被破坏。 举个 上图 A 数据包出现了重传并在 SEQ 号耗尽再次从 A 递增时第一次发的 A 数据包延迟到达了 Server这种情况下如果没有别的机制来保证Server 会认为延迟到达的 A 数据包是正确的而接收反而是将正常的第三次发的 SEQ 为 A 的数据包丢弃造成数据传输错误。 PAWS 就是为了避免这个问题而产生的在开启 tcp_timestamps 选项情况下一台机器发的所有 TCP 包都会带上发送时的时间戳PAWS 要求连接双方维护最近一次收到的数据包的时间戳Recent TSval每收到一个新数据包都会读取数据包中的时间戳值跟 Recent TSval 值作比较如果发现收到的数据包中时间戳不是递增的则表示该数据包是过期的就会直接丢弃这个数据包。对于上图的情况PAWS 就能做到在收到 Delay 到达时的 A 号数据包时识别出它是一个过期的数据包而将其丢掉。 开启 net.ipv4.tcp_tw_recycle如果开启该选项的话允许处于 TIME_WAIT 状态的连接被快速回收前提是打开时间戳 那么什么是 per-host 的 PAWS 机制呢 前面提到开启了 recycle 和 timestamps 选项就会开启一种叫做 per-host 的 PAWS 机制per-host 是对【对端 IP 做 PAWS 检查】而非对【IP 端口】四元组做 PAWS 检查。 但是如果客户端网络环境是用了 NAT 网关那么客户端环境的每一台机器通过 NAT 网关后都会是相同的 IP 地址在服务端看来就好像是在跟一个客户端打交道一样无法区分出来。 Per-host PAWS 机制利用 TCP option 里的 timestamp 字段的增长来判断串扰数据而 timestamp 是根据客户端各自的 CPU tick 得出的值。 当客户端 A 通过 NAT 网关和服务器建立 TCP 连接然后服务器主动关闭并且快速回收 TIME_WAIT 状态的连接后客户端 B 也通过 NAT 网关和服务器建立 TCP 连接注意客户端 A 和客户端 B 因为经过相同的 NAT 网关所以是用相同的 IP 地址与服务端建立 TCP 连接如果客户端 B 的 timestamp 比客户端 A 的 timestamp 小那么由于服务端的 per-host 的 PAWS 机制的作用服务端就会丢弃客户端主机 B 发来的 SYN 包。 因此tcp_tw_recycle 在使用了 NAT 的网络下是存在问题的如果它是对 TCP 四元组做 PAWS 检查而不是对【相同的 IP 做 PAWS 检查】那么就不会存在这个问题了。 accept队列满了 在 TCP 三次握手的时候Linux 内核会维护两个队列分别是 半连接队列SYN队列全连接队列accept 队列 服务端收到客户端发起的 SYN 请求之后内核会把该连接存储到半连接队列并向客户端响应 SYN ACK 接着客户端会返回 ACK服务端在收到第三次握手的 ACK 后内核会把连接从半连接队列移除然后创建新的完全的连接并将其添加到 accept 队列等待进程调用 accept 函数时把连接取出来。 半连接队列满了如果开启了 syncookies 功能即使半连接队列满了也不会丢弃 syn 包。 syncookies 是这么做的服务器根据当前状态计算出一个值放在己方发出的 SYN ACK 报文中发出当客户端返回 ACK 报文时取出该值验证如果合法就认为连接建立成功。 全连接队列满了在服务端并发处理大量信息时 如果 TCP accept 队列过小或者应用程序调用 accept() 不及时就会造成 accept 队列满了这时后续的连接就会被丢弃这样就会出现服务端请求数量上不去的现象。 解决这个问题我们可以使用 调大 accept 队列的最大长度调大的方式是通过调大 backlog 以及 somaxconn 参数检查系统或者代码为什么不调用 accept() 不及时 四、已建立连接的 TCP收到 SYN 会发生什么 大概意思就是一个建立的 TCP 连接客户端如果中途宕机了而服务端此时也没有数据要发送一直处于 Established 状态客户端恢复后向服务端建立连接此时服务端会怎么处理 我们都知道 TCP 连接是由【四元组】唯一确认的在这个场景中客户端的 IP服务端 IP目的端口并没有变化所以这个问题关键要看客户端发送的 SYN 报文中的源端口是否和上一次连接的源端口相同。 ①、客户端的 SYN 报文里的端口号与历史连接不同 如果客户端恢复后发送的 SYN 报文中的源端口号跟上一次连接的源端口号不一致此时服务端会认为是一个新的连接要建立于是就会通过三次握手来建立新的连接。 那旧连接里处于 Established 状态的服务端最后会怎么样呢 如果服务端发送了数据包给客户端由于客户端的连接已经被关闭此时客户端的内核就会回 RST 报文服务端收到后就会释放连接。 如果服务端一直没有发送数据包给客户端在超过一段时间后TCP 的保活机制就会启动检测客户端没有存货后接着服务端就会释放掉该连接。 ②、客户端的 SYN 报文里的端口号与历史连接相同 如果客户端恢复后发送的 SYN 报文中的源端口号跟上一次连接的源端口号一样也就是处于Established 状态的服务端收到了这个 SYN 报文会怎么办呢如图 处于 Established 状态的服务端如果收到了客户端的 SYN 报文注意此时的 SYN 报文其实是乱序的因为 SYN 报文的初始化序列号其实是一个随机数会回复一个携带了正确序列号和确认号的 ACK 报文这个 ACK 被称之为 Challenge ACK。接着客户端收到这个 Challenge ACK发现确认号ack num并不是自己期望收到的于是就会回 RST 报文服务端收到后就会释放该连接。 五、如何关闭一个 TCP 连接 可能大家第一反应就是【杀掉进程】不就行了吗 这个是最粗暴的方式杀掉客户端进程和服务端进程影响的范围会有所不同 在客户端杀掉进程的话就会发送 FIN 报文来断开这个客户端进程与服务端建立的所有 TCP 连接这种方式影响范围只有这个客户端进程所建立的连接而其他客户端或进程不会受影响。而在服务端杀掉进程影响就大了此时所有的 TCP 连接都会被关闭服务端无法继续提供访问服务。 所以关闭进程的方式不可取最好的方式要精细到关闭某一条 TCP 连接。 有个同学可能就会说伪造一个四元组相同的 RST 报文不就好了嘛 这个思路很好但是不要忘了还有个序列号的问题伪造的 RST 报文的序列号一定能被对方接受嘛如果 RST 报文的序列号不是对方期望收到的序列号这个 RST 报文会被对方丢弃的就达不到关闭的连接的效果。所以要伪造一个能关闭 TCP 连接的 RST 报文必须同时满足【四元组相同】和【序列号是对方期望的】这两个条件。直接伪造符合预期的序列号是比较困难的因为如果一个正在传输数据的 TCP 连接序列号都是时刻都在变化很难伪造一个正确序列号的 RST 报文 killcx 的工具 我们可以伪造一个四元组相同的 SYN 报文来拿到“合法”的序列号。 在最开始的时候如果处于 Established 状态的服务端收到四元组相同的 SYN 报文后会回复一个 Challenge ACK这个 ACK 报文里的【确认号】正是服务端下次想要接收的序列号就是通过这个拿到服务端下一次预期接收的序列号。然后用这个确认号作为 RST 报文的序列号发送给客户端此时服务端会认为这个 RST 报文里的序列号是合法的于是就会释放连接。 在 Linux 上有个叫 killcx 的工具就是基于上面这样的方式实现的他会主动发送 SYN 包获取 SEQ/ACK 号然后利用 SEQ/ACK 号伪造两个 RST 报文分别发送给客户端和服务端这样双方的 TCP 连接都会被释放这种方式活跃和非活跃的 TCP 连接都可以杀掉。 killcx 的工具使用方式也很简单如果在服务端执行 killcx 工具只需指明客户端的 IP 和端口号如果在客户端执行 killcx 工具则就指明服务端的 IP 和端口号。 它伪造客户端发送 SYN 报文服务端收到后就会回复一个携带了正确【序列号和确认号】的 ACK 报文Challenge ACK然后就可以利用这个 ACK 报文里的消息伪造两个 RST 报文 用 Challenge ACK 里的确认号伪造 RST 报文发送给服务端服务端收到 RST 报文后就会释放连接用 Challenge ACK 里的确认号伪造 RST 报文发送给客户端客户端收到 RST 也会释放连接 tcpkill 的工具 除了 killcx 工具能关闭 TCP 连接还有 tcpkill 工具也可以做到 这两个工具都是通过伪造 RST 报文来关闭指定的 TCP 连接但是它们拿到正确的序列号的实现方式是不同的。 tcpkill 工具是在双方进行 TCP 通信时拿到对方下一次期望收到的序列号然后将序列号填充到伪造的 RST 报文并将其发送给对方达到关闭 TCP 连接的效果killcx 工具是主动发送一个 SYN 报文对方收到后会回复一个携带了正确序列号和确认号的 ACK 报文这个 ACK 被称之为 Challenge ACK这时就可以拿到对方下一次期望收到的序列号然后将序列号填充到伪造的 RST 报文并将其发送给对方达到关闭的 TCP 连接的效果。 二者在获取对方下一次期望收到的序列号的方式是不同的。 tcpkill 工具属于被动连接就是在双方进行 tcp 通信的时候才能获取到正确的序列号很显然这种方式无法关闭非活跃的 TCP 连接只能用于关闭活跃连接。因为如果这条 TCP 连接一直没有任何数据传输则就永远获取不到正确的序列号。 killcx 工具则是主动获取它是一个主动发送一个 SYN 报文通过双方回复的 Challenge ACK 来获取正确的序列号所以这种方式无论 TCP 连接是否活跃都可以关闭。 六、四次挥手中收到乱序的 FIN 包会如何处理 问题再现假如服务端在二三次挥手之间发的数据或者是四次挥手之间前的数据包因为网络阻塞导致第三次挥手的 FIN 包比数据包先到主动关闭方那么主动关闭方收到 FIN 就会进入 timewait 状态这个时候那个延迟的数据包到了还能正常接收并处理嘛 这个表述其实有点问题因为如果 FIN 报文比数据包先抵达客户端此时 FIN 报文其实是一个乱序的报文此时客户端的 TCP 连接并不会从 FIN_WAIT_2 转移到 TIME_WAIT 状态。 因此我们需要关注【在 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  如果出于 TIME_WAIT 状态的连接收到【合法的 SYN】后就会重用此四元组的连接跳过 2MSL 而转变为 SYN_RECV 状态接着就能进行建立连接过程。双方都开启了时间戳TSval 是发送报文时的时间戳 上述过程处于 TIME_WAIT 状态的连接收到 SYN 后因为 SYN 的 seq400大于 rcv_nxt(301)并且 SYN 的TSval30大于 ts_recent(21)所以是一个【合法的 SYN】于是就会重用此四元组连接跳过 2MSL 而转变为 SYN_RECV 状态接着就能进行建立连接过程。 ②、收到非法的 SYN 如果处于 TIME_WAIT 状态的连接收到【非法的 SYN】后就会再回复一个第四次挥手的 ACK 报文客户端收到后发现并不是自己期望收到确认号ack num就会回 RST 报文给服务端 处于 TIME_WAIT 状态的连接收到 SYN 后因为 SYN 的 seq(200) 小于 rcv_nxt(301)所以是一个【非法的SYN】就会再回复一个与第四次挥手一样的 ACK 报文客户端收到后发现并不是自己期望收到确认号就回 RST 报文给服务端 这里有个疑问在 TIME_WAIT 状态收到 RST 会断开连接吗 会不会断开主要看 net.ipv4.tcp_rfc1337 这个内核参数默认情况下是 0 如果这个参数设置为 0 收到 RST 报文会提前结束 TIME_WAIT 状态释放连接如果这个参数设置为 1就会丢掉 RST 报文 八、TCP 连接一端断电和进程崩溃有什么区别 这里有几个关键词 没有开启 keepalive一直没有数据交互进程崩溃主机崩溃 无数据传输的场景 ①、主机崩溃情况 如果没有开启 TCP keepalive 且双方一直没有数据交互的情况下如果客户端的【主机崩溃】了会发生什么 客户端主机崩溃了服务端是无法感知到的在加上服务端没有开启 TCP keepalive又没有数据交互的情况下服务端的 TCP 连接将会一直处于 ESTABLISHED 连接状态直到服务端重启进程。所以我们得知一个点在没有使用 TCP 保活机制且双方不传输数据的情况下一方的 TCP 连接处在 ESTABLISHED 状态并不代表另一方的连接还一定正常。 ②、进程崩溃 TCP 的连接信息是由内核维护的所以当服务端的进程崩溃后内核需要回收该进程的所有 TCP 连接资源于是内核就会发送第一次挥手 FIN 报文后续的挥手过程也都是在内核中完成并不需要进程的参与所以即使服务端的进程退出了还是能与客户端完成 TCP 四次挥手的过程。所以即使没有开启 TCP keepalive 且双方也没有数据交互的情况下如果其中一方的进程发生了崩溃这个过程操作系统是可以感知到的于是就会发送 FIN 报文给对方然后与对方进行 TCP 四次挥手。 有数据传输的场景 ①、客户端主机宕机又迅速重启 在客户端主机宕机后服务端向客户端发送的报文会得不到任何的相应在一定时长后服务端就会触发超时重传机制重传未得到响应的报文。 服务端重传报文的过程中客户端主机重启完成后客户端的内核就会接受重传的报文然后根据报文的信息传递给对应的进程 如果客户端主机上没有进程绑定该 TCP 报文的目标端口号那么客户端内核就会回复 RST 报文重置该 TCP 连接如果客户端主机上有进程绑定该 TCP 报文的目标端口号由于客户端主机重启后之前的 TCP 连接的数据结构已经丢失客户端内核里协议栈会发现找不到该 TCP 连接的 socket 结构体于是就会回复 RST 报文重置该 TCP 连接 ②、客户端主机宕机一直没有重启 这种情况服务端超时重传报文的次数达到一定阈值后内核就会判定出该 TCP 有问题然后通过 Socket 接口告诉应用程序该 TCP 连接出问题了于是服务端的 TCP 连接就会断开 九、拔掉网线后原本的 TCP 连接还存在吗 拔掉网线几秒再插回去原本的 TCP 连接还存在吗 实际上TCP 连接在Linux 内核中是一个名为 struct socket 的结构体该结构体的内容包含 TCP 连接的状态等信息。当拔掉网线的时候操作系统并不会变更该结构的任何内容所以 TCP 连接的状态也不会发生改变。 接下来我们分场景讨论 拔掉网线后有数据传输拔掉网线后没有数据传输 ①、拔掉网线后有数据传输 在客户端拔掉网线后服务端向客户端发送的数据报文会得不到任何的响应在等待一段时候后服务端就会触发超时重传机制重传未得到响应的数据报文。 如果在服务端重传报文的过程中客户端刚好把网线插回去了由于拔掉网线并不会改变客户端的 TCP 连接状态并且还是处于 ESTABLISHED 状态所以这时客户端是可以正常接收服务端发来的数据报文的然后客户端就会回 ACK 响应报文。 此时客户端和服务端的 TCP 连接依然存在就感觉什么事情都没有发生。 但是如果在服务端重传报文后客户端也一直没有将网线插回去服务端超时重传报文的次数达到一定阈值后内核就会判定出该 TCP 有问题然后通过 Socket 接口告诉应用程序该 TCP 连接出问题了于是服务端的 TCP 连接就会断开。而等客户端插回网线后如果客户端向服务端发送了数据由于服务端已经没有与客户端相同四元组的 TCP 连接了因此服务端内核就会回复 RST 报文客户端收到后就会释放该 TCP 连接。 ②、拔掉网线后没有数据传输 针对这种情况还得看是否开启了 TCP keepalive 机制 如果没有开启 TCP keepalive 机制在客户端拔掉网线后并且双方都没有进行数据传输那么客户端和服务端的 TCP 连接将会一直保持存在 而如果开启了在客户端拔掉网线后即使双方都没有进行数据传输在持续一段时间后TCP 就会发送探测报文 如果对端是正常工作的。当 TCP 保活的探测报文发送给对端对端会正常响应这样 TCP 保活时间会被重置TCP 就会发送探测报文 如果对端是正常工作的。当 TCP 保活的探测报文发送给对端对端会正常响应这样 TCP 的保活机制会被重置等待下一个 TCP 保活时间的到来 如果对端主机宕机注意不是进程崩溃进程崩溃后操作系统在回收进程资源的时候会发送 FIN 报文而主机宕机则是无法感知的所以需要 TCP 保活机制来探测对方是不是发生了主机宕机或对端由于其他原因导致报文不可达。当 TCP 保活的探测报文发送给对端后没有响应连续几次后达到保活探测次数后TCP 会报告该 TCP 连接已死亡 十、tcp_tw_reuse 为什么默认是关闭的 其这题就是变相在问【如果 TIME_WAIT 状态持续时间过短或者没有会有什么问题】 可能有同学会说使用 tcp_tw_reuse 快速复用处于 TIME_WAIT 状态的 TCP 连接时是需要保证 net.ipv4.tcp_timestamps 参数是开启的默认就是开启而 tcp_timestamps 参数可以避免旧连接延迟报文这不是解决了没有 TIME_WAIT 状态时的问题了吗解决了一点但是没完全解决。 第一个问题 我们知道开启 tcp_tw_reuse 的同时也需要开启 tcp_timestamps意味着可以用时间戳的方式有效的判断回绕序列号的历史报文。 但是当查看源码后发现对于 RST 报文的时间戳即使过期了只要 RST 报文的序列号在对方的接收窗口内也是能被接受的。当判断的函数返回 true 就代表报文是一个历史报文于是就要丢掉这个报文。但是在丢掉这个报文时会先判断是不是 RST 报文如果不是 RST 报文才会将报文丢弃也就是说即使 RST 报文是一个历史报文并不会被丢弃。如下图 过程如下 客户端向一个还没有被服务端监听的端口发起了 HTTP 请求接着服务端就会回 RST 报文给对方很可惜的是 RST 报文被网络阻塞了。由于客户端迟迟没有收到 TCP 第二次握手于是重发了 SYN 包与此同时服务端已经开启了服务监听了对应的窗口。于是接下来客户端和服务端就进行了 TCP 三次握手数据传输四次挥手。因为客户端开启了 tcp_tw_reuse 于是快速复用 TIME_WAIT 状态的端口又与服务端建立了一个与刚才相同的四元组的连接接着前面被网络延迟 RST 报文这时抵达了客户端而且 RST 报文的序列号在客户端的接收窗口内由于防绕回序列号算法不会防止过期的 RST 所以 RST 报文就被客户端所接受了于是客户端的连接就断开了。 上述的场景就是开启了 tcp_tw_reuse 风险因为快速复用 TIME_WAIT 状态的端口导致新连接可能会被回绕序列号的 RST 报文断开了而如果不跳过 TIME_WAIT 状态而是停留 2MSL 时长那么这个 RST 报文就不会出现下一个新的连接。 第二个问题 开启 tcp_tw_reuse 来快速复用 TIME_WAIT 状态的连接如果第四次挥手的 ACK 报文丢失了服务端就会触发超时重传重传第三次挥手报文处于 syn_sent 状态的客户端收到服务端重传第三次挥手报文则会回 RST 给服务端。如下图 这时候有同学就会问如果 TIME_WAIT 状态被快速复用之后刚好第四次挥手的 ACK 报文丢失了那客户端复用 TIME_WAIT 状态后发送的 SYN 报文被处于 last_ack 状态的服务端收到了会发生什么呢 处于 last_ack 状态的服务端收到了 SYN 报文后会回复确认号与服务端上一次发送 ACK 报文一样的 ACK 报文这个 ACK 报文被称为 Challenge ACK并不是确认收到 SYN 报文 处于 last_sent 状态的客户端收到服务端的 Challenged ACK 后发现不是自己期望收到的确认号于是就会回复 RST 报文服务端收到后就会断开连接。 总的来说就是会导致被动连接的一方不能被正常关闭 十一、HTTPS 中 TLS 和 TCP 能同时握手吗 一般情况下不管是 TLS 握手次数如何都得先经过 TCP 三次握手后才能进行因为 HTTPS 是基于 TCP 传输协议实现的得先建立完可靠的 TCP 连接才能做 TLS 握手的事情。 在 HTTPS 中的 TLS 握手过程可以同时进行三次握手对不对呢 这个场景可能发生但是需要在特殊的条件下才能生效如果说没有任何前提条件说这句话就是耍流氓。需要同时满足的条件如下 客户端和服务端都开启了 TCP Fast Open 功能且 TLS 版本是 1.3客户端和服务端已经完成过一次通信 TCP Fast Open 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 所以第一次客户端和服务端通信的时候还是需要正常的三次握手流程。随后客户端有了 Cookie 这个东西它可以用来向服务器 TCP 证明先前与客户端 IP 地址的三向握手已成功完成。 对于客户端与服务端的后续通信客户端可以在第一次握手的时候携带应用数据从而达到绕过三次握手发送数据的效果过程如下 具体过程如下 客户端发送 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 的正常情况一致 所以如果客户端和服务端同时支持 TCP Fast Open 功能那么在完成首次通信后后续客户端与服务端的通信则可以绕过三次握手发送数据这就减少了握手带来的 1 个 RTT 的时间消耗 TLSv1.3 TCP Fast Open 在前面我们知道客户端与服务端同时支持 TCP Fast Open 功能的情况下在第二次以后到的通信过程中客户端可以绕过三次握手直接发送数据而且服务端也不需要等收到第三次握手后才发送数据。如果 HTTPS 的 TLS 版本是 1.3那么 TLS 过程只需要 1-RTT。因此如果【TCP Fast Open TLSv1.3】情况下在第二次以后的通信过程中TLS 和 TCP 的握手过程是可以同时进行的如果基于 TCP Fast Open 场景下的 TLSv 1.3 0-RTT 会话恢复过程不仅 TLS 和 TCP 的握手过程是可以同时进行的而且 HTTP 请求也可以在这期间内一同完成。 十二、TCP Keepalive 和 HTTP Keep-Alive 是一个东西吗 这两个完全是两样不同的东西实现的层面也不同 HTTP 的 Keep-Alive 是由应用层用户态实现的称为 HTTP 长连接TCP 的 Keepalive 是由 TCP 层内核态实现的称为 TCP 保活机制 HTTP 协议采用的【请求-应答】的模式也就是客户端发起了请求服务端这边才会响应一来一回的样子在进行 HTTP 请求时进行 TCP 的三次握手连接请求完毕时需要进行四次挥手一次连接只能请求一个资源效率太慢。所以引入了 HTTP 长连接通过一个 TCP 连接实现多次响应并且只要任意一端没有明确提出断开连接则保持 TCP 的连接状态。从 HTTP1.1 开始就是默认开启的了并且给 HTTP 流水线技术提供了可实现的基础一次性发送多个请求服务器还是按照顺序响应。web 服务软件会提供一个参数来设置 HTTP 长连接的超时时间。 TCP Keepalive 其实就是 TCP 的保活机制 如果两端的 TCP 连接一直没有数据交互达到了触发 TCP 保活机制的条件那么内核里的 TCP 协议栈就会发送探测报文 如果对端程序是正常工作的当 TCP 保活的探测报文发送给对端对端也会正常响应这样 TCP 保活机制会被重置等待下一次到来以下省略............................. 十三、TCP 协议的缺陷 13.1、升级 TCP 的工作很困难 由于 TCP 协议是在内核中实现的应用程序只能使用不能修改如果想要升级 TCP 协议那么只能升级内核。 而升级内核这个工作是很麻烦的麻烦的事情不是说升级内核这个操作是很麻烦而是由于内核升级涉及到底层软件和运行库的更新我们的服务程序就需要回归测试是否兼容新的内核版本所以服务器的内核升级也比较保守和缓慢。 很多 TCP 协议的新特性都是需要客户端和服务端同时支持才能生效比如 TCP Fast Open 这个特性虽然在 2013 年就被提出了但是 Windows 很多系统版本依然不支持这是因为 PC 端的系统升级后之后很严重Windows XP 现在还在有很多用户在使用尽管它已经存在很久了。 13.2、TCP 建立连接的延迟 TCP 三次握手的延迟被 TCP Fast Open快速打开这个特性解决了这个特性可以在【第二次建立连接】时减少 TCP 连接建立的时延 过程如下 在第一次建立连接的时候服务端在第二次握手产生一个 Cookie 已加密并通过 SYN、ACK 包一起发送给客户端于是客户端就会缓存这个 Cookie 所以第一次发起 HTTP Get 请求的时候还是需要 2 个 RTT的时延在下次请求的时候客户端在 SYN 包带上 Cookie 发给服务端就提前可以跳过三次握手的过程因为 Cookie 中维护了一些信息服务端可以从 Cookie 获取 TCP 相关的信息这时发起的 HTTP GET 请求就只需要一个 RTT 时延 针对 HTTPS 来说TLS 是在应用层实现的握手而 TCP 是在内核实现的握手这两个握手过程是无法结合到一起去的总得先完成 TCP 握手才能进行 TLS 握手。也正是 TCP 是在内核实现的所以 TLS 是无法对 TCP 头部加密的这意味着 TCP 的序列号都是明文传输存在安全问题 13.3、TCP 存在队头阻塞的问题 TCP 是字节流协议TCP 层必须保证收到的字节数据是完整且有序的如果序列号较低的 TCP 段在网络传输中丢失了即使序列号较高的 TCP 段已经被接收了应用层也无法从内核中读取到这部分数据如下图 图中发送方发送了很多 packet 每个 packet 都有自己的序号你可以认为是 TCP 的序列号其中 packet #3 在网络中丢失了即使 packet #4-6 被接受方收到后由于内核中的 TCP 数据不是连续的于是接收方的应用层就无法从内核中读取到只有等到 Packet #3 重传后接收方的应用层才可以从内核中读取到数据。 这就是 TCP 队头阻塞问题但这也不能怪它因为只有这样才能保证数据的有序性。例如 HTTP2中多个请求是跑在一个 TCP 连接中的那么当 TCP 丢包时整个 TCP 都要等待重传那么就会阻塞该 TCP 连接中的所有请求所以 HTTP2 队头阻塞问题就是因为 TCP 协议导致的 13.4、网络迁移需要重新建立 TCP 连接 基于 TCP 传输协议的 HTTP 协议由于是通过四元组确定一条 TCP 连接那么当移动设备的网络从 4G 切换到 WIFI 时意味着 IP 地址变化了那么就必须要断开连接然后重新建立 TCP 连接 十四、如何基于 UDP 协议实现可靠传输 很多同学可能第一反应就会说把 TCP 可靠传输的特性在应用层实现一遍想一想为什么 TCP 天然支持可靠传输为什么还需要基于 UDP 实现可靠传输呢这不是重复造轮子吗所以我们就应该先知道 TCP 协议的痛点而这些痛点是否能基于 UDP 协议实现的可靠传输协议中得到改进 TCP 协议的四个缺陷 升级 TCP 的工作很困难TCP 建立连接的延迟TCP 存在队头阻塞问题网络迁移需要重新建立 TCP 连接 14.1、QUIC 是如何实现可靠传输的 要基于 UDP 实现的可靠传输协议那么就要在应用层下功夫也就是设计好协议的头部字段 拿 HTTP/3 举例在 UDP 头部与 HTTP 消息之间共有三层头部 总体上看是这样的 Packet Header Packet Header 首次建立连接时和日常传输数据时使用的 Header 是不同的下面只画出了重要字段 Packet Header 分为两种 Long Packet Header 用于首次建立连接Short Packet Header 用于日常传输数据 QUIC 也是需要三次握手来建立连接的主要目的是为了协商连接 ID协商出连接 ID 之后后续传输时双方只需要固定住连接 ID从而实现连接迁移功能。所以你可以看到日常传输数据的 Short Packet Header 不需要传输 Source Connection ID 字段了只需要传输 Destination Connection ID。Short Packet Header 中的 Packet Number 是每个报文独一无二的编号它是严格递增的也就是说就算 Packet N 丢失了重传的 Packet N 的 Packet Number 已经不是 N而是一个比 N 大的值。 为什么要这样设计呢 我们先来看看 TCP 的问题TCP 在重传报文时的序列号和原始报文的序列号是一样的也正是这个特性引入了 TCP 重传的歧义问题。 如上图当 TCP 发生超时重传后客户端发起重传然后接收到了服务端确认的 ACK由于客户端原始报文和重传报文序列号都是一致的那么服务端针对这两个报文回复的都是相同的 ACK。这样的话客户端就无法判断出是【原始报文的响应】还是【重传报文的响应】这样在计算 RTT往返时间时应该选择从发送原始报文开始计算还是重传原始报文开始计算呢 如果算成原始请求的响应但实际上是重传的响应上图左会导致采样 RTT 变大如果算成重传请求的响应但实际上是原始请求的响应上图右又很容易导致采样 RTT 过小 RTO超时时间是基于 RTT 来计算的那么如果 RTT 计算不精确那么 RTO超时时间也会不精确这样可能导致重传的概率事件增大。 QUIC 报文中的 Packet Number 是严格递增的即使是重传报文它的 Packet Number 也是递增的这样就能更加精确计算出报文的 RTT。 如果 ACK 的 Packet Number 是 N M就根据重传报文计算采样 RTT。如果 ACK 的 Packet Number 是 N 就根据原始报文的时间计算采样 RTT没有歧义性的问题。 另外还有一个好处QUIC 使用的 Packet Number 单调递增的设计可以让数据包不再像 TCP 那样必须有序确认QUIC 支持乱序确认当数据包 Packet N 丢失后只要有新的已接受数据包确认当前窗口就会继续向右滑动。 待发送段获知数据包 Packet N 丢失后会将需要重传的数据包放到待发送队列重新编号比如数据包 Packet N M 后重新发送给接收端对重传数据包的处理跟新发送的数据包类似这样就不会因为丢包重传将当前窗口阻塞在原地从而解决了队头阻塞问题。 所以 Packet Number 单调递增的两个好处 可以更加精确计算 RTT没有 TCP 重传的歧义性问题可以支持乱序确认因为丢包重传将当前窗口阻塞在原地而 TCP 必须是顺序确认的丢包时会导致窗口不滑动 QUIC Frame Header 一个 Packet 报文中可以存放多个 QUIC Frame。如图 每一个 Frame 都有明确的类型针对类型的不同功能也不同自然格式也不同。 这里举例 Stream 类型的 Frame 格式Stream 可以认为就是一条 HTTP 请求长这样 Stream ID 作用多个并发传输的 HTTP 消息通过不同的 Stream ID 加以区别类似于 HTTP2的 Stream IDOffset 作用类似于 TCP 协议中的 Seq 序号保证数据的顺序性和可靠性Length 作用指明了 Frame 数据的长度 在前面介绍 Packet Header 时说到 Packet Number 是严格递增即使重传报文的 Packet Number 也是递增的既然重传数据包的 Packet NM 与丢失数据包的 Packet N 编号并不一致怎么确认这两个数据包的内容一样呢 所以引入了 Frame Header 这一层通过 Stream ID Offset 字段信息实现数据的有序性通过比较数据包的 Stream ID 与 Stream Offset如果那都是一致就说明这两个数据包的内容一致。 举个下图中数据包 Packet N 丢失了后面重传该数据包的编号为 Packet N 2丢失的数据包和重传的数据包 Stream ID 与 Offset 都一致说明这两个数据包的内容一致。这些数据包传输到接收端后接收端能根据 Stream ID 与 Offset 字段信息将 Stream x 和 Stream x y 按照顺序组织起来然后交给应用程序处理。 总的来说QUIC 通过单向递增的 Packet Number配合 Stream ID 与 Offset 字段信息可以支持乱序确认而不影响数据包的正确组装摆脱了 TCP 必须按照顺序确认应答 ACK 的限制解决了 TCP 因某个数据包重传而阻塞后续所有待发送数据包的问题。 14.2、QUIC 是如何解决 TCP  队头阻塞问题的 什么是 TCP 队头阻塞问题 TCP 队头阻塞的问题其实就是接收窗口的队头阻塞问题。 接收方收到的数据范围必须在接收窗口范围呢如果收到超过接收窗口范围的数据就会丢弃该数据比如下图接收窗口的范围是 32~51 字节如果收到第 52 字节以上的数据都会被丢弃。 接收窗口什么时候才能滑动当接收窗口收到有序数据时接收窗口才能往前滑动然后那些已经接收并且被确认的【有序】数据就可以被应用层读取。但是当接收窗口收到的数据不是有序的比如收到第 33~40 字节的数据这些数据也无法被应用层读取。只有当发送方重传了第 32 字节数据并且被接受方收到后接收窗口才会往前滑动然后应用层才能从内核读取第 32~40 字节的数据。导致接收窗口的队头阻塞问题是因为 TCP 必须按需处理数据也就是 TCP 层为了保证数据的有序性只有在处理完有序数据后滑动窗口才能往前滑动否则就停留停留【接收窗口】会使得应用层无法读取新的数据。其实也不能怪 TCP 它本来设计的就是为了保证数据的有序性。 HTTP/2 的队头阻塞 HTTP/2 抽象出了 Stream 的概念实现了 HTTP 并发传输一个 Stream 就代表 HTTP/1.1 里的请求和响应。 在 HTTP/2 的连接上不同的 Stream 帧是可以乱序发送的因此可以并发不同的 Stream因为每个帧的头部会携带 Stream ID 信息所以接收端可以通过 Stream ID 有序组装成 HTTP 消息而同一 Stream 内部的帧必须是严格有序的。 但是 HTTP/2  多个 Stream 都是在一条 TCP 链接上的传输这意味着多个 Stream 共用同一个 TCP 滑动窗口那么当发生数据丢失滑动窗口是无法往前移动的此时就会阻塞住所有的 HTTP 请求这属于 TCP 层队头阻塞。 没有队头阻塞的 QUIC  QUIC 也借鉴 HTTP/2 里面的 Stream 的概念在一条 QUIC 连接上可以并发发送多个 HTTP 请求Stream。但是 QUIC 给每一个 Stream 都分配了一个独立的滑动窗口这样使得一个连接上的多个 Stream 之间没有依赖关系否是相互独立的各自控制的滑动窗口。 QUIC 是如何做流量控制的 TCP 流量控制是通过让【接收方】告诉【发送方】它接收方的接收窗口有多大从而让【发送方】根据【接收方】的实际接收能力控制发送的数据量 QUIC 实现流量控制的方式 通过 window_update 帧告诉对端自己可以接收的字节数这样接收方就不会发送超过这个数量的数据通过 BlockFrame 告诉对端由于流量控制被阻塞了无法发送数据 前面说到TCP 的接收窗口在收到有序的数据后接收窗口才能往前滑动否则停止滑动。 QUIC 是基于 UDP 传输的而 UDP 没有流量控制因此 QUIC 实现了自己的流量控制机制QUIC 的滑动窗口滑动的条件跟 TCP 有一点区别但是同一个 Stream 也是要保证顺序的不然无法实现可靠的传输因此同一个 Stream 的数据包丢失了也会造成窗口无法滑动。 QUIC 的每个 Stream 都有各自的滑动窗口不同 Stream 互相独立队头的 Stream A 被阻塞后不妨碍 Stream BC 的读取。而对于 HTTP/2 而言所有的 Stream 都跑在一条 TCP 连接中而这些 Stream 共享一个滑动窗口因此对于同一个 Connection 内Stream A 被阻塞后Stream B、C 必须等待。QUIC 实现了两种级别的流量控制分别是 Stream 和 Connection 两种级别 Stream 级别的流量控制Stream 可以看作是一条 HTTP 请求每个 Stream 都有独立的滑动窗口所以每个 Stream 都可以做流量控制防止单个 Stream 消耗连接Connection的全部接收缓冲Connection 流量控制限制连接中所有 Stream 相加起来的总字节数防止发送方超过连接的缓冲容量 QUIC 对拥塞控制改进 TCP 拥塞控制算法迭代速度是很慢的而 QUIC 可以随浏览器更新QUIC 的拥塞控制算法就可以有较快的迭代速度由于 QUIC 在应用层所以就可以针对不同的应用设置不同的拥塞控制算法 QUIC 更快的连接建立 对于 HTTP/1和2TCP 和 TLS 是分层的分别属于内核实现的传输层分别属于内核实现的传输层和表示层因此它们很难合并在一起需要分批次来握手先 TCP 握手1RTT再 TLS 握手2 RTT所以需要 3RTT 的延迟才能传输数据就算 Session 会话复用也需要至少 2 个 RTT。 HTTP/3 在传输数据前虽然需要 QUIC 协议握手这个握手过程只需要 1 RTT握手的目的是为确认双方的【连接 ID】连接迁移就是基于连接 ID 实现的。 但是 HTTP/3 的 QUIC 协议并不是与 TLS 分层的而是 QUIC 内部包含了 TLS 它在自己的帧会携带 TLS 里的记录再加上 QUIC 使用的是 TLS 1.3因此仅需一个 RTT 就可以同时完成建立连接与密钥协商甚至在第二次连接的时候应用数据包可以和 QUIC 握手信息连接信息 TLS信息一起发送达到 0-RTT 的效果。 QUIC 是如何进行迁移连接的 QUIC 协议没有用四元组的方式来绑定连接而是通过连接 ID 来标记通信的两个端点客户端与服务器可以各自选择一组 ID 来标记自己因此即使移动设备的网络变化后导致 IP 地址变化了只要仍保有上下文信息如连接 ID、TLS 密钥等就可以无缝复用连接消除重连的成本没有丝毫卡顿达到了连接迁移的功能 十五、TCP 和 UDP 可以使用同一个端口吗 这里牵扯到几个问题 多个 TCP 服务进程可以同时绑定同一个端口吗重启 TCP 服务进程时为什么会出现“Address in use”的报错信息又该如何避免客户端的端口可以重复使用吗客户端 TCP 连接 TIME_WAIT 状态太多会导致端口资源耗尽而无法建立新的连接吗 TCP 和 UDP 可以同时绑定相同的端口吗 可以的在数据链路层中通过 MAC 地址来寻找局域网中的的主机。在网际层中通过 IP 地址来寻找网络中互联的主机或路由器。在传输层中需要通过端口进行寻址来识别同一计算机中同时通信的不同应用程序。所以传输层中的【端口号】的作用是为了区分同一个主机上不同应用程序的数据包。传输层有两个传输协议可以在 IP 包头的【协议号】字段知道该数据包是 TCP/UDP 所以可以根据这个信息确定送给哪个模块TCP/UDP处理送给 TCP/UDP 模块的报文根据【端口号】确定送给哪个应用程序处理。因此TCP/UDP 各自的端口号也相互独立如 TCP 有一个 80 端口UDP 也有一个 80 端口二者并不冲突。 多个 TCP 服务进程可以绑定同一个端口吗 在默认情况下针对【多个 TCP 服务进程可以绑定同一个端口吗】这个问题的答案是如果两个 TCP 服务进程同时绑定的 IP 地址和端口号都相同那么执行 bind() 时候就会出错错误是“Address already in use”。 注意如果 TCP 服务进程 A 绑定的地址是 0.0.0.0 和端口 8888 而如果 TCP 服务进程 B 绑定的地址是 192.168.1.100 地址和端口 8888那么执行 bind() 的时候也会出错。这是因为 0.0.0.0 地址比较特殊代表任意地址意味着绑定了 0.0.0.0 地址相当于把主机上的所有 IP 地址都绑定了。 Tip如果想多个进程绑定相同的 IP 地址和端口也是有办法的就是对 socket 设置 SO_REUSEPORT 属性 重启 TCP 服务进程时为什么会有 “Address in use” 的报错信息 TCP 服务进程需要绑定一个 IP 地址和一个端口然后就监听在这个地址和端口上等待客户端连接的到来。然后在实际中我们会经常碰到一个问题当 TCP 服务重启之后总会遇到“Address in use”的报错信息TCP 服务进程不能很快重启而是要过一会才能重启成功。 为什么呢 当我们重启 TCP 服务进程的时候意味着通过服务器端发起了关闭连接操作于是就会经过四次挥手而对于主动关闭方会在 TIME_WAIT 这个状态里停留一段时间这个时间大约为 2MSL 当TCP 服务进程重启时服务端会出现 TIME_WAIT 状态的连接TIME_WAIT 状态的连接使用的 IP PORT 仍然被认为是一个有效的 IP PORT 组合相同机器上不能够被在该 IP PORT 组合上进行绑定那么执行 bind() 函数时就会返回 Address already in use 的错误 那要如何避免“Address in use” 的报错信息 我们可以在调用 bind 前对 socket 设置 SO_REUSEADDR 属性可以解决这个问题。 int on 1; setsocket(listenfd, SOL_SOCKET, SO_REUSEADDR, on, sizeof(on)); 因为 SO_REUSEADDR 作用是如果当前启动进程绑定的 IP PORT 与处于 TIME_WAIT 状态的连接占用的 IP PORT 存在冲突但是新启动的进程使用了 SO_REUSEADDR 选项那么该进程就可以绑定成功。 前面也提到过这个问题如果 TCP 服务进程 A 绑定的地址是 0.0.0.0 和端口 8888而如果 TCP 服务进程 B 绑定的地址是 192.167.1.100 地址和端口 8888 那么执行 bind() 时候也会出错。这个时候也可以由 SO_REUSEADDR 解决因为它的另外一个作用绑定的 IP 地址 端口时只要 IP 地址不是正好相同那么允许绑定。 客户端的端口可以重复使用吗 TCP 连接是由四元组唯一确认的那么只要四元组中其中一个元素发生了变化那么就代表不同的 TCP 连接所以如果客户端已使用端口与另一服务器建立了连接那么客户端与服务端建立连接还是可以使用已经被使用过的端口因为内核是通过四元组信息来定位一个 TCP 连接的并不会因为客户端的端口号相同而导致连接冲突的问题。 多个客户端可以 bind 同一个端口吗 要看多个客户端绑定的 IP PORT 是否都相同了如果都相同那么在执行 bind() 的时候就会出错错误是 “Address already in use”一般而言客户端不建议用 bind 函数应该交由 connect 函数来选择端口会比较好因为客户端的端口通常没什么意义 客户端的 TCP 连接 TIME_WAIT 状态过多会导致端口资源消耗尽而无法建立新的连接吗 针对这个问题要看客户端是否都是与同一台服务器目标地址和目标端口一样建立连接。如果客户端都是与同一个服务器建立连接那么如果客户端 TIME_WAIT 状态的连接过多当端口资源被耗尽就无法与这个服务器再建立连接了。但是因为只要客户端连接的服务器不同端口资源可以重复使用的。所以如果客户端都是与不同的服务器建立连接即使客户端端口资源只有几万个客户端发起百万级连接也是没问题的。 如何解决客户端 TCP 连接 TIME_WAIT 过多导致无法与同一服务器建立连接的问题 可以打开 net.ipv4.tcp_tw_reuse 这个内核参数因为开启这个内核参数后客户端调用 connect 函数时如果选择的端口已经被相同四元组的连接占用时就会判断该连接是否处于 TIME_WAIT 状态如果该连接处于 TIME_WAIT 状态并且 TIME_WAIT 状态持续的时间超过了 1s那么就会重用这个连接然后就可以正常使用该端口了。 在调用 connect 函数时内核刚好选择了某一端口接着发现已经被相同的四元组连接占用了 如果没有开启那个参数的话内核就会选择下一端口然后继续判断直到找到一个没有被相同四元组的连接使用的端口如果端口资源耗尽还是没找到那么 connect 函数就会返回错误 如果开启了那个参数就会判断该四元组的连接状态是否处于 TIME_WAIT 状态如果连接处于 TIME_WAIT 状态并且该状态的持续时间超过了 1s那么就会重用该连接这时 connect 就会返回成功 十六、没有 listen 能建立 TCP 连接吗 是可以的客户端可以自己连自己的形成链连接TCP 自连接也可以两个客户端同时向对方发出请求建立连接TCP 同时打开这两个情况都有个共同点就是没有服务器参与也就是没有 listen 就能建立连接。 那没有 listen 为什么还能建立连接呢 我们知道在执行 listen 方法时会创建半连接队列和全连接队列三次握手的过程会在这两个队列中暂存连接的信息所以形成连接前提是有个地方存放着方便握手的时候能根据 IP 端口等信息找到对应的 socket 那么客户端会有半连接队列吗 显然没有因为客户端没有执行 listen 因为半连接队列和全连接队列都是在执行 listen 方法时内核自动创建的。但内核中还有个全局 hash 表可以用于存放 sock 连接信息在 TCP 自连接的情况中客户端在 connect 方法时最后会将自己的连接信息放入到这个全局的 hash 表中 然后将信息发出消息在经过回环地址重新回到 TCP 传输层的时候就会根据 IP 端口信息再一次从这个全局 hash 中取出消息。于是握手包一来一回最后成功建立连接。 十七、没有 accept 能建立 TCP 连接吗 建立连接的过程中根本不需要 accept 参与执行 accept 只是为了从全连接队列里取出一条连接。 全连接队列和半连接队列虽然都叫队列但其实全连接队列是个链表半连接队列是个哈希表 为什么半连接队列要设计成哈希表呢 先与全连接队列进行对比本质上是个链表因为也是线性结构说它是个队列也没毛病它里面放的都是已经建立完成的连接这个连接正等待被取走。而服务端取走连接的过程中并不关心具体是哪个连接只要是个连接就好所以直接从队列头中取就行这个过程时间复杂度为O1 而半连接队列则不一样因为队列里的都是不完整的连接等待着第三次握手的到来。那么现在有一个第三次握手来了则需要从队列中把相应 IP 端口的连接取出如果半连接队列还是个链表那我们就需要依次遍历才能拿到我们想要的那个连接算法复杂度是On了而如果将其设计成哈希表那么查找半连接的算法时间复杂度就是O1了。 还记得开启 tcp_syncookies 防止泛洪攻击吗 会不会有一个 cookies 队列呢 我们其实可以反过来想一下如果有 cookies 队列那它会跟半连接队列一样到头来还是会被 SYN 泛洪攻击打满实际上 cookies 并不会有一个专门的队列保存它是通过通信双方的IP地址时间戳MSS 等信息进行实时计算的保存在 TCP 报头的 seq 里。当服务端收到客户端发来的第三次握手时会通过 seq 还原出通信双方的IP地址时间戳MSS验证通过则建立连接。 cookies 方案为什么不直接取代半连接队列 目前看来syn_cookies 方案省下了半连接队列所需要的队列内存还能解决 SYN 泛洪攻击那为什么不直接取代半连接队列呢 cookies 虽然能够防 SYN 泛洪攻击但是也有一些问题。因为服务端并不会保存连接信息所以如果传输过程中数据包丢了也不会重发第二次握手信息。另外编码解码 cookies 都是比较耗 CPU 的利用这一点如果此时攻击者构造大量的第三次握手包ACK包同时带上各种瞎编的 cookies 信息服务端在收到 ACK 包后以为是正经 cookies跑去解码最后发现不是正经数据包才丢弃。这种通过构造大量的 ACK 包去消耗服务端的攻击叫 ACK 攻击收到的攻击的服务器可能会因为 CPU 资源耗尽而没能正常响应正经请求。 十八、用了 TCP 协议数据一定不会丢失吗 18.1、建立连接时丢包 在服务端第一次握手之后会先建立个半连接然后再发出第二次握手这时候需要有个地方可以暂存这个半连接队列这个队列叫半连接队列。如果之后第三次握手来了半连接队列就会升级为全连接然后暂存到另外一个叫全连接队列的地方坐等程序执行 accept 方法将其取走使用。是队列就有长度有长度就有可能会满如果他们满了那新来的包就会被丢弃。 18.2、流量控制丢包 应用层能发网络数据包的软件有那么多如果所有数据不加控制一股脑冲入到网卡网卡会吃不消那怎么办让数据按一定的规律排个队依次处理也就是所谓的 qdiscQueueing Discplines排队规则这也就是我们常说的流量控制机制。排队就首先有个队列而队列有个长度。我们可以通过 ifconfig 命令看到里面涉及的 txqueuelen 后面的数字 1000 其实就是流控队列的长度。当发送数据过快流控队列长度又不够大时就容易出现丢包的现象。 18.3、网卡丢包 网卡和驱动程序丢包的场景也经常出现原因有很多比如网线质量差接触不良..... ①、RingBuffer 过小导致丢包 在接收数据时会将数据暂存到 RingBuffer 接收缓冲区中然后等着内核触发软件中断慢慢收走如果这个缓冲区过小而这时候发送的数据又过快就有可能发生溢出此时也会产生丢包。 ②、网卡性能不足 网卡作为硬件传输速度是有上限的。当网络传输速度过大时达到网卡的上限就会出现丢包 18.3、接收缓冲区丢包 我们一般使用 TCP socket 进行网络编程时内核都会分配一个发送缓冲区和一个接收缓冲区。当我们想要发一个数据包会在代码里执行 send() 这时候数据包并不是一把梭直接就走网卡飞出去的而是将数据拷贝到内核发送缓冲区就完事返回了至于是什么时候发送发多少这个后续由内核决定。而接收缓冲区作用类似从外部网络收到的数据包就暂存在这个地方然后坐等用户空间的应用程序将数据包取走。如果缓冲区设置过小缓冲区已满如果还有数据发来就会丢包 18.4、两端之间的网络丢包 两端之间那么长的一条链路都属于外部网络这中间由各种路由器和交换机还有光缆什么的丢包也是经常发生。 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 报文时内核会马上回一个 ACK 应答报文但是服务端应用程序可能还有数据要发送所以并不能马上发送 FIN 报文而是将发送 FIN 报文的控制权交给服务端应用程序 如果服务端应用程序有数据要发送的话就发完数据后再调用关闭连接的函数如果服务端应用程序没有数据要发送的时候可以直接调用关闭连接的函数 从上面过程可知是否要发送第三次挥手的控制权不在内核而是在被动关闭方的应用程序因为应用程序可能还有数据要发送由应用程序决定什么时候调用关闭连接的函数当调用了关闭连接的函数内核就会发送 FIN 报文了所以服务端的 ACK 和 FIN 一般都会分开发送。 FIN 报文一定得调用关闭连接的函数才会这样吗 不一定如果进程退出了不管是不是正常退出还是异常退出。内核都会发送 FIN 报文与对方完成四次挥手。 什么情况会出现三次挥手 当被动关闭方在 TCP 回收过程中【没有数据要发送】并且【开启了 TCP 延迟确认机制】那么第二和第三次挥手就会合并传输这样就出现了三次挥手。 然后因为 TCP 延迟确认机制是默认开启的所以导致我们抓包时看见第三次挥手的次数比四次挥手还多。 什么是 TCP 延迟确认机制 当发送没有携带数据的 ACK 它的网络效率是很低的因为它也有 40 个字节的 IP 头和 TCP 头但却也没有携带数据报文。为了解决 ACK 传输效率低问题所以就衍生出了 TCP 延迟确认。TCP 延迟确认的策略 当有响应数据要发送时ACK 会随着响应数据一起立刻发送给对方当没有响应数据要发送时ACK 将会延迟一段时间以等待是否有响应数据可以一起发送如果在延迟等待发送 ACK 期间对方的第二个数据报文又到达了这时就会立刻发送 ACK 二十、TCP 序列号和确认号是如何变化的 万能公式 发送的 TCP 报文 公式一序列号 上一次发送的序列号 len数据长度。特殊情况如果上一次发送的报文是 SYN 报文或者 FIN 报文则改为上一次发送的序列号 1公式二确认号 上一次收到的报文中的序列号 len数据长度。特殊情况如果收到的是 SYN 报文或者 FIN 报文则改为上一次收到的报文中的序列号 1 序列号在建立连接时由内核生成的随机数作为其初始值通过 SYN 报文传给接收端主机每发送一次数据就【累加】一次该【数据字节数】的大小用来解决网络包乱序的问题。 确认号指下一次【期望】收到的数据序列号发送端收到接收方发来的 ACK 确认报文后就可以认为在这个序号以前的数据都已经被正常接收。用来解决丢包的问题。 控制位用来表示 TCP 报文是什么类型的报文例如 ACK报文数据报文SYN报文...... 为什么第二次和第三次握手报文中的确认号是将对方的序列号 1 作为确认号呢 SYN 报文是特殊的 TCP 报文用于建立连接时使用虽然 SYN 报文不携带用户数据但是 TCP 将 SYN 报文视为 1 字节的数据当对方收到了 SYN 报文后在回复 ACK 报文时就需要将 ACK 报文中的确认号设置为 SYN 的序列号 1这样做的目的有两个 告诉对方我方已经收到 SYN 报文告诉对方我方下一次【期望】收到的报文的序列号为此确认号比如客户端与服务端完成三次握手之后服务端接下来期望收到的是序列号为 client_isn 1 的 TCP 数据报文
http://www.pierceye.com/news/533819/

相关文章:

  • 网站兼容性问题线上设计师接单
  • 外包网站平台可以做电算化的网站
  • 教育网站设计案例学校网站设计
  • 网站建设入门教程pdf网络推广和seo
  • 闲鱼钓鱼网站怎么做百度网页版主页
  • 一次备案多个网站alexa排名查询
  • 郑州做招商的网站网站建设的流程推广方案
  • wordpress手机网站插件海口seo关键词优化
  • wordpress随机文章佛山网站优化美姿姿seo
  • 做酒类网站中铁三局最新消息
  • 网站建设教程给赚湖南岚鸿官 网英语培训学校网站建设多少钱
  • 电子商务网站的建设步骤有注册咨询公司经营范围
  • 手机端网站做app开发wordpress建站论坛
  • 四合一做网站微信公众平台怎么做微网站
  • 法治与安全做讲座网站系统工具
  • wap网站怎么做白石洲网站建设
  • 网站备案 关闭网站广州安全教育平台登录入囗
  • 做常州美食网站首页的背景图招聘网站建设费用多少
  • 制作网站需要wordpress网站的建设步骤包括什么
  • 有什么网站可以做微信支付宝支付宝闽侯县建设局网站
  • html5网站图标qq刷赞网站如何做分站
  • 免费asp网站源码下载网页视频怎么下载到本地视频手机
  • 深圳网站定制开发安徽建设人才网官网
  • 斐讯k3做网站工商注册名称核准查询
  • 兼职网站编辑深圳网站做的好的公司哪家好
  • 网站响应速度优化wordpress外贸主题购买
  • 没有后台的网站怎么做排名网页设计学校
  • 江苏网站建设哪家快点外贸商城网站系统
  • 菠菜网站做首存wordpress 警告
  • 无锡好的网站建设公司网站公司做的网站被攻击