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

免费视图网站建设wordpress开发工作流6

免费视图网站建设,wordpress开发工作流6,模板网站建设乐云seo效果好,宝安公司网站建设文章目录 重传机制超时重传快速重传SACK 方法Duplicate SACK 滑动窗口流量控制操作系统缓冲区与滑动窗口的关系窗口关闭糊涂窗口综合症 拥塞控制慢启动拥塞避免算法拥塞发生快速恢复 如何理解是 TCP 面向字节流协议#xff1f;如何理解字节流#xff1f;如何解决粘包#xf… 文章目录 重传机制超时重传快速重传SACK 方法Duplicate SACK 滑动窗口流量控制操作系统缓冲区与滑动窗口的关系窗口关闭糊涂窗口综合症 拥塞控制慢启动拥塞避免算法拥塞发生快速恢复 如何理解是 TCP 面向字节流协议如何理解字节流如何解决粘包固定长度的消息特殊字符作为边界自定义消息结构 SYN 报文什么时候情况下会被丢弃坑爹的 tcp_tw_recycleaccpet 队列满了半连接队列满了全连接队列满了 四次挥手中收到乱序的 FIN 包会如何处理TIME_WAIT状态下对接收到的数据包如何处理 在 TIME_WAIT 状态的 TCP 连接收到 SYN 后会发生什么收到合法 SYN**收到非法的 SYN** 在 TIME_WAIT 状态收到 RST 会断开连接吗 TCP 连接一端断电和进程崩溃有什么区别主机崩溃进程崩溃有数据传输的场景 拔掉网线后 原本的 TCP 连接还存在吗拔掉网线后有数据传输拔掉网线后没有数据传输 tcp_tw_reuse 为什么默认是关闭的为什么 tcp_tw_reuse 默认是关闭的第一个问题第二个问题 HTTPS 中 TLS 和 TCP 能同时握手吗TCP Fast OpenTLSv1.3 TCP Keepalive 和 HTTP Keep-Alive 是一个东西吗HTTP 的 Keep-AliveTCP 的 Keep-Alive TCP 协议有什么缺陷升级 TCP 的工作很困难TCP 建立连接的延迟TCP 存在队头阻塞问题网络迁移需要重新建立 TCP 连接 如何基于 UDP 协议实现可靠传输QUIC 是如何实现可靠传输的Packet HeaderQUIC Frame Header QUIC 是如何解决 TCP 队头阻塞问题的HTTP/2 的队头阻塞没有队头阻塞的 QUIC QUIC 是如何做流量控制的**Stream 级别的流量控制**Connection 流量控制 QUIC 对拥塞控制改进QUIC 更快的连接建立QUIC 是如何迁移连接的 服务端没有 listen客户端发起连接建立会发生什么没有 listen能建立 TCP 连接吗 没有 accept能建立 TCP 连接吗为什么半连接队列要设计成哈希表半连接队列要是满了会怎么样会有一个cookies队列吗cookies方案为什么不直接取代半连接队列 用了 TCP 协议数据一定不会丢吗建立连接时丢包流量控制丢包网卡丢包RingBuffer过小导致丢包网卡性能不足 接收缓冲区丢包两端之间的网络丢包**ping命令查看丢包****mtr命令** 发生丢包了怎么办用了TCP协议就一定不会丢包吗这类丢包问题怎么解决 TCP 序列号和确认号是如何变化的万能公式三次握手阶段的变化数据传输阶段的变化四次挥手阶段的变化 重传机制 TCP 实现可靠传输的方式之一是通过序列号与确认应答。 在 TCP 中当发送端的数据到达接收主机时接收端主机会返回一个确认应答消息表示已收到消息。 但在错综复杂的网络并不一定能如上图那么顺利能正常的数据传输万一数据在传输过程中丢失了呢 所以 TCP 针对数据包丢失的情况会用重传机制解决。 接下来说说常见的重传机制 超时重传快速重传SACKD-SACK 超时重传 重传机制的其中一个方式就是在发送数据时设定一个定时器当超过指定的时间后没有收到对方的 ACK 确认应答报文就会重发该数据也就是我们常说的超时重传。 TCP 会在以下两种情况发生超时重传 数据包丢失确认应答丢失 :::info 超时时间应该设置为多少呢 ::: 我们先来了解一下什么是 RTTRound-Trip Time 往返时延从下图我们就可以知道 RTT 指的是数据发送时刻到接收到确认的时刻的差值也就是包的往返时间。 超时重传时间是以 RTO Retransmission Timeout 超时重传时间表示。 假设在重传的情况下超时时间 RTO 「较长或较短」时会发生什么事情呢 上图中有两种超时时间不同的情况 当超时时间 RTO 较大时重发就慢丢了老半天才重发没有效率性能差当超时时间 RTO 较小时会导致可能并没有丢就重发于是重发的就快会增加网络拥塞导致更多的超时更多的超时导致更多的重发。 精确的测量超时时间 RTO 的值是非常重要的这可让我们的重传机制更高效。 根据上述的两种情况我们可以得知超时重传时间 RTO 的值应该略大于报文往返 RTT 的值。 至此可能大家觉得超时重传时间 RTO 的值计算也不是很复杂嘛。 好像就是在发送端发包时记下 t0 然后接收端再把这个 ack 回来时再记一个 t1于是 RTT t1 – t0。没那么简单这只是一个采样不能代表普遍情况。 实际上「报文往返 RTT 的值」是经常变化的因为我们的网络也是时常变化的。也就因为「报文往返 RTT 的值」 是经常波动变化的所以「超时重传时间 RTO 的值」应该是一个动态变化的值。 如果超时重发的数据再次超时的时候又需要重传的时候TCP 的策略是超时间隔加倍。 也就是每当遇到一次超时重传的时候都会将下一次超时时间间隔设为先前值的两倍。两次超时就说明网络环境差不宜频繁反复发送。 超时触发重传存在的问题是超时周期可能相对较长。那是不是可以有更快的方式呢 于是就可以用「快速重传」机制来解决超时重发的时间等待。 快速重传 TCP 还有另外一种快速重传Fast Retransmit机制它不以时间为驱动而是以数据驱动重传。 快速重传机制是如何工作的呢其实很简单一图胜千言。 快速重传的工作方式是当收到三个相同的 ACK 报文时会在定时器过期之前重传丢失的报文段。 快速重传机制只解决了一个问题就是超时时间的问题但是它依然面临着另外一个问题。就是重传的时候是重传一个还是重传所有的问题。 举个例子假设发送方发了 6 个数据编号的顺序是 Seq1 ~ Seq6 但是 Seq2、Seq3 都丢失了那么接收方在收到 Seq4、Seq5、Seq6 时都是回复 ACK2 给发送方但是发送方并不清楚这连续的 ACK2 是接收方收到哪个报文而回复的 那是选择重传 Seq2 一个报文还是重传 Seq2 之后已发送的所有报文呢Seq2、Seq3、 Seq4、Seq5、 Seq6 呢 如果只选择重传 Seq2 一个报文那么重传的效率很低。因为对于丢失的 Seq3 报文还得在后续收到三个重复的 ACK3 才能触发重传。如果选择重传 Seq2 之后已发送的所有报文虽然能同时重传已丢失的 Seq2 和 Seq3 报文但是 Seq4、Seq5、Seq6 的报文是已经被接收过了对于重传 Seq4 Seq6 折部分数据相当于做了一次无用功浪费资源。 可以看到不管是重传一个报文还是重传已发送的报文都存在问题。 为了解决不知道该重传哪些 TCP 报文于是就有 SACK 方法。 SACK 方法 还有一种实现重传机制的方式叫SACK Selective Acknowledgment 选择性确认。 这种方式需要在 TCP 头部「选项」字段里加一个 SACK 的东西它可以将已收到的数据的信息发送给「发送方」这样发送方就可以知道哪些数据收到了哪些数据没收到知道了这些信息就可以只重传丢失的数据。 如下图发送方收到了三次同样的 ACK 确认报文于是就会触发快速重发机制通过 SACK 信息发现只有 200~299 这段数据丢失则重发时就只选择了这个 TCP 段进行重复。 如果要支持 SACK必须双方都要支持。在 Linux 下可以通过 net.ipv4.tcp_sack 参数打开这个功能Linux 2.4 后默认打开。 Duplicate SACK Duplicate SACK 又称 D-SACK其主要使用了 SACK 来告诉「发送方」有哪些数据被重复接收了。 下面举例两个栗子来说明 D-SACK 的作用。 「接收方」发给「发送方」的两个 ACK 确认应答都丢失了所以发送方超时后重传第一个数据包3000 ~ 3499于是「接收方」发现数据是重复收到的于是回了一个 SACK 3000~3500告诉「发送方」 3000~3500 的数据早已被接收了因为 ACK 都到了 4000 了已经意味着 4000 之前的所有数据都已收到所以这个 SACK 就代表着 D-SACK。这样「发送方」就知道了数据没有丢是「接收方」的 ACK 确认报文丢了。 数据包1000~1499 被网络延迟了导致「发送方」没有收到 Ack 1500 的确认报文。而后面报文到达的三个相同的 ACK 确认报文就触发了快速重传机制但是在重传后被延迟的数据包1000~1499又到了「接收方」所以「接收方」回了一个 SACK1000~1500因为 ACK 已经到了 3000所以这个 SACK 是 D-SACK表示收到了重复的包。这样发送方就知道快速重传触发的原因不是发出去的包丢了也不是因为回应的 ACK 包丢了而是因为网络延迟了。 可见D-SACK 有这么几个好处 可以让「发送方」知道是发出去的包丢了还是接收方回应的 ACK 包丢了;可以知道是不是「发送方」的数据包被网络延迟了;可以知道网络中是不是把「发送方」的数据包给复制了; 在 Linux 下可以通过 net.ipv4.tcp_dsack 参数开启/关闭这个功能Linux 2.4 后默认打开。 滑动窗口 我们都知道 TCP 是每发送一个数据都要进行一次确认应答。当上一个数据包收到了应答了 再发送下一个。 这个模式就有点像我和你面对面聊天你一句我一句。但这种方式的缺点是效率比较低的。 如果你说完一句话我在处理其他事情没有及时回复你那你不是要干等着我做完其他事情后我回复你你才能说下一句话很显然这不现实。 所以这样的传输方式有一个缺点数据包的往返时间越长通信的效率就越低。 为解决这个问题TCP 引入了窗口这个概念。即使在往返时间较长的情况下它也不会降低网络通信的效率。 那么有了窗口就可以指定窗口大小窗口大小就是指无需等待确认应答而可以继续发送数据的最大值。 窗口的实现实际上是操作系统开辟的一个缓存空间发送方主机在等到确认应答返回之前必须在缓冲区中保留已发送的数据。如果按期收到确认应答此时数据就可以从缓存区清除。 假设窗口大小为 3 个 TCP 段那么发送方就可以「连续发送」 3 个 TCP 段并且中途若有 ACK 丢失可以通过「下一个确认应答进行确认」。如下图 图中的 ACK 600 确认应答报文丢失也没关系因为可以通过下一个确认应答进行确认只要发送方收到了 ACK 700 确认应答就意味着 700 之前的所有数据「接收方」都收到了。这个模式就叫累计确认或者累计应答。 :::info 窗口大小由哪一方决定 ::: TCP 头里有一个字段叫 Window也就是窗口大小。 这个字段是接收端告诉发送端自己还有多少缓冲区可以接收数据。于是发送端就可以根据这个接收端的处理能力来发送数据而不会导致接收端处理不过来。 所以通常窗口的大小是由接收方的窗口大小来决定的。 发送方发送的数据大小不能超过接收方的窗口大小否则接收方就无法正常接收到数据。 :::info 发送方的滑动窗口 ::: 我们先来看看发送方的窗口下图就是发送方缓存的数据根据处理的情况分成四个部分其中深蓝色方框是发送窗口紫色方框是可用窗口 在下图当发送方把数据「全部」都一下发送出去后可用窗口的大小就为 0 了表明可用窗口耗尽在没收到 ACK 确认之前是无法继续发送数据了。 在下图当收到之前发送的数据 32~36 字节的 ACK 确认应答后如果发送窗口的大小没有变化则滑动窗口往右边移动 5 个字节因为有 5 个字节的数据被应答确认接下来 52~56 字节又变成了可用窗口那么后续也就可以发送 52~56 这 5 个字节的数据了。 :::info 程序是如何表示发送方的四个部分的呢 ::: TCP 滑动窗口方案使用三个指针来跟踪在四个传输类别中的每一个类别中的字节。其中两个指针是绝对指针指特定的序列号一个是相对指针需要做偏移。 SND.WND表示发送窗口的大小大小是由接收方指定的SND.UNASend Unacknoleged是一个绝对指针它指向的是已发送但未收到确认的第一个字节的序列号也就是 #2 的第一个字节。SND.NXT也是一个绝对指针它指向未发送但可发送范围的第一个字节的序列号也就是 #3 的第一个字节。指向 #4 的第一个字节是个相对指针它需要 SND.UNA 指针加上 SND.WND 大小的偏移量就可以指向 #4 的第一个字节了。 那么可用窗口大小的计算就可以是 可用窗口大小 SND.WND -SND.NXT - SND.UNA :::info 接收方的滑动窗口 ::: 接下来我们看看接收方的窗口接收窗口相对简单一些根据处理的情况划分成三个部分 #1 #2 是已成功接收并确认的数据等待应用进程读取#3 是未收到数据但可以接收的数据#4 未收到数据并不可以接收的数据 其中三个接收部分使用两个指针进行划分: RCV.WND表示接收窗口的大小它会通告给发送方。RCV.NXT是一个指针它指向期望从发送方发送来的下一个数据字节的序列号也就是 #3 的第一个字节。指向 #4 的第一个字节是个相对指针它需要 RCV.NXT 指针加上 RCV.WND 大小的偏移量就可以指向 #4 的第一个字节了。 :::info 接收窗口和发送窗口的大小是相等的吗 ::: 并不是完全相等接收窗口的大小是约等于发送窗口的大小的。 因为滑动窗口并不是一成不变的。比如当接收方的应用进程读取数据的速度非常快的话这样的话接收窗口可以很快的就空缺出来。那么新的接收窗口大小是通过 TCP 报文中的 Windows 字段来告诉发送方。那么这个传输过程是存在时延的所以接收窗口和发送窗口是约等于的关系。 流量控制 发送方不能无脑的发数据给接收方要考虑接收方处理能力。 如果一直无脑的发数据给对方但对方处理不过来那么就会导致触发重发机制从而导致网络流量的无端的浪费。 为了解决这种现象发生TCP 提供一种机制可以让「发送方」根据「接收方」的实际接收能力控制发送的数据量这就是所谓的流量控制。 下面举个栗子为了简单起见假设以下场景 客户端是接收方服务端是发送方假设接收窗口和发送窗口相同都为 200假设两个设备在整个传输过程中都保持相同的窗口大小不受外界影响 操作系统缓冲区与滑动窗口的关系 前面的流量控制例子我们假定了发送窗口和接收窗口是不变的但是实际上发送窗口和接收窗口中所存放的字节数都是放在操作系统内存缓冲区中的而操作系统的缓冲区会被操作系统调整。 当应用进程没办法及时读取缓冲区的内容时也会对我们的缓冲区造成影响。 :::info 那操作系统的缓冲区是如何影响发送窗口和接收窗口的呢 ::: 当应用程序没有及时读取缓存时发送窗口和接收窗口的变化。 考虑以下场景 客户端作为发送方服务端作为接收方发送窗口和接收窗口初始大小为 360服务端非常的繁忙当收到客户端的数据时应用层不能及时读取数据。 我们看看第二个例子。 当服务端系统资源非常紧张的时候操作系统可能会直接减少了接收缓冲区大小这时应用程序又无法及时读取缓存数据那么这时候就有严重的事情发生了会出现数据包丢失的现象。 所以如果发生了先减少缓存再收缩窗口就会出现丢包的现象。 为了防止这种情况发生TCP 规定是不允许同时减少缓存又收缩窗口的而是采用先收缩窗口过段时间再减少缓存这样就可以避免了丢包情况。 窗口关闭 在前面我们都看到了TCP 通过让接收方指明希望从发送方接收的数据大小窗口大小来进行流量控制。 如果窗口大小为 0 时就会阻止发送方给接收方传递数据直到窗口变为非 0 为止这就是窗口关闭。 :::info 窗口关闭潜在的危险 ::: 接收方向发送方通告窗口大小时是通过 ACK 报文来通告的。 那么当发生窗口关闭时接收方处理完数据后会向发送方通告一个窗口非 0 的 ACK 报文如果这个通告窗口的 ACK 报文在网络中丢失了那麻烦就大了。 这会导致发送方一直等待接收方的非 0 窗口通知接收方也一直等待发送方的数据如不采取措施这种相互等待的过程会造成了死锁的现象。 :::info TCP 是如何解决窗口关闭时潜在的死锁现象呢 ::: 为了解决这个问题TCP 为每个连接设有一个持续定时器只要 TCP 连接一方收到对方的零窗口通知就启动持续计时器。 如果持续计时器超时就会发送窗口探测 ( Window probe ) 报文而对方在确认这个探测报文时给出自己现在的接收窗口大小。 如果接收窗口仍然为 0那么收到这个报文的一方就会重新启动持续计时器如果接收窗口不是 0那么死锁的局面就可以被打破了。 窗口探测的次数一般为 3 次每次大约 30-60 秒不同的实现可能会不一样。如果 3 次过后接收窗口还是 0 的话有的 TCP 实现就会发 RST 报文来中断连接。 糊涂窗口综合症 如果接收方太忙了来不及取走接收窗口里的数据那么就会导致发送方的发送窗口越来越小。 到最后如果接收方腾出几个字节并告诉发送方现在有几个字节的窗口而发送方会义无反顾地发送这几个字节这就是糊涂窗口综合症。 要知道我们的 TCP IP 头有 40 个字节为了传输那几个字节的数据要搭上这么大的开销这太不经济了。 就好像一个可以承载 50 人的大巴车每次来了一两个人就直接发车。除非家里有矿的大巴司机才敢这样玩不然迟早破产。要解决这个问题也不难大巴司机等乘客数量超过了 25 个才认定可以发车。 现举个糊涂窗口综合症的栗子考虑以下场景 接收方的窗口大小是 360 字节但接收方由于某些原因陷入困境假设接收方的应用层读取的能力如下 接收方每接收 3 个字节应用程序就只能从缓冲区中读取 1 个字节的数据在下一个发送方的 TCP 段到达之前应用程序还从缓冲区中读取了 40 个额外的字节 每个过程的窗口大小的变化在图中都描述的很清楚了可以发现窗口不断减少了并且发送的数据都是比较小的了。 所以糊涂窗口综合症的现象是可以发生在发送方和接收方 接收方可以通告一个小的窗口而发送方可以发送小数据 于是要解决糊涂窗口综合症就要同时解决上面两个问题就可以了 让接收方不通告小窗口给发送方 让发送方避免发送小数据 :::info 怎么让接收方不通告小窗口呢 ::: 接收方通常的策略如下: 当「窗口大小」小于 min( MSS缓存空间/2 ) 也就是小于 MSS 与 1/2 缓存大小中的最小值时就会向发送方通告窗口为 0也就阻止了发送方再发数据过来。 等到接收方处理了一些数据后窗口大小 MSS或者接收方缓存空间有一半可以使用就可以把窗口打开让发送方发送数据过来。 :::info 怎么让发送方避免发送小数据呢 ::: 发送方通常的策略如下: 使用 Nagle 算法该算法的思路是延时处理只有满足下面两个条件中的任意一个条件才可以发送数据 条件一要等到窗口大小 MSS 并且 数据大小 MSS 条件二收到之前发送数据的 ack 回包 只要上面两个条件都不满足发送方一直在囤积数据直到满足上面的发送条件。 注意如果接收方不能满足「不通告小窗口给发送方」那么即使开了 Nagle 算法也无法避免糊涂窗口综合症因为如果对端 ACK 回复很快的话达到 Nagle 算法的条件二Nagle 算法就不会拼接太多的数据包这种情况下依然会有小数据包的传输网络总体的利用率依然很低。 所以接收方得满足「不通告小窗口给发送方」 发送方开启 Nagle 算法才能避免糊涂窗口综合症。 另外Nagle 算法默认是打开的如果对于一些需要小数据包交互的场景的程序比如telnet 或 ssh 这样的交互性比较强的程序则需要关闭 Nagle 算法。 可以在 Socket 设置 TCP_NODELAY 选项来关闭这个算法关闭 Nagle 算法没有全局参数需要根据每个应用自己的特点来关闭 setsockopt(sock_fd, IPPROTO_TCP, TCP_NODELAY, (char *)value, sizeof(int));拥塞控制 :::info 为什么要有拥塞控制呀不是有流量控制了吗 ::: 前面的流量控制是避免「发送方」的数据填满「接收方」的缓存但是并不知道网络的中发生了什么。 一般来说计算机网络都处在一个共享的环境。因此也有可能会因为其他主机之间的通信使得网络拥堵。 在网络出现拥堵时如果继续发送大量数据包可能会导致数据包时延、丢失等这时 TCP 就会重传数据但是一重传就会导致网络的负担更重于是会导致更大的延迟以及更多的丢包这个情况就会进入恶性循环被不断地放大… 所以TCP 不能忽略网络上发生的事它被设计成一个无私的协议当网络发送拥塞时TCP 会自我牺牲降低发送的数据量。 于是就有了拥塞控制控制的目的就是避免「发送方」的数据填满整个网络。 为了在「发送方」调节所要发送数据的量定义了一个叫做「拥塞窗口」的概念。 :::info 什么是拥塞窗口和发送窗口有什么关系呢 ::: 拥塞窗口 cwnd是发送方维护的一个的状态变量它会根据网络的拥塞程度动态变化的。 我们在前面提到过发送窗口 swnd 和接收窗口 rwnd 是约等于的关系那么由于加入了拥塞窗口的概念后此时发送窗口的值是swnd min(cwnd, rwnd)也就是拥塞窗口和接收窗口中的最小值。 拥塞窗口 cwnd 变化的规则 只要网络中没有出现拥塞cwnd 就会增大 但网络中出现了拥塞cwnd 就减少 :::info 那么怎么知道当前网络是否出现了拥塞呢 ::: 其实只要「发送方」没有在规定时间内接收到 ACK 应答报文也就是发生了超时重传就会认为网络出现了拥塞。 :::info 拥塞控制有哪些控制算法 ::: 拥塞控制主要是四个算法 慢启动 拥塞避免 拥塞发生 快速恢复 慢启动 TCP 在刚建立连接完成后首先是有个慢启动的过程这个慢启动的意思就是一点一点的提高发送数据包的数量如果一上来就发大量的数据这不是给网络添堵吗 慢启动的算法记住一个规则就行当发送方每收到一个 ACK拥塞窗口 cwnd 的大小就会加 1。 这里假定拥塞窗口 cwnd 和发送窗口 swnd 相等下面举个栗子 连接建立完成后一开始初始化 cwnd 1表示可以传一个 MSS 大小的数据。当收到一个 ACK 确认应答后cwnd 增加 1于是一次能够发送 2 个当收到 2 个的 ACK 确认应答后 cwnd 增加 2于是就可以比之前多发2 个所以这一次能够发送 4 个当这 4 个的 ACK 确认到来的时候每个确认 cwnd 增加 1 4 个确认 cwnd 增加 4于是就可以比之前多发 4 个所以这一次能够发送 8 个。 慢启动算法的变化过程如下图 拥塞避免算法 前面说道当拥塞窗口 cwnd 「超过」慢启动门限 ssthresh 就会进入拥塞避免算法。 一般来说 ssthresh 的大小是 65535 字节。 那么进入拥塞避免算法后它的规则是每当收到一个 ACK 时cwnd 增加 1/cwnd。 接上前面的慢启动的栗子现假定 ssthresh 为 8 当 8 个 ACK 应答确认到来时每个确认增加 1/88 个 ACK 确认 cwnd 一共增加 1于是这一次能够发送 9 个 MSS 大小的数据变成了线性增长。 拥塞避免算法的变化过程如下图 所以我们可以发现拥塞避免算法就是将原本慢启动算法的指数增长变成了线性增长还是增长阶段但是增长速度缓慢了一些。 就这么一直增长着后网络就会慢慢进入了拥塞的状况了于是就会出现丢包现象这时就需要对丢失的数据包进行重传。 当触发了重传机制也就进入了「拥塞发生算法」。 拥塞发生 当网络出现拥塞也就是会发生数据包重传重传机制主要有两种 超时重传快速重传 这两种使用的拥塞发送算法是不同的接下来分别来说说。 :::info 发生超时重传的拥塞发生算法 ::: 当发生了「超时重传」则就会使用拥塞发生算法。 这个时候ssthresh 和 cwnd 的值会发生变化 ssthresh 设为 cwnd/2cwnd 重置为 1 是恢复为 cwnd 初始化值我这里假定 cwnd 初始化值 1 接着就重新开始慢启动慢启动是会突然减少数据流的。这真是一旦「超时重传」马上回到解放前。但是这种方式太激进了反应也很强烈会造成网络卡顿。 快速恢复 快速重传和快速恢复算法一般同时使用快速恢复算法是认为你还能收到 3 个重复 ACK 说明网络也不那么糟糕所以没有必要像 RTO 超时那么强烈。 正如前面所说进入快速恢复之前cwnd 和 ssthresh 已被更新了 cwnd cwnd/2 也就是设置为原来的一半;ssthresh cwnd; 然后进入快速恢复算法如下 拥塞窗口 cwnd ssthresh 3 3 的意思是确认有 3 个数据包被收到了重传丢失的数据包如果再收到重复的 ACK那么 cwnd 增加 1如果收到新数据的 ACK 后把 cwnd 设置为第一步中的 ssthresh 的值原因是该 ACK 确认了新的数据说明从 duplicated ACK 时的数据都已收到该恢复过程已经结束可以回到恢复之前的状态了也即再次进入拥塞避免状态 快速恢复算法的变化过程如下图 在快速恢复的过程中首先 ssthresh cwnd/2然后 cwnd ssthresh 3表示网络可能出现了阻塞所以需要减小 cwnd 以避免加 3 代表快速重传时已经确认接收到了 3 个重复的数据包随后继续重传丢失的数据包如果再收到重复的 ACK那么 cwnd 增加 1。加 1 代表每个收到的重复的 ACK 包都已经离开了网络。这个过程的目的是尽快将丢失的数据包发给目标。如果收到新数据的 ACK 后把 cwnd 设置为第一步中的 ssthresh 的值恢复过程结束。 首先快速恢复是拥塞发生后慢启动的优化其首要目的仍然是降低 cwnd 来减缓拥塞所以必然会出现 cwnd 从大到小的改变。 其次过程2cwnd逐渐加1的存在是为了尽快将丢失的数据包发给目标从而解决拥塞的根本问题三次相同的 ACK 导致的快速重传所以这一过程中 cwnd 反而是逐渐增大的。 如何理解是 TCP 面向字节流协议 如何理解字节流 之所以会说 TCP 是面向字节流的协议UDP 是面向报文的协议是因为操作系统对 TCP 和 UDP 协议的发送方的机制不同也就是问题原因在发送方。 :::info 先来说说为什么 UDP 是面向报文的协议 ::: 当用户消息通过 UDP 协议传输时操作系统不会对消息进行拆分在组装好 UDP 头部后就交给网络层来处理所以发出去的 UDP 报文中的数据部分就是完整的用户消息也就是每个 UDP 报文就是一个用户消息的边界这样接收方在接收到 UDP 报文后读一个 UDP 报文就能读取到完整的用户消息。 你可能会问如果收到了两个 UDP 报文操作系统是怎么区分开的 操作系统在收到 UDP 报文后会将其插入到队列里队列里的每一个元素就是一个 UDP 报文这样当用户调用 recvfrom() 系统调用读数据的时候就会从队列里取出一个数据然后从内核里拷贝给用户缓冲区。 :::info 再来说说为什么 TCP 是面向字节流的协议 ::: 当用户消息通过 TCP 协议传输时消息可能会被操作系统分组成多个的 TCP 报文也就是一个完整的用户消息被拆分成多个 TCP 报文进行传输。 这时接收方的程序如果不知道发送方发送的消息的长度也就是不知道消息的边界时是无法读出一个有效的用户消息的因为用户消息被拆分成多个 TCP 报文后并不能像 UDP 那样一个 UDP 报文就能代表一个完整的用户消息。 举个实际的例子来说明。 发送方准备发送 「Hi.」和「I am Xiaolin」这两个消息。 在发送端当我们调用 send 函数完成数据“发送”以后数据并没有被真正从网络上发送出去只是从应用程序拷贝到了操作系统内核协议栈中。 至于什么时候真正被发送取决于发送窗口、拥塞窗口以及当前发送缓冲区的大小等条件。也就是说我们不能认为每次 send 调用发送的数据都会作为一个整体完整地消息被发送出去。 如果我们考虑实际网络传输过程中的各种影响假设发送端陆续调用 send 函数先后发送 「Hi.」和「I am Xiaolin」 报文那么实际的发送很有可能是这几种情况。 第一种情况这两个消息被分到同一个 TCP 报文像这样 第二种情况「I am Xiaolin」的部分随 「Hi」 在一个 TCP 报文中发送出去像这样 第三种情况「Hi.」 的一部分随 TCP 报文被发送出去另一部分和 「I am Xiaolin」 一起随另一个 TCP 报文发送出去像这样。 类似的情况还能举例很多种这里主要是想说明我们不知道 「Hi.」和 「I am Xiaolin」 这两个用户消息是如何进行 TCP 分组传输的。 因此我们不能认为一个用户消息对应一个 TCP 报文正因为这样所以 TCP 是面向字节流的协议。 当两个消息的某个部分内容被分到同一个 TCP 报文时就是我们常说的 TCP 粘包问题这时接收方不知道消息的边界的话是无法读出有效的消息。 要解决这个问题要交给应用程序。 如何解决粘包 粘包的问题出现是因为不知道一个用户消息的边界在哪如果知道了边界在哪接收方就可以通过边界来划分出有效的用户消息。 一般有三种方式分包的方式 固定长度的消息特殊字符作为边界自定义消息结构。 固定长度的消息 这种是最简单方法即每个用户消息都是固定长度的比如规定一个消息的长度是 64 个字节当接收方接满 64 个字节就认为这个内容是一个完整且有效的消息。 但是这种方式灵活性不高实际中很少用。 特殊字符作为边界 我们可以在两个用户消息之间插入一个特殊的字符串这样接收方在接收数据时读到了这个特殊字符就把认为已经读完一个完整的消息。 HTTP 是一个非常好的例子。 HTTP 通过设置回车符、换行符作为 HTTP 报文协议的边界。 有一点要注意这个作为边界点的特殊字符如果刚好消息内容里有这个特殊字符我们要对这个字符转义避免被接收方当作消息的边界点而解析到无效的数据。 自定义消息结构 我们可以自定义一个消息结构由包头和数据组成其中包头包是固定大小的而且包头里有一个字段来说明紧随其后的数据有多大。 比如这个消息结构体首先 4 个字节大小的变量来表示数据长度真正的数据则在后面。 struct { u_int32_t message_length; char message_data[]; } message;当接收方接收到包头的大小比如 4 个字节后就解析包头的内容于是就可以知道数据的长度然后接下来就继续读取数据直到读满数据的长度就可以组装成一个完整到用户消息来处理了。 SYN 报文什么时候情况下会被丢弃 开启 tcp_tw_recycle 参数并且在 NAT 环境下造成 SYN 报文被丢弃TCP 两个队列满了半连接队列和全连接队列造成 SYN 报文被丢弃 坑爹的 tcp_tw_recycle TCP 四次挥手过程中主动断开连接方会有一个 TIME_WAIT 的状态这个状态会持续 2 MSL 后才会转变为 CLOSED 状态。 在 Linux 操作系统下TIME_WAIT 状态的持续时间是 60 秒这意味着这 60 秒内客户端一直会占用着这个端口。要知道端口资源也是有限的一般可以开启的端口为 32768~61000 。 如果客户端发起连接方的 TIME_WAIT 状态过多占满了所有端口资源那么就无法对「目的 IP 目的 PORT」都一样的服务器发起连接了但是被使用的端口还是可以继续对另外一个服务器发起连接的。这是因为内核在定位一个连接的时候是通过四元组源IP、源端口、目的IP、目的端口信息来定位的并不会因为客户端的端口一样而导致连接冲突。 但是 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 机制」。 :::info 首先给大家说说什么是 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 号数据包时识别出它是个过期的数据包而将其丢掉。 那什么是 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 检查」那么就不会存在这个问题了。 网上很多博客都说开启 tcp_tw_recycle 参数来优化 TCP我信你个鬼糟老头坏的很 tcp_tw_recycle 在 Linux 4.12 版本后直接取消了这一参数。 accpet 队列满了 在 TCP 三次握手的时候Linux 内核会维护两个队列分别是 半连接队列也称 SYN 队列全连接队列也称 accepet 队列 服务端收到客户端发起的 SYN 请求后内核会把该连接存储到半连接队列并向客户端响应 SYNACK接着客户端会返回 ACK服务端收到第三次握手的 ACK 后内核会把连接从半连接队列移除然后创建新的完全的连接并将其添加到 accept 队列等待进程调用 accept 函数时把连接取出来。 半连接队列满了 当服务器造成syn攻击就有可能导致 TCP 半连接队列满了这时后面来的 syn 包都会被丢弃。 但是如果开启了syncookies 功能即使半连接队列满了也不会丢弃syn 包。 syncookies 是这么做的服务器根据当前状态计算出一个值放在己方发出的 SYNACK 报文中发出当客户端 返回 ACK 报文时取出该值验证如果合法就认为连接建立成功如下图所示。 syncookies 参数主要有以下三个值 0 值表示关闭该功能1 值表示仅当 SYN 半连接队列放不下时再启用它2 值表示无条件开启功能 那么在应对 SYN 攻击时只需要设置为 1 即可 这里给出几种防御 SYN 攻击的方法 增大半连接队列开启 tcp_syncookies 功能减少 SYNACK 重传次数 方式一增大半连接队列 要想增大半连接队列我们得知不能只单纯增大 tcp_max_syn_backlog 的值还需一同增大 somaxconn 和 backlog也就是增大全连接队列。否则只单纯增大 tcp_max_syn_backlog 是无效的。 方式三减少 SYNACK 重传次数 当服务端受到 SYN 攻击时就会有大量处于 SYN_RECV 状态的 TCP 连接处于这个状态的 TCP 会重传 SYNACK 当重传超过次数达到上限后就会断开连接。 那么针对 SYN 攻击的场景我们可以减少 SYNACK 的重传次数以加快处于 SYN_RECV 状态的 TCP 连接断开。 全连接队列满了 在服务端并发处理大量请求时如果 TCP accpet 队列过小或者应用程序调用 accept() 不及时就会造成 accpet 队列满了 这时后续的连接就会被丢弃这样就会出现服务端请求数量上不去的现象。 我们可以通过 ss 命令来看 accpet 队列大小在「LISTEN 状态」时Recv-Q/Send-Q 表示的含义如下 Recv-Q当前 accpet 队列的大小也就是当前已完成三次握手并等待服务端 accept() 的 TCP 连接个数Send-Q当前 accpet 最大队列长度上面的输出结果说明监听 8088 端口的 TCP 服务进程accpet 队列的最大长度为 128 如果 Recv-Q 的大小超过 Send-Q就说明发生了 accpet 队列满的情况。 要解决这个问题我们可以 调大 accpet 队列的最大长度调大的方式是通过调大 backlog 以及 somaxconn 参数。检查系统或者代码为什么调用 accept() 不及时 四次挥手中收到乱序的 FIN 包会如何处理 因为如果 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状态下对接收到的数据包如何处理 如果是RST包的话并且系统配置sysctl_tcp_rfc1337默认情况下为0参见/proc/sys/net/ipv4/tcp_rfc1337的值为0这时会立即释放time_wait传输控制块丢掉接收的RST包。如果是ACK包则会启动TIME_WAIT定时器后丢掉接收到的ACK包。接下来是对SYN包的处理。如果在TIME_WAIT状态下接收到序列号比上一个连接的结束序列号大的SYN包可以接受并建立新的连接 。 在 TIME_WAIT 状态的 TCP 连接收到 SYN 后会发生什么 针对这个问题关键是要看 SYN 的「序列号和时间戳」是否合法因为处于 TIME_WAIT 状态的连接收到 SYN 后会判断 SYN 的「序列号和时间戳」是否合法然后根据判断结果的不同做不同的处理。 先跟大家说明下 什么是「合法」的 SYN 合法 SYN客户端的 SYN 的「序列号」比服务端「期望下一个收到的序列号」要大并且 SYN 的「时间戳」比服务端「最后收到的报文的时间戳」要大。非法 SYN客户端的 SYN 的「序列号」比服务端「期望下一个收到的序列号」要小或者 SYN 的「时间戳」比服务端「最后收到的报文的时间戳」要小。 上面 SYN 合法判断是基于双方都开启了 TCP 时间戳机制的场景如果双方都没有开启 TCP 时间戳机制则 SYN 合法判断如下 合法 SYN客户端的 SYN 的「序列号」比服务端「期望下一个收到的序列号」要大。非法 SYN客户端的 SYN 的「序列号」比服务端「期望下一个收到的序列号」要小。 收到合法 SYN 如果处于 TIME_WAIT 状态的连接收到「合法的 SYN 」后就会重用此四元组连接跳过 2MSL 而转变为 SYN_RECV 状态接着就能进行建立连接过程。 用下图作为例子双方都启用了 TCP 时间戳机制TSval 是发送报文时的时间戳 上图中在收到第三次挥手的 FIN 报文时会记录该报文的 TSval 21用 ts_recent 变量保存。然后会计算下一次期望收到的序列号本次例子下一次期望收到的序列号就是 301用 rcv_nxt 变量保存。 处于 TIME_WAIT 状态的连接收到 SYN 后因为 SYN 的 seq400 大于 rcv_nxt301并且 SYN 的 TSval30 大于 ts_recent21所以是一个「合法的 SYN」于是就会重用此四元组连接跳过 2MSL 而转变为 SYN_RECV 状态接着就能进行建立连接过程。 收到非法的 SYN 如果处于 TIME_WAIT 状态的连接收到「非法的 SYN 」后就会再回复一个第四次挥手的 ACK 报文客户端收到后发现并不是自己期望收到确认号ack num就回 RST 报文给服务端。 用下图作为例子双方都启用了 TCP 时间戳机制TSval 是发送报文时的时间戳 上图中在收到第三次挥手的 FIN 报文时会记录该报文的 TSval 21用 ts_recent 变量保存。然后会计算下一次期望收到的序列号本次例子下一次期望收到的序列号就是 301用 rcv_nxt 变量保存。 处于 TIME_WAIT 状态的连接收到 SYN 后因为 SYN 的 seq200 小于 rcv_nxt301所以是一个「非法的 SYN」就会再回复一个与第四次挥手一样的 ACK 报文客户端收到后发现并不是自己期望收到确认号就回 RST 报文给服务端。 在 TIME_WAIT 状态收到 RST 会断开连接吗 会不会断开关键看 net.ipv4.tcp_rfc1337 这个内核参数默认情况是为 0 如果这个参数设置为 0 收到 RST 报文会提前结束 TIME_WAIT 状态释放连接。如果这个参数设置为 1 就会丢掉 RST 报文。 TCP 连接一端断电和进程崩溃有什么区别 主机崩溃 知道了 TCP keepalive 作用我们再回过头看题目中的「主机崩溃」这种情况。 :::info 在没有开启 TCP keepalive,且双方一直没有数据交互的情况下,如果客户端的「主机崩溃」了会发生什么。 ::: 客户端主机崩溃了服务端是无法感知到的在加上服务端没有开启 TCP keepalive又没有数据交互的情况下服务端的 TCP 连接将会一直处于 ESTABLISHED 连接状态直到服务端重启进程。 所以我们可以得知一个点在没有使用 TCP 保活机制且双方不传输数据的情况下一方的 TCP 连接处在 ESTABLISHED 状态并不代表另一方的连接还一定正常。 进程崩溃 :::info 那题目中的「进程崩溃」的情况呢 ::: TCP 的连接信息是由内核维护的所以当服务端的进程崩溃后内核需要回收该进程的所有 TCP 连接资源于是内核会发送第一次挥手 FIN 报文后续的挥手过程也都是在内核完成并不需要进程的参与所以即使服务端的进程退出了还是能与客户端完成 TCP四次挥手的过程。 我自己做了实验使用 kill -9 来模拟进程崩溃的情况发现在 kill 掉进程后服务端会发送 FIN 报文与客户端进行四次挥手。 所以即使没有开启 TCP keepalive且双方也没有数据交互的情况下如果其中一方的进程发生了崩溃这个过程操作系统是可以感知的到的于是就会发送 FIN 报文给对方然后与对方进行 TCP 四次挥手。 有数据传输的场景 如果服务端会发送数据由于客户端已经不存在收不到数据报文的响应报文服务端的数据报文会超时重传当重传总间隔时长达到一定阈值内核会根据 tcp_retries2 设置的值计算出一个阈值后会断开 TCP 连接如果服务端一直不会发送数据再看服务端有没有开启 TCP keepalive 机制 如果有开启服务端在一段时间没有进行数据交互时会触发 TCP keepalive 机制探测对方是否存在如果探测到对方已经消亡则会断开自身的 TCP 连接如果没有开启服务端的 TCP 连接会一直存在并且一直保持在 ESTABLISHED 状态。 拔掉网线后 原本的 TCP 连接还存在吗 今天聊一个有趣的问题拔掉网线几秒再插回去原本的 TCP 连接还存在吗 可能有的同学会说网线都被拔掉了那说明物理层被断开了那在上层的传输层理应也会断开所以原本的 TCP 连接就不会存在的了。就好像 我们拨打有线电话的时候如果某一方的电话线被拔了那么本次通话就彻底断了。 真的是这样吗 上面这个逻辑就有问题。问题在于错误的认为拔掉网线这个动作会影响传输层事实上并不会影响。 实际上TCP 连接在 Linux 内核中是一个名为 struct socket 的结构体该结构体的内容包含 TCP 连接的状态等信息。当拔掉网线的时候操作系统并不会变更该结构体的任何内容所以 TCP 连接的状态也不会发生改变。 我在我的电脑上做了个小实验我用 ssh 终端连接了我的云服务器然后我通过断开 wifi 的方式来模拟拔掉网线的场景此时查看 TCP 连接的状态没有发生变化还是处于 ESTABLISHED 状态。 通过上面这个实验结果我们知道了拔掉网线这个动作并不会影响 TCP 连接的状态。 接下来要看拔掉网线后双方做了什么动作。 所以 针对这个问题要分场景来讨论 拔掉网线后有数据传输拔掉网线后没有数据传输 拔掉网线后有数据传输 在客户端拔掉网线后如果服务端发送了数据报文那么在服务端重传次数没有达到最大值之前客户端就插回了网线那么双方原本的 TCP 连接还是能正常存在就好像什么事情都没有发生。在客户端拔掉网线后如果服务端发送了数据报文在客户端插回网线之前服务端重传次数达到了最大值时服务端就会断开 TCP 连接。等到客户端插回网线后向服务端发送了数据因为服务端已经断开了与客户端相同四元组的 TCP 连接所以就会回 RST 报文客户端收到后就会断开 TCP 连接。至此 双方的 TCP 连接都断开了。 拔掉网线后没有数据传输 如果双方都没有开启 TCP keepalive 机制那么在客户端拔掉网线后如果客户端一直不插回网线那么客户端和服务端的 TCP 连接状态将会一直保持存在。如果双方都开启了 TCP keepalive 机制那么在客户端拔掉网线后如果客户端一直不插回网线TCP keepalive 机制会探测到对方的 TCP 连接没有存活于是就会断开 TCP 连接。而如果在 TCP 探测期间客户端插回了网线那么双方原本的 TCP 连接还是能正常存在。 tcp_tw_reuse 为什么默认是关闭的 既然打开 net.ipv4.tcp_tw_reuse 参数可以快速复用处于 TIME_WAIT 状态的 TCP 连接那为什么 Linux 默认是关闭状态呢 其实这题在变相问「如果 TIME_WAIT 状态持续时间过短或者没有会有什么问题」 因为开启 tcp_tw_reuse 参数可以快速复用处于 TIME_WAIT 状态的 TCP 连接时相当于缩短了 TIME_WAIT 状态的持续时间。 可能有的同学会问说使用 tcp_tw_reuse 快速复用处于 TIME_WAIT 状态的 TCP 连接时是需要保证 net.ipv4.tcp_timestamps 参数是开启的默认是开启的而 tcp_timestamps 参数可以避免旧连接的延迟报文这不是解决了没有 TIME_WAIT 状态时的问题了吗 为什么 tcp_tw_reuse 默认是关闭的 第一个问题 我们知道开启 tcp_tw_reuse 的同时也需要开启 tcp_timestamps意味着可以用时间戳的方式有效的判断回绕序列号的历史报文。 但是在看我看了防回绕序列号函数的源码后**发现对于 RST 报文的时间戳即使过期了只要 RST 报文的序列号在对方的接收窗口内也是能被接受的。 ** 下面 tcp_validate_incoming 函数就是验证接收到的 TCP 报文是否合格的函数其中第一步就会进行 PAWS 检查由 tcp_paws_discard 函数负责。 当 tcp_paws_discard 返回 true就代表报文是一个历史报文于是就要丢弃这个报文。但是在丢掉这个报文的时候会先判断是不是 RST 报文如果不是 RST 报文才会将报文丢掉。也就是说即使 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 报文。 处于 syn_sent 状态的客户端收到服务端的 Challenge ACK后发现不是自己期望收到的确认号于是就会回复 RST 报文服务端收到后就会断开连接。 HTTPS 中 TLS 和 TCP 能同时握手吗 「HTTPS 是先进行 TCP 三次握手再进行 TLSv1.2 四次握手」这句话一点问题都没有怀疑这句话是错的人才有问题。 「HTTPS 中的 TLS 握手过程可以同时进行三次握手」这个场景是可能存在到但是在没有说任何前提条件而说这句话就等于耍流氓。需要下面这两个条件同时满足才可以 客户端和服务端都开启了 TCP Fast Open 功能且 TLS 版本是 1.3客户端和服务端已经完成过一次通信 TCP Fast Open :::info 我们先来了解下什么是 TCP Fast Open ::: 常规的情况下如果要使用 TCP 传输协议进行通信则客户端和服务端通信之前先要经过 TCP 三次握手后建立完可靠的 TCP 连接后客户端才能将数据发送给服务端。 其中TCP 的第一次和第二次握手是不能够携带数据的而 TCP 的第三次握手是可以携带数据的因为这时候客户端的 TCP 连接状态已经是 ESTABLISHED表明客户端这一方已经完成了 TCP 连接建立。 就算客户端携带数据的第三次握手在网络中丢失了客户端在一定时间内没有收到服务端对该数据的应答报文就会触发超时重传机制然后客户端重传该携带数据的第三次握手的报文直到重传次数达到系统的阈值客户端就会销毁该 TCP 连接。 说完常规的 TCP 连接后我们再来看看 TCP Fast Open。 TCP Fast Open 是为了绕过 TCP 三次握手发送数据在 Linux 3.7 内核版本之后提供了 TCP Fast Open 功能这个功能可以减少 TCP 连接建立的时延。 要使用 TCP Fast Open 功能客户端和服务端都要同时支持才会生效。 不过开启了 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 :::info 说完 TCP Fast Open再来看看 TLSv1.3。 ::: 在最开始的时候我也提到 TLSv1.3 握手过程只需 1-RTT 的时间它到整个握手过程如下图 TCP 连接的第三次握手是可以携带数据的如果客户端在第三次握手发送了 TLSv1.3 第一次握手数据是不是就表示「HTTPS 中的 TLS 握手过程可以同时进行三次握手」。 不是的因为服务端只有在收到客户端的 TCP 的第三次握手后才能和客户端进行后续 TLSv1.3 握手。 TLSv1.3 还有个更厉害到地方在于会话恢复机制在重连 TLvS1.3 只需要 0-RTT用“pre_shared_key”和“early_data”扩展在 TCP 连接后立即就建立安全连接发送加密消息过程如下图 在前面我们知道客户端和服务端同时支持 TCP Fast Open 功能的情况下在第二次以后到通信过程中客户端可以绕过三次握手直接发送数据而且服务端也不需要等收到第三次握手后才发送数据。 如果 HTTPS 的 TLS 版本是 1.3那么 TLS 过程只需要 1-RTT。 因此如果「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 协议采用的是「请求-应答」的模式也就是客户端发起了请求服务端才会返回响应一来一回这样子。 由于 HTTP 是基于 TCP 传输协议实现的客户端与服务端要进行 HTTP 通信前需要先建立 TCP 连接然后客户端发送 HTTP 请求服务端收到后就返回响应至此「请求-应答」的模式就完成了随后就会释放 TCP 连接。 TCP 的 Keep-Alive TCP 的 Keepalive 这东西其实就是 TCP 的保活机制它的工作原理我之前的文章写过这里就直接贴下以前的内容。 如果两端的 TCP 连接一直没有数据交互达到了触发 TCP 保活机制的条件那么内核里的 TCP 协议栈就会发送探测报文。 如果对端程序是正常工作的。当 TCP 保活的探测报文发送给对端, 对端会正常响应这样 TCP 保活时间会被重置等待下一个 TCP 保活时间的到来。 如果对端主机宕机注意不是进程崩溃进程崩溃后操作系统在回收进程资源的时候会发送 FIN 报文而主机宕机则是无法感知的所以需要 TCP 保活机制来探测对方是不是发生了主机宕机或对端由于其他原因导致报文不可达。当 TCP 保活的探测报文发送给对端后石沉大海没有响应连续几次达到保活探测次数后TCP 会报告该 TCP 连接已经死亡。 所以TCP 保活机制可以在双方没有数据交互的情况通过探测报文来确定对方的 TCP 连接是否存活这个工作是在内核完成的。 TCP 协议有什么缺陷 主要有四个方面 升级 TCP 的工作很困难TCP 建立连接的延迟TCP 存在队头阻塞问题网络迁移需要重新建立 TCP 连接 接下来针对这四个方面详细说一下。 升级 TCP 的工作很困难 TCP 协议是诞生在 1973 年至今 TCP 协议依然还在实现更多的新特性。 但是 TCP 协议是在内核中实现的应用程序只能使用不能修改如果要想升级 TCP 协议那么只能升级内核。 而升级内核这个工作是很麻烦的事情麻烦的事情不是说升级内核这个操作很麻烦而是由于内核升级涉及到底层软件和运行库的更新我们的服务程序就需要回归测试是否兼容新的内核版本所以服务器的内核升级也比较保守和缓慢。 很多 TCP 协议的新特性都是需要客户端和服务端同时支持才能生效的比如 TCP Fast Open 这个特性虽然在2013 年就被提出了但是 Windows 很多系统版本依然不支持它这是因为 PC 端的系统升级滞后很严重Windows Xp 现在还有大量用户在使用尽管它已经存在快 20 年。 所以即使 TCP 有比较好的特性更新也很难快速推广用户往往要几年或者十年才能体验到。 TCP 建立连接的延迟 基于 TCP 实现的应用协议都是需要先建立三次握手才能进行数据传输比如 HTTP 1.0/1.1、HTTP/2、HTTPS。 现在大多数网站都是使用 HTTPS 的这意味着在 TCP 三次握手之后还需要经过 TLS 四次握手后才能进行 HTTP 数据的传输这在一定程序上增加了数据传输的延迟。 TCP 三次握手的延迟被 TCP Fast Open 快速打开这个特性解决了这个特性可以在「第二次建立连接」时减少 TCP 连接建立的时延。 TCP Fast Open 这个特性是不错但是它需要服务端和客户端的操作系统同时支持才能体验到而 TCP Fast Open 是在 2013 年提出的所以市面上依然有很多老式的操作系统不支持而升级操作系统是很麻烦的事情因此 TCP Fast Open 很难被普及开来。 还有一点针对 HTTPS 来说TLS 是在应用层实现的握手而 TCP 是在内核实现的握手这两个握手过程是无法结合在一起的总是得先完成 TCP 握手才能进行 TLS 握手。 也正是 TCP 是在内核实现的所以 TLS 是无法对 TCP 头部加密的这意味着 TCP 的序列号都是明文传输所以就存安全的问题。 一个典型的例子就是攻击者伪造一个的 RST 报文强制关闭一条 TCP 连接而攻击成功的关键则是 TCP 字段里的序列号位于接收方的滑动窗口内该报文就是合法的。 为此 TCP 也不得不进行三次握手来同步各自的序列号而且初始化序列号时是采用随机的方式不完全随机而是随着时间流逝而线性增长到了 2^32 尽头再回滚来提升攻击者猜测序列号的难度以增加安全性。 但是这种方式只能避免攻击者预测出合法的 RST 报文而无法避免攻击者截获客户端的报文然后中途伪造出合法 RST 报文的攻击的方式。 TCP 存在队头阻塞问题 TCP 是字节流协议TCP 层必须保证收到的字节数据是完整且有序的如果序列号较低的 TCP 段在网络传输中丢失了即使序列号较高的 TCP 段已经被接收了应用层也无法从内核中读取到这部分数据。如下图 网络迁移需要重新建立 TCP 连接 基于 TCP 传输协议的 HTTP 协议由于是通过四元组源 IP、源端口、目的 IP、目的端口确定一条 TCP 连接。 那么当移动设备的网络从 4G 切换到 WIFI 时意味着 IP 地址变化了那么就必须要断开连接然后重新建立 TCP 连接。 而建立连接的过程包含 TCP 三次握手和 TLS 四次握手的时延以及 TCP 慢启动的减速过程给用户的感觉就是网络突然卡顿了一下因此连接的迁移成本是很高的。 如何基于 UDP 协议实现可靠传输 现在市面上已经有基于 UDP 协议实现的可靠传输协议的成熟方案了那就是 QUIC 协议已经应用在了 HTTP/3。 QUIC 是如何实现可靠传输的 要基于 UDP 实现的可靠传输协议那么就要在应用层下功夫也就是要设计好协议的头部字段。 拿 HTTP/3 举例子在 UDP 报文头部与 HTTP 消息之间共有 3 层头部 Packet Header Packet Header 首次建立连接时和日常传输数据时使用的 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 大的值。 :::info 为什么要这么设计呢 ::: 我们先来看看 TCP 的问题TCP 在重传报文时的序列号和原始报文的序列号是一样的也正是由于这个特性引入了 TCP 重传的歧义问题。 如果算成原始请求的响应但实际上是重传请求的响应上图左会导致采样 RTT 变大。如果算成重传请求的响应但实际上是原始请求的响应上图右又很容易导致采样 RTT 过小。 QUIC 报文中的 Pakcet Number 是严格递增的 即使是重传报文它的 Pakcet Number 也是递增的这样就能更加精确计算出报文的 RTT。 另外还有一个好处QUIC 使用的 Packet Number 单调递增的设计可以让数据包不再像 TCP 那样必须有序确认QUIC 支持乱序确认当数据包Packet N 丢失后只要有新的已接收数据包确认当前窗口就会继续向右滑动。 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 N2丢失的数据包和重传的数据包** Stream ID 与 Offset 都一致说明这两个数据包的内容一致。**这些数据包传输到接收端后接收端能根据 Stream ID 与 Offset 字段信息将 Stream x 和 Stream xy 按照顺序组织起来然后交给应用程序处理。 总的来说QUIC 通过单向递增的 Packet Number配合 Stream ID 与 Offset 字段信息可以支持乱序确认而不影响数据包的正确组装摆脱了TCP 必须按顺序确认应答 ACK 的限制解决了 TCP 因某个数据包重传而阻塞后续所有待发送数据包的问题。 QUIC 是如何解决 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 之间没有依赖关系都是相互独立的各自控制的滑动窗口。 假如 Stream2 丢了一个 UDP 包也只会影响 Stream2 的处理不会影响其他 Stream与 HTTP/2 不同HTTP/2 只要某个流中的数据包丢失了其他流也会因此受影响。 QUIC 是如何做流量控制的 QUIC 实现流量控制的方式 通过 window_update 帧告诉对端自己可以接收的字节数这样发送方就不会发送超过这个数量的数据。通过 BlockFrame 告诉对端由于流量控制被阻塞了无法发送数据。 在前面说到TCP 的接收窗口在收到有序的数据后接收窗口才能往前滑动否则停止滑动。 QUIC 是基于 UDP 传输的而 UDP 没有流量控制因此 QUIC 实现了自己的流量控制机制QUIC 的滑动窗口滑动的条件跟 TCP 有一点差别但是同一个 Stream 的数据也是要保证顺序的不然无法实现可靠传输因此同一个 Stream 的数据包丢失了也会造成窗口无法滑动。 **QUIC 的 每个 Stream 都有各自的滑动窗口不同 Stream 互相独立队头的 Stream A 被阻塞后不妨碍 StreamB、C的读取。**而对于 HTTP/2 而言所有的 Stream 都跑在一条 TCP 连接上而这些 Stream 共享一个滑动窗口因此同一个Connection内Stream A 被阻塞后StreamB、C 必须等待。 QUIC 实现了两种级别的流量控制分别为 Stream 和 Connection 两种级别 **Stream 级别的流量控制**Stream 可以认为就是一条 HTTP 请求每个 Stream 都有独立的滑动窗口所以每个 Stream 都可以做流量控制防止单个 Stream 消耗连接Connection的全部接收缓冲。**Connection 流量控制**限制连接中所有 Stream 相加起来的总字节数防止发送方超过连接的缓冲容量。 Stream 级别的流量控制 最开始接收方的接收窗口初始状态如下 接着接收方收到了发送方发送过来的数据有的数据被上层读取了有的数据丢包了此时的接收窗口状况如下 可以看到接收窗口的左边界取决于接收到的最大偏移字节数此时的接收窗口 最大窗口数 - 接收到的最大偏移数。 这里就可以看出 QUIC 的流量控制和 TCP 有点区别了 TCP 的接收窗口只有在前面所有的 Segment 都接收的情况下才会移动左边界当在前面还有字节未接收但收到后面字节的情况下窗口也不会移动。QUIC 的接收窗口的左边界滑动条件取决于接收到的最大偏移字节数。 那接收窗口右边界触发的滑动条件是什么呢看下图 在前面我们说过 QUIC 支持乱序确认具体是怎么做到的呢 接下来举个例子 如图所示当前发送方的缓冲区大小为8发送方 QUIC 按序offset顺序发送 29-36 的数据包 31、32、34数据包先到达基于 offset 被优先乱序确认但 30 数据包没有确认所以当前已提交的字节偏移量不变发送方的缓存区不变。 30 到达并确认发送方的缓存区收缩到阈值接收方发送 MAX_STREAM_DATA Frame协商缓存大小的特定帧给发送方请求增长最大绝对字节偏移量。 协商完毕后最大绝对字节偏移量右移发送方的缓存区变大同时发送方发现数据包33超时 发送方将超时数据包重新编号为 42 继续发送 Connection 流量控制 而对于 Connection 级别的流量窗口其接收窗口大小就是各个 Stream 接收窗口大小之和。 QUIC 对拥塞控制改进 QUIC 协议当前默认使用了 TCP 的 Cubic 拥塞控制算法我们熟知的慢开始、拥塞避免、快重传、快恢复策略同时也支持 CubicBytes、Reno、RenoBytes、BBR、PCC 等拥塞控制算法相当于将 TCP 的拥塞控制算法照搬过来了。 QUIC 是如何改进 TCP 的拥塞控制算法的呢 QUIC 是处于应用层的应用程序层面就能实现不同的拥塞控制算法不需要操作系统不需要内核支持。这是一个飞跃因为传统的 TCP 拥塞控制必须要端到端的网络协议栈支持才能实现控制效果。而内核和操作系统的部署成本非常高升级周期很长所以 TCP 拥塞控制算法迭代速度是很慢的。而 QUIC 可以随浏览器更新QUIC 的拥塞控制算法就可以有较快的迭代速度。 TCP 更改拥塞控制算法是对系统中所有应用都生效无法根据不同应用设定不同的拥塞控制策略。但是因为 QUIC 处于应用层所以就可以针对不同的应用设置不同的拥塞控制算法这样灵活性就很高了。 QUIC 更快的连接建立 HTTP/3 在传输数据前虽然需要 QUIC 协议握手这个握手过程只需要 1 RTT握手的目的是为确认双方的「连接 ID」连接迁移就是基于连接 ID 实现的。 但是 HTTP/3 的 QUIC 协议并不是与 TLS 分层而是QUIC 内部包含了 TLS它在自己的帧会携带 TLS 里的“记录”再加上 QUIC 使用的是 TLS1.3因此仅需 1 个 RTT 就可以「同时」完成建立连接与密钥协商甚至在第二次连接的时候应用数据包可以和 QUIC 握手信息连接信息 TLS 信息一起发送达到 0-RTT 的效果。 QUIC 是如何迁移连接的 QUIC 协议没有用四元组的方式来“绑定”连接而是通过连接 ID来标记通信的两个端点客户端和服务器可以各自选择一组 ID 来标记自己因此即使移动设备的网络变化后导致 IP 地址变化了只要仍保有上下文信息比如连接 ID、TLS 密钥等就可以“无缝”地复用原连接消除重连的成本没有丝毫卡顿感达到了连接迁移的功能。 服务端没有 listen客户端发起连接建立会发生什么 服务端如果只 bind 了 IP 地址和端口而没有调用 listen 的话然后客户端对服务端发起了连接建立服务端会回 RST 报文。 没有 listen能建立 TCP 连接吗 答案是可以的客户端是可以自己连自己的形成连接TCP自连接也可以两个客户端同时向对方发出请求建立连接TCP同时打开这两个情况都有个共同点就是没有服务端参与也就是没有listen就能建立连接。 :::info 那没有listen为什么还能建立连接 ::: 我们知道执行 listen 方法时会创建半连接队列和全连接队列。 三次握手的过程中会在这两个队列中暂存连接信息。 所以形成连接前提是你得有个地方存放着方便握手的时候能根据 IP 端口等信息找到对应的 socket。 :::info 那么客户端会有半连接队列吗 ::: 显然没有因为客户端没有执行listen因为半连接队列和全连接队列都是在执行 listen 方法时内核自动创建的。 但内核还有个全局 hash 表可以用于存放 sock 连接的信息。 这个全局 hash 表其实还细分为 ehashbhash和listen_hash等但因为过于细节大家理解成有一个全局 hash 就够了 在 TCP 自连接的情况中客户端在 connect 方法时最后会将自己的连接信息放入到这个全局 hash 表中然后将信息发出消息在经过回环地址重新回到 TCP 传输层的时候就会根据 IP 端口信息再一次从这个全局 hash 中取出信息。于是握手包一来一回最后成功建立连接。 TCP 同时打开的情况也类似只不过从一个客户端变成了两个客户端而已。 没有 accept能建立 TCP 连接吗 建立连接的过程中根本不需要accept()参与 执行accept()只是为了从全连接队列里取出一条连接。 虽然都叫队列但其实全连接队列icsk_accept_queue是个链表而半连接队列syn_table是个哈希表。 为什么半连接队列要设计成哈希表 先对比下全连接队列他本质是个链表因为也是线性结构说它是个队列也没毛病。它里面放的都是已经建立完成的连接这些连接正等待被取走。而服务端取走连接的过程中并不关心具体是哪个连接只要是个连接就行所以直接从队列头取就行了。这个过程算法复杂度为O(1)。 而半连接队列却不太一样因为队列里的都是不完整的连接嗷嗷等待着第三次握手的到来。 那么现在有一个第三次握手来了则需要从队列里把相应IP端口的连接取出如果半连接队列还是个链表那我们就需要依次遍历才能拿到我们想要的那个连接算法复杂度就是O(n)。 而如果将半连接队列设计成哈希表那么查找半连接的算法复杂度就回到**O(1)**了。 因此出于效率考虑全连接队列被设计成链表而半连接队列被设计为哈希表。 半连接队列要是满了会怎么样 一般是丢弃但这个行为可以通过 tcp_syncookies 参数去控制。 会有一个cookies队列吗 我们可以反过来想一下如果有cookies队列那它会跟半连接队列一样到头来还是会被SYN Flood 攻击打满。 实际上cookies并不会有一个专门的队列保存它是通过通信双方的IP地址端口、时间戳、MSS等信息进行实时计算的保存在TCP报头的seq里。 当服务端收到客户端发来的第三次握手包时会通过seq还原出通信双方的IP地址端口、时间戳、MSS验证通过则建立连接。 cookies方案为什么不直接取代半连接队列 目前看下来syn cookies方案省下了半连接队列所需要的队列内存还能解决** SYN Flood攻击**那为什么不直接取代半连接队列 凡事皆有利弊cookies方案虽然能防 SYN Flood攻击但是也有一些问题。因为服务端并不会保存连接信息所以如果传输过程中数据包丢了也不会重发第二次握手的信息。 另外编码解码cookies都是比较耗CPU的利用这一点如果此时攻击者构造大量的第三次握手包ACK包同时带上各种瞎编的cookies信息服务端收到ACK包后以为是正经cookies憨憨地跑去解码耗CPU最后发现不是正经数据包后才丢弃。 这种通过构造大量ACK包去消耗服务端资源的攻击叫ACK攻击受到攻击的服务器可能会因为CPU资源耗尽导致没能响应正经请求。 用了 TCP 协议数据一定不会丢吗 建立连接时丢包 在服务端第一次握手之后会先建立个半连接然后再发出第二次握手。这时候需要有个地方可以暂存这些半连接。这个地方就叫半连接队列。 如果之后第三次握手来了半连接就会升级为全连接然后暂存到另外一个叫全连接队列的地方坐等程序执行accept()方法将其取走使用。 是队列就有长度有长度就有可能会满如果它们满了那新来的包就会被丢弃。 流量控制丢包 应用层能发网络数据包的软件有那么多如果所有数据不加控制一股脑冲入到网卡网卡会吃不消那怎么办 让数据按一定的规则排个队依次处理也就是所谓的qdisc(Queueing Disciplines排队规则)这也是我们常说的流量控制机制。 排队得先有个队列而队列有个长度。 我们可以通过下面的ifconfig命令查看到里面涉及到的txqueuelen后面的数字1000其实就是流控队列的长度。 当发送数据过快流控队列长度txqueuelen又不够大时就容易出现丢包现象。 当遇到这种情况时我们可以尝试修改下流控队列的长度。比如像下面这样将eth0网卡的流控队列长度从1000提升为1500。 网卡丢包 RingBuffer过小导致丢包 上面提到在接收数据时会将数据暂存到RingBuffer接收缓冲区中然后等着内核触发软中断慢慢收走。如果这个缓冲区过小而这时候发送的数据又过快就有可能发生溢出此时也会产生丢包。 网卡性能不足 网卡作为硬件传输速度是有上限的。当网络传输速度过大达到网卡上限时就会发生丢包。这种情况一般常见于压测场景。 接收缓冲区丢包 我们一般使用TCP socket进行网络编程的时候内核都会分配一个发送缓冲区和一个接收缓冲区。 当我们想要发一个数据包会在代码里执行send(msg)这时候数据包并不是一把梭直接就走网卡飞出去的。而是将数据拷贝到内核发送缓冲区就完事返回了至于什么时候发数据发多少数据这个后续由内核自己做决定。 而接收缓冲区作用也类似从外部网络收到的数据包就暂存在这个地方然后坐等用户空间的应用程序将数据包取走。 这两个缓冲区是有大小限制的。 那么问题来了如果缓冲区设置过小会怎么样 ** 对于发送缓冲区**执行send的时候如果是阻塞调用那就会等等到缓冲区有空位可以发数据。 如果是非阻塞调用就会立刻返回一个 EAGAIN 错误信息意思是 Try again。让应用程序下次再重试。这种情况下一般不会发生丢包。 当接受缓冲区满了事情就不一样了它的TCP接收窗口会变为0也就是所谓的零窗口并且会通过数据包里的win0告诉发送端“球球了顶不住了别发了”。一般这种情况下发送端就该停止发消息了但如果这时候确实还有数据发来就会发生丢包。 两端之间的网络丢包 前面提到的是两端机器内部的网络丢包除此之外两端之间那么长的一条链路都属于外部网络这中间有各种路由器和交换机还有光缆啥的丢包也是很经常发生的。 这些丢包行为发生在中间链路的某些个机器上我们当然是没权限去登录这些机器。但我们可以通过一些命令观察整个链路的连通情况 ping命令查看丢包 倒数第二行里有个100% packet loss意思是丢包率100%。 但这样其实你只能知道你的机器和目的机器之间有没有丢包。 那如果你想知道你和目的机器之间的这条链路哪个节点丢包了有没有办法呢? mtr命令 mtr命令可以查看到你的机器和目的机器之间的每个节点的丢包情况。 发生丢包了怎么办 这个好办用TCP协议去做传输。 用了TCP协议就一定不会丢包吗 TCP保证的可靠性是传输层的可靠性。也就是说TCP只保证数据从A机器的传输层可靠地发到B机器的传输层。 至于数据到了接收端的传输层之后能不能保证到应用层TCP并不管。 假设现在我们输入一条消息从聊天框发出走到传输层TCP协议的发送缓冲区不管中间有没有丢包最后通过重传都保证发到了对方的传输层TCP接收缓冲区此时接收端回复了一个ack发送端收到这个ack后就会将自己发送缓冲区里的消息给扔掉。到这里TCP的任务就结束了。 TCP任务是结束了但聊天软件的任务没结束。 **聊天软件还需要将数据从TCP的接收缓冲区里读出来如果在读出来这一刻手机由于内存不足或其他各种原因导致软件崩溃闪退了。 ** 发送端以为自己发的消息已经发给对方了但接收端却并没有收到这条消息。 于是乎消息就丢了。 虽然概率很小但它就是发生了。 这类丢包问题怎么解决 现在我们重新将服务器加回来。 大家有没有发现有时候我们在手机里聊了一大堆内容然后登录电脑版它能将最近的聊天记录都同步到电脑版上。也就是说服务器可能记录了我们最近发过什么数据假设每条消息都有个id服务器和聊天软件每次都拿最新消息的id进行对比就能知道两端消息是否一致就像对账一样。 对于发送方只要定时跟服务端的内容对账一下就知道哪条消息没发送成功直接重发就好了。 如果接收方的聊天软件崩溃了重启后跟服务器稍微通信一下就知道少了哪条数据同步上来就是了所以也不存在上面提到的丢包情况。 **可以看出TCP只保证传输层的消息可靠性并不保证应用层的消息可靠性。如果我们还想保证应用层的消息可靠性就需要应用层自己去实现逻辑做保证。 ** 那么问题叒来了两端通信的时候也能对账为什么还要引入第三端服务器 主要有三个原因。 第一如果是两端通信你聊天软件里有1000个好友你就得建立1000个连接**。但如果引入服务端你只需要跟服务器建立1个连接就够了聊天软件消耗的资源越少手机就越省电。**第二就是安全问题如果还是两端通信随便一个人找你对账一下你就把聊天记录给同步过去了这并不合适吧。如果对方别有用心信息就泄露了。引入第三方服务端就可以很方便的做各种鉴权校验。第三是软件版本问题。软件装到用户手机之后软件更不更新就是由用户说了算了。如果还是两端通信且两端的软件版本跨度太大很容易产生各种兼容性问题但引入第三端服务器就可以强制部分过低版本升级否则不能使用软件。但对于大部分兼容性问题给服务端加兼容逻辑就好了不需要强制用户更新软件。 TCP 序列号和确认号是如何变化的 万能公式 发送的 TCP 报文 **公式一序列号 上一次发送的序列号 len数据长度。特殊情况如果上一次发送的报文是 SYN 报文或者 FIN 报文则改为 上一次发送的序列号 1。 **公式二确认号 上一次收到的报文中的序列号 len数据长度。特殊情况如果收到的是 SYN 报文或者 FIN 报文则改为上一次收到的报文中的序列号 1。 三次握手阶段的变化 数据传输阶段的变化 可以看到客户端与服务端完成 TCP 三次握手后发送的第一个 「TCP 数据报文的序列号和确认号」都是和「第三次握手的 ACK 报文中序列号和确认号」一样的。 四次挥手阶段的变化 公式一序列号 上一次发送的序列号 len数据长度。特殊情况如果上一次发送的报文是 SYN 报文或者 FIN 报文则改为 上一次发送的序列号 1。公式二确认号 上一次收到的报文中的序列号 len数据长度。特殊情况如果收到的是 SYN 报文或者 FIN 报文则改为上一次收到的报文中的序列号 1。 参考小林coding
http://www.w-s-a.com/news/435521/

相关文章:

  • 泰安网站建设制作电话号码百度sem竞价托管公司
  • 苏网站建设网页设计和网页美工
  • 跨境电商平台网站广州地铁站路线图
  • 吉林省交通建设集团有限公司网站企业网站推广的策略有哪些
  • 网站内链怎么做更好郑州网站建设哪家便宜
  • 建设大型购物网站运城哪里做网站
  • php企业网站通讯录管理系统做网站在线支付系统多少钱?
  • 怎么区分用vs和dw做的网站贝贝网网站开发背景
  • 无锡网站建设制作建设信息网查询
  • 彩票系统网站开发建设人力资源网官网
  • 有专门下载地图做方案的网站吗网站建设平台计划书
  • 网站闭站保护10个著名摄影网站
  • 安徽省建设工程信息网官网首页网站关键词排名优化工具
  • 深圳网站建设 百业网站专题教程
  • 公司seo是指什么意思如何来做网站优化
  • 化妆品网站建设平台的分析湖南网站搜索排名优化电话
  • 织梦网站修改教程视频教程管理类网站开发价格
  • 如何让新网站快速收录企业建站的作用是什么
  • 在线制作简历的网站做的最好的微电影网站
  • h5制作的网站网络游戏投诉平台
  • 做外贸网站好还是内贸网站好珠海新盈科技有限公 网站建设
  • php和网站开发网络软营销
  • 大型做网站的公司有哪些wordpress注册链接无效
  • 推荐门户网站建设公司网站开发移动端
  • 公司网站的栏目设置成都十大监理公司排名
  • 安溪住房和城乡建设网站关岭县建设局网站
  • 网站域名注销备案徐州房产网
  • 筑聘网windows优化大师自动安装
  • 龙华高端网站设计门户网站建设方案公司
  • 网站开发作用网站建设哪家专业