自己攻击自己
还有一个很骚的解决方案,我自己瞎想的,就是自己攻击自己。
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,公式我就不贴了,反正我不懂,不懂就不哔哔了。
为什么还需要快速重传机制?
超时重传是按时间来驱动的,如果是网络状况真的不好的情况,超时重传没问题,但是如果网络状况好的时候,只是恰巧丢包了,那等这么长时间就没必要。
于是又引入了数据驱动的重传叫快速重传,什么意思呢?就是发送方如果连续三次收到对方相同的确认号,那么马上重传数据。
因为连续收到三次相同 ACK 证明当前网络状况是 ok 的,那么确认是丢包了,于是立马重发,没必要等这么久。
图来自网络
看起来好像挺完美的,但是你有没有想过我发送1、2、3、4这4个包,就 2 对方没收到,1、3、4都收到了,然后不管是超时重传还是快速重传反正对方就回 ACK 2。
这时候要重传 2、3、4 呢还是就 2 呢?
SACK 的引入是为了解决什么问题?
SACK 即 Selective Acknowledgment,它的引入就是为了解决发送方不知道该重传哪些数据的问题。
我们来看一下下面的图就知道了。
图来自网络
SACK 就是接收方会回传它已经接受到的数据,这样发送方就知道哪一些数据对方已经收到了,所以就可以选择性的发送丢失的数据。
如图,通过 ACK 告知我接下来要 5500 开始的数据,并一直更新 SACK,6000-6500 我收到了,6000-7000的数据我收到了,6000-7500的数据我收到了,发送方很明确的知道,5500-5999 的那一波数据应该是丢了,于是重传。
而且如果数据是多段不连续的, SACK 也可以发送,比如 SACK 0-500,1000-1500,2000-2500。就表明这几段已经收到了。
D-SACK 又是什么东西?
D-SACK 其实是 SACK 的扩展,它利用 SACK 的第一段来描述重复接受的不连续的数据序号,如果第一段描述的范围被 ACK 覆盖,说明重复了,比如我都 ACK 到6000了你还给我回 SACK 5000-5500 呢?
说白了就是从第一段的反馈来和已经接受到的 ACK 比一比,参数是 tcp_dsack,Linux 2.4 之后默认开启。
那知道重复了有什么用呢?
1、知道重复了说明对方收到刚才那个包了,所以是回来的 ACK 包丢了。2、是不是包乱序的,先发的包后到?3、是不是自己太着急了,RTO 太小了?4、是不是被数据复制了,抢先一步呢?
滑动窗口干嘛用?
我们已经知道了 TCP 有序号,并且还有重传,但是这还不够,因为我们不是愣头青,还需要根据情况来控制一下发送速率,因为网络是复杂多变的,有时候就会阻塞住,而有时候又很通畅。
所以发送方需要知道接收方的情况,好控制一下发送的速率,不至于蒙着头一个劲儿的发然后接受方都接受不过来。
因此 TCP 就有个叫滑动窗口的东西来做流量控制,也就是接收方告诉发送方我还能接受多少数据,然后发送方就可以根据这个信息来进行数据的发送。
以下是发送方维护的窗口,就是黑色圈起来的。
图来自网络
图中的 #1 是已收到 ACK 的数据,#2 是已经发出去但是还没收到 ACK 的数据,#3 就是在窗口内可以发送但是还没发送的数据。#4 就是还不能发送的数据。
然后此时收到了 36 的 ACK,并且发出了 46-51 的字节,于是窗口向右滑动了。
图片来自网络
TCP/IP Guide 上还有一张完整的图,画的十分清晰,大家看一下。
如果接收方回复的窗口一直是 0 怎么办?
上文已经说了发送方式根据接收方回应的 window 来控制能发多少数据,如果接收方一直回应 0,那发送方就杵着?
你想一下,发送方发的数据都得到 ACK 了,但是呢回应的窗口都是 0 ,这发送方此时不敢发了啊,那也不能一直等着啊,这 Window 啥时候不变 0 啊?
于是 TCP 有一个 Zero Window Probe 技术,发送方得知窗口是 0 之后,会去探测探测这个接收方到底行不行,也就是发送 ZWP 包给接收方。
具体看实现了,可以发送多次,然后还有间隔时间,多次之后都不行可以直接 RST。
假设接收方每次回应窗口都很小怎么办?
你想象一下,如果每次接收方都说我还能收 1 个字节,发送方该不该发?
TCP + IP 头部就 40 个字节了,这传输不划算啊,如果傻傻的一直发这就叫 Silly Window。
那咋办,一想就是发送端等着,等养肥了再发,要么接收端自己自觉点,数据小于一个阈值就告诉发送端窗口此时是 0 算了,也等养肥了再告诉发送端。
发送端等着的方案就是纳格算法,这个算法相信看一下代码就知道了。
简单的说就是当前能发送的数据和窗口大于等于 MSS 就立即发送,否则再判断一下之前发送的包 ACK 回来没,回来再发,不然就攒数据。
接收端自觉点的方案是 David D Clark’s 方案,如果窗口数据小于某个阈值就告诉发送方窗口 0 别发,等缓过来数据大于等于 MSS 或者接受 buffer 腾出一半空间了再设置正常的 window 值给发送方。
对了提到纳格算法不得不再提一下延迟确认,纳格算法在等待接收方的确认,而开启延迟确认则会延迟发送确认,会等之后的包收到了再一起确认或者等待一段时候真的没了再回复确认。
这就相互等待了,然后延迟就很大了,两个不可同时开启。
已经有滑动窗口了为什么还要拥塞控制?
前面我已经提到了,加了拥塞控制是因为 TCP 不仅仅就管两端之间的情况,还需要知晓一下整体的网络情形,毕竟只有大家都守规矩了道路才会通畅。
前面我们提到了重传,如果不管网络整体的情况,肯定就是对方没给 ACK ,那我就无脑重传。
如果此时网络状况很差,所有的连接都这样无脑重传,是不是网络情况就更差了,更加拥堵了?
然后越拥堵越重传,一直冲冲冲!然后就 GG 了。
所以需要个拥塞控制,来避免这种情况的发送。
拥塞控制怎么搞?
主要有以下几个步骤来搞:
1、慢启动,探探路。2、拥塞避免,感觉差不多了减速看看 3、拥塞发生快速重传/恢复
慢启动,就是新司机上路慢慢来,初始化 cwnd(Congestion Window)为 1,然后每收到一个 ACK 就 cwnd++ 并且每过一个 RTT ,cwnd = 2*cwnd 。
线性中带着指数,指数中又夹杂着线性增。
然后到了一个阈值,也就是 ssthresh(slow start threshold)的时候就进入了拥塞避免阶段。
这个阶段是每收到一个 ACK 就 cwnd = cwnd + 1/cwnd并且每一个 RTT 就 cwnd++。
可以看到都是线性增。
然后就是一直增,直到开始丢包的情况发生,前面已经分析到重传有两种,一种是超时重传,一种是快速重传。
如果发生超时重传的时候,那说明情况有点糟糕,于是直接把 ssthresh 置为当前 cwnd 的一半,然后 cwnd 直接变为 1,进入慢启动阶段。
如果是快速重传,那么这里有两种实现,一种是 TCP Tahoe ,和超时重传一样的处理。
一种是 TCP Reno,这个实现是把 cwnd = cwnd/2 ,然后把 ssthresh 设置为当前的 cwnd 。
然后进入快速恢复阶段,将 cwnd = cwnd + 3(因为快速重传有三次),重传 DACK 指定的包,如果再收到一个DACK则 cwnd++,如果收到是正常的 ACK 那么就将 cwnd 设为 ssthresh 大小,进入拥塞避免阶段。
可以看到快速恢复就重传了指定的一个包,那有可能是很多包都丢了,然后其他的包只能等待超时重传,超时重传就会导致 cwnd 减半,多次触发就指数级下降。
所以又搞了个 New Reno,多加了个 New,它是在没有SACK 的情况下改进快速恢复,它会观察重传 DACK 指定的包的响应 ACK 是否是已经发送的最大 ACK,比如你发了1、2、3、4,对方没收到 2,但是 3、4都收到了,于是你重传 2 之后 ACK 肯定是 5,说明就丢了这一个包。
不然就是还有其他包丢了,如果就丢了一个包就是之前的过程一样,如果还有其他包丢了就继续重传,直到 ACK 是全部的之后再退出快速恢复阶段。
简单的说就是一直探测到全部包都收到了再结束这个环节。
还有个 FACK,它是基于 SACK 用来作为重传过程中的拥塞控制,相对于上面的 New Reno 我们就知道它有 SACK 所以不需要一个一个试过去,具体我不展开了。
还有哪些拥塞控制算法?
从维基上看有这么多。
本来我还想哔哔几句了,哔哔了之后又删了,感觉说了和没说一样,想深入但是实力不允许,有点惆怅啊。
各位看官自个儿查查吧,或者等我日后修炼有成再来哔哔。
总结
–
说了这么多来总结一下吧。
TCP 是面向连接的,提供可靠、有序的传输并且还提供流控和拥塞控制,单独提取出 TCP 层而不是在 IP层实现是因为 IP 层有更多的设备需要使用,加了复杂的逻辑不划算。
三次握手主要是为了定义初始序列号为了之后的传输打下基础,四次挥手是因为 TCP 是全双工协议,因此双方都得说拜拜。
SYN 超时了就阶梯性重试,如果有 SYN攻击,可以加大半队列数,或减少重试次数,或直接拒绝。
TIME_WAIT 是怕对方没收到最后一个 ACK,然后又发了 FIN 过来,并且也是等待处理网络上残留的数据,怕影响新连接。
TIME_WAIT 不建议设小,或者破坏 TIME_WAIT 机制,如果真想那么可以开启快速回收,或者重用,不过注意受益的对象。
超时重传是为了保证对端一定能收到包,快速重传是为了避免在偶尔丢包的时候需要等待超时这么长时间,SACK 是为了让发送方知道重传哪些。
D-SACK 是为了让发送方知道这次重传的原因是对方真的没收到还是自己太心急了 RTO 整小了,不至于两眼一抹黑。
滑动窗口是为了平衡发送方的发送速率和接收方的接受数率,不至于瞎发,当然还需要注意 Silly Window 的情况,同时还要注意纳格算法和延迟确认不能一起搭配。
而滑动窗口还不够,还得有个拥塞控制,因为出行你我他,安全靠大家,TCP 还得跳出来看看关心下当前大局势。
最后
–
至此就差不多了,不过还是有很多很多细节的,TCP 协议太复杂了,这可能是我文章里面图画的最少的一篇了,你看复杂到我图都画不来了哈哈哈。
今天我就说了个皮毛,如有纰漏请赶紧后台联系鞭挞我。
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新
如果你觉得这些内容对你有帮助,可以添加V获取:vip1024c (备注前端)
最后
小编的一位同事在校期间连续三年参加ACM-ICPC竞赛。从参赛开始,原计划每天刷一道算法题,实际上每天有时候不止一题,一年最终完成了 600+:
凭借三年刷题经验,他在校招中很快拿到了各大公司的offer。
入职前,他把他的刷题经验总结成1121页PDF书籍,作为礼物赠送给他的学弟学妹,希望同学们都能在最短时间内掌握校招常见的算法及解题思路。
整本书,我仔细看了一遍,作者非常细心地将常见核心算法题和汇总题拆分为4个章节。
CodeChina开源项目:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】
而对于有时间的同学,作者还给出了他结合众多数据结构算法书籍,挑选出的一千多道题的解题思路和方法,以供有需要的同学慢慢研究。
20192)]
最后
小编的一位同事在校期间连续三年参加ACM-ICPC竞赛。从参赛开始,原计划每天刷一道算法题,实际上每天有时候不止一题,一年最终完成了 600+:
凭借三年刷题经验,他在校招中很快拿到了各大公司的offer。
入职前,他把他的刷题经验总结成1121页PDF书籍,作为礼物赠送给他的学弟学妹,希望同学们都能在最短时间内掌握校招常见的算法及解题思路。
整本书,我仔细看了一遍,作者非常细心地将常见核心算法题和汇总题拆分为4个章节。
CodeChina开源项目:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】
而对于有时间的同学,作者还给出了他结合众多数据结构算法书籍,挑选出的一千多道题的解题思路和方法,以供有需要的同学慢慢研究。