每个时代,都不会亏待会学习的人。
在进入今天主题之前我先抛几个问题,这篇文章一共提出 23 个问题。
TCP 握手一定是三次?TCP 挥手一定是四次? 为什么要有快速重传,超时重传不够用?为什么要有 SACK,为什么要有 D-SACK? 都知道有滑动窗口,那由于接收方的太忙了滑动窗口降为了 0 怎么办?发送方就永远等着了? Silly Window 又是什么? 为什么有滑动窗口流控还需要拥塞控制? 快速重传一定要依赖三次重复 ACK ?
这篇文章我想由浅到深地过一遍 TCP,不是生硬的搬出各个知识点,从问题入手,然后从发展、演进的角度来看 TCP。
起初我在学计算机网络的时候就有非常非常多的疑问,脑子里简直充满了十万个为什么,而网络又非常的复杂,发展了这么多年东西真的太多了,今天我就大致的浅显地说一说我对 TCP 这些要点的理解。
好了,废话不多说,开始上正菜。
TCP 即 Transmission Control Protocol,可以看到是一个传输控制协议,重点就在这个控制。
控制什么?
控制可靠、按序地传输以及端与端之间的流量控制。够了么?还不够,它需要更加智能,因此还需要加个拥塞控制,需要为整体网络的情况考虑。
这就是出行你我他,安全靠大家。
我们知道网络是分层实现的,网络协议的设计就是为了通信,从链路层到 IP 层其实就已经可以完成通信了。
你看链路层不可或缺毕竟咱们电脑都是通过链路相互连接的,然后 IP 充当了地址的功能,所以通过 IP 咱们找到了对方就可以进行通信了。
那加个 TCP 层干啥?IP 层实现控制不就完事了嘛?
之所以要提取出一个 TCP 层来实现控制是因为 IP 层涉及到的设备更多,一条数据在网络上传输需要经过很多设备,而设备之间需要靠 IP 来寻址。
假设 IP 层实现了控制,那是不是涉及到的设备都需要关心很多事情?整体传输的效率是不是大打折扣了?
我举个例子,假如 A 要传输给 F 一个积木,但是无法直接传输到,需要经过 B、C、D、E 这几个中转站之手。
这里有两种情况:
你觉得哪种效率高?明显是第二种,转发的设备不需要关心这些事,只管转发就完事!
所以把控制的逻辑独立出来成 TCP 层,让真正的接收端来处理,这样网络整体的传输效率就高了。
我们已经知道了为什么需要独立出 TCP 这一层,并且这一层主要是用来干嘛的,接下来就来看看它到底是怎么干的。
我们都知道 TCP 是面向连接的,那这个连接到底是个什么东西?真的是拉了一条线让端与端之间连起来了?
所谓的连接其实只是双方都维护了一个状态,通过每一次通信来维护状态的变更,使得看起来好像有一条线关联了对方。
在具体深入之前我们需要先来看看一些 TCP 头的格式,这很基础也很重要。
我就不一一解释了,挑重点的说。
首先可以看到 TCP 包只有端口,没有 IP。
Seq 就是 Sequence Number 即序号,它是用来解决乱序问题的。
ACK 就是 Acknowledgement Numer 即确认号,它是用来解决丢包情况的,告诉发送方这个包我收到啦。
标志位就是 TCP flags 用来标记这个包是什么类型的,用来控制 TPC 的状态。
窗口就是滑动窗口,Sliding Window,用来流控。
明确了协议头的要点之后,我们再来看三次握手。
三次握手真是个老生常谈的问题了,但是真的懂了么?不是浮在表面?能不能延伸出一些点别的?
我们先来看一下熟悉的流程。
首先为什么要握手,其实主要就是为了初始化Seq Numer,SYN 的全称是 Synchronize Sequence Numbers,这个序号是用来保证之后传输数据的顺序性。
你要说是为了测试保证双方发送接收功能都正常,我觉得也没毛病,不过我认为重点在于同步序号。
那为什么要三次,就拿我和你这两个角色来说,首先我告诉你我的初始化序号,你听到了和我说你收到了。
然后你告诉我你的初始序号,然后我对你说我收到了。
这好像四次了?如果真的按一来一回就是四次,但是中间一步可以合在一起,就是你和我说你知道了我的初始序号的时候同时将你的初始序号告诉我。
因此四次握手就可以减到三次了。
不过你没有想过这么一种情形,我和你同时开口,一起告诉对方各自的初始序号,然后分别回应收到了,这不就是四次握手了?
我来画个图,清晰一点。
看看是不是四次握手了? 不过具体还是得看实现,有些实现可能不允许这种情况出现,但是这不影响我们思考,因为握手的重点就是同步初始序列号,这种情况也完成了同步的目标。
不知道大家有没有想过 ISN 的值要设成什么?代码写死从零开始?
想象一下如果写死一个值,比如 0 ,那么假设已经建立好连接了,client 也发了很多包比如已经第 20 个包了,然后网络断了之后 client 重新,端口号还是之前那个,然后序列号又从 0 开始,此时服务端返回第 20 个包的ack,客户端是不是傻了?
所以 RFC793 中认为 ISN 要和一个假的时钟绑定在一起
ISN 每四微秒加一,当超过 2 的 32 次方之后又从 0 开始,要四个半小时左右发生 ISN 回绕。
所以 ISN 变成一个递增值,真实的实现还需要加一些随机值在里面,防止被不法份子猜到 ISN。
也就是 client 发送 SYN 至 server 然后就挂了,此时 server 发送 SYN+ACK 就一直得不到回复,怎么办?
我脑海中一想到的就是重试,但是不能连续快速重试多次,你想一下,假设 client 掉线了,你总得给它点时间恢复吧,所以呢需要慢慢重试,阶梯性重试。
在 Linux 中就是默认重试 5 次,并且就是阶梯性的重试,间隔就是1s、2s、4s、8s、16s,再第五次发出之后还得等 32s 才能知道这次重试的结果,所以说总共等63s 才能断开连接。
你看到没 SYN 超时需要耗费服务端 63s 的时间断开连接,也就说 63s 内服务端需要保持这个资源,所以不法分子就可以构造出大量的 client 向 server 发 SYN 但就是不回 server。
使得 server 的 SYN 队列耗尽,无法处理正常的建连请求。
所以怎么办?
可以开启 tcp_syncookies,那就用不到 SYN 队列了。
SYN 队列满了之后 TCP 根据自己的 ip、端口、然后对方的 ip、端口,对方 SYN 的序号,时间戳等一波操作生成一个特殊的序号(即 cookie)发回去,如果对方是正常的 client 会把这个序号发回来,然后 server 根据这个序号建连。
或者调整 tcpsynackretries 减少重试的次数,设置 tcpmaxsyn_backlog 增加 SYN 队列数,设置 tcp_abortonoverflow SYN 队列满了直接拒绝连接。
四次挥手和三次握手成双成对,同样也是 TCP 中的一线明星,让我们重温一下熟悉的图。
为什么挥手需要四次?因为 TCP 是全双工协议,也就是说双方都要关闭,每一方都向对方发送 FIN 和回应 ACK。
就像我对你说我数据发完了,然后你回复好的你收到了。然后你对我说你数据发完了,然后我向你回复我收到了。
所以看起来就是四次。
从图中可以看到主动关闭方的状态是 FINWAIT1 到 FINWAIT2 然后再到 TIMEWAIT,而被动关闭方是 CLOSEWAIT 到 LAST_ACK。
状态一定是这样变迁的吗?让我们再来看个图。
可以看到双方都主动发起断开请求所以各自都是主动发起方,状态会从 FINWAIT1 都进入到 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,这样不就三次挥手了?
断开连接发起方在接受到接受方的 FIN 并回复 ACK 之后并没有直接进入 CLOSED 状态,而是进行了一波等待,等待时间为 2MSL。
MSL 是 Maximum Segment Lifetime,即报文最长生存时间,RFC 793 定义的 MSL 时间是 2 分钟,Linux 实际实现是 30s,那么 2MSL 是一分钟。
那么为什么要等 2MSL 呢?
如果服务器主动关闭大量的连接,那么会出现大量的资源占用,需要等到 2MSL 才会释放资源。
如果是客户端主动关闭大量的连接,那么在 2MSL 里面那些端口都是被占用的,端口只有 65535 个,如果端口耗尽了就无法发起送的连接了,不过我觉得这个概率很低,这么多端口你这是要建立多少个连接?
快速回收,即不等 2MSL 就回收, Linux 的参数是 tcptwrecycle,还有 tcp_timestamps 不过默认是打开的。
其实上面我们已经分析过为什么需要等 2MSL,所以如果等待时间果断就是出现上面说的那些问题。
所以不建议开启,而且 Linux 4.12 版本后已经咔擦了这个参数了。
前不久刚有位朋友在群里就提到了这玩意。
一问果然有 NAT 的身影。
现象就是请求端请求服务器的静态资源偶尔会出现 20-60 秒左右才会有响应的情况,从抓包看请求端连续三个 SYN 都没有回应。
比如你在学校,对外可能就一个公网 IP,然后开启了 tcptwrecycle(tcp_timestamps 也是打开的情况下),在 60 秒内对于同源 IP 的连接请求中 timestamp 必须是递增的,不然认为其是过期的数据包就会丢弃。
学校这么多机器,你无法保证时间戳是一致的,因此就会出问题。
所以这玩意不推荐使用。
重用,即开启 tcptwreuse 当然也是需要 tcp_timestamps 的。
这里有个重点,tcp_tw_reuse 是用在连接发起方的,而我们的服务端基本上是连接被动接收方。
tcptwreuse 是发起新连接的时候,可以复用超过 1s 的处于 TIME_WAIT 状态的连接,所以它压根没有减少我们服务端的压力。
它重用的是发起方处于 TIME_WAIT 的连接。
这里还有一个 SOREUSEADDR ,这玩意有人会和 tcptw_reuse 混为一谈,首先 tcp_twreuse 是内核选项而 SOREUSEADDR 是用户态选项。
然后 SOREUSEADDR 主要用在你启动服务的时候,如果此时的端口被占用了并且这个连接处于 TIMEWAIT 状态,那么你可以重用这个端口,如果不是 TIME_WAIT,那就是给你个 Address already in use。
所以这两个玩意好像都不行,而且 tcptwreuse 和tcptwrecycle,其实是违反 TCP 协议的,说好的等我到天荒地老,你却偷偷放了手?
要么就是调小 MSL 的时间,不过也不太安全,要么调整 tcpmaxtw_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,公式我就不贴了,反正我不懂,不懂就不哔哔了。
超时重传是按时间来驱动的,如果是网络状况真的不好的情况,超时重传没问题,但是如果网络状况好的时候,只是恰巧丢包了,那等这么长时间就没必要。
于是又引入了数据驱动的重传叫快速重传,什么意思呢?就是发送方如果连续三次收到对方相同的确认号,那么马上重传数据。
因为连续收到三次相同 ACK 证明当前网络状况是 ok 的,那么确认是丢包了,于是立马重发,没必要等这么久。
看起来好像挺完美的,但是你有没有想过我发送1、2、3、4这4个包,就 2 对方没收到,1、3、4都收到了,然后不管是超时重传还是快速重传反正对方就回 ACK 2。
这时候要重传 2、3、4 呢还是就 2 呢?
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 其实是 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 上还有一张完整的图,画的十分清晰,大家看一下。
上文已经说了发送方式根据接收方回应的 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 过来,并且也是等待处理网络上残留的数据,怕影响新连接。
TIMEWAIT 不建议设小,或者破坏 TIMEWAIT 机制,如果真想那么可以开启快速回收,或者重用,不过注意受益的对象。
超时重传是为了保证对端一定能收到包,快速重传是为了避免在偶尔丢包的时候需要等待超时这么长时间,SACK 是为了让发送方知道重传哪些。
D-SACK 是为了让发送方知道这次重传的原因是对方真的没收到还是自己太心急了 RTO 整小了,不至于两眼一抹黑。
滑动窗口是为了平衡发送方的发送速率和接收方的接受数率,不至于瞎发,当然还需要注意 Silly Window 的情况,同时还要注意纳格算法和延迟确认不能一起搭配。
而滑动窗口还不够,还得有个拥塞控制,因为出行你我他,安全靠大家,TCP 还得跳出来看看关心下当前大局势。
至此就差不多了,不过还是有很多很多细节的,TCP 协议太复杂了,这可能是我文章里面图画的最少的一篇了,你看复杂到我图都画不来了哈哈哈。
今天我就说了个皮毛,如有纰漏请赶紧后台联系鞭挞我。
https://www.ionos.com/digitalguide/server/know-how/introduction-to-tcp/
https://www.ibm.com/developerworks/cn/linux/l-tcp-sack/
https://coolshell.cn/articles/11564.html/
领取专属 10元无门槛券
私享最新 技术干货