如今,不少程序员都是“互联网程序员”。按说他们应该对互联网的基本协议还是比较清楚的。遗憾的是,至少根据我的面试经验,很多人在这方面错过了太多的课程。单单说TCP/IP协议的分层,难倒了很多人。至于TCP/IP的“三次握手”,能说出来的人相当少。如果问“为什么是三次握手”,基本上没人能回答。一般的回答是“这个太难了”,或者“毕业太久了,这个忘记了”。暂时先补习一下,把TCP三次握手背下来应对面试,确实可以。但是要回答为什么TCP是三次握手而不是二次或四次握手,光靠后面是不行的——不信你去网上搜一下,各种答案都有,众说纷纭,不少提问者一头雾水。TCP相关的知识重要吗?我认为这很重要。不管这些年互联网怎么变,TCP协议本身是可以承载的。如果仔细研究一下,你会发现它的设计确实别出心裁,有很多值得借鉴的设计思想。那么TCP真的难吗?为什么很多人死记硬背TCP握手过程,而且很难复述?我想原因是大家只把它当成“既有事实”,就像中学背历史背政治一样。但是TCP并不是不合逻辑的废话。一旦弄清楚了设计思路和逻辑,就会发现其实并不难理解。那么,今天我就给大家做一个简单的说明。首先说一下“three-wayhandshake”的翻译。真心觉得翻译有误(我翻译出版过百万字以上的技术资料,我相信我还是有把握的)。之前的“三次握手”过程想不起来了,因为总觉得“三次握手”和“握手”是双方一起运动的过程,显然不符合建立连接的过程。后来发现很可能是“三次握手”出了问题。“three-wayhandshake”的原文是三次握手,three-way更贴切的翻译大概是“三步”,所以整个名词的意思是“需要三步来建立握手机制”。这样解释的好处是“步”给人的感觉比较形象,即“单步”。事实上,RFC793中指出,握手过程也可以称为三消息握手,它是由三个消息建立的握手。那么,为什么要分三步建立握手呢?我们可以暂时忽略这个问题,想想如果自己设计握手机制应该怎么做。我们都知道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/ACKhook保持一致。上面是一个TCP数据报,里面包含了很多标识连接状态的控制位。最常见的有SYN、ACK、FIN:SYN表示synchronize,在建立连接时使用;ACK表示acknowledge,意思是“确认”消息已经收到;FIN表示finish,断开连接时使用。另外需要注意的两件事是SEQNO和ACKNO。SEQNO是序列号。服务端和客户端都会维护自己的SEQNO,表示“发送了多少数据”,单位为字节;ACKNO为AcknowledgeNumber,用于回复确认已接收到SEQNO对应的数据。到达。单独来讲,这些概念很容易理解,只是注意不要混淆控制位的ACK和ACKNO——ACK是一个布尔值,用来标识数据报的类型,ACKNO是用来确认接收到的值数据。根据以上知识,我们可以知道,在连接建立之初,数据报中的控制位SYN应该置1,表示“新连接”;同时,它应该包含SEQNO.此时的SEQNO有一个特殊的名字叫做ISN,即InitialSequenceNumber(注意ISN只是用来称呼这个特殊的SEQNO,并没有特殊的ISN字段)。当服务器收到第一个SYN消息时,它当然需要发送一个ACK??响应,但是它如何确认SEQNO“是”新连接的ISN,而不是来自来得太晚的旧连接??所以必须向客户确认。正是因为第二步是ACK和SYN“合二为一”的唯一响应,所以客户端在收到这条消息时,就知道需要响应其中的SYN,并验证其中的ACK(如果仔细阅读RFC793会知道的,有一段特别提到:Athreewayhandshakeisnecessarybecause...)第三步,client返回的报文中包含SYN对应的ACK,表示已经收到server的报文,同时设置SEQNO=ISN+1,确认ISN验证通过。服务器收到此消息并确认需要建立新连接。至此,连接建立。大流程是这样子的,不难理解。但是仔细想想,还是有很多问题需要考虑的。比如状态问题,既然TCP是网络通信,就会有延迟,那么当“信息已经发送,但还没有收到确认”的时候,就应该有一个明确的状态,否则就会出现状态混乱。事实上,TCP确实做到了这一点。背后有一个完整的状态机,保证每个动作发生后的每时每刻的状态都是完全可控的,一切尽在掌握之中,没有任何“孤点”和“死胡同”。上图是TCP状态转换图的一部分,涵盖了建立连接的状态。有兴趣的读者可以自行看看。它在科学技术领域也很常用。在设计波音737时,一开始,没有人知道如何放置发动机。设计师JoeSutter在纸上画出机身和发动机的模型,将发动机模型剪下来,贴在飞机的各个部位。地方,最后发现挂在翅膀下最合适)。在之前的软件设计文章中,我多次提到状态图和状态转换函数。不管是用户生命周期,还是订单流转流程,都可以用这个工具来解决。遗憾的是,我发现还是有很多设计师不理解或者不习惯使用,很遗憾。回到TCP建立连接的过程,我们同样需要关注ISN。建立连接时,首先要确定ISN,通过ISN对齐客户端和服务器端的计数。通常的教科书都说ISN是随机生成的,这样就保证了唯一性。随机性的目的是为了保持唯一性,但不要以为“随机性不会重复”,简单的“取随机数”很容易碰撞。所以传统的“随机”方案是维护一个时钟和一个32位的计数器,时钟每4毫秒计数器加1。因为2^32毫秒差不多是4个半小时(MSL,MaxSegmentLifetime),基本上超过了网络中任何数据包可能的传输时间,所以这个ISN可以认为是最好的。但这个方案也存在风险。由于这样的ISN是连续的,中间的恶意程序或许可以预测ISN的生成规则,从而伪造ISN……总之,ISN的生成是一个有趣的设计问题,这里不再展开。有兴趣的可以自行搜索资料阅读。我在开发中遇到过很多程序员。一旦他们需要避免重复,他们就会想到“生成随机数”,而不管随机数是否有可能发生碰撞。更有什者,一旦遇到像ISN这样的情况,想当然的把初始值设置为0,真是哭笑不得(有没有想过为什么ISN不能设置为0,欢迎留言讨论)。说完建立连接的握手,再来看终止连接的挥手。大家通常都知道TCP是“三次握手,四次挥手”(虽然我不认同“次”,但既然已经约定俗成,这里还是继续用总称吧)。那么,为什么要挥手四次呢?知道答案的人比能解释“三次握手”的人还要多。通常的回答是:TCP是一种双向通信协议。要结束连接,双方都必须发送终止信号,告诉对方后面没有数据要发送了,等待对方确认,所以一共需要2+2=4次。如果你之前看过建立连接的过程,你可能会有这样的疑问:既然建立连接的时候可以省去一步,把服务器返回的SYN和ACK结合起来,那是不是连接结束的时候也返回服务器?通过合并计算机的SYN和FIN来节省一步如何?恭喜你想到了这个问题,因为你不仅仅满足于“知其然”,而是希望“知其所以然”。但是我们也要想到,既然TCP连接的建立和终止都是同一群人定义的,既然他们在建立连接的时候能想到省一步,那他们就没有理由在终止的时候不省联系。没有“节省”肯定是有原因的。是的,确实是有原因的,而且这个原因很好理解,因为建立连接和终止连接的场景是不一样的。在连接建立之前,客户端和服务器都不会互相发送任何数据,所以当服务器用SYN返回ACK时,客户端当然知道这是从服务器收到的第一个数据包。当连接结束时,client向server发送一个FIN,说“我不会继续在这里发送数据”,server回应ACK,就没有问题了。但是此时,之前服务端向客户端发送数据的操作可能还没有完成,服务端还在向客户端传输数据。如果服务端结合了FIN和ACK,就会出现这样的情况:客户端的数据还没有收到,服务端突然收到消息“以后没有数据了,终止连接”。显然,这种情况是不应该出现的,所以ACK和FIN不能结合在一起,所以必须经过四步才能终止连接。最近和实习生聊天,聊了下开发中遇到的各种问题以及对应的模型。每个人都被迷住了。事后有人问我:为什么我们在工作中没有遇到这么有趣的问题呢?我知道,这是一个典型的问题。其实答案也很典型:因为你没有深究问题背后的原型。一旦了解了背后的原型,就会有“以知推无明”的能力和“以无知发现已知”的眼光。和朋友聊天,有一个共同的判断:TCP握手和挥手看似简单,但真正让今天的开发者设计握手和挥手流程。据估计,其中一半以上无法设计出稳定、可靠、高效的握手方式。和挥手的过程。这样一来,很多业务系统中业务层面的通信极不可靠,协议设计错漏百出,也是无奈的结果。再说一句。我在面试中遇到过这样一个人。他不是名校毕业的,有五年的工作经验。除了能流利地回答流行框架和热点问题,他还熟悉数据库理论、网络基础知识、数据结构和算法。事实充分证明,并不是每个人在工作后就把大学知识全部丢掉,也证明这样的人选确实可以担当大任。
