引子

不管你相不相信,每个程序的日志都记录了一个完整的故事,一个由代码和数据相互交谈而产生的故事。就像你们人类的故事一样,这里面既有日常琐事,又有奇事趣闻,自然也少不了各种喜怒哀乐。只要你仔细聆听,就一定能够听懂。

请注意,当我在说这些话的时候,我并不是在打一个比喻,而是在描述确凿的事实,就像太阳的东升西落那样不容置疑。每个日志中记录的故事,在那个世界都曾经真切的发生过。如果你还是不相信,请阅读下面这个故事,它是我的一段亲身经历。这个故事就记录在一个程序日志里,而这个日志文件至今还躺在主人的笔记本电脑中的某个位置,随时供人查阅。

哦,对了,你还不知道我是谁?其实跟故事本身比起来,这并不那么重要。我只是数字空间 (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 个字节没有到达。

在一片白光之中,世界重新归于一片寂静。

我们虽然已不再存在于这片内存之中,但程序的日志早已把这发生的一切都记录了下来。此刻,日志文件正静静地躺在世界的某个角落,等待着喜欢听故事的你来随时查阅。

(完)


原文链接:http://zhangtielei.com/posts/blog-3-bytes-story.html
欢迎关注博主的微信公众号 ↓↓↓

              ██████    ██████  ████              ██
  ██████████  ██    ██  ██  ██████          ████  ██  ██████████
  ██      ██  ████████████████████████████████    ██  ██      ██
  ██      ██  ██  ██  ████  ████    ████  ██  ██  ██  ██      ██
  ██      ██  ██      ██  ████    ████  ██      ████  ██      ██
  ██████████  ██    ████        ██████  ██  ██  ████  ██████████
              ██  ██  ██  ██  ██  ██  ██  ██  ██  ██
██████████████████████████    ██  ██          ████████████████████
██  ██    ██        ██  ██████  ██  ████    ██████  ██      ██  ██
██      ██████      ████  ████      ████  ████    ██      ██████
    ████████  ██    ████████    ██  ██████  ████  ████  ██████  ██
    ██████  ██  ██████  ██    ██    ██    ██      ██████████  ████
  ██████  ██      ████  ████████  ██      ██  ████  ██    ████  ██
██          ████████    ████      ██    ████████  ██    ██      ██
██████████    ██  ██    ██        ██  ████    ██  ████    ██  ██
██  ██  ██  ████        ████████                      ████    ████
  ██  ██  ██  ████        ██  ████████████  ████      ████      ██
    ████  ██████    ████  ██          ██  ████████████  ██  ██  ██
██                  ██  ██  ██  ██        ██        ██  ██    ██
██  ██      ██  ██  ██████  ██████    ████        ██    ████    ██
████  ██████    ██  ██  ██████      ████  ██      ██      ██    ██
  ██    ██  ████████      ██  ████  ████  ██  ████    ██    ██████
  ██    ████  ██████  ████    ██  ██  ██  ██████  ██████    ██████
████    ██  ██  ██      ██  ████  ██  ██      ████        ██  ██
    ██            ████  ██████    ██    ████  ██            ████
████████████████        ██  ██    ████  ██        ██████      ████
              ██  ██  ██  ██████    ██  ██    ██  ██  ██  ████
  ██████████  ████  ██            ████  ██    ██  ██████  ██  ██
  ██      ██  ██      ██  ██        ██        ██          ██    ██
  ██      ██  ██      ██████      ██      ██████  ████████████  ██
  ██      ██  ████      ████            ██  ██  ██      ████    ██
  ██████████  ██        ████████████████  ██  ████████    ██    ██
              ██████  ██  ████  ████      ██  ██    ██████      ██