TCP没那么难吧?一文带你详细了解

栏目: 服务器 · 发布时间: 5年前

如今相当多的 程序员 都是“互联网程序员”,按说,应该对互联网的基础协议相当清楚。可惜至少就我的面试经验来看,许多人这方面缺课太多,简单说说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,既然它是双向、可靠的通讯,可以想见,建立连接就必须确认双方到对方的通讯都是可靠的,所以大概需要四步,发送四次消息。

TCP没那么难吧?一文带你详细了解

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

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

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

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

TCP没那么难吧?一文带你详细了解

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

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

TCP没那么难吧?一文带你详细了解

上面是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也确实做到了这点,它背后有一台完整的状态机,确保每时每刻,每个动作发生之后,状态都完全可控,一切尽在掌握,不会出现任何“孤点”和“断头路”。

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次。

TCP没那么难吧?一文带你详细了解

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

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

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

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

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

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

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


以上所述就是小编给大家介绍的《TCP没那么难吧?一文带你详细了解》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Rationality for Mortals

Rationality for Mortals

Gerd Gigerenzer / Oxford University Press, USA / 2008-05-02 / USD 65.00

Gerd Gigerenzer's influential work examines the rationality of individuals not from the perspective of logic or probability, but from the point of view of adaptation to the real world of human behavio......一起来看看 《Rationality for Mortals》 这本书的介绍吧!

SHA 加密
SHA 加密

SHA 加密工具

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具