TCP 疑难杂症解析(2023年更新)

时间:2023-02-05 10:54:41

十多年前写过一篇文章:TCP 疑难杂症全景解析,这么多年过去了,是时候追加一些内容了。

随着 CDN 的部署,各大厂都在传输优化方向投入了大量的资源,避不开针对 TCP 优化。同时云计算又将大家卷入 DC 网络,依然还避不开 TCP。但无论 CDN 还是 DC,似乎都遭遇了瓶颈,TCP 优化不动了。是网络资源用尽了,还是 TCP 自己到顶了,如果是后者(显然是),那 TCP 的问题到底在哪?为什么 TCP 这么复杂,它的问题到底在哪?如果设计一个新协议,或至少迭代下一代 TCP,我们要怎么做?

TCP 大家都懂,但 TCP 的孬事未必都知道。无论从哪个方面看,TCP 都复杂。标准复杂,实现复杂,feature 多,拥塞控制复杂,信息不明确歧义多,启发判断复杂,误判,一个说不完的列表。但一开始 TCP 却是非常简单和明确。

抛开所有与 TCP 相关的 RFC,只看 RFC675 和 RFC793 才能感受这种简单。特别是 RFC675,这是原始的尚未标准化的 TCP,很多人并不知道它。如 RFC675 所述,1974 年附近 IP 尚未从 TCP 分离,它们是一体的,彼时 TCP 自身分了两块,上层主导可靠和保序,下层主导网络传输,后来,下层从 TCP 剥离,成了独立的 IP 协议,1980 年代初,剥离后的 TCP 和 IP 分别被标准化,即 TCP/IP。从此 IP 成了标准的 IPv4,不光承载 TCP,还承载 UDP 等几乎一切上层协议,由其 protocol 字段标识。

很多人不知道上面这段历史,TCP/IP 不是一个设计好的分层协议,它一开始并不分层,它是逐渐演化的。1974 年 RFC675 的 TCP 是这样的:
TCP 疑难杂症解析(2023年更新)
可以看出,原始 TCP 中关于网络传输的信息都有,并且抽象出了 packet 在 local network 上传输,这也就是后来的 IP 协议。

但被 RFC793 标准化后的 TCP 已经没了网络传输相关的逻辑,IP 与 TCP 解耦,不再专为 TCP 服务,从此,TCP 开始吃力补充这方面的缺失,事后来看,方向并不正确,但这是复杂性开始的根源。

TCP 不是良好的网络传输抽象,或者说它根本不包含网络传输逻辑,TCP 是一个对应用友好的流抽象,标准化后的 TCP 不携带任何与网络传输相关的信息,请问它如何控制网络传输。【这段是核心】

标准化后的 TCP 并不适合直接在 IP 上传输流,TCP 流没有边界,可被任意拼装和切割,这为处理带来了不确定性,另一方面,TCP 本身没有网络相关的信息,处理网络传输异常将非常吃力,至少,它应该找个贴着标签的箱子把自己装进去,然后 receiver 去找这个箱子。在春节假期旅游期间我曾有过一些思考,参见:重新设计 TCP 协议

谈一下标准化后的 TCP 本身。

TCP 可靠且保序,但实现可靠和保序的手段非常粗糙,receiver 只甄别正确的消息,错了就丢掉,直到正确,sender 超时就 g-b-n,不断推进 una,简单,直白。TCP 的首要目标是保证可靠保序语义,次要目标是节省端资源,确实,在 1970 年代仅用少量内存便可运行 TCP,但没有免费的午餐,这边省了那边费,粗糙的手段无疑将复杂丢给了网络,至少,g-b-n 造成了很多不必要的传输,无疑这是资源带宽,需要优化。

后面的事大家都知道,快速重传,nagle 算法,delay ack,reno,sack,reneging,timestamps,cubic,tlp,rack,bbr,诸如此类,无论哪一个被引入都旨在为网络减负,但 TCP 核心已经在那,目标从主要和次要变成了两个,一是节省主机资源,二是节省网络资源。换句话说,标准 TCP 已经在端侧节省了内存,现在要节省带宽了。

最极简且相对优雅的优化来自快速重传,即 “3 个重复 ACK 触发的重传”,但后续优化让 TCP 趋向复杂。

无论从端到端视角还是从网络视角,TCP 都不是一只故意将头埋在土里的鸵鸟,对可能发生且必须处理的事视而不见,TCP 显式屏蔽了它们,因为 TCP 根本就没有能看到网络问题的眼。

从 sender 写到 receiver 读,TCP 和 IP 接口时没做任何事。TCP 流直接交给 IP,没有报文约束大小,sender 只通过唯一的 ack 字段判断传输是否成功。积累确认确实简化了传输控制,但让网络传输的异常处理变得复杂。

而这些异常处理本应与 TCP 无关(这是 IP 的事,可 IP 已经走了),就像把货交给快递员并做了担保,剩下的事就和我无关,我对运输过程既不专业也没兴趣。

TCP 的问题恰在此,TCP 的可靠和保序只是一份签了字的保单,剩下交给封装,丢了,封装负责,陪你一份,TCP 缺失了对流的封装,缺失了网络传输抽象,以至于 TCP 在可靠和保序逻辑本身处理网络异常时面对很多不确定,巧妇亦难为无米之炊,正如让一个皮鞋厂瘸子工人亲自跋山涉水把一双皮鞋送到买家手上一样,且连鞋盒子都没有。

举几个例子。

reneging 机制初为 receirver 内存考虑,将不多几个与传输相关的逻辑也模糊化了,sender 不知道 receiver 是否 reneging,以至于 sender 不得不用猜测应对 reneging 的歧义,即便如此,这些 trick 也只是 maybe,而不是 must。

取消掉 reneging,sender 能省掉很多代码和内存,而 reneging 在今天非常罕见,因此,保留 reneging 罕为 receiver 腾内存,反而浪费 sender 内存,sacked 块几乎可立即释放,但 reneging 要求它们被 una 覆盖后才能释放,这么一个几乎没用的机制,但它却引发了很多问题,这不没事找事吗?

有一篇专门针对 reneging 文章:Transport layer reneging。TCP 类似这种没事找事的 feature 很多,大多数都是为解决一个彼时特定的问题,除了引入了复杂性,解决彼时特定问题的方案在未预期的将来带来更多未知问题的可能性更值得深思。

又如 delay ack。

delay ack 是不确定的,何时 delay,delay 多久,哪个 ack 被 delay,对 sender 不明确,偶尔的 delay 时间计入 rtt,影响拥塞控制度量,若不计入 rtt,则可能造成过早超时进而不必要重传,这造成 rtt,rto 计算歧义。sender 可通过积累确认的 first ack,last ack 以及 last inflight 报文是否小于一个 mss (恰恰利用了另一个 “优化”,nagle 算法…)来逼近准确值,但依然是 maybe。

delay ack 影响下,测量 “抛掉 delay 时间的精确 rtt” 确实没必要,因为 ack 时钟本身就被这个 delay 顺延了,但问题并非 rtt 准确与否,而是一致性。如果每个 ack 都被固定 delay,这就合理,但问题是某些情况下 delay,某些情况又必须立即 ack,就引入了歧义。

先说点好处,delay ack 减少了 ack 数量,给 sender 一定时间积累数据,给 receiver 反向 send 以捎带 ack 以机会。减少 ack 数量的意义并不仅仅在于节省带宽资源,更可以为 rtt 测量以及拥塞控制减少反向路径噪声的引入。ack 只是一个自时钟,但更应该在意 ack 报文里携带的信息,而不是 ack 本身。不做就不会错,ack 太多的话,反向 ack 路径的抖动噪声就更容易混入,而反向路径的抖动其实无伤大雅。

这些好处似乎帮 delay ack 扳回一局,但没有任何标准明确规定 delay 的时长以及必须停止delay 的条件,而这个不确定又导致了另一个问题,即 rto 必须有一个足够大的下界,能覆盖最大的 delay ack 时长,而该 “足够大” 的 rto 往往是某些时延敏感度小于这个下界(比如,Linux 是 200ms)的应用得不到即时重传而超时断开的根源,这个 issue 在遭遇 incast 时尤其使人崩溃。incast 发生时几乎一定会丢包,且可能丢整窗,至少也是大概率丢窗尾,rto 的值决定 sender 的重传敏感度,如果等 200ms(Linux 为例) 才超时,在数据中心是难以接受的。

delay ack 和 min rto 之间的纠缠,详见:Delay ACK 的对话

但另一方面(注意,又是 but),如果你私自减少了最小 rto 以支持时延敏感应用,在更短 rto 的情况下造成 sender 误判 ack 不会来了,从而超时重传,显然这是虚假重传,并没有丢包,只是因为 ack 因为 delay 而迟到了。可 sender 无法分辨这种情况。delay ack 好处不忍放弃,带来的问题也显而易见,为了解决这个问题的手段处境非常尴尬,做了解决问题,但又带来了另一个(注意,另一个新的…)新的问题,如此没完没了。

终极解决方案似乎也不难,干脆禁掉 delay ack(总要顾一方面吧),或再增加一个 option,允许协商是否禁止 delay ack。我也曾想过一个:Delay ACK 辩证考

TCP 被设计(并没有真正被设计,只是人们对其通用性抱有期望)成场景无关的协议,无论是局域网,还是跨洋传输,都可以在一套参数下部署运行,这是 TCP 的魅力,也因此带来了问题,既然想通用,就没有明确的 if-else,分辨差异的工作就要交给 “启发算法”,而启发算法是一定有概率误判的,如何启发,如何处理误判?请思考。

就着 delay ack 与 rtt,rto 的关系,说说 rtt,rto 本身,这次说精度问题。也许在长肥网络不重要,但在数据中心的短肥网络,一个共识是,rto 精度一定要和 rtt 精度在一个数量级。rtt 显然是 us 级,但 TCP 的 rto 却是 ms 级,显然违反了上述共识。让我们看看这意味着什么。

在数据中心常见的 incast 场景,很多TCP host 在 us 时间精度的时间差中几乎(如果不是同时的话)一起扇出数据,造成微突发拥塞而丢包,但由于它们的 rto 是 ms 精度,低了 1000 个数量级,这将导致它们在同一个 ms 检测到超时,进而在同一个 ms 一起重传,这将是又一次的 incast,随后的 backoff 将再次重复 incast,这是一种典型的全局同步现象。摧毁全局同步恶疾的良药就是引入随机,显然在 TCP 标准中 rto 以及随后的 backoff 都没有随机因素,解决这个问题倒是不难,rto(无论初始还是 backoff) 加上 -n~n 的随机 ms 即可,反正 min rto 也不小了,n 取 10~20 也不算大。而这需要修改 TCP 实现。我对 incast 的非 TCP 解法也是这个思路,详见:DC incast 的彻底解决

直接将 TCP rto 改成 us 级如何呢?当然可以,但有两个问题,首先这无疑再次增加了复杂性,每个 TCP 协议栈实现必须支持精度协商(再增加个 option 吗?Google 在这方面已经有了方案,Google 已经支持了 us 级时戳协商,但并未解决 rto 的问题。参见:us 级时戳),即必须同时支持两套精度,其次,实现 us 级 rto 所需的高精度定时器将会给主机处理带来负担。

这么妖娆,但这些只是冰山之一角。

sack 是 TCP 不多的几个与网络传输有关的 feature 之一,它比 “3 个重复积累 ACK 引发 g-b-n” 更精确 mark lost,但它只是有空间限制的 option,只能容纳最多 4 个区间,其大用也仅限于此,被 sack 的段由于和 reneging 的可能性胶合在一起不能释放,虽优于 g-b-n,但在协议层面依然没地方能做出标记,让 sender 有能力区分一个 ack 是原始报文触发还是重传触发,都可以区分 sack 段了,却无法区分 sack 段之外的重传段带回的 ack,这里还是个 maybe。其实区分重传的 ack 只需要一个 bit。

无法区分原始报文和重传报文的 ack,紧接着就是虚假重传问题,随之而来的应对方案就是 undo 的引入,undo 会有误判,那么 Eifel 算法呢?可它依赖 timestamps,先说 timestamps,其它的后面还会提。

timestamps 具有争议,它作为一个 option 在 TCP 标准化后 10 多年后才引入的,信封上可是一开始就有时戳的。有人觉得 ts 方便了 rtt 计算,但它更多的意义在 paws。说起 rtt,ts 其实可以做得更多,但它没有做,限制在于留给 TCP option 的空间太少了。sack 只能放 4 段,ts 也只能 10 字节。在明确不 care 回绕的场景,使能 ts 反而让每个 TCP 报文为网络带来 10 字节的额外开销。

我们希望 ts 协助预测拥塞,但它只能测量双向路径时延,考虑一条不对称往返路径,ack 方向的拥塞也会被误判为拥塞,而事实上,ack 只是提供一个激发时钟,再次强调,精确的拥塞控制应基于 ack 报文里携带的信息,而不是 ack 到达规律本身。

我提供了一种测量单向延时抖动的方法:测量 TCP 单向抖动,但又是 delay ack 让它成了 maybe 而不精确,TCP 显然没有多余的空间容纳 delay ack 信息反馈给 sender(其实也就需要 1 bit)。就更不必说学 Google swift 的样子打多个时戳了。

ts 精度问题与 rtt 和 rto 精度问题类似且相关,前面提到了,这里简单说几句,不多说,TCP ts 精度是 ms,这在 datacenter 是粗糙的,但将 4 字节时戳改为 us 精度,当有一天超高速网络环境比如 1Tbps,是不是也要考虑 ts 本身回绕呢?TCP 的几乎所有问题不都是彼时解决问题,此时却添堵的解决方案吗?但这就是演化的本质,如果 TCP 基于 TLV 或至少是变长头,所有问题就更简单,但这显然是马后炮。

如果梳理一下目前所有的 TCP option,会发现 kind 6 和 kind 7 是缺失的,其实它们 obsoleted by option 8,而 kind 8 就是 ts。那么 kind 6 和 kind 7 是什么呢?详见 RFC1072,它们最初是一对 option,即 tcp echo 和 tcp reply,专门用于测量 rtt,其中也是受到各种特殊场景比如 delay ack 影响而导致各种 situations to consider,最终它们被 ts 取代后,测量 rtt 反而成了次要功能。这是 TCP 演化过程的一个普通小插曲,正是这类插曲组成了 TCP 演化历程的全部。

各类歧义导致在正确但不精准的 TCP 上做拥塞控制非常困难。TCP 拥塞控制从绝对正确的 AIMD 开始,同样一个正确但不精准的策略,AIMD 保证绝对不会发生拥塞崩溃且公平,但代价是带宽利用率非常低。要提高带宽利用率,就需要精确的信息输入,可 TCP 偏偏没有精确的信息输入。

TCP 拥塞控制最大的障碍就是重传,一但进入重传,cwnd-based 和 rate-based 算法均要将 cwnd 作为 limit 并拉到一个足够低的值,由于重传歧义,inflight 不再容易计算,除了 packet_out 和 acked,还有猜测的 lost, 为小心翼翼遵守守恒律(否则会拥塞崩溃)必须计算 inflight,这依赖足够(足够即可)精准 mark lost,但无论 g-b-n,到 reordering-sack,还是最近的 rack-sack,均不足够精准,它们在某些非预期场景都会出现虚假重传,dsack 甚至触发有问题的 undo,Eifel 算法怎么样?好,但不完备,Eifel 算法只能保证满足条件的一定 undo,但无力检测不满足条件但也可以 undo 的状态。这些都拜信息不精确所赐。

不光如此,流式抽象需要滑动窗口,而滑动窗口也会因空洞重传而憋住,滑动窗口憋死,有 cwnd 配额也无数据可发了,详见:TCP 吞吐缺陷的根源

nak 怎么样?为什么 TCP 没有采纳 nak 而选择了 ack?这个 topic 可写 3 万字,问就是 TCP 一开始选择的就是 ACK,有过激烈的讨论,但可能只是恰好。显然,nak 可以协助 sender 更快更精确 mark lost,但即便想增加 nak 支持,TCP option 已经没有地方了,或者还是像 sack 一样的半吊子。

关于 nak,很多人都在质疑它丢包判断逻辑不闭环,比如 nak 自身丢了,nak 触发的重传报文丢了,只能依赖定时器。这明显是 ack/sack 先入为主了。如果放弃 ack/sack,直接由接收动作触发 nak,和计时器又有什么关系呢?让 nak 和 ack/sack 一样被触发就好了。比如说收到连续的 1,2,3,4,5,就什么都不做,如果继续收到 6,8,10,11,发现 2 个 hole,那么就立即 nak 7/9,接下来收到 7,12,13,15,那么 nak 9/14,以此类推,只要有 hole 就 nak,如果收到的是连续的 seq,什么都不做,岂不是更自然的方式?

没有消息就是好消息,这是我们从历史中总结出来的经验,用以指导高效的工作和生活,到了 TCP,ack 的方式反而成了莫名其妙但正确合理的方式。这就是先入为主的力量。

来看看 QUIC 怎么样?

很多 TCP 的歧义,QUIC 都消除了,但这并不意味着 QUIC 是好的。如本文介绍,问题就在那里,QUIC 见招拆招,但 TCP 彼时看到的是彼时才看到的问题,QUIC 解决此时的问题,但不一定能解决未来的问题,正如 TCP 一样,正是解决彼时那些问题的手段在如今弄巧成拙。有谁能保证 QUIC 如今的那些好 feature 在将来的新环境下不会像 TCP 一样弄巧成拙呢?

我也从来不觉得 QUIC 是 TCP 的替代。QUIC 明说是专门解决 HTTP 场景低效问题的。携带很多歧义进化的 TCP 只会让事情越发复杂,QUIC 消除了这些歧义,特别是从 stream 中抽象出了 packet,被 UDP 承载,显然为 TCP(如果还能这么称呼的话)补充了 “网络传输层”,但作为通用协议,加密,流复用做得有些过了,或者说,QUIC 根本就无意做 TCP 的后继,它只是 HTTP/3 的承载协议。

应该怎么做?我指的是传输。

放弃 TCP 流式传输,增加个封装,传输的是 packet 而不是 stream,简单的方法是在拥塞控制的调制下一把梭哈,receiver 没收到哪个 packet 就索要哪个 packet,一直收不到一直索要。没有 ack,没有 send/recv buffer,或者 buffer 只为优化。试想 rdma 的传输方式,receiver 就是整个内存,而非一条流,就理解了。对于 sender,不存在什么时候删除数据的问题,数据一直都在,发送的只是副本,或者由 receiver 显式指示删除。

也许你会觉得这把一切都甩给了应用层,但注意,这里的 sender 和 receiver 并非指应用层,而是网络传输的上层,即传输控制层。端到端的事务交给上面的传输控制子层,而网络的事务交给下面的网络传输子层,比如拥塞控制。

拥塞控制本应在 IP 层完成,如此才能有全局视角,将所有协议的全部流量纳入统一的拥塞控制,公平性问题也就迎刃而解,但木已成舟,我们不得不针对每一个传输协议单独做拥塞控制,而这个位置就在网络传输子层,在这个框架下,拥塞控制将不再受重传的影响。参见:重新设计 TCP 协议

TCP 的问题除了歧义,还有一点,它是自闭环的,无法从外界获取连接以外的信息,缺乏与上下层互通的接口。举个例子,如果有 tcp_get_linktype 接口,当它获知自己处在 Wi-Fi 环境时,是不是可以优化自己的拥塞控制决策呢?

是时候说说连接了。是 TCP 流抽象要求必须有连接,还是连接推波助澜了流抽象?Both !

TCP 连接性的问题主要体现在两方面,一是 3 次握手(实际上是 4 次握手,两个方向合并了一次)和 4 次挥手这种连接开销增加了延时,二是系统维护连接状态占用了必须的内存,而这些连接并非时刻活动的。

当要传输一笔数据时,有什么理由要约束它一定要通过流的方式传输呢?TCP 将这笔数据抽象成一条流无非就是怕它途中散掉,因为没有箱子装这些字节。为了维持这条流不散掉,首先要创建传输它的虚电路,这就是 TCP 必要要创建连接的起源。有了流的传输抽象这个前提,全双工 TCP 最大化了虚电路的利用率,虽然在后面的日子里绝大部分时间 TCP 都是单向传输,很少有对称传输的。

但传输一笔数据更合理的方式是将这笔数据按照顺序分装在编号的箱子(即 packet)里,传输这些箱子即可。无需事先创建虚电路,不需要连接,因为传输这些箱子的细节与这笔数据的 owner 无关。只需要能区分每个箱子属于哪个 owner 即可。

在上述合理的方式下,箱子无需识别数据 sequence,箱子只要贴两个标签,一个标签识别箱子编号,它只做唯一性标识(用于重传),甚至不需要和数据 seq 保持一致强序,另一个标签识别 owner。

你可能已经想到这个识别 owner 的标签就是端口号,但将它分作两级更合适。第一级用若干 bit 位识别进程,第二级再用若干 bit 位识别该进程发送消息的轮次。这便是真正的消息传输了。我举个例子。进程 A 申请到 1234 这个一级端口号,要发送一笔数据 “浙江温州皮鞋湿,下雨进水不会胖。”,这笔数据用 1000 这个二级 id 号标识,当对端确定收到这笔数据时,该进程再想发送数据时,二级 id 号变成 1001(进程内唯一即可)。

只要消除了流抽象,就不再需要虚电路,不再需要连接,不再需要维护状态。转变一个方向,所谓序号不再追踪消息的字节序列本身,而是追踪消息的轮次,状态就消失了。平时我们常用的 ping 命令,就是无状态的,我们发现,它有个 id 标识具体 ping 的轮次,还有个 icmp_seq 标识 packet,合理的传输大致就是这个意思。

所以你看,即便是 TCP 的连接性,也只需要将流抽象与网络传输解耦,它就消失不见了。当然,也就不再是 TCP 了。如今不是有很多传输协议在这样做吗?

你可能会说,如果传输流媒体这种本源的字节流不是很适合使用 TCP 吗?还是那句话,数据就是数据,传输就是传输,流媒体的流本质只是在 owner 看来是的,网络只能看到 packet。

回到现实 TCP 的使用,流抽象决定了端到端流控的最直白的方式,对连续字节流进行流控而不是对收到的数据量进行流控,这导致接收 buffer 和带宽利用率低下。明明还有空闲内存和带宽,一个 hole 便塞住了发送。阻塞的是应用数据,而不该阻塞网络发送。换句话说,更合理的流控应该针对标量,而非矢量。详见: TCP 和 宽容 以及 TCP 滑动窗口辩证考

当 TCP 由于底层原因导致传输无法继续时,它自身有重传机制,但程序员无法控制(除了一个非标的 TCP_USER_TIMEOUT),程序员不做重传,只能做重连,而重连开销巨大。应用将流式抽象坦诚给了 TCP,可 TCP 却没有暴露任何接口给应用。

举一个恰当的例子以结束。

考虑打电话的场景,电话是一条语音流,应该流式传输,这太合理了,所以电路交换网是电话的承载网络,这像极了 TCP 的传输方式。可我们最终发现,语音流竟然也能被分组交换网承载,2000 年后,voip 风靡一时,当时尚存在争议 IP 网络很难满足语音 qos,然而现在,微信通话,各种视频在 IP 网络上传输难道不是显而易见的吗?

IP 作为 packet 转发公共设施,承载各式各样的数据。若非事先已经有了分组交换网,对语音业务而言,很难 “自发” 发明出分组交换机制,因为在事先建立好连接的电路上直接传输语音流太正常太合理太符合语音流的本源特征(怕乱序,怕散掉)了,安土重迁,转到分组交换太难了。

有趣的是,分组交换网是现代操作系统进程间通信促进的,而不是传统业务比如电话促进的,我写过一篇随笔:现代操作系统与 TCP/IP

分组交换网最开始传输进程间通信的消息分组而不是传输语音流,但当语音流在分组交换网上 “试试看过” 后,反而简化了操作,提高了传输效率和带宽利用率。顿悟无状态(1),无连接(2)的分组(3)交换才是真正合理的传输方式。类比现实中的运输(运输一块玻璃,你会直接运这块玻璃吗?过程中搬着它小心翼翼怕碎了…),难道不都是分组交换吗?

日光之下并无新事,TCP 的演化可能也会经历类似的转变,可能会换个名字,不再叫 TCP(但肯定不是 QUIC),但不可否认的是,这个过程已经开始(数据中心网络尤甚),并且才刚刚开始。

一开始,我在 TCP 的历程中领悟到 “悟以往之不谏,知来者之可追”,但后来,我发现迄至今日的终点并非迷途,它讲述的恰恰是 “筚路蓝缕,以启山林” 的故事,这是互联网自身的故事,这个故事会很长,它的任何段落的主角都不完美,这是一部真正的历史,正如我们自身的历史一样,没人可以完美,但正是缺陷是继续演化的动机,而目标不是,没人知道目标是什么,没人知道途中会遇到什么。因为本没有路,今天的创新很容易变成明天的桎梏,这让我们欣然接受。
TCP 是演化而非设计出来的,这个过程还在继续,且远不止 TCP。

浙江温州皮鞋湿,下雨进水不会胖。