TCP 没有多难吧?

发布于 2023-08-10 12:50:33 字数 7448 浏览 54 评论 0

如今相当多的程序员都是互联网程序员,按说,应该对互联网的基础协议相当清楚。可惜至少就我的面试经验来看,许多人这方面缺课太多,简单说说 TCP/IP 协议分层就已经难倒了不少人。至于 TCP/IP 的三次握手,能说上来的人就相当少了,如果再问问为什么是三次握手,基本就没人能答上来了。一般的回答都是这个太难,或者毕业太久,这个忘记了。

如果临时抱佛脚,把 TCP 的三次握手背下来应付面试,确实能做到。但是要回答 TCP 为什么是三次握手,而不是两次或者四次握手,光靠背就不行了——不信你去网络上搜搜看,各种回答都有,众说纷纭,不少提问者一头雾水。

TCP 相关的知识重要吗?我觉得挺重要的,这些年来无论互联网怎么变化,TCP 协议本身都可以承载,仔细探究会发现它的设计的确够巧妙,有许多值得借鉴的设计思想。

那么 TCP 真的很难吗?为什么许多人背 TCP 的握手流程痛苦不堪,复述起来困难重重?我觉得,原因在于大家只把它当成既存事实,就像上中学时候背历史政治那样对待。但 TCP 可不是毫无逻辑的胡说,一旦你搞清了设计思想和逻辑,就会发现理解起来一点也不困难。所以,今天我来做个简单讲解。

首先说说三次握手这个译名,我确实觉得翻译有误(翻译出版过一百多万字技术资料,我自信还是有把握的)。我以前总记不住三次握手的过程,因为总觉得握了三次手,握手是双方共同往中间凑的过程,这明显和建连流程不符合。后来才发现,三次握手的说法大概有问题。

三次握手的原文是 three-way handshake,three-way 更合适的翻译恐怕是三步,所以整个名词的意思是需要三个步骤才能建立握手的机制。这么解释的好处是,步给人感觉更形象,就是单方面迈一步而已。实际上,RFC 793 里说明了,握手过程也可以叫 three-message handshake,通过三条消息来建立的握手。

那么,为什么要三步才能建立握手呢?我们可以暂时不理这个问题,想想如果我们自己来设计握手机制,应当怎么办。

我们都知道,TCP 是可靠的通讯协议,其可靠性就在于,任何一方要向另一方发数据(SYN),都必须收到确认回应(ACK)。同时 TCP 也是双向的通讯协议,所以通讯的两方都可以主动发送消息。

这里要澄清的一点,对许多互联网程序员来说,TCP 是掩盖在 HTTP 之下的,大家熟悉的 HTTP,它的经典通讯模式是一问一答的,没有请求就没有应答。不过这只是 HTTP 的特性,不是 TCP 的特性。在 TCP 协议里,客户端和服务器都可以随时主动向对方发送数据——也正是因为如此,改用 HTTP/2 之后服务器可以主动推送信息给客户端,而不必改动 TCP 协议。

回到 TCP,既然它是双向、可靠的通讯,可以想见,建立连接就必须确认双方到对方的通讯都是可靠的,所以大概需要四步,发送四次消息。

如果软件设计都这么简单,那就太好了。可惜,世界上没有那么简单的事情。仔细观察这幅图,我们会发现几个问题:

第一,网络通讯的成本是很高的,延迟往往无法预测,哪怕能少发送一次消息,也可以大大降低成本,提高效率。所以,建立连接的步骤上限应当是四步,下限是两步,越少越好。

第二,两轮 SYN/ACK 之间必须有关联,因为它们的功能相对独立,都是确认到对方的通讯可靠,却同属于一个建立连接的逻辑操作。如果两轮完全独立,那么如果两轮中间间隔了特别特别长的时间,根本不是一个正常的建立连接的操作,程序却无法识别,这显然是不行的。所以,第二轮 SYN/ACK 必须要能够和第一轮 SYN/ACK 关联起来。

再仔细看看,第二步和第三步都是从服务端给客户端发消息,所以是不是可以合并起来?这样起码可以节省了一次网络通讯。

像上面这样直接在第二步把 ACK 和 SYN 合并起来,问题就解决了?

按照之前的分析,节省消息发送次数只是考虑之一,还需要考虑的是,第二轮 SYN/ACK 必须和第一轮 SYN/ACK 挂钩。

上面是 TCP 的数据报,包含了许多的控制位,用来标识连接的状态。其中最常见的是 SYN、ACK、FIN:SYN 表示 synchronize,在建立连接时使用;ACK 表示 acknowledge,表示确认收到了消息;FIN 表示 finish,在断开连接时使用。

还要注意的两个东西是 SEQ NO 和 ACK NO。SEQ NO 即 Sequence Number,服务端和客户端都会维护自己的 SEQ NO,表示已经发送了多少数据,单位是字节;ACK NO 即 Acknowledge Number,用来回复确认,对应 SEQ NO 的数据已经收到。单独说起来,这些概念都容易理解,只是注意不要混淆控制位的 ACK 和 ACK NO——ACK 是布尔值用来标识数据报的类型,ACK NO 是数值用来确认已经收到的数据。

基于上面的知识我们可以知道,在建立连接之初,数据报中的控制位 SYN 应当设定为 1,表示新建连接;同时应当包含 SEQ NO。此时的 SEQ NO 有个专门的名字叫 ISN,也就是 Initial Sequence Number(要注意,ISN 只是用来称呼这个特殊 SEQ NO,并不存在专门的 ISN 字段)。

在服务端收到第一个 SYN 消息的时候,它当然需要发送 ACK 响应,但它如何确认其中的 SEQ NO 就是新建连接的 ISN,而不是来自姗姗来迟的某个古老连接呢?所以必须向客户端确认。恰恰因为第二步是 ACK,SYN 合二为一的独特响应,所以收到这个消息时,客户端就知道,既需要响应其中的 SYN,也需要核实其中的 ACK(如果你仔细读过 RFC793 就会知道,其中专门有一段提到了:A three way handshake is necessary because……)

到了第三步,客户端返回的消息里既包含对应 SYN 的 ACK,表示收到了服务端的消息,同时设定 SEQ NO=ISN+1,确认核实了 ISN。服务端收到这条消息,确认无误是要建立新连接。至此,连接建立完毕。

大流程看起来就是这样,也不难理解。不过仔细想想,还是有不少问题得考虑的。比如状态问题,既然 TCP 是网络通讯,会发生延迟,那么在信息已经发送,但还没有收到确认的时候,应当是有个明确状态的,否则会发生状态的错乱。实际上 TCP 也确实做到了这点,它背后有一台完整的状态机,确保每时每刻,每个动作发生之后,状态都完全可控,一切尽在掌握,不会出现任何孤点和断头路。

上图是 TCP 的状态转移图的局部,覆盖了建立链接的状态,感兴趣的读者可以按照自己实地走走看(说个题外话,自己模拟在图上走走看起来土,其实高科技领域也挺常用。设计波音 737 的时候,开始大家都不知道发动机怎么摆比较好,设计师乔·萨特就在纸上画出机身和发动机的模型,把发动机模型剪下来在飞机各处摆放,最终发现吊在翼下最合适)。

我在之前关于软件设计的文章里几次提到状态图、状态转移函数,无论是用户生命周期、订单流转过程,都可以用这个工具来解决。遗憾的是,我发现还有许多设计人员不懂得或者不习惯用使用它,实在很可惜。

回到 TCP 建立连接的过程,我们还要注意 ISN。在建立连接时必须先确定 ISN,通过它把客户端和服务器的计数对齐。通常的教材上说,ISN 是随机生成的,这样就保证了唯一性。随机的目的是保持唯一,但千万不要以为随机就不会重复,简单的取随机数是很容易碰撞的。所以传统的随机方案是维护一个时钟和一个 32 位的计数器,时钟每过 4 毫秒,计数器自增 1。因为 2^32 毫秒就是差不多 4 个半小时(MSL,Max Segment Lifetime),这基本超出了任何数据包在网络中的可能传输时间,所以可以认为这种 ISN 是独一无二的。

但这种方案也有风险,既然这样的 ISN 是连续的,那么中途的恶意程序可能能够预测 ISN 的生成规律,从而伪造 ISN…… 总之 ISN 的生成是个有趣的设计问题,这里不展开了,有兴趣可以自己搜索资料阅读。

我在开发中遇到不少程序员,一旦需要避免重复,就想到生成随机数,根本不管随机数也可能碰撞。更有甚者,一旦遇到类似 ISN 的场合,就想当然把初始值设定为 0,真是让人欲哭无泪(有没有想过 ISN 为什么不能设定为 0 呢,欢迎留言讨论)。

说完了建立连接的握手,我们再来看终止连接的挥手。通常大家都知道,TCP 是三次握手,四次挥手(虽然我很不赞成次,但既然它已经约定俗成,这里还是延用通用的说法吧)。那么,为什么要四次才能挥手呢?

知道这个答案的人比能讲清楚三次握手的要多。通常的答案都是:TCP 是双向通讯协议,要结束连接,双方都必须发送终止信号,告诉对方后续再没有数据发过来了,并等待对方确认,所以一共需要 2+2=4 次。

如果你之前看过建立连接的过程,大概会有这样的疑问:既然建立连接的时候可以节省一步,把服务端返回 SYN 和 ACK 合并到一起,那么结束连接的时候,是否也可以把服务端返回的 SYN 和 FIN 合并起来,节省一步呢?

想到了这个问题就值得恭喜,因为你不是只满足于知其然,而希望知其所以然。不过我们也需要想到,既然 TCP 连接的建立和终止都是同一批人定义的,既然他们能想到在建立连接时节省一步,那么他们没有理由在终止连接时不做节省。之所以没有节省,一定是有理由存在的。

没错,确实是有理由的,而且这个理由很好理解,因为建立和终止连接的场景是不一样的。在建立连接之前,客户端和服务器端都不会向对方发送任何数据,所以在服务端返回 ACK 的时候带上 SYN,客户端当然知道这是从服务端收到的第一个数据包。

而在结束连接时,客户端向服务端发送 FIN,表示我这边不会继续发送数据过来了,服务端响应 ACK,这都没有问题。但此时,服务端之前向客户端发送数据的操作可能还没有完成,服务端仍然在向客户端传输数据。如果服务端把 FIN 和 ACK 合并起来,就会出现这样的情况:客户端的数据还没有接受完,忽然收到服务端的消息后续没有数据了,终止连接。显然,这种情况不应当出现,所以不能把 ACK 和 FIN 合并在一起,所以终止连接必须要四步。

最近和实习生聊天,说起开发中遇到的各种问题,以及对应的模型,大家听得入迷。事后有人问我:为什么我们工作中遇不到这么有意思的问题呢?我知道,这是个比较典型的问题。其实答案也很典型:因为你没有去深究问题背后的原型。懂得了背后的原型,就具备了从已知推导无知的本领,也具备了从无知中发现已知的眼光。

我和朋友聊开发有个共同的判断:TCP 的握手和挥手看起来简单,但真让如今的开发人员去设计握手和挥手流程,估计有超过一半的人设计不出稳定、可靠、高效的握手和挥手流程。这样说来,许多业务系统里业务层面的通讯极不可靠,协议设计错漏百出,也是无奈的结果了。

补充一句。我曾在面试中遇到过这样的人,非名校毕业,已经有五年工作经验,除了对流行的框架和热点问题对答如流,对数据库理论、网络基础知识、数据结构和算法依然如数家珍。事实充分证明,不是所有人工作之后就把大学的知识丢个精光的,事实也证明,这样的候选人确实能担大任。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

0 文章
0 评论
24 人气
更多

推荐作者

内心激荡

文章 0 评论 0

JSmiles

文章 0 评论 0

左秋

文章 0 评论 0

迪街小绵羊

文章 0 评论 0

瞳孔里扚悲伤

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文