| |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
-> 网络协议 -> 【网络协议】万文长篇,带你深入理解 TCP;场景复现,掌握鲜为人知的细节(下) -> 正文阅读 |
|
[网络协议]【网络协议】万文长篇,带你深入理解 TCP;场景复现,掌握鲜为人知的细节(下) |
文章目录前言由于内容细致,导致篇幅过长,因此将分为三部分来讲述,目录如下: 【网络协议】万文长篇,带你深入理解 TCP;场景复现,掌握鲜为人知的细节(上)
【网络协议】万文长篇,带你深入理解 TCP;场景复现,掌握鲜为人知的细节(中)
【网络协议】万文长篇,带你深入理解 TCP;场景复现,掌握鲜为人知的细节(下)
? 握手失败? 第一次握手丢失了,会发生什么?当客户端想和服务端建立 TCP 连接的时候,首先第一个发的就是 SYN 报文,然后进入到 在这之后,如果客户端迟迟收不到服务端的 SYN-ACK 报文(第二次握手),就会触发超时重传机制。 不同版本的操作系统可能超时时间不同,有的 1 秒的,也有 3 秒的,这个超时时间是写死在内核里的,如果想要更改则需要重新编译内核,比较麻烦。 当客户端在 1 秒后没收到服务端的 SYN-ACK 报文后,客户端就会重发 SYN 报文,那到底重发几次呢? 在 Linux 里,客户端的 SYN 报文最大重传次数由
通常,第一次超时重传是在 1 秒后,第二次超时重传是在 2 秒,第三次超时重传是在 4 秒后,第四次超时重传是在 8 秒后,第五次是在超时重传 16 秒后。没错,每次超时的时间是上一次的 2 倍。 当第五次超时重传后,会继续等待 32 秒,如果服务端仍然没有回应 ACK,客户端就不再发送 SYN 包,然后断开 TCP 连接。 所以,总耗时是 1+2+4+8+16+32=63 秒,大约 1 分钟左右。 ? 场景复现 在服务端先 ban 掉客户端的 IP 可以自己设置
? 第二次握手丢失了,会发生什么?当服务端收到客户端的第一次握手后,就会回 第二次握手的
所以,如果第二次握手丢了,就会发送比较有意思的事情,具体会怎么样呢? 因为第二次握手报文里是包含对客户端的第一次握手的 ACK 确认报文,所以,如果客户端迟迟没有收到第二次握手,那么客户端就觉得可能自己的 SYN 报文(第一次握手)丢失了,于是客户端就会触发超时重传机制,重传 SYN 报文。 然后,因为第二次握手中包含服务端的 SYN 报文,所以当客户端收到后,需要给服务端发送 ACK 确认报文(第三次握手),服务端才会认为该 SYN 报文被客户端收到了。 那么,如果第二次握手丢失了,服务端就收不到第三次握手,于是服务端这边会触发超时重传机制,重传 SYN-ACK 报文。 在 Linux 下,SYN-ACK 报文的最大重传次数由 因此,当第二次握手丢失了,客户端和服务端都会重传:
? 这时候解除服务端 ban 掉的 客户端的IP 客户端设置了防火墙,屏蔽了服务端的网络包,为什么 tcpdump 还能抓到服务端的网络包? 添加 iptables 限制后, tcpdump 是否能抓到包 ,这要看添加的 iptables 限制条件:
网络包进入主机后的顺序如下:
? 第三次握手丢失了,会发生什么?客户端收到服务端的 SYN-ACK 报文后,就会给服务端回一个 ACK 报文,也就是第三次握手,此时客户端状态进入到 因为这个第三次握手的 ACK 是对第二次握手的 SYN 的确认报文,所以当第三次握手丢失了,如果服务端那一方迟迟收不到这个确认报文,就会触发超时重传机制,重传 SYN-ACK 报文,直到收到第三次握手,或者达到最大重传次数。 注意,ACK 报文是不会有重传的,当 ACK 丢失了,就由对方重传对应的报文。
? 挥手失败? 第一次挥手丢失了,会发生什么?当客户端(主动关闭方)调用 close 函数后,就会向服务端发送 FIN 报文,试图与服务端断开连接,此时客户端的连接进入到 正常情况下,如果能及时收到服务端(被动关闭方)的 ACK,则会很快变为 如果第一次挥手丢失了,那么客户端迟迟收不到被动方的 ACK 的话,也就会触发超时重传机制,重传 FIN 报文,重发次数由 当客户端重传 FIN 报文的次数超过 ? 第二次挥手丢失了,会发生什么?当服务端收到客户端的第一次挥手后,就会先回一个 ACK 确认报文,此时服务端的连接进入到 在前面我们也提了,ACK 报文是不会重传的,所以如果服务端的第二次挥手丢失了,客户端就会触发超时重传机制,重传 FIN 报文,直到收到服务端的第二次挥手,或者达到最大的重传次数。 这里提一下,当客户端收到第二次挥手,也就是收到服务端发送的 ACK 报文后,客户端就会处于 对于 close 函数关闭的连接,由于无法再发送和接收数据,所以 这意味着对于调用 close 关闭的连接,如果在 60 秒后还没有收到 FIN 报文,客户端(主动关闭方)的连接就会直接关闭。 ? 第三次挥手丢失了,会发生什么?当服务端(被动关闭方)收到客户端(主动关闭方)的 FIN 报文后,内核会自动回复 ACK,同时连接处于 此时,内核是没有权利替代进程关闭连接,必须由进程主动调用 close 函数来触发服务端发送 FIN 报文。 服务端处于 如果迟迟收不到这个 ACK,服务端就会重发 FIN 报文,重发次数仍然由
? 第四次挥手丢失了,会发生什么?当客户端收到服务端的第三次挥手的 FIN 报文后,就会回 ACK 报文,也就是第四次挥手,此时客户端连接进入 在 Linux 系统, 然后,服务端(被动关闭方)没有收到 ACK 报文前,还是处于 如果第四次挥手的 ACK 报文没有到达服务端,服务端就会重发 FIN 报文,重发次数仍然由前面介绍过的 为什么是三次握手?现在耳熟能详的 TCP 连接就是三次握手,四次挥手,那么你有想过 为什么是三次握手,而不是两次或者四次呢? 相信比较平常回答的是:“因为三次握手才能保证双方具有接收和发送的能力”。这样的回答是没问题的,但是这回答是片面的,并没有说出主要的原因。 在前面我们知道了什么是 TCP 连接:用于保证可靠性和流量控制维护的某些状态信息,这些信息的组合,包括 Socket、序列号和窗口大小称为连接。 所以,重要的是为什么三次握手才可以初始化 Socket、序列号和窗口大小并建立 TCP 连接。 接下来以三个方面分析三次握手的原因:
? 原因一:避免历史连接RFC 793 指出的 TCP 连接使用三次握手的首要原因:
简单来说,三次握手的首要原因是为了防止旧的重复连接初始化造成混乱。 网络环境是错综复杂的,往往并不是如我们期望的一样,先发送的数据包,就先到达目标主机;可能会由于网络拥堵等乱七八糟的原因,会使得旧的数据包,先到达目标主机。那么这种情况下 TCP 三次握手是如何避免的呢? 客户端连续发送多次 SYN 建立连接的报文,在网络拥堵情况下:
如果是两次握手连接,就不能判断当前连接是否是历史连接,三次握手则可以在客户端(发送方)准备发送第三次报文时,客户端因有足够的上下文来判断当前连接是否是历史连接:
所以,TCP 使用三次握手建立连接的最主要原因是防止历史连接初始化了连接。 ? 原因二:同步双方初始序列号TCP 协议的通信双方, 都必须维护一个「序列号」, 序列号是可靠传输的一个关键因素,它的作用:
可见,序列号在 TCP 连接中占据着非常重要的作用,所以当客户端发送携带「初始序列号」的 SYN 报文的时候,需要服务端回一个 ACK 应答报文,表示客户端的 SYN 报文已被服务端成功接收,那当服务端发送「初始序列号」给客户端的时候,依然也要得到客户端的应答回应,这样一来一回,才能确保双方的初始序列号能被可靠的同步。 四次握手与三次握手: 四次握手其实也能够可靠的同步双方的初始化序号,但由于第二步和第三步可以优化成一步,所以就成了「三次握手」。 而两次握手只保证了一方的初始序列号能被对方成功接收,没办法保证双方的初始序列号都能被确认接收。 ? 原因三:避免资源浪费如果只有「两次握手」,当客户端的 SYN 请求连接在网络中阻塞,客户端没有接收到 ACK 报文,就会重新发送 SYN ,由于没有第三次握手,服务器不清楚客户端是否收到了自己发送的建立连接的 ACK 确认信号,所以每收到一个 SYN 就只能先主动建立一个连接,这会造成什么情况呢? 如果客户端的 SYN 阻塞了,重复发送多次 SYN 报文,那么服务器在收到请求后就会建立多个冗余的无效链接,造成不必要的资源浪费。 两次握手会造成资源浪费: 即两次握手会造成消息滞留情况下,服务器重复接受无用的连接请求 SYN 报文,而造成重复分配资源。 ? 小结TCP 建立连接时,通过三次握手能:
不使用「两次握手」和「四次握手」的原因:
? 如何避免 SYN 攻击?? 什么是 SYN 攻击?在了解如何避免 SYN 攻击前,我们得先了解什么是 SYN 攻击; 我们都知道 TCP 连接建立是需要三次握手,假设攻击者短时间伪造不同 IP 地址的 SYN 报文,服务端每接收到一个 SYN 报文,就进入 也就是上面曾讲述的 第三次握手失败; 常用的工具有 LOIC,Hping3 等,演示攻击如下: 因为虚拟机的配置低,所以很容易就实现 CPU 爆满的状况了,这时候服务器就很难响应其他服务请求,更严重的可能会直接宕机; ? 避免 SYN 攻击方式一第一种解决方式是通过修改 Linux 内核参数,控制队列大小和当队列满时应做什么处理。 当网卡接收数据包的速度大于内核处理的速度时,会有一个队列保存这些数据包。
? 避免 SYN 攻击方式二我们先来看下 Linux 内核的 SYN (未完成连接建立)队列与 Accpet (已完成连接建立)队列是如何工作的: 1、正常流程;
2、应用程序过慢; 如果应用程序过慢时,就会导致「 Accept 队列」被占满。 3、受到 SYN 攻击; 如果不断受到 SYN 攻击,就会导致「 SYN 队列」被占满。
? MTU 与 MSS 那些事儿最大报文段长度 MSS 与 最大传输单元 MTU 均是协议用来定义最大长度的。不同的是,MTU 应用于 OSI 模型的第二层数据链接层,并无具体针对的协议,限制了数据链接层上可以传输的数据包的大小,也因此限制了上层(网络层)的数据包大小;MSS 针对的是 OSI 模型里的第四层传输层的 TCP 协议,因为 MSS 应用的协议在数据链接层的上层,所以 MSS 会受到 MTU 的限制。
那么问题来了,为什么由 Wireshark 抓到的数据包的 既然在握手阶段就协商了 原来在实际场景中,TCP 包头中会带有12字节的选项,时间戳(Timestamps),这样,单个 TCP 包实际传输的最大量就缩减为1448字节了,如下所示: ? 那 MTU = MSS + IP 头长度 + TCP 头长度, 这是根据以太网帧结构所决定的, 在不选择填充 802.1Q 标签,负载拉满(即为 MTU 值)的前提下,以太网的最大帧大小应该是 前导码+帧开始符+MAC目标地址+MAC源地址+以太类型+负载+冗余校验 = 7+1+6+6+2+1500+4 = 1526 字节,那为什么 Wireshark 抓来的数据包的最大帧却只有1514字节呢? 原来是因为当数据帧到达网卡时,在物理层上网卡要先去掉前导码和帧开始定界符,然后对帧进行 CRC 校验:如果帧校验和错误,就丢弃此帧;如果帧校验和正确,就判断该帧的 MAC 目的地址是否符合自己的接收条件,如果符合,就将此帧交付 设备驱动程序 做进一步处理。这时 Wireshark 才能抓到数据,因此,Wireshark 抓到的是去掉前导码、帧开始分界符、CRC校验之外的数据,其最大值是 6+6+2+1500=1514 字节; TIME_WAIT 的巧妙设计? 为什么 TIME_WAIT 等待的时间是 2MSL?MSL ( MSL 与 TTL 的区别: MSL 的单位是时间,而 TTL 是经过路由跳数。所以 MSL 应该要大于等于 TTL 消耗为 0 的时间,以确保报文已被自然消亡。 TIME_WAIT 等待 2 倍的 MSL,比较合理的解释是: 网络中可能存在来自发送方的数据包,当这些发送方的数据包被接收方处理后又会向对方发送响应,所以一来一回需要等待 2 倍的时间。 比如被动关闭方没有收到断开连接的最后的 ACK 报文,就会触发超时重发 FIN 报文,另一方接收到 FIN 后,会重发 ACK 给被动关闭方, 一来一去正好 2 个 MSL。 2MSL 的时间是从客户端接收到 FIN 后发送 ACK 开始计时的。如果在 TIME-WAIT 时间内,因为客户端的 ACK 没有传输到服务端,客户端又接收到了服务端重发的 FIN 报文,那么 2MSL 时间将重新计时。 在 Linux 系统里 2MSL 默认是 60 秒,那么一个 MSL 也就是 30 秒。Linux 系统停留在 其定义在 Linux 内核代码里的名称为
如果要修改 ? 为什么需要 TIME_WAIT 状态?主动发起关闭连接的一方,才会有 需要
? 原因一:防止旧连接的数据包假设
所以,TCP 就设计出了这么一个机制,经过 2MSL 这个时间,足以让两个方向上的数据包都被丢弃,使得原来连接的数据包在网络中都自然消失,再出现的数据包一定都是新建立连接所产生的。 ? 原因二:保证连接正确关闭在 RFC 793 指出 TIME-WAIT 另一个重要的作用是:
也就是说,TIME-WAIT 作用是等待足够的时间以确保最后的 ACK 能让被动关闭方接收,从而帮助其正常关闭。 假设 TIME-WAIT 没有等待时间或时间过短,断开连接会造成什么问题呢?
如果 TIME-WAIT 等待足够长的情况就会遇到两种情况:
所以客户端在 ? TIME_WAIT 过长有什么危害?如果服务器有处于 过多的
第二个危害是会造成严重的后果的,要知道,端口资源也是有限的,一般可以开启的端口为 32768~61000,也可以通过参数设置指定 客户端受端口资源限制:
服务端受系统资源限制:
如何优化 这里给出优化 ? 方式一:net.ipv4.tcp_tw_reuse 和 tcp_timestamps如下的 Linux 内核参数开启后,则可以复用处于 有一点需要注意的是,
使用这个选项,还有一个前提,需要打开对 TCP 时间戳的支持,即 由于引入了时间戳,我们在前面提到的 2MSL 问题就不复存在了,因为重复的数据包会因为时间戳过期被自然丢弃。 ? 方式二:net.ipv4.tcp_max_tw_buckets这个值默认为 18000,当系统中处于 这个方法过于暴力,而且治标不治本,带来的问题远比解决的问题多,不推荐使用。 ? 方式三:程序中使用 SO_LINGER我们可以通过设置 socket 选项,来设置调用 close 关闭连接行为。
如果 但这为跨越 如果已经建立了连接,但是客户端突然出现故障了怎么办? TCP 有一个机制是保活机制。这个机制的原理是这样的: 定义一个时间段,在这个时间段内,如果没有任何连接相关的活动,TCP 保活机制会开始作用,每隔一个时间间隔,发送一个探测报文,该探测报文包含的数据非常少,如果连续几个探测报文都没有得到响应,则认为当前的 TCP 连接已经死亡,系统内核将错误信息通知给上层应用程序。 在 Linux 内核可以有对应的参数可以设置保活时间、保活探测的次数、保活探测的时间间隔,以下都为默认值:
也就是说在 Linux 系统中,最少需要经过 2 小时 11 分 15 秒才可以发现一个「死亡」连接。 这个时间是有点长的,我们也可以根据实际的需求,对以上的保活相关的参数进行设置。 如果开启了 TCP 保活,需要考虑以下几种情况: 第一种,对端程序是正常工作的。当 TCP 保活的探测报文发送给对端, 对端会正常响应,这样 TCP 保活时间会被重置,等待下一个 TCP 保活时间的到来。 第二种,对端程序崩溃并重启。当 TCP 保活的探测报文发送给对端后,对端是可以响应的,但由于没有该连接的有效信息,会产生一个 RST 报文,这样很快就会发现 TCP 连接已经被重置。 第三种,是对端程序崩溃,或对端由于其他原因导致报文不可达。当 TCP 保活的探测报文发送给对端后,石沉大海,没有响应,连续几次,达到保活探测次数后,TCP 会报告该 TCP 连接已经死亡。 ? 初始序列号 ISN为什么不同?主要原因是为了防止历史报文被下一个相同四元组的连接接收。 如果一个已经失效的连接被重用了,但是该旧连接的历史报文还残留在网络中,如果序列号相同,那么就无法分辨出该报文是不是历史报文,如果历史报文被新的连接接收了,则会产生数据错乱。所以,每次建立连接前重新初始化一个序列号主要是为了通信双方能够根据序号将不属于本连接的报文段丢弃。 另一方面是为了安全性,防止黑客伪造的相同序列号的 TCP 报文被对方接收。 ? 初始序列号 ISN 是如何随机产生的?起始 ISN 是基于时钟的,每 4 毫秒 + 1,转一圈要 4.55 个小时。 RFC1948 中提出了一个较好的初始化序列号 ISN 随机生成算法。
M 是一个计时器,这个计时器每隔 4 毫秒加 1。 ? TIME_WAIT 状态不是会持续 2 MSL 时长,历史报文不是早就在网络中消失了吗?是的,如果能正常四次挥手,由于 但是来了,我们并不能保证每次连接都能通过四次挥手来正常关闭连接。 假设每次建立连接,客户端和服务端的初始化序列号都是从 0 开始: 过程如下:
可以看到,如果每次建立连接,客户端和服务端的初始化序列号都是一样的话,很容易出现历史报文被下一个相同四元组的连接接收的问题。 ? 客户端和服务端的初始化序列号不一样不是也会发生这样的事情吗?是的,即使客户端和服务端的初始化序列号不一样,也会存在收到历史报文的可能。 但是我们要清楚一点,历史报文能否被对方接收,还要看该历史报文的序列号是否正好在对方接收窗口内,如果不在就会丢弃,如果在才会接收。 如果每次建立连接客户端和服务端的初始化序列号都「不一样」,就有大概率因为历史报文的序列号「不在」对方接收窗口,从而很大程度上避免了历史报文,比如下图: 相反,如果每次建立连接客户端和服务端的初始化序列号都「一样」,就有大概率遇到历史报文的序列号刚「好在」对方的接收窗口内,从而导致历史报文被新连接成功接收。 所以,每次初始化序列号不一样能够很大程度上避免历史报文被下一个相同四元组的连接接收,注意是很大程度上,并不是完全避免了。 ? 客户端和服务端的初始化序列号不一样不是也会发生这样的事情吗?是的,但是也不是完全避免了。 为了能更好的理解这个原因,我们先来了解序列号(SEQ)和初始序列号(ISN)。
通过前面我们知道,序列号和初始化序列号并不是无限递增的,会发生回绕为初始值的情况,这意味着无法根据序列号来判断新老数据。 不要以为序列号的上限值是 4GB,就以为很大,很难发生回绕。在一个速度足够快的网络中传输大量数据时,序列号的回绕时间就会变短。如果序列号回绕的时间极短,我们就会再次面临之前延迟的报文抵达后序列号依然有效的问题。 为了解决这个问题,就需要有 TCP 时间戳。 试看下面的示例,假设 TCP 的发送窗口是 1 GB,并且使用了时间戳选项,发送方会为每个 TCP 报文分配时间戳数值,我们假设每个报文时间加 1,然后使用这个连接传输一个 6GB 大小的数据流。 32 位的序列号在时刻 D 和 E 之间回绕。假设在时刻B有一个报文丢失并被重传,又假设这个报文段在网络上绕了远路并在时刻 F 重新出现。如果 TCP 无法识别这个绕回的报文,那么数据完整性就会遭到破坏。 使用时间戳选项能够有效的防止上述问题,如果丢失的报文会在时刻 F 重新出现,由于它的时间戳为 2,小于最近的有效时间戳(5 或 6),因此防回绕序列号算法(PAWS)会将其丢弃。 防回绕序列号算法要求连接双方维护最近一次收到的数据包的时间戳(Recent TSval),每收到一个新数据包都会读取数据包中的时间戳值跟 Recent TSval 值做比较,如果发现收到的数据包中时间戳不是递增的,则表示该数据包是过期的,就会直接丢弃这个数据包。 ? 客户端和服务端的初始化序列号都是随机生成,能很大程度上避免历史报文被下一个相同四元组的连接接收,然后又引入时间戳的机制,从而完全避免了历史报文被接收的问题。 ? 你知道 TCP 的最大连接数吗?有一个 IP 的服务器监听了一个端口,它的 TCP 的最大连接数是多少? 服务器通常固定在某个本地端口上监听,等待客户端的连接请求。因此,客户端 IP 和 端口是可变的,其理论值计算公式如下: 当然,服务端最大并发 TCP 连接数远不能达到理论上限:
? 后记欢迎各位大佬指正,在评论区多多讨论; 站在巨人的肩膀上看 TCP,感谢参考: |
|
网络协议 最新文章 |
使用Easyswoole 搭建简单的Websoket服务 |
常见的数据通信方式有哪些? |
Openssl 1024bit RSA算法---公私钥获取和处 |
HTTPS协议的密钥交换流程 |
《小白WEB安全入门》03. 漏洞篇 |
HttpRunner4.x 安装与使用 |
2021-07-04 |
手写RPC学习笔记 |
K8S高可用版本部署 |
mySQL计算IP地址范围 |
|
上一篇文章 下一篇文章 查看所有文章 |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年12日历 | -2024/12/28 20:21:29- |
|
网站联系: qq:121756557 email:121756557@qq.com IT数码 |
数据统计 |