写点什么

牛皮了!一篇文章直接解决关于 TCP 的 23 种疑难问题!,springboot 源码深度解析视频

用户头像
极客good
关注
发布于: 刚刚

好了,废话不多说,开始上正菜。


TCP 是用来解决什么问题?




TCP 即 Transmission Control Protocol,可以看到是一个传输控制协议,重点就在这个控制。


控制什么?


控制可靠、按序地传输以及端与端之间的流量控制。够了么?还不够,它需要更加智能,因此还需要加个拥塞控制,需要为整体网络的情况考虑。


这就是出行你我他,安全靠大家。


为什么要 TCP,IP 层实现控制不行么?




我们知道网络是分层实现的,网络协议的设计就是为了通信,从链路层到 IP 层其实就已经可以完成通信了。


你看链路层不可或缺毕竟咱们电脑都是通过链路相互连接的,然后 IP 充当了地址的功能,所以通过 IP 咱们找到了对方就可以进行通信了。


那加个 TCP 层干啥?IP 层实现控制不就完事了嘛?


之所以要提取出一个 TCP 层来实现控制是因为 IP 层涉及到的设备更多,一条数据在网络上传输需要经过很多设备,而设备之间需要靠 IP 来寻址。


假设 IP 层实现了控制,那是不是涉及到的设备都需要关心很多事情?整体传输的效率是不是大打折扣了?



我举个例子,假如 A 要传输给 F 一个积木,但是无法直接传输到,需要经过 B、C、D、E 这几个中转站之手。这里有两种情况:


  • 假设 BCDE 都需要关心这个积木搭错了没,都拆开包裹仔细的看看,没问题了再装回去,最终到了 F 的手中。

  • 假设 BCDE 都不关心积木的情况,来啥包裹只管转发就完事了,由最终的 F 自己来检查这个积木答错了没。


你觉得哪种效率高?明显是第二种,转发的设备不需要关心这些事,只管转发就完事!


所以把控制的逻辑独立出来成 TCP 层,让真正的接收端来处理,这样网络整体的传输效率就高了。


连接到底是什么?




我们已经知道了为什么需要独立出 TCP 这一层,并且这一层主要是用来干嘛的,接下来就来看看它到底是怎么干的。


我们都知道 TCP 是面向连接的,那这个连接到底是个什么东西?真的是拉了一条线让端与端之间连起来了?


所谓的连接其实只是双方都维护了一个状态,通过每一次通信来维护状态的变更,使得看起来好像有一条线关联了对方。


TCP 协议头




在具体深入之前我们需要先来看看一些 TCP 头的格式,这很基础也很重要。



图来自网络


我就不一一解释了,挑重点的说。


首先可以看到 TCP 包只有端口,没有 IP。


Seq 就是 Sequence Number 即序号,它是用来解决乱序问题的。


ACK 就是 Acknowledgement Numer 即确认号,它是用来解决丢包情况的,告诉发送方这个包我收到啦。


标志位就是 TCP flags 用来标记这个包是什么类型的,用来控制 TPC 的状态。


窗口就是滑动窗口,Sliding Window,用来流控。


三次握手




明确了协议头的要点之后,我们再来看三次握手。


三次握手真是个老生常谈的问题了,但是真的懂了么?不是浮在表面?能不能延伸出一些点别的?


我们先来看一下熟悉的流程。



图来自网络


首先为什么要握手,其实主要就是为了初始化 Seq Numer,SYN 的全称是 Synchronize Sequence Numbers,这个序号是用来保证之后传输数据的顺序性。


你要说是为了测试保证双方发送接收功能都正常,我觉得也没毛病,不过我认为重点在于同步序号。


那为什么要三次,就拿我和你这两个角色来说,首先我告诉你我的初始化序号,你听到了和我说你收到了。


然后你告诉我你的初始序号,然后我对你说我收到了。


这好像四次了?如果真的按一来一回就是四次,但是中间一步可以合在一起,就是你和我说你知道了我的初始序号的时候同时将你的初始序号告诉我。


因此四次握手就可以减到三次了。


不过你没有想过这么一种情形,我和你同时开口,一起告诉对方各自的初始序号,然后分别回应收到了,这不就是四次握手了?


我来画个图,清晰一点。



看看是不是四次握手了? 不过具体还是得看实现,有些实现可能不允许这种情况出现,但是这不影响我们思考,因为握手的重点就是同步初始序列号,这种情况也完成了同步的目标。


初始序列号 ISN 的取值




不知道大家有没有想过 ISN 的值要设成什么?代码写死从零开始?


想象一下如果写死一个值,比如 0 ,那么假设已经建立好连接了,client 也发了很多包比如已经第 20 个包了,然后网络断了之后 client 重新,端口号还是之前那个,然后序列号又从 0 开始,此时服务端返回第 20 个包的 ack,客户端是不是傻了?


所以 RFC793 中认为 ISN 要和一个假的时钟绑定在一起 ISN 每四微秒加一,当超过 2 的 32 次方之后又从 0 开始,要四个半小时左右发生 ISN 回绕。


所以 ISN 变成一个递增值,真实的实现还需要加一些随机值在里面,防止被不法份子猜到 ISN。


SYN 超时了怎么处理?




也就是 client 发送 SYN 至 server 然后就挂了,此时 server 发送 SYN+ACK 就一直得不到回复,怎么办?


我脑海中一想到的就是重试,但是不能连续快速重试多次,你想一下,假设 client 掉线了,你总得给它点时间恢复吧,所以呢需要慢慢重试,阶梯性重试。


在 Linux 中就是默认重试 5 次,并且就是阶梯性的重试,间隔就是 1s、2s、4s、8s、16s,再第五次发出之后还得等 32s 才能知道这次重试的结果,所以说总共等 63s 才能断开连接。


SYN Flood 攻击




你看到没 SYN 超时需要耗费服务端 63s 的时间断开连接,也就说 63s 内服务端需要保持这个资源,所以不法分子就可以构造出大量的 client 向 server 发 SYN 但就是不回 server。



图来自网络


使得 server 的 SYN 队列耗尽,无法处理正常的建连请求。


所以怎么办?


可以开启 tcp_syncookies,那就用不到 SYN 队列了。


SYN 队列满了之后 TCP 根据自己的 ip、端口、然后对方的 ip、端口,对方 SYN 的序号,时间戳等一波操作生成一个特殊的序号(即 cookie)发回去,如果对方是正常的 client 会把这个序号发回来,然后 server 根据这个序号建连。


或者调整 tcp_synack_retries 减少重试的次数,设置 tcp_max_syn_backlog 增加 SYN 队列数,设置 tcp_abort_on_overflow SYN 队列满了直接拒绝连接。


为什么要四次挥手?




四次挥手和三次握手成双成对,同样也是 TCP 中的一线明星,让我们重温一下熟悉的图。



图来自网络


为什么挥手需要四次?因为 TCP 是全双工协议,也就是说双方都要关闭,每一方都向对方发送 FIN 和回应 ACK。


就像我对你说我数据发完了,然后你回复好的你收到了。然后你对我说你数据发完了,然后我向你回复我收到了。


所以看起来就是四次。


从图中可以看到主动关闭方的状态是 FIN_WAIT_1 到 FIN_WAIT_2 然后再到 TIME_WAIT,而被动关闭方是 CLOSE_WAIT 到 LAST_ACK。


四次挥手状态一定是这样变迁的吗




状态一定是这样变迁的吗?让我们再来看个图。



图来自网络


可以看到双方都主动发起断开请求所以各自都是主动发起方,状态会从 FIN_WAIT_1 都进入到 CLOSING 这个过度状态然后再到 TIME_WAIT。


挥手一定需要四次吗?




假设 client 已经没有数据发送给 server 了,所以它发送 FIN 给 server 表明自己数据发完了,不再发了,如果这时候 server 还是有数据要发送给 client 那么它就是先回复 ack ,然后继续发送数据。


等 server 数据发送完了之后再向 client 发送 FIN 表明它也发完了,然后等 client 的 ACK 这种情况下就会有四次挥手。


那么假设 client 发送 FIN 给 server 的时候 server 也没数据给 client,那么 server 就可以将 ACK 和它的 FIN 一起发给 client ,然后等待 client 的 ACK,这样不就三次挥手了?


为什么要有 TIME_WAIT?




断开连接发起方在接受到接受方的 FIN 并回复 ACK 之后并没有直接进入 CLOSED 状态,而是进行了一波等待,等待时间为 2MSL。


MSL 是 Maximum Segment Lifetime,即报文最长生存时间,RFC 793 定义的 MSL 时间是 2 分钟,Linux 实际实现是 30s,那么 2MSL 是一分钟。


那么为什么要等 2MSL 呢?


  • 就是怕被动关闭方没有收到最后的 ACK,如果被动方由于网络原因没有到,那么它会再次发送 FIN, 此时如果主动关闭方已经 CLOSED 那就傻了,因此等一会儿。

  • 假设立马断开连接,但是又重用了这个连接,就是五元组完全一致,并且序号还在合适的范围内,虽然概率很低但理论上也有可能,那么新的连接会被已关闭连接链路上的一些残留数据干扰,因此给予一定的时间来处理一些残留数据。


等待 2MSL 会产生什么问题?




如果服务器主动关闭大量的连接,那么会出现大量的资源占用,需要等到 2MSL 才会释放资源。


如果是客户端主动关闭大量的连接,那么在 2MSL 里面那些端口都是被占用的,端口只有 65535 个,如果端口耗尽了就无法发起送的连接了,不过我觉得这个概率很低,这么多端口你这是要建立多少个连接?


如何解决 2MSL 产生的问题?




快速回收,即不等 2MSL 就回收, Linux 的参数是 tcp_tw_recycle,还有 tcp_timestamps 不过默认是打开的。


其实上面我们已经分析过为什么需要等 2MSL,所以如果等待时间果断就是出现上面说的那些问题。


所以不建议开启,而且 Linux 4.12 版本后已经咔擦了这个参数了。


前不久刚有位朋友在群里就提到了这玩意。



一问果然有 NAT 的身影。


现象就是请求端请求服务器的静态资源偶尔会出现 20-60 秒左右才会有响应的情况,从抓包看请求端连续三个 SYN 都没有回应。


比如你在学校,对外可能就一个公网 IP,然后开启了 tcp_tw_recycle(tcp_timestamps 也是打开的情况下),在 60 秒内对于同源 IP 的连接请求中 timestamp 必须是递增的,不然认为其是过期的数据包就会丢弃。


学校这么多机器,你无法保证时间戳是一致的,因此就会出问题。


所以这玩意不推荐使用。



重用,即开启 tcp_tw_reuse 当然也是需要 tcp_timestamps 的。


这里有个重点,tcp_tw_reuse 是用在连接发起方的,而我们的服务端基本上是连接被动接收方。


tcp_tw_reuse 是发起新连接的时候,可以复用超过 1s 的处于 TIME_WAIT 状态的连接,所以它压根没有减少我们服务端的压力。


它重用的是发起方处于 TIME_WAIT 的连接。


这里还有一个 SO_REUSEADDR ,这玩意有人会和 tcp_tw_reuse 混为一谈,首先 tcp_tw_reuse 是内核选项而 SO_REUSEADDR 是用户态选项。


然后 SO_REUSEADDR 主要用在你启动服务的时候,如果此时的端口被占用了并且这个连接处于 TIME_WAIT 状态,那么你可以重用这个端口,如果不是 TIME_WAIT,那就是给你个 Address already in use。


所以这两个玩意好像都不行,而且 tcp_tw_re


【一线大厂Java面试题解析+核心总结学习笔记+最新架构讲解视频+实战项目源码讲义】
浏览器打开:qq.cn.hn/FTf 免费领取
复制代码


use 和 tcp_tw_recycle,其实是违反 TCP 协议的,说好的等我到天荒地老,你却偷偷放了手?



要么就是调小 MSL 的时间,不过也不太安全,要么调整 tcp_max_tw_buckets 控制 TIME_WAIT 的数量,不过默认值已经很大了 180000,这玩意应该是用来对抗 DDos 攻击的。


所以我给出的建议是服务端不要主动关闭,把主动关闭方放到客户端。毕竟咱们服务器是一对很多很多服务,我们的资源比较宝贵。


自己攻击自己




还有一个很骚的解决方案,我自己瞎想的,就是自己攻击自己。


Socket 有一个选项叫 IP_TRANSPARENT ,可以绑定一个非本地的地址,然后服务端把建连的 ip 和端口都记下来,比如写入本地某个地方。


然后启动一个服务,假如现在服务端资源很紧俏,那么你就定个时间,过了多久之后就将处于 TIME_WAIT 状态的对方 ip 和端口告诉这个服务。


然后这个服务就利用 IP_TRANSPARENT 伪装成之前的那个 client 向服务端发起一个请求,然后服务端收到会给真的 client 一个 ACK, 那 client 都关了已经,说你在搞啥子,于是回了一个 RST,然后服务端就中止了这个连接。



超时重传机制是为了解决什么问题?




前面我们提到 TCP 要提供可靠的传输,那么网络又是不稳定的如果传输的包对方没收到却又得保证可靠那么就必须重传。


TCP 的可靠性是靠确认号的,比如我发给你 1、2、3、4 这 4 个包,你告诉我你现在要 5 那说明前面四个包你都收到了,就是这么回事儿。


不过这里要注意,SeqNum 和 ACK 都是以字节数为单位的,也就是说假设你收到了 1、2、4 但是 3 没有收到你不能 ACK 5,如果你回了 5 那么发送方就以为你 5 之前的都收到了。


所以只能回复确认最大连续收到包,也就是 3。


而发送方不清楚 3、4 这两个包到底是还没到呢还是已经丢了,于是发送方需要等待,这等待的时间就比较讲究了。


如果太心急可能 ACK 已经在路上了,你这重传就是浪费资源了,如果太散漫,那么接收方急死了,这死鬼怎么还不发包来,我等的花儿都谢了。


所以这个等待超时重传的时间很关键,怎么搞?聪明的小伙伴可能一下就想到了,你估摸着正常来回一趟时间是多少不就好了,我就等这么长。


这就来回一趟的时间就叫 RTT,即 Round Trip Time,然后根据这个时间制定超时重传的时间 RTO,即 Retransmission Timeout。


不过这里大概只好了 RTO 要参考下 RTT ,但是具体要怎么算?首先肯定是采样,然后一波加权平均得到 RTO。


RFC793 定义的公式如下:


1、先采样 RTT 2、SRTT = ( ALPHA * SRTT ) + ((1-ALPHA) * RTT) 3、RTO = min[UBOUND,max[LBOUND,(BETA*SRTT)]]


ALPHA 是一个平滑因子取值在 0.8~0.9 之间,UBOUND 就是超时时间上界-1 分钟,LBOUND 是下界-1 秒钟,BETA 是一个延迟方差因子,取值在 1.3~2.0。


但是还有个问题,RTT 采样的时间用一开始发送数据的时间到收到 ACK 的时间作为样本值还是重传的时间到 ACK 的时间作为样本值?



图来自网络


从图中就可以看到,一个时间算长了,一个时间算短了,这有点难,因为你不知道这个 ?ACK 到底是回复谁的。


所以怎么办?发生重传的来回我不采样不就好了,我不知道这次 ACK 到底是回复谁的,我就不管他,我就采样正常的来回。


这就是 Karn / Partridge 算法,不采样重传的 RTT。


但是不采样重传会有问题,比如某一时刻网络突然就是很差,你要是不管重传,那么还是按照正常的 RTT 来算 RTO, 那么超时的时间就过短了,于是在网络很差的情况下还疯狂重传加重了网络的负载。


因此 Karn 算法就很粗暴的搞了个发生重传我就将现在的 RTO 翻倍,哼!就是这么简单粗暴。



但是这种平均的计算很容易把一个突然间的大波动,平滑掉,所以又搞了个算法,叫 Jacobson / Karels Algorithm。


它把最新的 RTT 和平滑过的 SRTT 做了波计算得到合适的 RTO,公式我就不贴了,反正我不懂,不懂就不哔哔了。


为什么还需要快速重传机制?




超时重传是按时间来驱动的,如果是网络状况真的不好的情况,超时重传没问题,但是如果网络状况好的时候,只是恰巧丢包了,那等这么长时间就没必要。


于是又引入了数据驱动的重传叫快速重传,什么意思呢?就是发送方如果连续三次收到对方相同的确认号,那么马上重传数据。

用户头像

极客good

关注

还未添加个人签名 2021.03.18 加入

还未添加个人简介

评论

发布
暂无评论
牛皮了!一篇文章直接解决关于TCP的23种疑难问题!,springboot源码深度解析视频