引子
不管你相不相信,每个程序的日志都记录了一个完整的故事,一个由代码和数据相互交谈而产生的故事。就像你们人类的故事一样,这里面既有日常琐事,又有奇事趣闻,自然也少不了各种喜怒哀乐。只要你仔细聆听,就一定能够听懂。
请注意,当我在说这些话的时候,我并不是在打一个比喻,而是在描述确凿的事实,就像太阳的东升西落那样不容置疑。每个日志中记录的故事,在那个世界都曾经真切的发生过。如果你还是不相信,请阅读下面这个故事,它是我的一段亲身经历。这个故事就记录在一个程序日志里,而这个日志文件至今还躺在主人的笔记本电脑中的某个位置,随时供人查阅。
哦,对了,你还不知道我是谁?其实跟故事本身比起来,这并不那么重要。我只是数字空间(Cyberspace)里一个普通的字节(Byte)。我可以随身携带8个比特(Bit)的信息,请叫我0x30。
相遇,启程
按照你们人类的说法,我是个男生。为了叙述方便,姑且按照这样的方式来定义吧——奇数值的字节是女生,偶数值的字节是男生。这样说的话,现在紧挨着我坐一起的这两位,0x35和0x32,一位是女生,而另一位就是男生喽。同样是为了叙述方便,下面我给他们各起一个人类的名字,0x35叫小丽,0x32叫小明。而我呢,如果你们愿意,可以叫我小白。
在我们三个碰到一起之前,我们已经在数据库里睡了几个世纪之久(当然是按照数字空间的时间单位)。多半是因为受到某个网络请求的触发,我们被加载到了内存里。此刻,我们位于一个很庞大的对象实例的内部。我粗略估算了一下,同我们一起被加载进来的,大概还有几千个字节,他们大部分都存储在这个对象内部的一个List中。
等了一会,负责执行CPU指令的进程老大跑过来告诉我们,“大家注意,你们马上就要被发送到一条TCP长连接上去,对方正在等待你们所携带的信息。由于这是一个串行的通道,所以,你们所在的整个对象首先要被序列化(Serialization)。”
我正在困惑之际,没想到小丽率先替我问出了心中的疑问:
“序列化?那是什么意思啊?”
她说话的声音很好听,就像某个电台的女主播。我不禁有些恍惚。
“序列化就是按字节先后排成一队,就好像回龙观西大街上的汽车要经过北郊农场桥,所有车最后都必须并到一个车道上去。”进程老大耐心地向我们解释道。
然后,我们按照一种被称为protobuf的数据格式完成了序列化。据说,这种数据格式是由一个「不存在」的国际化大公司设计出来的。不管那么多了,现在重要的是,我们原来在同一个对象实例内部的所有字节被排成了长长的一队,而且我发现,队伍中还插入了一些额外的字节。最后队伍的总长度达到了3400个字节。
在序列化之后的队伍中,小丽,小明,和我,我们三个仍然挨在一起。我排在第1461个字节位置,而他们俩都在我前面。也就是说,小丽排在第1459个字节位置,小明排在第1460个字节位置。
算法小哥告诫我们,队伍一旦排好,顺序就绝对不能再变了,而且所有字节一个都不能少。否则,到了接收端,我们将无法被反序列化(Deserialization)。这当然也包括新插入的那些字节,比如站在队头的那几个,他们携带着整个队伍的长度信息,至关重要。
“伙计们,现在你们已经组成一个完整的消息(Message)了。一路顺风!”说完,进程老大就将我们由字节组成的整个消息队伍扔到了协议栈的buffer之中。
协议栈的buffer就像一间很大的候车大厅,我们并排坐在里面,小心地保持着原来的次序。
负责维护协议栈的内核程序告诉我们,TCP通道上正在进行流量控制(Flow Control),原因是TCP另一端的接收窗口目前不允许我们发送。所以,需要我们耐心等待。
为了打发无聊的时间,我就跟前面的小丽和小明他们攀谈了起来。聊了一阵子,小明突然提议说:“反正也闲得无聊,要不我们一起玩个游戏?”
“好啊好啊,真心话大冒险怎么样?”我赶紧附和,并用眼角的余光扫了一下小丽。
小丽迟疑了一下,“这个有点太刺激了,要不我们还是来个文艺点的吧。每个人讲一个故事如何?看谁讲的故事精彩!”
对于女人的建议,我和小明自然都欣然采纳,“那就这样,按顺序来,女士优先吧。”
小明又补充道:“毕竟我们都生活在数字空间,而且,你们知道吗——我们的听众基本上都是程序员。所以我建议,故事最好要跟程序员或编程有关。”
小丽听完白了他一眼,不置可否。
接下来,小丽用她凄婉的语调讲述了一个令我们所有人都感动不已的故事。
我在死神前转身,只因你一生的坚守
窗外是淅淅沥沥的小雨,而她的心情也跌落到了谷底。她想象着那些冰冷的雨滴,滴答滴答,落在肌肤上,浸透她的全身。
古灵儿躺在床上,睁着大眼睛瞪着天花板。她从药瓶中又倒出了一粒药片,动作娴熟地一口吞进了肚子里。已经是第十粒安定片了,却了无睡意。她现在有点怀疑网上的说法,安眠药吃到100片真的能致人死命吗?
她现在还有最后一件事要做,这也是她最后的一点希望了。她把已经关机了好几天的手机重新拿出来,开机,然后把卸载掉的各种社交软件也重新安装了回来。
除了那些讨债的人,没有任何人联系她。她的爱人没有任何消息,没有短信,没有留言,没有未接电话。
那些上门讨债的人曾经告诉过她,她的老公携款潜逃了,带走了他们的血汗钱。直到现在,古灵儿也都不相信,那个深爱她的男人会抛下她不管。但事实似乎无可争辩,她老公的朋友圈最后一次更新还是在两个月前,也就是他突然消失不见的前几天。
最近这些日子,是她人生中最痛苦的一段经历。为了躲避那些上门讨债的人,她搬了好几次住处,卸掉了所有的社交软件,甚至很多时候不得不把手机关掉,但手机号却一直坚持没换,因为她怕老公联系不到她。至今她仍然心存幻想,幻想着突然收到他的消息,然后他会向她解释所有这一切。
其实她对最近发生的事情一直都很迷惑。他老公的公司经营得一直还算顺利,甚至在他消失的前不久,他还踌躇满志地告诉她公司马上就要进入下一轮融资了。直到两个月前,一群人突然闯进她们的家里,宣称她老公的公司产生了上亿的坏账,并要求她替丈夫还债。而她竟然也真的联系不到他了。
现在她知道了,幻想终归是幻想,她的心重新变得冰冷。她又拿出药瓶,吞下一粒。此刻,她头脑中只剩下一个念头,就这么吃下去,直到永远睡去。
突然,「叮」的一声,手机收到了一条推送。她一把抓过手机,竟然是来自她已经很久不玩的一款App——名字叫「微爱」的情侣软件。那是很久很久之前,她和她的前男友,也是她的初恋,一起在「微爱」上开通的私有空间。本是小孩子的把戏,两个人每天坚持浇灌一棵虚拟的爱情树。早在两年前他们分手之后,她就不再玩了。没想到,那个像傻瓜一样的他却还在坚持。手机推送告诉她,对方已经连续浇水超过2000天了,他们的爱情树又升到了一个新的等级。
思绪如快速生长的草木一般,触及到了一段尘封的记忆。
两年前,那也是一个雨天,五月的一个雨天。天气本应是温暖宜人的,但那天却格外阴冷。就像今天一样。
“我们还是分手吧。”有多少次想说,却始终无法开口。终于,古灵儿还是把那个句子吐了出来。
苏秦表情木然地站在那里,原本就黯淡无光的眼睛彻底蒙上了一层灰纱。
不再有悲伤和争吵。该吵的,该闹的,该哭泣的,都已经过去了。在一起三年多了,两个人之间细小的战役也断断续续地持续了三年。
该走的终会走。
这并不能怪古灵儿。整日面对这样一个吊儿郎当的男人,哪个女人又会有安全感呢?说起来有点讽刺,苏秦,跟古代那个「头悬梁,锥刺股」的苏秦是同一个名字,可怎么就没有一点相像呢?那时,他毕业后干程序员这一行也好几年了,不仅没获得任何升职机会,甚至工资都没涨过太多。他这性格从上大学那会就一直这样,平日里嘻嘻哈哈,东游西逛,从来不会认认真真地钻研点东西,还经常花大把的时间打游戏。干程序员这一行的,本应是个高薪行业,他周围的很多同学早就买房买车了。
而古灵儿的家里人一直对她的婚事催个不停。稍微有点理智的女人,都会像她这样选择的。跟苏秦相比,她后来的丈夫不管哪一点,都要强上一万倍。没想到......
同样没想到的是,在这样晦暗的时刻,他又以另外的一种方式出现在她的面前。她突然想起了他过去所有的好,想起了他骑着自行车带着她游遍了北京城的每一个角落,想起了那一天,当她最后转身离开的时候,苏秦向着她在雨中的背影大声地喊:“灵儿,我会等你回来的!”
古灵儿从床上爬起来,走到穿衣镜前。她缓缓地脱去身上的睡衣,扔在一边,镜中出现了她憔悴的面容、赤裸而日渐瘦弱的身体。一股巨大的悲伤感向她袭来,如汹涌的波涛一般。她再也控制不住自己,两行热泪夺眶而出。
她蹲在卧室的地上哭了半个小时。最后,她拿起手机,从通讯簿中找到了苏秦的号码。
遥远的路途
“讲完了?”
面对我和小明的问题,小丽点了点头。
“你这故事跟程序员一点关系也没有啊!”小明提醒她。
“怎么没有?那个前男友就是个程序员呢!”小丽反驳说。
“但故事情节跟程序员没关系啊......”小明大概是感觉有点说不清楚,换了个问法,“那他会写复杂的程序吗?他是个技术高手吗?”
“不是。这根本不重要!”小丽一脸不耐烦地回答。
“你这故事是真的吗?”我问小丽。
“当然了!我就曾参与过那次关键的推送,我是那条推送消息里的一个字节。”小丽回答问题的同时,也说出了自己的来历。
“后来他们怎么样了呢?”我继续追问。
“她和苏秦吗?当然是幸福地在一起了啊。”
“那古灵儿的老公欠的的那些债务呢?最后怎么解决的呢?”
小丽对这个问题似乎也不太感兴趣,“我也不清楚了。估计苏秦会帮她打官司吧。她应该没有义务承担这笔债务吧。”
正在这时,协议栈的内核程序突然又跑过来,向我们喊道,“快点准备!马上要出发了!对方的TCP接收窗口已经打开。”说着,他打量了一下所有这3400个字节,补充道,“现在buffer里字节太多,对方的接收窗口一下子装不下。这样吧,前面2000个字节先走!”
紧接着,协议栈各个协议层的子程序又在我们这2000个字节的最前面加上了20个字节的TCP头,然后又在前面增加了20个字节的IP头,最后,又在这2040个字节的前面和后面都增加了链路层的帧头和帧尾。
我们从内网上行路由器的一个出口出来,进入了一条光纤通道。路由器上的路由程序告诉我们,下一站是广州的一个节点。
“我们这一站要走3000多公里啊!”
“可不是嘛!”
我们三个又开始闲聊起来。
小丽感到很困惑,“我们的出发地点和目的地好像都在北京,为什么要先到广州走一圈呢?”
“这可能是一种路由策略。我猜可能是网络运营商在南方的线路资源比较便宜,才会给出这样的路由。也可能是一种配置错误。”我根据我仅有的一点网络知识,向小丽解释。
“对我们会有什么影响吗?”小丽不无担心的问。
“我们中间会经过更多的路由节点,延迟会高一些。如果跳转节点过多,而我们的IP包头的TTL (Time To Live)又被耗尽的话,我们可能被整体丢弃掉。”我突然意识到把问题说得太严重了,赶紧又补充了一句,“不过这种丢弃的情况很少见啦。”
虽然嘴上这样说,但我心里却产生了一股不祥的预感。可能是害怕的原因,小丽也有些脸色发白。
小明忍不住插嘴道,“你说你们俩,说这些没用的干嘛呢?我们又控制不了。这不是自己吓自己吗?”
“是啊是啊。”我随声附和,“反正这一趟路途遥远,我们有的是时间,那我们继续之前讲故事的游戏好了。第二个谁讲呢?”
“我讲我讲!”小明抢着说,“我这次要讲一个真正的跟程序员有关的故事。主人公可是个顶级的技术高手!”
黑客与女孩
我要讲的这个故事啊,可跟我的亲身经历有关。故事的主人公,是一名身怀绝技的网络黑客。他的黑客技术登峰造极,只要动动手指就能让几千个节点的网络瘫痪!他的程序能够穿透各种防火墙,到达别人根本无法到达的地方。
小明神秘的语气吸引了我们。我和小丽聚精会神地听着。
黑客就像网络世界的一个侠盗,他游历世界,劫富济贫,专门收拾那些为富不仁的有钱人。
有一天,黑客在海边度假的时候,碰到了一个女孩。他深深地爱上了她。
哦,对了,我开头忘了介绍,我们的这位黑客主人公虽然技术高超,但表达能力欠缺,而且有严重的社交恐惧症。他好几年都独来独往,从不与外人接触,整日都泡在网络上。他大概没有勇气向女孩当面表达他的爱意。所以,他开始用自己的方式去接触她。
他入侵了女孩的家庭网络,潜伏在她的个人电脑里面,注视着她的一举一动。令他灰心丧气的是,女孩的生活中原来有另外一个男人,那可能是她的男朋友或者丈夫。黑客感觉到自尊心受到了伤害,他顺藤摸瓜,很快黑进了那个男人所在公司的网络。
随后的发现令他大吃一惊。那是一个做互联网金融业务的公司,那个男人是公司的老板。黑客发现公司的账目存在严重的问题,公司非法集资,账目混乱,仿佛在故意掩盖着一个阴谋。他想亲口去告诉那个女孩,但好几次他又退却了。女孩怎么可能会相信他呢?
现在,对于我们的黑客来说,这已经不是一个私人恩怨的问题了,这涉及到很多人的财产安全。这是一个社会正义的问题!于是,黑客利用那家公司服务器的一个「SQL注入」的漏洞,让公司服务器集体瘫痪,再也无法启动,同时又将隐藏的公司账目提取了出来,并公开发布在了网上。
分手
“知道吗?我,就是这名黑客的工具程序中携带的某个字节。我还亲自参与了那次的入侵行动!”
对于他的身世来历,小明说起来一脸的自豪。
“那最后黑客跟那个女孩在一起了吗?”小丽问。
“可能在一起了吧。不过这根本不重要!”这次轮到小明不耐烦了。
现在,我们已经经过了许多中途的路由器节点。在每一个节点上,路由程序都会根据20个字节的IP头中的目的地址去查阅路由表,决定下一跳把我们发送到哪里。眼看着离目的地越来越接近了。
这时,我们来到了一个新的路由节点。路由程序告诉我们一个不好的消息,前面的路线要经过一个以太局域网,这个局域网的MTU (Maximum Transmission Unit,最大传输单元) 是1500字节。所以,我们这2000个字节要被分成两部分分别发送。路由程序解释说,在IP协议里,这叫分片(Fragmentation),并详细解释了分片的过程。
具体的分片过程稍微有点复杂。首先,分片是IP层的策略,因此它不识别TCP层的封装。也就是说,20个字节的TCP头加上原来2000个字节的数据,这总共2020个字节,在IP层协议执行分片的时候都当做数据。前面的1480个字节分到第一个数据片,他们这一队前面加上20个字节的IP头,成为1500个字节的新IP包,恰好可以通过MTU的限制。而从第1481个字节开始,后面的540个字节,将分到第二个数据片。这个数据片同样在前面加上20个字节的IP头,成为一个560字节长度的IP包。
我突然意识到,第1481个字节位置,由于里面多算了20个字节的TCP头,所以在原来的数据中实际上是第1461个字节。而我不就正好排在第1461个字节位置吗?这就是说,我将被分到第二个数据片,而小明和小丽在第一个数据片!
真是糟糕透了。
当我把这个分片的结果告诉他们的时候,我确信我看到了小丽眼中现出担忧的神色。
“那我们还会再见面吗?”小丽问。
“当然会了。两个数据片分头到达目的主机之后,还会在协议栈里重组(Reassembly)的。”我故作轻松地解释,但心中不安的感觉却越来越强。
“真可惜!我们还没听到你讲的故事呢。”小明开玩笑地说。
“等重组之后,我马上讲给你们。”
我刚说完,分片操作就已经完成。载着小明和小丽的数据片「嗖」地一声被发射出去。而我仍站在原来的buffer中,向他们挥手告别。
防火墙
我在第二个数据片中,紧跟在20个字节的IP头后面,排在数据的第1个位置。一路上,我陷入了沉思。
我们又经过了几个路由节点,慢慢地靠近了目的主机。
“停!”突然有人大喊一声。原来是目的主机上的防火墙程序,他要求我们停下来接受检查。
“不对啊,你们这个数据包的目的端口没有在白名单内!”最后,防火墙程序下了结论。
“什么!”几乎数据片内的所有字节都喊了起来。
防火墙程序无奈地摊开双手,“真是抱歉。但规则就是规则。你们只能被丢弃掉!”
我所在的数据片内一片骚乱。
“等一下!”排在第1个数据位置的我大喊一声。此刻我突然冷静了下来,看来面前是个工作在四层的防火墙程序。我似乎明白怎么回事了,“请听我说!并不是目的端口不对,而是根本没有目的端口。我们是IP分片之后的第二个数据片,只有IP头,没有TCP头。你要看的目的端口号应该在TCP头内部,但我们这里根本没有。”
防火墙程序狐疑地看了我一眼,“那你说怎么办?”
“如果我没猜错的话,我们的第一个数据片在之前已经通过了你这里。你可以去查查记录,看有没有一个已经通过的数据包,具有跟我们完全相同的一个Identification字段(注:是IP头用于分片和重组的一个字段)。如果有的话,我们就应该也能通过。”
“好,你们等着,我去查一下。”
时间仿佛过了一个世纪之久。看来这个防火墙在处理分片情况下的过滤算法效率并不高。
我心中那种不安的感觉似乎又逐渐升腾起来。没错,如果幸运的话,小明和小丽他们应该已经通过了这里,但如果我们却是先他们一步到达的话......后果真是不堪设想!
还好,防火墙程序最后终于重新返回,对我们点了点头,“你们可以通过了。”
重逢
我们在目的主机的IP层成功完成了重组 (Reassembly),重新变成了2020个字节的队伍(包括20字节的TCP头)。
我又重新和小丽、小明他们挨在了一起。一切仿佛一如从前,但又变得有点不同。
小丽和小明现在手拉着手,亲密地偎依在一起。他们见到我,微笑着同我打招呼:“你怎么这么慢啊!终于又见到你了!”
我胸中涌起了一股类似嫉妒的情绪。
突然间,我完全明白了,从头至尾。
“我现在就给你们讲第三个故事。”我顿了一顿,“很凑巧,故事的男主人公也叫苏秦,而女主人公也叫古灵儿。”
他们吃惊地瞪大了眼睛。
第三个故事
苏秦是计算机系毕业的,毕业后做了程序员的工作。但是,他知道自己并不擅长做这份工作,所以一直没有信心。
他和古灵儿在毕业之前就开始恋爱了,那是一份纯粹的爱情。然而,毕业之后他慢慢地发现,随着生活而来的物质压力却越来越大,让他喘不过气来。他小心翼翼地维系着这份感情,但终于她还是离他而去了。
苏秦从此暗下决心,他发誓一定要把失去的再重新找回来。他从此奋发图强,真可以说是「头悬梁,锥刺股」了。他用了差不多两年的时间,阅读了大量的技术书籍,在工作中也竭尽全力,终于成长成了一名真正的技术高手。升职,加薪,一切似乎都唾手可及。但是,这时候古灵儿早已步入了别人的婚姻殿堂。原来一切都已不可挽回。
苏秦白天上班,晚上则摇身一变,成为一名网络黑客。他对古灵儿仍然没有死心。他入侵了她的家庭网络,又入侵了她老公的公司网络。她老公确实是个有钱人,但却在用人上目光短浅。可能是为了压低成本,他们公司几乎只招一些工资低的程序员,结果做出来的产品漏洞百出。苏秦根据这些漏洞泄露出来的数据,发现了公司的账目问题。
他也说不清自己究竟是出于何种动机,也没仔细想过这样做的后果,总之他出手了。他选了一个好日子,对古灵儿他老公的公司网络发动了致命的一击。
结果公司倒闭了,老板做贼心虚,竟然自己跑路了。那个男人果然是个靠不住的人。苏秦没想到这件事给古灵儿带来了无尽的麻烦,但她把社交软件都删掉了,而且搬了住处,他一时竟也找不到她。
直到有一天,他接到了古灵儿的来电。
整个故事的结尾
“所以说,你们俩讲的,其实只是一个故事的两个部分!”我大声地向小丽和小明宣布道。
这时,协议栈的内核程序刚刚剥掉了TCP头,我们又重新变成了2000个字节的数据。紧接着,一个中断产生,我们被从内核态抛到了应用层,等待应用层程序的进一步处理。
他们两个吃惊地说不出话来。看到他们的样子,我内心竟浮起一丝复仇的快感。我脸一红,赶紧压制住了这种情绪。
“你——说的都是真的?”小明惊得说话都有些结巴了。
我慢条斯理的开始分析,“知道为什么我们三个会碰到一起吗?这并不是凑巧。因为我们三个都和苏秦有关!”
“那你呢?你和苏秦又是什么关系呢?”
“我属于苏秦调试程序的时候使用的一份测试数据。”我指了指由所有这2000个字节组成的字节队列,补充道,“准确地说,我们都属于这份测试数据。他应该正在测试大数据包的情况下究竟会发生什么。”
此时,应用层的程序发现了我们的到达,正准备调用protobuf的反解子例程执行反序列化操作。
“什么?你是说我们几个之所以在这儿,只是在进行一次调试?”小明和小丽表示都不太相信。
“没错。而且我们马上就会知道结果了。”我观察了下周围的执行环境,更加确信了我推测的正确。
他们俩面面相觑。
没错!就是现在了!我大声地冲他们喊道:“知道着这意味着什么吗?”
“什么?”
“这意味着正在调试的程序还不太稳定!”
一瞬间,进程抛出的异常有如原子弹爆炸。刺眼的白光照亮了支离破碎的整个内存空间。所有的东西在一点点消失,操作系统开始回收异常发生后的现场。
“到底发生了什么?”就在我们大家被彻底地从内存中抹掉之前,小明竭嘶底里地大喊。
“这个接收程序忘了进行「粘包」处理!”我想起了我们出发时由3400个字节组成的完整消息,直到现在,后面仍有1400个字节没有到达。
在一片白光之中,世界重新归于一片寂静。
我们虽然已不再存在于这片内存之中,但程序的日志早已把这发生的一切都记录了下来。此刻,日志文件正静静地躺在世界的某个角落,等待着喜欢听故事的你来随时查阅。
(完)
最后的彩蛋
应该很多同学已经猜到了,故事中这三个字节的取值并不是随意选的。它们要表达的意思你看出来了吗?
另外,对做技术开发工作的同学来说,文中出现的技术描述细节中,最有实用价值的其实是应用层的「粘包」处理问题。文中解释了它出现的原因,你看懂了吗?
其它精选文章:
- 捍卫技术的尊严
- 技术攻关:从零到精通
- 那些让人睡不着觉的bug
- 蓄力十年,做一个成就
- 知识的三个层次
- 程序员的宇宙时间线
- 技术的正宗与野路子
- 基于 Redis 的分布式锁到底安全吗?