- 上联:虎啸龙吟乘风冬远去
- 下联:兔腾鹿跃飞花春沓来
- 横批:新雀初飞
今年的联,把我家狗子也算上了,愿它在天上能陪我们平平安安:
- 上联:扑朔迷离傍地走
- 下联:兴云吐雾迎春归
- 横批:汪喵唧祥
今年最后悔的事,就是没有强势反对,让我家狗子一直在阳台待着,钝刀子割肉,温水煮青蛙。在杭州降温那几天,最终没有扛住。如果有后悔药可以吃,说什么也要从一开始强烈反对。
2020 年,是我第一次自己拟春联,当时把家里所有住宿的活物都列了一遍,其中单独半联就是给猫子狗子的。不知道什么时候起,也不知道为什么,狗子的晚年会是这样的。果然还是因为自己太软弱了。现在只希望它能跟《一条狗的使命》一样,有一天能重逢吧。就是最后一年时光是这么度过的,它还会不会选择我,会不会怨恨我。至少决定从医院带回家,迷离之际,它的尾巴还是在摇的——比起医院,它还是想回家的吧。
在此我接受一切爱宠人士对我的唾骂。愿世间所有的宠物都能得到善待。
原本以为去年的关键字“希望”,真的可以让一切好起来。实际上今年按部就班一步步走,也的确开始步入了正规。谁曾想在年底的时候,现实给我重重锤上了一击。这是自己一年多前埋下的因,现在吃到了自己种下的果。
今年关键字:后悔。
纵使后悔,这世间终究是没有后悔药的。
没有太多想总结的。在上面已经总结完了。相对于这件事来说,其它生活都不值一提。就当今年留了一年白吧。
团队同学主要分布在北广杭。而年中一段时间由于一个要为业务“走出去”的命题,开始了多次与业务方直接对接,甚至是驻场闭关。就为了求一个好口碑。
而上海——除了年初的时候 BP 需要去了一次。年底也因为某个分享场子做嘉宾而去了。虽然最终因为工作调度原因,临时赶回了杭州。但是,为数不多的几次出差中,一共两次发生了不好的事情,一次就是年底的上海——杭州骤降温,等我回去的时候已经钾离子高了。而还有一次,是去年出差广州,因为发生了一些事,才让狗子待在了阳台。
我甚至在想,是不是少这两次出行,就会不一样。
今年的工作是继续做角色的转变。为一个方向负责,就需要为它的前前后后全链路都负责。这也是为什么我要到处跑,跟一切可能成为业务方的人去聊、去对接、去洽谈。不能只做文臣,要做一个武将。也是在这个“走出去”的过程中,我与一些愿意接入我们的业务方建立了合作与联系。其中有一个的预估请求量级超过了当前我们这边原有所有业务方的总和;还有一个,则奠定了我们在公司某个重要项目中的合作关系,以及让后面的一些事情更顺理成章——至少从我自己有限的视角看是这样的。
如果没有今年的角色转变,那么可能在组织架构调整后,我们做的事情也不会是继续有延续性的这块事情了。一切都好像是事先标定好一样。如果之前没有各种铺垫,后续的事也不会发生。感觉这就像是我种的另一个因,得的另一个果。蝴蝶效应,就跟阳台上的狗子一样。
这也多亏了年中逼着我做这些事,不然单凭自己的视角和舒适圈,很难挣扎着走出来。
年底一波调整,开始了新的事情——指业务。技术产品上还是有延续性的,还是高密度部署解决方案,但这不是全部。它成了解决我们业务问题的手段之一,而不是目的。有了一年多的业务落地和线上运行,它的成熟度可以支撑它在新业务领域中的运行。而新的事情本身则又是我另一个老本行——CloudIDE。在 2019、2020 年的时候,我曾做过一年 CloudIDE。虽然当时做的不是 OpenSumi 框架,也不是容器调度,但没吃过猪肉还是见过猪跑的。在年底闭关的时候,还是能发挥一些自己曾经做过这个事的作用的。
还有一点就是,看上面如何就最新的事情开始调兵遣将、沙盘推演,还是收获颇多的。不用往上看太多级(太多级我也看不到),就往上看一两级就足够学了。这个调度能力和团队执行力,是我自工作以来从未见过的——哪怕是在蚂蚁、在淘宝。很庆幸自己找到了这么一个团队。如果大家对我们团队有兴趣,对我们的事情有兴趣,可以随时联系我。我们这边北上广深杭均有岗位,前端、后端都需要。
AI 兴起,团队的工作很自然会有这方面的建设。但由于比较敏感,在这里只能说我们这边也开始投入了,具体是什么,也不好说。不过我也的确在 AI 上面花了一些钱,包括但不限于 ChatGPT、Midjourney 等。了解一个事情,让对其的印象不那么抽象——最好就是去使用它。
总之今年的工作,是对去年角色转变的一个继承,今年继续着一个架构师的转型之路——一个“能打”的架构师,努力让自己“能打”的同时,逐渐学会 Leadership,又不把技术丢掉。自此,我感觉终于可以脱掉所谓 P6 的头衔了。
虽然不是真的大红大紫,但也被承认有过技术成功、影响力成功,接下去就看能不能习得组织成功,以及看能不能把握住机会,尝试搭上大船一起完成商业成功。
今年对外有两次分享,一次是关于高密度部署,另一次则是关于 Node.js 的未来。另外,也出了一本关于 Node.js 的掘金小册,但是由于年中大部分时间在医院,年底连续肝代码,导致最后还未最终完成。
对于前端来说,重要的是做好 User Interface,而 User Interface 一定是 Graphical User Interface 吗?任何人机交互相关的内容,都可以属于前端的范畴。
磨洋工,终于在年中的时候完成了《软件开发珠玑》的翻译,目前出版社的编辑在排版、审校中。不出意外的话,2024 年终于可以面市了。
古早时期过于追逐技术,忽略了在整个软件、开发体系中非常重要的“软件工程”这一组成部分。现在回过头去看,非常后悔在大学、研究生时期把相关课程“压箱底”了,并不在意,学得也很抽象,只求不挂科。这导致我后来在实际工作过程中走了很多的弯路。
软件开发从来不止是“技术活”,这里的“技术”特指编码。它涉及方方面面,更多时候,这都是事关“人”的问题。在工作多年后,踩了足够多的坑,也遇到过职业瓶颈期,我才明白这个道理——属实是有些不开窍了。在实际的工作过程中,我们会发现“写代码”通常是最简单的一件事情,甚至不一定会成为最重要的事情。如何参与梳理需求让后期开发更稳健,如何看待设计、体验相关的事情,如何进行项目管理来使各方各面达到预期,如何使团队上下一心往一处使劲,如何抓质量,如何进行过程改进等等,这些都是我需要狂补的课程。
在我翻译完这本书之后,我对软件工程,对需求、设计、项目管理、团队合作、质量和过程改进有了一次新的认知,我也可以尝试着为自己摘下“只会写代码”的名头了。相信在未来的职业生涯中,这本书所给我带来的知识能在各种角落中发光发热,在某种意义上可以帮我突破一些瓶颈。
没法出去玩,没法出去玩,没法出去玩,没法出去玩。(幽怨)
第二点也没做到。还有最后一点。
所有命运馈赠的礼物,都已在暗中标好了价格。
Ask me anything: https://github.com/xadillax/ama
]]>
- 上联:春趁其势以逮牛尾
- 下联:岁攀南风来迎虎喵
- 横批:双生时兮
今年拟的:
- 上联:虎啸龙吟乘风冬远去
- 下联:兔腾鹿跃飞花春沓来
- 横批:新雀初飞
春意伴随凛冬的远去渐渐回暖,但愿早些过了疫情的阴霾吧。今年外公走了,在年初我急性胰腺炎住院那段期间。虽然已经很久没见过外公了,但是似乎一切都好像才几个月之前一样,带着我去街上烧饼店买烧饼,冬日一起坐在小天井晒太阳。
开始奔四了,今年两次住院的经历让我开始惜命。一次是大过年的由于高血脂引起的急性胰腺炎,疼得死去活来大半夜去急诊,还因为过年期间没有医生,硬生生在住院部熬了一个星期,期间下了个雪,百无聊赖写了点东西。
《壬寅大年初七卧病观雪》
几丝柳絮悄入窗,忽而鹅毛漫琉璃。
只因卧床恰回首,堪堪取景飞花城。
2023 年春联的飞花就来自这里。
另一次是一个困扰了我四五年的问题,看了各种科室都误解的头疼,最终在口腔科发现是茎突过长导致的,做了个小手术切除了过长的那段茎突,并笑称自己是杭州君麻吕。
加上今年一整年的疫情,让心情好不起来,总觉得今年是个大凶之年——风水意义上的。的确在阳历年关这会儿还真就赶上潮流阳了。除此之外,还有就是大环境的各种裁员消息,还有身边的人半年没找到合适的工作等等,让人有种兔死狐悲的感觉。只能说是苦中作乐吧。
所以希望新年的春联可以帮着真破旧迎新,这些阴霾都随着虎年去吧,让兔年真的可以春城无处不飞花。
今年关键字:希望。
春联上的“新雀”自然就是 2021 年新添的小丁了。一岁已过,愿能初飞。基于此,我 2022 年的网易云音乐年度报告不再是二次元,听的最多的歌是《小苹果》,396 次。
娱乐基本上已经没了,什么 Switch,什么 PS5,都已吃灰。年中的时候,在部门中了两个 PICO 3,也都是玩了两天就吃灰了。不过话说回来,PICO 上的体育游戏还是有点可玩性的,以及爱丽丝梦游仙境那个游戏。剩下的总给人感觉是半成品。至于用 PICO 看电影就更别想了,在电视上看电影都成了奢望。
本来打算年底这会儿去看阿凡达 2,结果就在买的票上时间的前一天,🐑了。没看成。这会儿估计也找不到好的时机看了吧,估摸着等到能看那会儿,这电影自然也就下架了。
虽说以前的常规娱乐不再,但今年好歹是多了个新的娱乐活动——散步。今年出去“散步”的次数应该数比往年都多。以前难得去一次西溪湿地、九溪、西湖等等,今年都去了好多次了,还包括一些小众的景点、公园等等。也因此,今年发现了一些以前没注意到的美景。但这么点路,还是开车去的,运动量根本算不上“徒步”,只能说是“散步”了。
出不去玩,在公司上班时,也会忙里偷闲从公司十二楼拍各种状态下的风景。
今年塑料小人不多,三两个 Myethos 的农药手办。一个地平线 2 的大象手办,青蛇白蛇里的宝青坊主,以及二妹又一尊。泡泡玛特就数不过来了。
天下生意,有来有往。我要的,你给得了吗?
看的东西有了,还入了几个听的东西。
年中在 Discogs 买了《Hymn of the Soul》的七寸黑胶,限量版的二手。可能英文名大家没听过,写日文就会觉得眼熟了——《全ての人の魂の詩》,天鹅绒房间的主题曲——P5 天下第一!卖家年中自 USPS 寄出,到现在还没到,快递状态一直是:
Not Trackable
USPS Tracking is unavailable for this product for CHINA.
也不知道它在路上待得还习惯不。P5 是天下第一,今年买的老头环和地平线,玩了几个小时就吃灰了。
基本上的足迹都是上面提到的杭城散步。剩下的都是出差了。
本来预计年底还应该去跟北京团队,以及业务方有一波接洽。也受限于特殊事件及疫情没去成。想恢复成 2020 那时,想去各个地方见风景,想继续“每去一个新的城市,都要找一家当地不是那么有名但是有着不错驻唱的酒吧去欣赏”——来自一个中年油腻男的牢骚。
今年两次都是南下。去广州看看团队同学,顺便团建;去深圳 ArchSummit 分享了一下我正在做的事情。今年也算是上了小蛮腰,去了白天鹅的人了。
期望明年足迹能变多吧。
今年的工作有了一定的变化。我又重新开始带团队了,虽然还是大头兵的那种,不过相比起来,多年前在大搜车时简直就是过家家,只是对大家进行任务的分配,连项目管理都算不上。角色的转变,让我又强行从舒适区中拔出来——毕竟兵熊熊一个,将熊熊一窝。今年要对成员负责,除了日常写代码之外,更多的转变是我要做更多的规划,要保证路线不出错——否则很可能带着一帮弟兄们直接成了炮灰,尤其是现在这种全局经济下行的形式下。
上半年还在呼哧咖啦埋头写代码,下半年代码量就急剧下降了。更多的是要挑起方向的责任来,把更多写代码的后背交给同事去做,毕竟他们有更多比我出彩的地方,而我要做的更多的是保证航道不偏,以及以更具前瞻性的眼光看待整个事情。
其实这个转变过程还是有一定痛苦的,尤其是在代码量下降的时候,我会觉得自己的价值变小了。在明知道价值并没有变小,只是从一种形态转换成了另一种形态的情况下,还是会慌的。毕竟不知道自己在新的价值领域中发挥得究竟怎么样。其实路线还是有好多次跑偏的,这其中堂主还是给予了很大的帮助和信任。
今年我开始全情专注于高密度部署解决方案,以及 Web-interoperable Runtime,这在公司内部叫 Goofy Worker。原来撺掇的 China Open Node.js Framework 交给段潇涵去带,其逐渐孵化成功,成了现在的 Artus.js,反响挺好,D2 上也分享了一波。
所以今年来说,工作上的关键字是“转型”。从一个低级 IC 往一个真正的 Tech Leader 上转,好在队友们都挺靠谱的,能交出后背。只能说大家一起成长一起进步吧,共勉。负责一款技术产品在公司内部落地,需要对其前因后果进行负责,要深入去各业务方合作、抱大腿,避免“自嗨”。“我认为我这个东西很好,你要来用”,这种思想不能有。你得真真切切能解决业务方的问题,对他们来说,关心的无非是他们能得到什么收益,稳定性、机器成本、人力成本,以及转化率等等。凡是在这之外的一切,都是自嗨。什么标准,什么更好的架构,这是自己的事情,业务方才不关心。他们怎么样能更好地把业务堆上去才是王道。而负责一个团队的存活时,需要考虑的事情也是我以前从未考虑过的事情,关心每个靠谱队友的成长,帮助其把握、争取好在团队中的定位,这些事情都会因人而异。不同同学的特征会有不同的成长路线,或者职业规划,这些事情我也是一点点在摸索。这个责任太大了,一个做不好,可能会影响到一个同学的职业路程。虽然拉长时间来看,不一定会影响他最终的状态,但如果在我手上被推迟了一两年,也是一个让人心惊肉跳的事情。现在开始理解了我历届 TL 的辛酸了,有我这么一个刺头在,真的难搞。如果之前的我在现在的我的团队里面,我又应该怎么搞?技术是没什么问题了,业务方关心不?
无心插柳
有个无心插柳的小插曲。多年前,我曾在花瓣网研究“主题色提取”,并开源了一个主题色提取的 Node.js 库。我的硕士毕业论文也是相关的内容。没想到在多年后的今年,字节跳动有团队在用这个库。他们把图片的主题色提取出来,进行优化,并最终显示在 App 上。
期间通过飞书找到我,问了一些问题。我自然毫不客气地就帮他们把逻辑迁移到了所谓的 Goofy Worker 上,收益还不小。
除了转型给我带来的挑战之外,还有一个问题就是异地的团队所带来的协作问题。直到年中,方向在杭州多了一个同学之前,除了我一个人,剩下的同学全在广州。这也是我去广州出差的原因。如果大家在同一个 Base,那么很多事情、问题,都有可能是在中午吃饭的时候顺带提一嘴就能解决的;而在异地的时候,就需要经常性地去比较正式的沟通,才能达到比较好的协作效果。普通的沟通协作尚且如此,对于一个 Tech Leader 来说,这个问题就更明显了。尤其是,当时只有我一个人在杭州。不过好在经过一年左右的磨合之后,这方面也顺畅多了。
总结下来,以前是体力累、脑力累,P6 的风波则是情绪累,现在我已经可以拿这个事情自嘲完全没问题了,甚至我的飞书状态是万年不变的“一个 P6”。现在的状态更多的是一个心力累,累的原因则是还没完全适应现在的角色转变。不过从我这几年工作观察下来,即使我适应了这个转变,“心累”将会是一个长期的状态,只能说尽力去适应“心累”吧。
期望明年在工作上能更“游刃有余”吧。不过在现在的经济形势下,谁知道明天会发生什么呢?原来蚂蚁体验技术部那一波熟悉的同事,就这一两年时间已经是“散是满天星”了。有去蔚来扛大旗,有跟我一样来了字节,也有去腾讯搞事情……走了这么一遭,也没白来。还是苏千的那句话:
期望在社区做一些非公司名义的事情,这样不管大家去到哪,至少还有一股力量把大家连接在一起。
某种意义上,Artus.js 算一个吧。
今年还有一个值得高兴的是,高中的 OI 三人组之一 xmerge 今年也从谷歌跳到了我厂,在花旗国做 NLP 相关的事情。
今年恢复了分享,一个是技术到了,另一个是也的确得为团队做的事情发声。2021 年的时候,一直在埋头钻研,事情做到一半,也不好拿出来分享。不过今年,原来在淘系的技术也成熟了,他们把 Noslate 开源了出来。这个就是我在 2021 年总结里面说的“基于 V8 开发了 Serverless Worker(Shinki.js)”,也算是一份迟到的答卷。
而今年的分享则有三次,一次是关于我自身的技术成长,另外两次则是为团队的事情发声。
还有一个事情,就是撺掇了几个团队的同事,让他们写一本关于 npm 的书,可能明年就可以跟大家见面啦。也算是某种意义上的“帮助其成长”?
在这里我要给《Software Development Pearls》中文版的编辑道个歉,真的是因为今年太心累了,所以这本书进展缓慢。不过我仍旧是以“我自身的最高水平”去对待这本书,虽然进度慢了,但自我认知上质量还是在线的。
这不是一本技术书籍,而是涵盖了软件工程各领域的一本书籍。包括需求分析、设计、项目管理、测试等等。恨只恨我大学的时候对“软件工程”嗤之以鼻,只有一个模糊的印象,全把技能点点在了代码上面,剩下的都还给老师了。现在补起课来格外痛苦。我有一个大胆的想法,之前高中时代跟我一起搞 OI 的好机油们,现在都散落在世界各个角落,也都涵盖了软件工程的各个领域,到时候想集合他们一起各写个推荐语印在书上。
今年做了三次分享,其中一次是参会。ArchSummit 全球架构师峰会。极客时间其实也挺不容易的,疫情之下,这些会议都不好搞。今年难得重拾起来,其实在深圳的那次还是战战兢兢,到场率也并没有以前那么爆棚——以前的会场座位都全坐满,甚至地上还坐了一群人。
ArchSummit 上参加的是前端 Serverless 研发体系建设专场,贡献的话题就是我今年一年所做的事情,Web-interoperable Runtime 及高密度部署。前者是我们搞的 JavaScript 运行时,后者是这个运行时在实践中所使用的架构。
去语雀围观。
剩下两次都是线上分享。一次是掘金的公开课,首次公开地讲述了我的技术成长历程,也算是对自己打小入坑的一次回顾总结吧。从小霸王入坑,以游戏为目标,最终入了 Node.js 的“歧途”。
去语雀围观。
还有一次则是线上的前端早早聊性能专场。算是对今年所做事情分享的一个“提前练兵”。我现在已经算是做过各种线上线下大会的分享,也组织过小会。在“参会”这条路上,应该就还剩“出品”这个事情没做了。
今年申请了知乎认证,神奇的是我申请的认证是“作家”,代表作《Node.js:来一打 C++ 扩展》,居然认证成功了。以后我高低算个“作家”了🤪。
以及,接着“小霸王”的上头劲,以及之前天猪业做过一次长文的回顾,我也在上面做了一次文字版的回顾。
记录下这些文字的原因很简单,其实里面有些事情的细节我已经模糊了。我怕我再不记录下来,以后记忆力减退,老了后,我都不知道我以前都干过哪些事。现在有一个遗憾就是,我已经忘了小学时候的电脑老师给我的那本关于用 Turbo C 写图形的书是什么了。我就记得里面有画房子、画狗什么的。所以再不记下来,以后更多事情都会消散于细胞的衰老——毕竟这些事,如果我不记得,就更没人记得了。
再比如,依稀记得小时候去蹭同学家电脑玩,有一款游戏长得跟 M 豆人一样,可以制造关卡自己玩。但我甚至都不知道这个游戏的名字。
啊,不能再说下去了,人到一定年纪就喜欢说这些事情。真的是年纪到了。
适应新角色,只能说马马虎虎吧。不至于无法适应,但说自己做得有多好,也没有。毕竟这两个角色都是第一次扮演,我也没有经验。
第二点,也只能说尽自己所能去做,至于效果如何,也都是在摸索中。
第三点,因为今年还在适应角色的转变,心累,所以进度比较缓慢。再次跟编辑大大道个歉。
最后,从各方面说,有很开心的点,也有苦中作乐。除了感觉今年年份比较凶,大抵都还好吧。心态比以前平和多了。
Ask me anything: https://github.com/xadillax/ama
]]>
- 上联:夔牛水牛黄牛牛牛旺
- 下联:靓崽狗崽猫崽崽崽安
- 横批:码祥稿俊
结果今年的代码就一片祥和,生活上也应验多了点变化。于是今年决定再编一联。
- 上联:春趁其势以逮牛尾
- 下联:岁攀南风来迎虎喵
- 横批:双生时兮
不知道新的 2022 年会发生什么,拭目以待了。
今年关键字:触底反弹。
今年多了点变化,家里由原来的两口人添置了新丁。算是生命周期里面的一个转折点吧。毕竟老大不小了。也是由于这一点,今年基本上没有出去旅游过。在起名上,也是绞尽了脑汁,甚至为其攥了几句短句。
夫〇〇行过,皆留〇与〇。〇者,声也;〇者,形也。声形并茂,乃绘〇〇。
——死月 于公元二零二一年五月四日凌晨
这个改变后,好似以前很多事情都显得并不重要了,渐渐自己也看开了。反而事情朝着好的方向发展。
今年只有一次足迹。按习惯来讲,应该是一个 <li>
列表,但只有一项又显得矫情。
这甚至都不应该归为“生活”一栏,因为并没有任何的娱乐活动,匆匆两天,一直在忙活。也是由于没有出去别的什么地方,这小节能发的照片也就只有一张了。
对,那个左下方背对着你们的就是我。
其实接近年底有例行的一些照片,不过都比较私人,就不放到公众平台了。
今年理想汽车车主浓度加一。
在商场附近发现一家比较好吃的小酒馆(尤其是黄金蒜风干鸡),在离职期间想约同事一起去的时候,发现他偏偏“每周一闭关”,算是一个小遗憾吧。
自从出游收敛以后,就开始转而向内生活转变了。游戏通关、云通关以及开坑了:
以及入了一台 PS5——不过吃灰已久。与之对应的,就是新来的塑料小人们了。
还有年初入的宝儿姐没有入镜。下次来个几柜子的全家福。
还有就是太久没唱歌了,那天心血来潮去录音棚录了首《赤伶》。好想去 K 歌。
↑ 点击试听,聒噪你的耳朵。得亏有调音师,不然更没法听了。
而今年的琴就练了首《游园设施》,但没拍视频。黑胶倒是入了一大把,颇有种“明明不是文化人,偏偏要装有文化”的感觉。
今年差点又翻车,又莫名其妙多个帖子。
一直也没时间和机会来谈这一点。这次就趁年终总结的机会来说说今年的工作吧。
自去年从蚂蚁转岗到淘宝后,其实一直挺开心的——毕竟做的事情比较契合自己的灵魂。既有足够的前沿度、又有足够的技术深度,并且还得到组织的认可。对我来说,这三样都能达标是非常珍贵的体验。
自 2017 年去蚂蚁体验技术部后,由于自身和团队方向的原因,导致做的事情并没有太大价值,至少在当时当下。但体验技术部是个非常好的团队,同事也非常 Nice。所以也就温温吞吞过了。但凡事可一可二不可三。后来在研发效能部做 CloudIDE 的时候,彻底迷失了自我。这里没有说 CloudIDE 产品本身不好的意思。产品是好的,不然我也不会主动想去这个方向做事。只是这个项目在当时太重前端轻后端了,我作为一个典型的后端 Node.js 工程师,足足写了好几个月的前端。一方面自己写得痛苦,另一方面又没有什么产出。最后在淘系前端找来的时候,我选择了和平分手,于团队于我都是正确的选择。
作为九零后的头趟水,已经从奔三变成奔四了——在身边都是优秀的小年轻的环境下,尽是小我五六岁的 P7、三四岁的 P8,而我仍然是万年老 6,无疑给我造成了巨大的焦虑,毕竟潜规则是整个互联网的,甚至是整个职场的,无关某一两个公司。好在去年年底和今年抓救命稻草似的抓住了今年的重点项目,算是缓过神来。既然已经追不上超不过了,就选择缩短与天才们的距离吧。
如果说去年转岗后做的事情是 PoC,那么今年就是将这些事情落地了。
首先是 Node.js PGO 极速启动方案上线,提升了改造项目们约 100% 的启动速度。又基于 V8 开发了 Serverless Worker(Shinki.js),目前已在内部双十一试验成功。对此感觉比较模糊的同学,可以对标一下 CloudFlare Worker、Deno Deploy 等等。值得说出来的一点是我们的 Worker 做到了亚毫秒级(<= 1ms)的启动,以及架构是高密度部署(理论上一个 Pod 可以部署几十上百个云函数)。关于这里的一些干货或者介绍,大家可以回味一下凌恒老师在 Node 地下铁沙龙 #12 北京场的分享《云原生时代的 Alinode》。
与神奇的同事们公事了一年后,感觉身边还是有那么多人坚守在技术深度的道路上,甚是欣慰——毕竟曾经一度我有一种“在大公司深钻没活路”的错觉。淘系一系列的改变也让纯粹的技术人重新燃起了些许希望。2021 年一整年,没有后续的跳槽,仍然是我在阿里巴巴集团最开心、如鱼得水的一年。
很感谢淘系前端 Node.js 架构团队给了我今年的机会去晋升。虽然结果出来了,不过我仍然还是期望自己对外一直是 P6,以此来警醒自己。
其实今年上半年就有猎头找我,说字节跳动互娱这边要找个能带 Node.js 的同学。以前也一直有猎头找我,我都没回,或者聊几句看看行情。
今年这次是被这个猎头的专业性打动了。大多数的猎头都是广撒网加人,加了之后直接丢一个职位列表,然后开始接触。甚至不是很清楚前端跟 Node.js 的关系与区别。毕竟不是技术出身的同学,我们也不能过多要求他们,大家也都是在很努力地为自己的职业而奋斗。只是今年找到我的这位猎头居然对圈子十分了解,而且是定点找我说需要能带 Node.js 的人,杭州想来想去就那么几个,这让我着实眼前一亮,于是萌生出了试试看的想法。当时只是决定试试看,毕竟我自身身价已经落后市场价太远了,也拿不到很好的结果。
有想认识这位猎头的同学可以私聊我。
而且事实上,第一次面试结果不是很理想,我把 Offer 拒了。而在同时,堂主也请我吃了几次饭,让我尤为感动的一点是玉北居然从上海跑过来一起吃饭,当天回。虽然我不知道当事人实际上怎么想的,我反正就是自恋地一厢情愿认为他来杭州也是一起吃饭和聊天。
WebInfra 也是需要一个 Node.js Infra 方向的负责人,在大家诚意的打动下,我开始了第二次的面试。结果大家也都知道了,我从阿里巴巴离职,来了字节跳动。
不是说原淘系前端 Node 团队不好,只是站在职业规划的立场上,我认为这边的 Scope 更适合我。我依旧可以做前沿且深度的事,又有了足够的 Scope,与小伙伴们一起把这块基建搞起来。还有一点就是,鸡蛋不要放在一个篮子里,好歹对冲一下。
于是,今年工作上最大的变动就是最终我从阿里的低级 IC 转变成了字节跳动一个小方向的负责人。顺便打各广告:
字节跳动 Web Infra - Node.js 基础架构招人中:分别招基础平台(全栈工程师)、基础生态(Node.js 工程师)以及底层技术(C++ / Node.js 工程师)。
目前还在做的一件事就是,联合了蚂蚁和蔚来,想着如何给国内的 Node.js 生态搞些活水,重新温热一下。目前正在做一套全新的框架规范,叫 China Open Node.js Framework(CONF)。明年可能想搞一个类似于 China Node.js Conf 的会议,大家敬请期待吧。
今年逐渐从之前的台前走到了幕后。
比去年更开心的事是,今年写的代码落地了,产生价值了。
《JavaScript 悟道》终于出版。大家喜恶参半。道格拉斯的个人风格太鲜明,导致在翻译的时候老搞不清他到底是哪头的。在翻译的言辞上也是做了很多润色和本地化,不过有挺多因为一些和谐原因被编辑给毙了。
比如 Wat? 一章的标题,我个人认为最合理的翻译是【卧槽!】。还有一段讲的是英文中的 This 和代码中的 This 分不清,老讲来讲去像跟美国三四十年代的两个谐星一起结对编程一样(谐星名字我忘了)。我给翻译成:
它(This)是一个指示代词。在编程语言中使用它(this)会让其难以人类语言表述。老这么讲来讲去,你就会觉得自己是跟郭德纲与于谦结对编程一样。
结果因为各种各样的原因,这段话直接就整段没了。本来它是第 16 章的最后一段话——在【this
真是个坏家伙】之后。
还有个彩蛋是,当时出版社设计了两稿封面。其中落选的那稿用的是我妹花了几个小时在 iPad 上画的原图。以及这本书我最开始想到的中文译名是《JavaScript 异闻录》。
今年五月份的时候,作为出品人出品了武汉的 Node.js 地下铁。找了腾讯、淘宝、Wiredcraft、有赞和蚂蚁的工程师一起来分享。算是为我明年重搞 Node.js 生态热热身吧。总之期待明年可以把国内 Node.js 生态搞热吧,毕竟 Node.js 在国内沉寂太久了,大家逐渐忘了它居然还是可以写服务端应用的。
大家有兴趣可自行下载 Slide。
做幕后还有个好处就是,万一哪天穷得叮当响出去面试了,人家问我有没有过 OpenJS Node.js 的开发者认证,我可以吹逼说题都是我审校的。就像这次字节的面试被问八股时间循环,我直接跟面试官讲了 Node.js 的事件循环是怎么实现的,我自己写的 V8 Serverless Worker 的事件循环是怎么实现的。这样哪怕我忘了里面的一些需要翻看八股的细节背书答案,我也可以借此把人唬住。
而 CONF 社区,目前也还在筹备阶段,所以大家进去也看不到什么太有价值的内容。
这个不在这里过多赘述了,十一月的时候我在知乎上发了篇文章,里面还附上一个用 Node.js 写的基于 OpenGL 的桌面版 NES 模拟器。
至于为什么心血来潮要做这个项目——毕竟我小时候学编程的初衷可是想做一个自己的游戏小世界,让别人在这个世界里面跑来跑去呢。
这种心态有点像 SAO 中的茅场晶彦。
工作之后才莫名其妙在 Node.js 的道路上越走越偏。
去年并没有给自己定 Checklist,算是放飞自我了吧。反而没有定 Checklist 之后,事情在往好的方向发展——至少目前我认为是好的方向。
经历了这几年的职场波动之后,有些事情看开了。做事情反而有时候会抱着利他的心态。
剩下的,就随缘吧。
随缘箭法。
Ask me anything: https://github.com/xadillax/ama
]]>vm
时,可千万小心。冷不丁就哪里埋了坑。有时候补了这里可能又漏了那里。尤其是频繁新建 vm
的时候,例如来一个请求,组合一段代码,放进 vm
中执行。先上一段最小复现代码。
// test.js |
注意:在异步
while
中每次循环的末尾都手动调用一次gc()
函数。
首先,我们看看正常的时候应该是怎么样的。大家可以跟着一起用 Node.js 12 执行。
注意:一定是 Node.js 12。
$ node --expose-gc --max-heap-size=100 test.js |
乍一看没什么问题,用 top
查看,内存会涨到约 100M 上下,然后迅速跌到非常小的值,如此循环往复。事实上,它就是没什么问题。
但问题坏就坏在 Node.js 14 和 16 上。至少在 V8 修复这个 Bug 之前(甚至很有可能 V8 不会认为这是个 Bug)的 Node.js 14 和 16 版本都会有这个问题。
这里,我用 Node.js 14.16.0 以及 16.8.0 进行实验。不排除后续版本会修复这个问题。
依旧是上面那段脚本:
$ node --expose-gc --max-heap-size=100 test.js |
感觉像在玩恐怖游戏一样,一人站一个角,跑着跑着,就跑没了。并留下了一段话:
<--- Last few GCs ---> |
看吧,莫名其妙 OOM 了。
造成这个问题的原因有好几个,缺一不可。就像东方列车案件一样,一人来一刀。我们一一解析。
首先,第一刀就是 V8 的 Compilation Cache。这个 Compilation Cache 跟我们日常熟知的 vm
API 中的 cachedData
不一样。它是更底层的一个缓存 Hash 表,整个 V8 Isolate 共用一份,以传进去的源码字符串本身作为 key
进行查找和存储。
在 Node.js 的 vm
中编译(或者说解释)一段脚本时,最终依赖的对象叫 UnboundScript
。这是一个尚未绑定至 Context
的脚本对象。在编译过程中,会逐步调用至以下代码:
... |
用人话解释就是:用源码去检索 Compilation Cache 中是否存在相同 key
的对象。若存在,直接返回已经存在的缓存;否则,正常进行反序列化,并将结果储存在 Compilation Cache 中(V8 分配的堆内存上),并由源码字符串作为 key
。
根据观察得出的结论,这种缓存技术在真实世界的网页中能够达到 80% 的命中率。并且由于这种缓存直接存在于内存中,所以它的速度会比较快。
虽然 Node.js 并不是 Chrome,但它也用了 V8,所以这个 Compilation Cache 也同样存在。
我们可以在一开始的源码中加入点料来验证这一点:在 times++
一行之后加入:
if (times === 330) require('v8').writeHeapSnapshot(require('path').join(__dirname, 'temp.heapsnapshot')); |
这样,当执行了 330 次循环后,会在当前目录下生成一个 temp.heapsnapshot
的 Heap dump 文件。再执行这个脚本,会发现它在 OOM 之前保留了一份现场。用 Chrome 的开发者工具打开这个 Heap dump 文件,我们可以发现:
字符串有将近 8000 个,别的一些不重要——我们可以看到有不少源码字符串根本没有被回收,而一个动辄 100K。而从下方 Object
一栏中可以看到,其都属于 Compilation Cache。
这说明了,哪怕我们手动执行了 gc()
,这些 Compilation Cache 中的内容(如源码字符串)并没有被回收。
根据上面的实验结果,我们不能武断地认为其不会被回收。事实上 Compilation Cache 也是在 GC 策略里面的。只不过它的策略与一般的 V8 JavaScript 对象不同。而且事实上,不管是 Node.js 12 所使用的 V8(v7.x)还是 Node.js 14 / 16 所使用的 V8(v8.x / v9.x),Compilation Cache 的回收策略是一样的。
想想 Node.js 12 执行这段代码的结果:内存会涨到约 100M 上下,然后迅速跌到非常小的值,如此循环往复。
也就是说,V8 堆内存到达上限后,会对 Compilation Cache 进行回收。我们可以验证一下,在执行的命令行上面加一个参数:
$ node --trace-gc --expose-gc --max-heap-size=100 test.js |
然后继续在 Node.js 12 下执行,就能得到类似这样的输出:
... |
即内存一路上涨,等到涨到顶的时候,GC 报了个问题:
allocation failure GC in old space requested
老生代空间不够申请了。然后触发了下一条 GC:
last resort GC in old space requested
这是一条 Last Resort GC,在该次 GC 之后,整体的内存又降到了一个非常低的水位。
对的,这就是 V8 的策略。我们知道 V8 的 GC 策略中,有一步是将新生代的内存给迁移到老生代去的。这个时候需要从老生代空间申请内存。若申请不到,就执行一次 Last Resort GC。我们可以看看 Node.js 14 / 16 的结果:
[2433812:0x4743cd0] 5820 ms: Mark-sweep 72.3 (92.1) -> 71.8 (92.1) MB, 5.7 / 0.0 ms (average mu = 0.723, current mu = 0.701) testing GC in old space requested |
一直是 testing GC in old space requested
,没等到进行 Last Resort 就挂了。
知道差别之后,我们先来看看 Last Resort GC 到底做了些什么:
void Heap::CollectAllAvailableGarbage(GarbageCollectionReason gc_reason) { |
首先,Heap
中有三个 GC 函数,CollectGarbage
、CollectAllGarbage()
,还有一个就是上面的 CollectAllAvailableGarbage()
。其中 CollectAllGarbage()
基本等同于调用指定参数下的 CollectGarbage()
。通常情况下,Testing GC 就是调用的 CollectAllGarbage()
,而 Last Resort 的 GC 只会调用 CollectAllAvailableGarbage()
。我们看到这个 CollectAllAvailableGarbage()
中就有清除 Compilation Cache 的逻辑。
这就与它的作用相符了。
“last resort gc” means that there was an allocation failure that a normal GC could “resolve”.
当有堆内存分配失败(到达上限)时,V8 会以 Last Resort 为由做一次 CollectAllAvailableGarbage()
的 GC,看看能不能把杂七杂八的各种没用的东西都回收掉。如果回收了之后,仍无法分配,那就只能干瞪眼并触发进程崩溃了。
而 Compilation Cache 的 GC 机制,就是 CollectAllGarbage()
不会回收它(就是我们看到从 Trace GC 中看到的 testing GC in old space requested
),只有 CollectAllAvailableGarbage()
才会将其回收。而 CollectAllAvailableGarbage()
调起的理由之一就是 Last Resort,即尝试分配堆内存失败时(也就是堆内存到达上限了)。
这就是为什么在 Node.js 12 中,这段代码会一直涨到 100M 左右,然后内存分配失败,接着执行 Last Resort GC,最后内存掉下来。
知道了这里有问题之后,我们就可以临时解决这个问题了。其实只要把 Compilation Cache 禁掉就可以了。
$ node --trace-gc --expose-gc --max-heap-size=100 --no-compilation-cache test.js |
我们在上一节中粗略介绍了 Last Resort 这种 GC 的时机。那么它到底是如何运作的呢。看看下面这段 V8 代码:
HeapObject Heap::AllocateRawWithRetryOrFailSlowPath( |
这是 Node.js 14 对应的 V8 代码,我已将一些关键注释标上,大家应该都能看懂。实际上 Node.js 12 基本一样,就是函数名有点不一样。总得来讲非常简单,就是尝试分配,若失败就 Last Resort GC,再次尝试分配,若还失败则 OOM 崩溃。
其实在第一次分配失败之前,它的依赖函数 AllocateRawWithLightRetrySlowPath()
还有个小 Trick:
HeapObject Heap::AllocateRawWithLightRetrySlowPath( |
整体连起来就是,如果分配内存失败,则先尝试两次 CollectGarbage()
。这种做法就已经可以解决大多数的内存分配失败的问题了。若两次 CollectGarbage()
还无法清理出内存,则再尝试一次 CollectAllAvailableGarbage()
。
实际上,Node.js 12、14 和 16 的 V8 在堆内存分配失败时的 GC 策略都一样,都是上面的逻辑。分配失败了,先尝试进行几次不一样的 GC,真不行了再最终 OOM。
既然一样,为什么 Node.js 12 好好的,而 Node.js 14 和 16 就会挂呢?
--always-promote-young-mc
在 V8 的 v8.0.1 版本中,其引入了一个新的 Flag——--always-promote-young-mc
。我愿称之为推陈出新。Node.js 14 用的就是 V8 的 v8.* 版本。
Add FLAG_always_promote_young_mc that always promotes young objects during a Full GC when enabled. This flag guarantees that the young gen and the sweeping remembered set are empty after a full GC.
This CL also makes use of the fact that the sweeping remembered set is empty and only invalidates an object when there were old-to-new slots recorded on its page.
每次 Full GC 的时候,这个 Flag 会保证在 GC 之后的新生代空间等为空,新生代的对象会全迁移至老生代。
我们看看它在代码中的实际作用吧。
... |
当 --always-promote-young-mc
打开的时候,每次 Full GC 都会尝试往老生代迁移。既然要迁移,肯定是要先老生代申请一块内存,才能迁移。若此时老生代内存申请失败(堆内存达到上限),则直接抛出 OOM 错误:MarkCompactCollector: young object promotion failed。这个错误跟我们用 Node.js 14 执行代码最终的输出对上了。而这个 TryEvacuateObject()
最后兜兜转转会调用我们在之前提到的 AllocateRaw()
函数(AllocateRawWithLightRetrySlowPath()
中调用的也是这个)了。
所以,整条崩溃链就是:
--always-promote-young-mc
开关打开,所以执行推陈出新操作;这简直就是一个死锁。至于 V8 到底认为这个是个 Bug 还是个 Feature,那我就不知道了。Bug 我是提了,大家可以跟我一起跟进。
我们明白了 --always-promote-young-mc
会导致目前的 Bug。那就跟之前 Compilation Cache 临时解法一样,将其关掉即可。
$ node --expose-gc --max-heap-size=100 --no-always-promote-young-mc test.js |
看!一切……别高兴太早。
设置了似乎并没什么用。这又是为什么呢?
--array-buffer-extension
这又是一个 V8 的 Flag。与别的 Flag 不同的是,它这一个只读的 Flag,且是在编译时就指定了的。
虽然这个 Flag 在之前就有,但是在 V8 的 v8.3 版本中,为这个 Flag 做了一次性能提升。
Backing stores of ArrayBuffers are allocated outside V8’s heap using ArrayBuffer::Allocator provided by the embedder. These backing stores need to be released when their ArrayBuffer object is reclaimed by the garbage collector. V8 v8.3 has a new mechanism for tracking ArrayBuffers and their backing stores that allows the garbage collector to iterate and free the backing store concurrently to the application. More details are available in this design document (https://docs.google.com/document/d/1-ZrLdlFX1nXT3z-FAgLbKal1gI8Auiaya_My-a0UJ28/edit#heading=h.gfz6mi5p212e). This reduced total GC pause time in ArrayBuffer heavy workloads by 50%.
有兴趣的小可爱们可以自行去看看上面提到的设计文档。总之来说,在 V8 的 v8.3 版本之后,打开这个开关可以提高 ArrayBuffer
约 50% 的性能。
正是因为这样,Node.js 在 v14.5.0 中就将这个开关在编译时由关闭状态变成了打开状态。(https://github.com/nodejs/node/commit/2c59f9bbe29df1ee3e714671de1433369992eba7#diff-d53f68b29a1c48c958c2e6779cc25c916a986357c6010dd01421c17adcf2f09bR150)
别以为我扯远了。这个 Flag 与 --always-promote-young-mc
息息相关。在 V8 中,Flag 们有相互依赖的关系。
DEFINE_IMPLICATION(array_buffer_extension, always_promote_young_mc) |
上面的宏的展开的意思就是说:
--array-buffer-extension
开关处于关闭状态,则 --always-promote-young-mc
可为任意值;--array-buffer-extension
开关处于开启状态,则 --always-promote-young-mc
会被强制开启。也就是说,哪怕你自己 --no-always-promote-young-mc
,由于 Node.js 在编译时就将 --array-buffer-extension
开关打开,--always-promote-young-mc
也会被强制开启。
大家可以试试看早于 Node.js 14.5.0 的版本,那个时候 Node.js 的 --array-buffer-extension
开关还处于关闭状态。也就是说,在该版本中,我们是可以通过执行:
$ node --expose-gc --max-heap-size=100 --no-always-promote-young-mc test.js |
来规避这个问题的。Node.js 14.5.0 之后,开关打开,你就关不掉了。
导致该 OOM 有几个问题:
UnboundScript
,其依赖 V8 Compilation Cache,该 Cache 只有在 CollectAllAvailableGarbage()
时才会被回收,导致内存一直上涨;--no-compilation-cache
关闭,但如此一来则无法享受 Compilation Cache;GC 的 OOM 无法通过 --no-always-promote-young-mc
关闭,因为其前置开关被 Node.js 在编译时强制开启。说了那么多,临时解决办法其实已经贴在各小节中了。要问我最终解决办法是什么,就俩:
即使在 Node.js 14 / 16 下,若我们使用 Inspector 进入进程调试,那么一切表现又正常了。因为 Inspector 的一些策略会不一样,GC 自然也不一样。有兴趣的小可爱们可自行去探查一番。
]]>Every execution context has associated with it a variable object. Variables and functions declared in the source text are added as properties of the variable object. For function code, parameters are added as properties of the variable object.
然后将其与“面试官”绑在一起。其实在书面理解上沿用活跃对象的概念没什么问题,但是照抄原文又不指明出处,就会让人误以为如今的规范中也还定义了活跃对象这一概念。其实上引文中包含了活跃对象(Activation Object, AO,有时也称活动对象、激活对象)与可变对象(Variable Object, VO,有时也称变量对象)的内容,摘抄自 ECMAScript 3 Spec 的两处并组装起来。
今天,这里与大家一起浅尝一下 JavaScript 中的活跃对象。
在 ECMAScript 1 和 ECMAScript 3 中,的确是有着关于活跃对象的定义。
当控制进入函数代码的执行上下文时,创建一个活动对象并将它与该执行上下文相关联, 并使用一个名为
arguments
、特征为{ DontDelete }
的属性初始化该对象。该属性的初始值是稍后将要描述的一个参数对象。接下来,这个活动对象将被用作变量初始化的可变对象。
活动对象纯粹是一种规范性机制,在 ECMAScript 访问它是不可能的。只能访问其成员而非该活动对象本身。对一个基对象为活动对象的引用值应用调用运算符时,这次调用的
this
值 为null
。——ECMAScript Language Specification 262 Edition 3 Final, 10.1.6 活跃对象
但也仅限于 ECMAScript 1 和 3 了。我们现在在网上(尤其是中文搜索环境中)获取到的关于活跃对象和可变对象(Variable Object)的文章,大多都是为我们描述的 ECMAScript 1 和 3,早已过时。
如果大家对这块内容仍然感兴趣(实际上我也建议大家感兴趣),可以参阅:
在 ES5 及之后的 ES 版本,已经不存在活跃对象(AO)及一系列周边内容的概念了。取而代之,是一个叫词法环境(Lexical Environments)的定义。
也就是说,严谨来讲,现代的 ECMAScript 早已没有了活跃对象这一概念,所以当网络上文章中“面试官跟你聊起 AO”这些内容出现的时候,其实就是“市面文章一大抄”的体现。他们还会有理有据地把 ECMAScript Spec 原文给你列出来(参照文首的摘抄)。
关于词法环境,大家可以参阅:
这里就不赘述了。
经过上面两节内容,我们可以知道,活跃对象是 ECMAScript 1 / 3 中的内容。后续的版本中,其就不复存在了。但是活跃对象这个概念就不能再被提起了吗?
其实也不是,它对应的概念还是可以延续下来的。只不过不能让人误以为现代 ECMAScript 中还有其定义,我们现在再聊起活跃对象时,应该知道它只是广义的抽象,而不再是狭义的定义了。广义的活跃对象在不同的场景下也可以有不同的名字,如活跃记录(Activation Record)、栈帧(Stack Frame)等。
每当函数被调用的时候,其都会创建一个活跃对象。该对象对开发者不可见,是一个隐藏的数据结构,其中包含了一些函数在执行时必要的信息和绑定,以及返回值的地址等等。
在 C 语言中,这个对象会在一个栈中被分配生成。当函数返回的时候,该对象会被销毁(或者出栈)。
你看,此处“活跃对象”被引申到 C 语言了。它指的是一个抽象的存在,意为栈帧(Stack Frame)。
JavaScript 与 C 语言不同,它是从堆中分配该对象。且这个活跃对象并不会在函数返回时被自动销毁,它的生命周期与普通对象的垃圾回收机制类似,是根据引用数量决定的。
一个活跃对象包含:
return
之后的控制权转移;undefined
进行初始化;this
,如果函数作为一个方法被调用,那么 this
通常就是它的宿主对象。其实 ES5+ 之后的广义“活跃对象”就是对于 ES 1 / 3 定义的活跃对象的一个扩展,并将其应用到了词法环境中。
至此为止,关于“活跃对象”的浅析就足矣。当下环境中,我们不是不能再谈论“活跃对象”,而是不能乱谈,还谈得有鼻子有眼的。现如今的“活跃对象”是一个类似于活跃记录和栈帧的广义抽象概念。
不然,仍旧用老旧的文章去回答所谓“面试官”的问题,很有可能被刷掉哦。
闭包也是老生常谈的一个概念。为什么在这篇文章中要提起这么个看起来八竿子打不着的概念呢?
闭包一直没有一个非常严谨的定义。如:
闭包就是能够读取其他函数内部变量的函数。
闭包就是能够读取外层函数变量的函数。
等等等等。
再例如:
「函数」和「函数内部能访问到的变量」(也叫环境)的总和,就是一个闭包。
上述的解释也都是我从网上的各文章中摘抄出来的。其实理解起来很容易,但是语言描述出来并不那么严谨。大家知道那么回事就好了。
不过,在我们有了广义活跃对象之后,我们可以从另一个角度来定义闭包了。怎么说呢?函数是可嵌套的。当一个嵌套的函数对象被创建时,它会包含一个外层函数对象所对应的活跃对象引用
有了这层关系,闭包就好定义了:
一个拥有外层函数对象所对应的活跃对象引用的函数对象就被称为闭包。
言简意赅。虽然不至于像“能读取外层函数中的变量”那样亲民朴实,但也非常言简了。同时,有了“活跃对象”作为大前提,已经帮忙做了很多前提条件定义,所以这个定义也能达到意赅的效果。
以后再面试起什么是闭包,大家可以尝试从这个角度解释哦。前提是你真的懂了,不要到时候又被面试官给绕进去了。
]]>今年关键字:自我否定。
上半年基本上都宅在家中度过。尤其是年初那会儿,在家办公,真的是一天天门都不迈出。每天自己烧饭,一家人其乐融融,还真是怀念这种生活呢。
疫情原因,今年的足迹并不多。本来 11 月底要去上海躺平设计家做一个分享,最后也因为突发的新增病例取消了。
在千岛湖宅了几天,平复了一下自己上半年自我否定的心情,跟 @贯高 @天猪 他们深夜撸串;以及偶遇一家破旧的充满烟味的小酒吧,听驻唱到凌晨。
我甚至都记不得千岛湖那家酒吧的名字,这种感觉真好。
这次的体验让我跟 @芙兰 有了个决定,就是以后每去一个新的城市,都要找一家当地不是那么有名但是有着不错驻唱的酒吧去欣赏,驻唱不需要有多少颜值,唱得也不一定需要多好,比如大连某酒吧有个唱《山丘》破音的小胖子我就很喜欢。
8 月份去上海,在韩骏大佬《Visual Studio Code 权威指南》的新书发布会做了回嘉宾,很大一部分原因是因为我之前在蚂蚁集团做 Cloud IDE 吧。
深圳那次则是还了上半年应下来的技术分享债,其实我后来就与 IDE 的开发无关了,去做一些自己更感兴趣以及更擅长的一些事,不过还是很感谢腾讯热心的小伙伴们。
去大连找了个没人的小沙滩躺着听海,感受海浪的安详;同样去了感觉很不错的酒吧听歌,就跟之前提到的一样,里面那个小胖子唱《山丘》破音了(越过山。啾)
在苍南跟大学时候的小伙伴小聚了一天,还认识了一个很有意思的茶艺小姐姐,打开了一些新的思路。
最后在大明山摔伤了自己的小尾巴骨,不过最后也算是能在新手村好好滑完了,也算是一种新的体验了。穿着灰色的滑雪鞋,感觉自己是一条鲨鱼辣椒。
今年又入了几个塑料小人,比如说在泡澡的ねずこ。
以及,本就拥挤的家里又多了一堵墙。
琴没怎么学会。疫情后,琴班也荒废了。只学会了一首《全ての人の魂の詩》,现在已经忘差不多了。还好当时会的时候有录下来,不然就可惜了。感觉自己的生活基本上也就是宅、游戏、音乐。
今年生日收到的礼物居然是周董的全专辑黑胶,以及一台 LP60。 Couldn’t be happier!
说到游戏,今年入了好多。不完全列举有对马岛之魂、天穗、Spirit of the North、Human: Fall flat、渡神纪、轩辕剑 7、Hitman 2、十三机兵、最终幻想 VII 重制版、P5S 等等。最终通关的也只有 FFVII 和 Spirit of the North,目前正在赛博空间打工。不过由于本人是手残党,游戏均非本人通关和玩耍,热衷观看 @芙兰 通关,现在已把 Judy 推倒。
最后,虞姬也终于拿到了银 50 的牌牌。2021 年继续努力。
很对不起大家,今年又占用大家的公共资源了。
去年的总结中有过一句话。
但是令我感动的是,小伙伴们都好帮我。每次看到他们这么努力帮我,感觉都有愧。感谢宗羽,感谢陆老师,感谢常老师和五哥。再不争气就真是我自己的问题了。
的确是今年又不争气了,感觉自己就是个扶不起的阿斗。
今年的工作真的是一场重头戏,感觉是年度魔幻大片。说实话,我已经连着两年绩效不好了。在今年年中的时候,我曾一度认为自己根本不是做程序员的料子,做的东西也得不到认可。就拿非 IDE 的东西来说,我做构建的优化,用上一些缓存的技术,直接被 @苏千 怼得哑口无言:你一直在外围做这些东西有什么用?为什么不去做一个能打败 Webpack 的东西出来?
是啊,我做这些东西又有什么用呢?就跟国内互联网风气一样,换汤不换药,只敢在外围蹭。事实上,排除能力高低不说,我也想做一个构建速度足够快的工具,但是现如今已经浪费了几年的我还承得起做失败所带来的后果吗?如果做失败了,就意味着我又一年浪费了。我不知道上位者们(非贬义词)站在上帝视角看我当前阶段时候的是怎么样一个感受,我感觉我自己都能列举出几个关键字:执念、畏手畏脚。这些都是我自己给我自己加上的枷锁。可能过几年,我再回过头来看当下,的确只是小磕小碰,就如上位者们现在看我的视角一样。然而对于现在的我来说,这些“否定”就是我当下的“全部”。
这与阶段有关,就像小时候一样,一个玩具就是我的全部。如果玩具坏了,可能我的整个世界都崩塌了。所以,可能几年后我能很轻描淡写地提起今年的事情,但是现在的我还做不到。
我其实就是个程序员,喜欢写代码。然而这两年的歪路让我越来越觉得写代码是个很难的事情,我没法让我的代码为自己、为公司,为整个社会提供价值。
Cloud IDE 是一个非常有想象空间的项目,然而大前端(Cloud IDE的交互)与大后端(容器侧的事情)都非我擅长。我花了半年的时间去改变自己,甚至写了几个月的前端,发现自己真的不行。我只有抱着一个看着并没有什么用的 IDE 网关惴惴不安。
与很多前辈们在交流的时候,他们给了我各种意见。比如四个象限,想做且擅长、想做且不擅长、不想做且擅长、不想做且不擅长的事我都需要能扛起来(可能我的记忆有偏差,可能只需要扛两三个象限),我要往这个方向发展;再比如换个赛道,做自己擅长的事,但可能那样成不了更好的自己。
不过最终我选择了后者,我只想好好写代码,想要自己的代码能服务于大众。不擅长的事为什么不交给更专业的人来做呢?如果公司不需要我,炒了我就好了,强迫自己只能变成长处无法发挥,短处不如别人,结果就是我现在这样的下场。
这都是个人的选择,无关方法的好坏。
在那段时间,在所有人都否定我,我也在自我否定的时候,感谢 @芙兰 一直认为我是最棒的,有她陪在身边真好。除此之外,也非常感谢 @苏千 和 @玉伯 都不嫌我烦地在几个小时聊天中给予了我很多建议。有一段 @玉伯 的人才观和团队观让我醍醐灌顶。
除了家人外,最感谢的还是 @舒文 老师了。他主动找的我,没否定我的偏科,甚至觉得公司是应该要有我这类奇葩的一席之地。并为我建议了可能更适合我的团队,也就是我现在所处的团队,淘系的 Node 架构团队。当时其实我真的萌生了离职的念头,最后是被他的真挚所打动,无论我离不离职,来不来我当下的团队,其实都与他无关的。
为什么说它适合我呢?因为我在之前团队一无是处的那些能力和技术,恰好是这个团队核心的竞争力之一。
虽然来的时间不久,但至少我已经可以优化 Alinode 的源码,将一些正式 Serverless 函数项目的启动时间提升将近 120%**;也实现了一个启动时间为微秒级的 JavaScript Runtime**;函数的部署密度也可以进一步提高。而且身边的同事在该领域也个个勇猛。也许我现在正在做的事情算是勉强可以跟苏千当时说的“为什么不去做一个更快的构建工具”相提并论了吧,其实我是可以的。
现在回过头去看,也许我前两年真的是进错行,而不是自己太菜鸡了。前两天刚看了《心灵奇旅》,感觉自己上半年跟下半年的自己就分别是那些在 the Zone 中的灰色灵魂和彩色灵魂。放下执念,会轻松很多。多留一些时间给自己的生活与家人,迷失自我不值得。
值得高兴的是,今年的代码写得比去年多多了。我真的是热爱写代码!
《Modern Vim》终于出版,薄薄的一本。算是我在书籍翻译的一次初试水吧,感谢博文视点的 @皎子 愿意给这本书一个机会,才让它有机会与大家见面。
至于《How JavaScript Works》这本书,道格拉斯的文风真的是清奇。怎么说呢,这是一本骂骂咧咧的书,但是让人看着莫名地爽。要翻出这种神韵还真的有些难,比如:
强烈建议你不要简单粗暴地复制粘贴那些你并不理解的代码。虽然我们经常戏称自己是“复制粘贴工程师”,但这种做法实际上是很不可取的。这虽然比不上你看都不看一眼就去安装一个你不清楚的软件这么蠢,但也实在算不上是一种明智之举。在当前国际的安全技术水平下,最好的安全过滤器是你的大脑,请务必善用。
我之前尽可能地在本书中避免提到大多数 JavaScript 中的糟粕,但是在本章中我却要把这些丑陋怪物的内裤都扯下来,一丝不挂。我将列举一些在《卧槽》以及同类演讲中出现的问题,并向你展示它们是如何工作的。这个事情可能并不会让你觉得有趣,甚至你可能会感觉有些被冒犯了。
如果其中一个
include
包中包含流氓内容(现实中是会有这种情况的),在my_little_get_inclusion
函数下它也闹腾不出什么浪花来,但如果我们直接从fs
对其进行访问,则可能会有严重后果。科学越进步,人类离坟墓越近。
上文均出自我的《How JavaScript Works》译稿的初稿。
并不想过多赘述了。看前文的知乎链接吧。
今年就参加了两场。一场是讲师,一场是嘉宾。每年来几次,不来几次的话很可能会故步自封的。大家对 Cloud IDE 有兴趣的话,可以看看我的演讲视频。不过我现在不做这个了,我在做一些更让自己眼睛放光的事情。
今年参会的时候与 @Hax 贺老聊了挺久的,也交换了不少的信息,收获良多。虽然他们的立场很不一样,但我感觉贺老的语言风格可能跟道格拉斯的写书风格还真有点像。(贺老别打我,若被冒犯了我就删掉)
对于学习技术这一块,并不追新了。我权当深入以前并没那么深入的技术就算完成了这个 Check point 吧。
但是状态这个,今年真不行,比去年还颓废。
第一二两点,受疫情影响,也没办法。唉。
不写 Checklist 了,反正写了明年也完不成,何必让自己心烦呢。一家人健健康康平平安安就好了。
Ask me anything: https://github.com/xadillax/ama
]]>child_process
模块中与 stdio
参数相关的函数需要加上 on('data')
事件处理。哪些与 stdio
相关呢?如 child_process.spawn()
中 options
就有个可选参数 stdio
,你可以指定其为 inherit
、pipe
、ignore
等。
怎么算加上 on('data')
事件处理呢?监听这个事件算一个,将 stdio
指定为类似 ignore
这类操作也是算的。
接下去我就以 child_process.spawn()
为例展开讲吧。
child_process.spawn(command[, args][, options])
我们先来看看 child_process.spawn()
函数:
command
:要执行的命令;[,args]
:执行命令时的命令行参数;[,options]
:扩展选项。我们不关心前面的内容,只关心 options
中的 stdio
属性。
options.stdio
可以是一个数组,也可以直接是一个字符串。
如果 options.stdio
是一个数组,则它指定了子进程对应序号的 fd
应该是什么。默认不配置的情况下,spawn()
出来的子进程对象(设为 child
)中会有 child.stdin
、child.stdout
和 child.stderr
三个 Stream
对象,而子进程的 stdin
、stdout
和 stderr
三个 fd
会通过管道会被重定向到该三个流中——相当于 options.stdio
配置了 'pipe'
。
如果 options.stdio
是一个字符串,则代表子进程前三个 fd
都是该字符串对应的含义。如 'pipe'
与 [ 'pipe', 'pipe', 'pipe' ]
等价。
数组中的每个 fd
都可以是下面的类型(无耻摘录文档):
'pipe'
:在两个进程之间建立管道。在当前进程中,该管道以 child.stdio[]
流暴露;而 child.stdin
、child.stdout
和 child.stderr
分别对应 child.stdio[0-2]
。子进程的对应 fd
会被重定向到当前进程的对应流中;'ipc'
:在两个进程之间建立 IPC 信道,主子进程通过 IPC 互通有无(前提是两个进程都得是 Node.js 进程),不过该类型不应用于 std*
,而应该是数组中后续的 fd
中;'ignore'
:将 /dev/null
给到对应的 fd
;'inherit'
:字面意思是继承当前进程,该配置会将子进程的对应 fd
通过当前进程的流重定向到当前进程对应的 fd
中,不过只有前三项(stdin
、stdout
和 stderr
)会生效,后续 fd
若配置了 inherit
等同于 ignore
;Stream
:直接是与子进程相关的 TTY、文件、Socket、管道等可读或者可写流对象,该流对象底层的 fd
会与子进程对应的 fd
进行共享,不过前提是流中得有个底层的文件描述符,像一个未打开的文件流对象就还没有对应的描述符;Stream
类似,对应的是一个文件描述符;null
/ undefined
:保持对应 fd
的默认值,前三个 fd
默认为 pipe
,之后的为 ignore
。了解了之后,我们就可以做限定了,本文标题的意思即 pipe
这类需要消费子进程 stdio
的操作我们需要真的消费才行。
其实原因也在文档中写明了,我会在本文的最后再放出来。
先开始做实验吧。
我们先准备子进程文件:
// child.js |
文件中有两句
str
声明,一句为注释。当我们要用短字符串的时候,就用原代码;当我们要用长字符串的时候,两句源码与注释互相替换一下。
我们写如下的主进程代码:
// index.js |
运行一下 $ node index.js
。一切正常,我们的 'hello'
也被输出了。没问题。然后在上面的代码中加入:
child.stdout.on('data', () => {}); |
再运行一下,似乎没什么变化。脱裤子放屁。我们再加点料吧:
// index.js |
再运行一下,把 '123'
输出了。一切如我们所料一样。
接下去,我们要注释掉子进程的短字符串,把长字符串放出来吧。
首先是「短字符串测试」中的最后一段代码,即有 chunk => data += chunk.toString()
这段代码的文件。运行一下 $ node index.js
看结果。
嚯,输出了一堆的 '0'
,就像这样:
看着太心烦了,把 data
相关的代码去掉吧,stdout
的 data
事件监听改回这样:
child.stdout.on('data', () => {}); |
然后在 console.log
那里也改回 'hello'
。再运行一遍,世界清净了,只剩 hello
。
到目前为止,一切看起来都还算正常。
接下去要开始翻车了,我们把 child.stdout.on
这一整句去掉,让主进程代码恢复成最初的样子,顺便加点料:
// index.js |
$ node index.js
,按下手中的回车键执行吧:
「噢,死月真是个沙雕呢。」之连环暴击。
我们的程序卡住了。上面的源码很短,一眼就能看出来是因为没执行到 process.exit(0)
才卡住的。没执行到 process.exit()
的原因其实是因为没有触发 child.on('exit')
事件,再往上推,则是子进程没有退出。
不信他没退出的话,在「死月沙雕」的期间看看进程存活状态就知道了。
$ ps aux | grep node |
先不看答案,我们动手 GDB 一下看看卡哪了。大家编一个 Node.js 的 Debug 版本也要好久,为了简化过程,我们用 C 写一个最简单的子进程就能做好这个实验。
// child.c |
然后编译一下:
$ gcc child.c -g |
生成了 a.out
,然后改一下 JavaScript 主进程源码的 spawn()
函数:
const child = cp.spawn('/tmp/lab/a.out'); |
跑起来之后肯定依旧是沙雕一日游。这个时候我们拿到 PID 进行 GDB 一下吧。
$ ps aux | grep a.out |
我们看到是卡在 child.c
的第 4 行 printf
了。它上面的执行栈也是一路 printf
卡到底。
现在我们知道了,当我们不处理这些文章开始说的事件时候,子进程有可能会卡在形如 printf
等往 stdout
、stderr
这些 fd
写的操作上。
我们回过头去看看,我们的实验代码主子进程之间是通过什么来联立 stdout
的。根据最开始的文档摘录,噢,原来是 pipe
呢!
通常情况下,Linux 下的管道缓冲区为 65536 字节。然而 Node.js 子进程 stdio
的值若为 pipe
,则其实是建立了一个 Unix Domain Socket。
也就是说,子进程的 stdout
是一条与主进程之间建立起来的 Unix Domain Socket。其两端的进程均将该管道看做一个文件,子进程负责往其中写内容,而主进程则从中读取。
让我们把视线放到工地上。
管道是有大小的。如果我们堵住管道的出口,那么我们一直往管道里面灌水,最终会导致水灌不进去堵住了。这句话同样适用于我们上面的代码。
也就是说,我们最开始没有翻车的代码,因为输出的内容太少,占不满管道缓冲区,所以不会阻塞程序执行,最终得以安全退出;而后面翻车则是因为我们输出的内容太多了,导致不一会儿缓冲区就满了,而我们的主进程又没去消费,所以就翻车了。
为什么我们 on('data')
了就能消费,而不加就没消费呢。按理说 Node.js 都读过来,emit
了事,就能继续读下一趴了。其实不是的。
看看 Node.js 的判断 Readable Stream 是否要读取新内容的逻辑(https://github.com/nodejs/node/blob/v12.18.3/lib/_stream_readable.js#L586-L621)。
function maybeReadMore_(stream, state) { |
前面其它正常的前提我们抛开不讲,如流正在读啊,还能读到数据啊什么的。
当 Readable Stream 内部的 Buffer 长度没到水位线(通常是 16384),或者其处于 flowing
状态且缓存没数据的时候,该流会继续从源读数据。
一个 Readable Stream 最开始的 flowing
状态是 null
。也就是说在这个状态下,当达到缓存水位线之后,就不会继续读数据了。
那什么时候这个状态会变呢?在这里:https://github.com/nodejs/node/blob/v12.18.3/lib/_stream_readable.js#L868-L897。
当你调用了 stream.on()
的时候,它会判断你这次调用所监听的事件。若事件是 'data'
且当前的 flowing
状态不为 false
的话:
if (ev === 'data') { |
Readable Stream 就会执行 resume()
。在 resume()
中(https://github.com/nodejs/node/blob/v12.18.3/lib/_stream_readable.js#L955-L969):
// pause() and resume() are remnants of the legacy readable stream API |
会将 flowing
设置为 true
。正如 Node.js 文档中说的一样:
All Readable streams begin in paused mode but can be switched to flowing mode in one of the following ways:
- Adding a ‘data’ event handler.
- Calling the stream.resume() method.
- Calling the stream.pipe() method to send the data to a Writable.
即所有的 Readable Stream 一开始都处于暂停状态,对其添加 data
事件才会开始切为 flowing
状态。而在暂停状态下,stdio
的 pipe
流会先缓存略大于或等于水位线的数据。
在暂停状态下,只有你添加了 data
事件处理器才会开始读取数据并丢给你;而如果你处于 flowing
状态,只移除消费者,那么这些数据就会丢失——因为流其实并没有暂停。
文档上虽说一开始处于暂停状态时我们没去监听数据,那么流就不会产生数据。实际上在内部实现上是产生了数据,而这部分数据是被缓存起来了。
好了,回到最开始。我之前说了“其实原因也在文档中写明了,我会在本文的最后再放出来”。现在是时间了,看看这里:https://nodejs.org/api/child_process.html#child_process_child_process。
By default, pipes for stdin, stdout, and stderr are established between the parent Node.js process and the spawned child. These pipes have limited (and platform-specific) capacity. If the child process writes to stdout in excess of that limit without the output being captured, the child process will block waiting for the pipe buffer to accept more data. This is identical to the behavior of pipes in the shell. Use the { stdio: ‘ignore’ } option if the output will not be consumed.
默认情况下,spawn
等会在 Node.js 进程与子进程间建立 stdin
、stdout
和 stderr
的管道。管道容量有限(不同平台容量不同)。如果子进程往 stdout
写入内容,而另一端没有捕获导致管道满了的话,在管道腾出空间前,子进程就会一直阻塞。该行为与 Shell 中的管道一致。如果我们不关心输出内容的话,请设置 { stdio: 'ignore' }
。
看吧,就是这个理儿。如果我们将其设为 ignore
的话,其三个 std*
就会导到 /dev/null
去。
所以标题的标题党就是这个意思。
你一旦建立了子进程,且其 stdout
之类的是一个 pipe
,你就必须对它的数据负责。哪怕你只是监听了这个事件,里面写个空函数,Node.js 也会认为你消费了,不然 Node.js 会把子进程的数据一直挂载在它 Stream
的缓存中,最后到一个水位(大于 16384 的时候)之后就停止读取子进程数据了。然后就会导致子进程写阻塞。
结论就是,你在 child_process.spawn()
一个进程的时候,请务必监听 stdio
里面各 Stream 的 'data'
事件。若你不关心输出,请将 stdio
设置为 ignore
或者 inherit
。
今年关键字:中年危机。
今年还是老地方滚来滚去。杭州、成都、上海。
值得一提的几个点,一个快三十岁的中年油腻大叔,居然一时冲动,买了一张工作日下班去苏州的车票,听了一场东方的音乐会,然后第二天早上滚回公司上班,虽然不远,但是也算是一时兴起的说走就走的旅行。一个奔三的抠脚大汉,居然听管弦乐听到泪流满面。这是音乐的力量,也是青春的力量。
以及,终于买到了周杰伦的内场门票。以前遥不可及那种场景,终于也是在自己身上发生。还好,生活并不是一直止步不前,或者在退步。至少还是有一些自己想做的事情是可以做到的。
由于去年写了本《Node.js:来一打 C++ 扩展》,所以也去参加了一次电子工业出版社技术出版高峰论坛,顺便在九溪半日游。
今年分享有两场,分别是上海的 FDCon 2019 和成都的 Web 全栈大会,议题都是《聊聊 Node.js 构建部署时我们要关心的数据》,炒冷饭。算是给去年的自己一个交代吧,讲真第一次分享的时候,心理实际上还是带有一丝的不甘心。后来经过「为什么强如『死月』在阿里也只有 P6?」风波,也算释然了,然后就是浓浓的中年危机,毕竟六年老屁了。这个答案里面也有我对自我认知的一个阶段性总结。
至于塞班,今年团队 Outing 的唯一选择,没办法。虽然之前度蜜月的时候已经去过了,架不住还是再去了一次。与上次不同的是,虽然没有潜水证,但也在蓝洞体验了一把潜水,较之上一次的浮潜来说,的确很爽。尤其是哪个蓝幽幽的蓝洞。
今年入了好些塑料小人,以及一堆的盲盒。塑料小人这次是按小众来,比如格温德琳、玛利亚、雷霆等。比如下面是部分画面。
然后就是有一次去逛商场的时候,莫名其妙就买了个雅马哈的 CD 机。后来就买了一堆以前没有的 CD,比如潘玮柏的所有签名 CD、已经绝版的仙剑 1 和仙剑 3 的原生 CD 等等。一堆幻想乡有的没的 CD,包括上次幻奏乐景的现场 Live CD。
今年钢琴学了大半首的 Suteki Da Ne,在生日当天,老婆送了我钢琴课,终于开始正式学钢琴了。感觉学谱子的进度突飞猛进。新年第一个月的中旬就要去交作业了,刚学了舞-HiMe 的「It’s Only the Fairy Tale」。
去年招待了几次小伙伴,今年招待小伙伴的花样变多了,组织了好多场的剧本杀。印象比较深的是《记忆碎片》和《凤求凰》两个剧本。复盘在内网的语雀里面,就不分享了。下图是《记忆碎片》一场的抽角色环节。
去年是有点水土不服,所以今年自然拿到了非常不理想的绩效。年中也出了个知乎事件,那段时间其实比较低迷。好在同事小伙伴们支持我,家人也支持我,不至于辣么难受。但的确自己值得反思,为什么那么多年了,还是这个破水平。
今年上半年从做框架的组调到了做基础平台的组。后来加入了 CloudIDE 项目,负责将内部 IDE 内核进行云化。经历了一两个月的救火,终于逐渐找到了自己的节奏。基于 Tengine 完成了一个 CloudIDE 所用的网关。目前在负责稳定性相关的专项治理,其实对于治理这类活,心里还是挺虚的。以往的工作中,致力于挖坑,并不是太擅长填坑。
坑王驾到。
所以,今年的工作足迹就比较简单了。继续完善去年构建部署时数据的坑,然后就是一头扎进了 CloudIDE 中。由于真的是在摸索的过程中,或者说没有一个太具象的事情(并不是去写功能什么的),而是在搞稳定性相关的治理,所以现在回想一下,也说不出个具体的事情。但还是希望自己的方向不会再跟去年一样偏了吧。
但是令我感动的是,小伙伴们都好帮我。每次看到他们这么努力帮我,感觉都有愧。感谢宗羽,感谢陆老师,感谢常老师和五哥。再不争气就真是我自己的问题了。
今年写代码的时间明显少于以前了。对于一个才 P6 的人来说,这真的不是一个好的讯号。2020 年真的不能这样了。教练我想好好写代码。虽然说写代码在阿里是最简单的事,或者是大家都必会的基础,但是今年的这个代码量真心让我自己心慌。不知道时间为什么都花到别的事情上去了。
Modern Vim 一书是去年我自己心血来潮想翻译一本关于 Vim 的书的,2020 上半年应该能出版的。而 How JavaScript Works 这本则是将近年终的时候,图灵社区的编辑找到我,想我来试试看。本来我是觉得时间不够,没有想太多。后来发现这本书是道格拉斯写的,于是就接下来了。希望 2020 年下半年本书能与大家见面。
这是第三次讲风波了。这里就不赘述了。反正后来我在回答了问题之后,又另写了篇文章做结贴。
不过,也托它的“福”,从原来的 3000+ 粉到现在的破万了。
今年参加了两场技术分享,把去年的工作总结了一下。后来又因为处于 CloudIDE 这个项目中,于是又去年底的 VSCode Day 凑了个热闹,当了回听众,跟韩骏大佬以及一堆做 Web IDE 的大佬们面了个基,感觉也是受益匪浅。
值得一提的是,去年成都全栈大会是双十一前夕,分享完中午就马上赶回杭州了。今年特意看了不是双十一,不用这么赶了。结果后来才发现第二天是周杰伦演唱会,然后分享完第二天一大老早又风风火火赶回杭州了。Σ(o゚д゚oノ)
去年的 Checklist 真的是不想再说什么了。果然人越到中年越没了当年的雄心壮志,被磨平了。
已经不想展望了。感觉得过且过吧,这样挺好,一年比一年能做到的少,人到中年,心态也变了。
至于晋升,已经不强求了。越是想,就越容易走偏,行事的手法逐渐变形。放平心态,该是我的就该是我的;不该是我的,那自然就是水平没到。的确,我今年的状态不对,太颓靡了。
随缘箭法。
——半藏
Ask me anything: https://github.com/xadillax/ama
]]>今年的足迹比去年还少,更别提前年了。就在将近年末的时候听了一场马克西姆的演奏会,Live 效果就是好;以及在双十一的时候去成都参加了 FreeCodeCamp 的 Web 前端交流大会,充当了一次讲师提供了一场《给 Node.js 插上 C++ 的翅膀》的演讲;再往前推就是年中的时候团建 Outing 去了清迈玩,很险的是差点选了去普吉岛,如果去普吉岛的话肯定会选择去皇帝岛的路线,而今年沉船就是在那几天,感觉也是捡回了一条命。
体重今年曾一度到了 80 公斤,感觉自己不能再这么下去了,于是开始减脂。由于不想太管住嘴,所以还算比较佛系。但好在小有成效,心肺明显好了很多,并且也“慢慢”掉回到了 75 左右,不过体脂比以前的 75 还是要低的。然后由于有了自己的小窝,所以今年基本上都是宅在家里,100 寸的屏幕加上家庭影院玩游戏真的很爽。每每家里来客人,总要 PSVR 一番。
(猜猜上面两个都是谁 (๑•̀ㅂ•́)و✧)
另外,别的不说,推荐一个最近玩的路亚钓鱼游戏,Monster of the Deep,近距离的席德妮妹子赛高。
以及,多次招待了朋友和同事们。下图是大学时候集训队有爱的同学们。
今年的工作依然是比较坎坷。刚进蚂蚁有点水土不服,工作的方式跟以前大有不同,年中的时候特别焦虑,好在慢慢熬了过来,慢慢开始接受了现在的习惯。
目前在蚂蚁做的事情还是底层框架、基础设施相关的,在做一些应用、框架质量和治理相关的事情。相较以前更多地去关注技术和研发,现在更需要的是自己把控好整件事情,要 Hold 住整个产品的方向。真的是应了大家的话,进了阿里系之后,写代码反而是最简单的事情,大家需要努力提升自己的软技能。
在蚂蚁也一年了,周年那天也拿到了自己“一年香”的币,还是感慨万分的。团队的小伙伴们真的是又牛又棒,很喜欢融洽的氛围。大家如果不嫌弃有兴趣一起共事的话,也可以投简历过来哇,我们现在需要各种前端、Node.js 开发、3D 开发(Unreal 等),以及设计师、PD 等等等等。
唯一有点遗憾的是,对于某一特定技术钻研的时间变少了,技术输出也变少了,例如今年对于 Node.js 的贡献少之又少,文章等也变少了,今年一次 NodeParty 都没参加以及组织,感觉明年要更努力了。
正如上一节所说,今年对于社区的输出变少了。什么“太忙了”什么的都是借口,就是自己的时间管理并没有达到自己想要的水准。
正如去年的总结里面所说,这是去年的目标,而今年《Node.js:来一打 C++ 扩展》终于出版了。而自己出书有一点很爽的就是,可以自己埋下各种彩蛋,大家都去找找看吧。
可惜 Node.js 发展太快,去年书中的内容还是 Node.js v6,而现在已经到 10.x 了。不过好在底层都是相通的,大家还是可以一读。
此外,由于跟负责之前这本书的编辑熟了,于是又揽下了一本 Vim8 和 Neovim 相关的书的翻译。
(发现封面是二小姐的翅膀了吗?)
去年年底和今年输出了几篇技术文章,托了团队专栏的福,涨了一波粉。以及知乎日报也收录了我的一个答案(对 BAT、TMD 这类公司而言,1-3 年的工程师在技术面时面试官最看重的有哪些?),所以又涨了一波。也算是今年的一个意外收获吧。
托了前几项的福,今年受邀参加了 Web 前端交流大会。并不擅长提供 Topic 的我,居然被大家冠上了“幽默”文风的特性,也许是因为我的一句“老夫撸起袖子就是干”吧。
然后就是得到了一张证书。
可惜今年成都太匆忙了,由于是双十一,当天还要半夜去接某人天猫双十一归来,所以分享完毕都没来得及跟大家交流就马上上了飞机回了杭州。下次再来成都一定要好好玩玩。
下半年的时候,RocketMQ 社区负责 SDK 的同学(阿里 RocketMQ 团队的)通过我之前写的 Aliyun ONS SDK 找到了我,说他们正在建设多语言 SDK,都是基于 RocketMQ C SDK 来的。由于我们这边恰好有一个业务用到 RocketMQ,而且 Egg.js 针对 SOFA 开源也有可能需要一个 RocketMQ 的插件,于是我也就应了下来。
由于之前写过 ONS,所以这里在技术实现上还算是轻车熟路的,成本基本上在于社区的基础建设上。
大家有兴趣也可以一起来共建哇。
唉,去年的 Checklist 只完成了四项半。第一项由于也是出境游但不是欧美,所以只能算是完成了一半吧。感觉今年过得迷迷糊糊的很大一部分原因还是在于对于工作上的迷茫和焦虑,导致没有太多时间去想别的事情。感觉工作以来第一次有这种感觉吧,果然阿里系对于自身的提升还是非常有挑战性的。
Ask me anything: https://github.com/xadillax/ama
]]>最近有在做 RocketMQ 社区的 Node.js SDK,是基于 RocketMQ 的 C SDK 封装的 Addon,而 C 的 SDK 则是基于 C++ SDK 进行的封装。
然而,却出现了一个诡异的问题,就是当我在消费信息的时候,发现在 macOS 下得到的消息居然是乱码,也就是说 Linux 下居然是正常的。
首先我们要知道一个函数是
const char* GetMessageTopic(CMessageExt* msg)
,用于从一个msg
指针中获取它的 Topic 信息。
乱码的代码可以有好几个版本,是我在排查的时候做的各种改变:
// 往 JavaScript 的 `object` 对象中插入键名为 `topic` 的值为 `GetMessageTopic` |
并且很诡异的是,当我在调试第三种写法的时候,我发现在 const char* orig = GetMessageTopic(msg);
这一部的时候 orig
的值是正确的。而一步步单步运行下去,一直到 memcpy
执行结束的时候,orig
内存块里面的字符串居然被莫名其妙修改成乱码了。
参考如下:
这就不能忍了。
当我锲而不舍的时候,发现当我改成这样之后,返回的值就对了:
string GetMessageColumn(CMessageExt* msg, char* name) |
但问题在于,在“其它操作”中,orig
还是会变成一堆乱码。当前返回能正确的原因是因为我在它变成乱码之前,用可以“不触发”变成乱码的操作先把 orig
的字符串给赋值到另一个字符数组中,最后返回那个新的数组。
问题看似解决了,但是这种诡异、危险的行为始终是我心中的一颗丧门钉,不处理总之是慌的。
在排查的过程中,我去看了 RocketMQ 的 C++ 和 C SDK 的实现,我把重要的内容摘出来:
class MQMessage { |
我们阅读一下这段代码,在 GetMessageTopic
中,先得到了一个 getTopic
的 STL 字符串,然后调用它的 c_str()
返回 const char*
。一切看起来是那么美好,没有问题。
但我后来在多次调试的时候发现,对于同一个 msg
进行调用 GetMessageTopic
得到的指针居然不一样!我是不是发现了什么新大陆?
诚然,msg->getTopic()
返回了一个字符串对象,并且是通过拷贝构造从 m_topic
那边来的。依稀记得大学时候看的 STL 源码解析,根据 STL 字符串的 Copy-On-Write 来说,我没做任何改变的情况下,它们不应该是同源的吗?
事实证明,我当时的这个“想当然”就差点让我查不出问题来了。
在我捉鸡了好久之后一直毫无头绪之后,在参考资料 1 中获得了灵感,我开始打开脑洞(请原谅我这个坑还找了很久,毕竟我主手武器还是 Node.js),会不会现在的 String 都不是 Copy-On-Write 了?但是 Linux 下又是正常的哇。
后来我在网上找是不是有人跟我遇到一样的问题,最后还是找到了端倪。
不同的 stl 标准库实现不同, 比如 CentOS 6.5 默认的 stl::string 实现就是 『Copy-On-Write』, 而 macOS(10.10.5)实现就是『Eager-Copy』。
说得白话一点就是,不同库实现不一样。Linux 用的是 libstdc++,而 macOS 则是 libc++。而 libc++ 的 String 实现中,是不写时拷贝的,一开始赋值就采用深拷贝。也就是说就算是两个一样的字符串,在不同的两个 String 对象中也不会是同源。
其实深挖的话内容还有很多的,例如《Effective STL》中的第 15 条也有提及 String 实现有多样性;以及大多数的现代编译器中 String 也都有了 Short String Optimization 的特性;等等。
得到了上面的结论之后,这个 Bug 的原因就知道了。
((MQMessageExt *) msg)->getTopic()
得到了一个函数中的栈内存字符串变量。
c_str()
还是源字符串指向的指针,所以函数声明周期结束,这个栈内存中的字符串被释放,c_str()
指向的内存还坚挺着;c_str()
的生命周期是跟着字符串本身来的,一旦函数调用结束,该字符串就被释放了,相应地 c_str()
对应内存中的内容也被释放。综上所述,在 macOS 下,我通过 GetMessageTopic()
得到的内容其实是一个已经被释放内存的地址。虽然通过 for
可以趁它的内存块被复制之前赶紧抢救出来,但是这种操作一块已经被释放的内存行为总归是危险的,因为它的内存块随时可能被覆盖,这也就是之前乱码的本质了。
对于 STL 在这两个平台上不同的行为,我也抽出了一个最小化的 Demo,各位看官可以在自己的电脑上试试看:
|
上面的代码在 Linux 下(如 Ubuntu 14.04)运行会输出两个一样的指针地址,而在 macOS 下执行则输出的是两个不一样的指针。
在语言、库的使用中,我们不能去使用一个没有明确在文档中定义的行为的“特性”。例如文档中没跟你说它用的是 Copy-On-Write 技术,也就说明它可能在未来任何时候不通知你就去改掉,而你也不容易去发现它。你就去用已经定义好的行为即可,就是说 c_str()
返回的是字符串的一个真实内容,我们就要认为它是跟随着 String 的生命周期,哪怕它其中有黑科技。
毕竟,下面这个才是 C++ reference 中提到的定义,我们不能臆想人家一定是 COW 行为:
Returns a pointer to a null-terminated character array with data equivalent to those stored in the string.
The pointer is such that the range
[c_str(); c_str() + size()]
is valid and the values in it correspond to the values stored in the string with an additional null character after the last position.
这一样可以引申到 JavaScript 上来,例如较早的 ECMAScript 262 第三版对于一个对象的定义中,键名在对象中的顺序也是未定义的,当时就不能讨巧地看哪个浏览器是怎么样一个顺序来进行输出,毕竟对于未定义的行为,浏览器随时改了你也不能声讨它什么。
好久没写文了,码字能力变弱了。
以上。
千呼万唤始出来,犹抱琵琶生哪吒。
真的不好意思自卖自夸,所以索性直接把编辑推荐语、大佬们的评语贴上来好了。
Node.js 作为近几年新兴的一种编程运行时,托 V8 引擎的福,在作为后端服务时有比较高的运行效率,在很多场景下对于我们的日常开发足够用了。不过,它还为开发者开了一个使用C++ 开发 Node.js 原生扩展的口子,让开发者进行项目开发时有了更多的选择。
《Node.js:来一打 C++ 扩展》以 Chrome V8 的知识作为基础,配合 GYP 的一些内容,将教会大家如何使用 Node.js提供的一些 API 来编写其 C++ 的原生扩展。此外,在后续的进阶章节中,还会介绍原生抽象NAN 以及与异步相关的 libuv 知识,最后辅以几个实例来加深理解。不过,在学习本书内容之前,希望读者已经具备了初步的 Node.js 以及 C++ 基础。
阅读《Node.js:来一打 C++ 扩展》,相当于同时学习Chrome V8 开发、libuv 开发以及 Node.js 的原生 C++ 扩展开发知识,非常值得!
最后十分感谢包括 Node.js TSC 之一的 Anna、几位 Collaborator 以及各位业界的大佬帮忙写推荐语,感谢 @yorkie 大佬和 @justjavac 大佬帮忙作序。
《Node.js:来一打 C++ 扩展》在深度上远远超过了目前市面上的Node书籍。全书自始至终围绕一个主题展开:从介绍 Node.js 的包和模块规范开始,深入解析(包括但不限于剖析 Node.js 自身的源码) Node.js 的模块是如何在运行时被引入的,尤其是如何引入 C++ 模块的;接下来详细讲解了在什么时候、为何要编写 C++ 模块;借此契机,深入介绍了 Node.js 的基石 Chrome V8 和 libuv,以及异步非阻塞的原理——不仅如此,本书更教你如何在底层去驾驭它们。所以,本书以 Node.js 的 C++ 扩展为中心,衍生出对 Node.js 底层风光的层层剖析,最后再回归到如何编写 Node.js 的 C++ 扩展,一气呵成。读来酣畅淋漓,痛快不已!
买这一本书相当于买了“Node.js 的底层风光、C++ 扩展编写”、“Chrome V8”和“libuv”三本书!
读完本书后,你甚至能为 Node.js 自身的添砖加瓦做出非凡贡献。
目前在预售阶段,顺便蹭 618 活动。
1 Node.js 的 C++ 扩展前驱知识储备 1 |
This book contains absolutely everything you need to know about how all the pieces of Node.js’ C++ code work and interact, explaining the necessary concepts without needing prior knowledge about the internals of V8, libuv or other pieces of Node.js. It shows well how Node.js’ own built-in modules are constructed using the APIs provided by V8, so that they are usable from JavaScript, and how you can create the same kind of modules from scratch.
After having read this book, you will be able to write a production-quality, future-proof C++ extension for Node.js if you need to do that, or maybe even make changes Node.js itself if you’re interested in that!
这本书包含了所有你需要了解的有关于 Node.js C++ 代码是如何运行和交互的知识,解释了一些你不需要知道 V8 的内部机制就能理解的必要概念,另外该书还介绍了 libuv 以及其他一些内容的方方面面。这本书还展示了 Node.js 的内置模块是如何使用 V8 的 API 进行构建并在 JavaScript 层面能提供使用的——并且你也能用这种方法从头开始创建相同类型的模块。
读完这本书,你将学到如何写出产品级质量的、面向未来的 Node.js C++ 扩展。感兴趣的话,你甚至可以对 Node.js 自身进行修改!
——安娜·亨宁森(Anna Henningsen, addaleax),Node.js 技术指导委员会成员(Node.js TSC)
Node.js 不是第一个将 JavaScript 带入服务器端领域的技术,然而却成为了史上最热门、最有影响力的工具之一。究其原因,其一,在于 Node.js 适逢后端高并发潮流,巧妙结合 Reactor 模型和 JavaScript 所擅长的回调风格,大大降低了开发高并发服务器应用的成本;其二,在于恰逢浏览器大战,前端技术突飞猛进,急需一个适合 JavaScript 和前端工程师的一套生态和工具链,Node.js 刚好成为前端 JavaScript 最易上手掌握的命令行环境。在 Node.js 发展这么火热之后,Node.js 的开发体验在不断提升,上手门槛也在不断降低。
然而,如果大家真正想突破自己成为个中高手,无论是后端程序员希望在服务器端及架构方面有所建树,还是前端程序员想跨越边界,你们都应该去了解 Node.js 的底层机制,去学习写一些 Node.js 的扩展。从 Node.js 的内在机制,我们可以学习到更多有关计算机体系的知识如内存管理、多线程编程等等,真正向一个架构师、大牛迈进。
死月的书,给我们在这些方面带来了一个非常系统的指南。死月通过精彩的内容告诉大家:底层的知识并不枯燥,用 C++ 写一个扩展很有意思也很简单。作为 Node.js 工程师/爱好者的你,值得拥有本书。
——曹力(ShiningRay),酷链科技 CEO,前暴走漫画 CTO,前糗事百科联合创始人,高级 Node.js 技术专家,《JavaScript 高级程序设计》译者
Native module is one of the most underappreciated features of Node.js. But even in the age of asm.js and WebAssembly, it is an irreplaceable part of the Node.js ecosystem due to its versatility and performance. XadillaX’s book provides a refreshing introduction (or reintroduction), and is a must-read for all low-level Node.js engineers.
原生模块是 Node.js 中最被低估的功能之一。因为它的性能和多样性,使其即使是在 asm.js 和 WebAssembly 时代,仍旧能作为 Node.js 生态系统中不可替代的部分存在。死月的书对其作了一个令人耳目一新的介绍,是所有的底层(Low-Level)Node.js 工程师必读之物。
——顾天骋(Timothy Gu),pug、ejs 前 Maintainer,Node.js Core Collaborator 之一
本书全面讲解了 V8、libuv 的原理并且手把手教你编写一打 Node.js 的 C++ 扩展,是目前市面上相关领域非常空缺的技术书籍。如果想更深入了解 Node.js 的实现原理,除了熟读内置 API 文档之外,阅读这本书会是一个很好的选择。
——雷宗民(老雷),《Node.js 实战》作者之一
这是一本角度刁钻的 Node.js 相关书籍,与市面上大多数的 Node.js 书籍定位不同。它借为 Node.js 开发 C++ 扩展为基石,顺带介绍了 Chrome V8 和 libuv 的内容,填补了市场上这一类书籍的空白,值得一读。
——李启雷博士,趣链科技 CTO
死月一直把实战贯穿在整本书之内,无论是基础部分的 V8 练习,还是使用 Node.js 经典的 Addon 开发、用 NAN 来改写,或是 libuv 里的 WatchDog 案例、EFSW 的封装,甚至在第八章里还特意剖析了两个 C++ 模块,把之前讲解的基础知识部分综合起来,可以边学边练。
这本《Node.js:来一打 C++ 扩展》,在如今追求大而全的时代,单纯的讲 Node.js 的某一个方面,而且讲的特别棒的书,真的难得。
——刘琥(响马),西祠胡同创始人,fibjs 作者
当你掌握了 Node.js 的上层使用,下一步进阶的方向就是研究 Node.js 的底层原理。本书为学习 Node.js 的实现机制打开了一扇门。书中介绍的上下文(Context)、句柄(Handle)、句柄作用域(Handle Scope)等概念直接来自于源码,对于阅读 Node.js 及 V8 的源码具有极高的参考价值。
——潘旻琦(pmq20),Node.js 技术专家,Node.js Collaborator 之一,RubyConf 讲师之一
国内 Node.js 偏向于原理的书目前只有朴灵的《深入浅出 Node.js》一本,至今 4 年过去了,Node.js 已经从 v0.10 发展到 v9 版本,中间再没有这样的系统的有深度的书籍。
很高兴死月的新书弥补了这一遗憾。本书以 C++ 为主线,涵盖 Node.js 最核心的 libuv 和 V8,对理解 Node.js 原理有极大的好处。当然最大的好处在于使用 C++ 编写 Node.js Addon 可以让 Node.js 有更广阔的应用空间。我们都知道 Node.js 擅长的是 I/O 密集型任务,对于 CPU 密集型运算这是极好的弥补。
特别推荐大家阅读此书,Node.js 应用极其广泛的今天,使用 C++ 编写 Node.js Addon 是更出彩的部分,你值得拥有。
——桑世龙(狼叔),StuQ 明星讲师,Node.js 技术布道者,《更了不起的 Node.js》作者
死月对 Node.js 底层机制有非常深入的了解。阅读本书,除了学习 C++ 扩展开发,还会跟随死月了解 V8、libuv,相信读后大家对于 Node.js 的理解会更上一层楼。
——孙信宇(芋头),大搜车无线架构团队负责人,前端乱炖站长
C++ 扩展其实是从外在,用 C++ 的角度去观察 Node.js 内在的形式。因为 Node.js 整个系统自身几乎就是构建在 C/C++ 之上的,只是内部称之为 built-in,在 user-land 则称之 Addon,它们本质上其实没有区别。死月凭借他在 C/C++ 的深厚积累,选择从 C++ 扩展作为突破口,带大家领略 Node.js 底层的风光,在书里,你能看到真正发挥巨大价值的 V8、libuv 亦是精彩纷呈。
死月将 C++ 扩展写得这么透,我是服的。
——田永强(朴灵),高级 Node.js 技术专家,《深入浅出 Node.js》作者
开发 C++ 扩展,可以扩充 Node.js 平台的本地 API,扩充 Node.js 应用的能力。这本书详细介绍了包括 libuv、V8 在内的各种必要知识,是该领域不可多得的好书。对 C++ 开发者来说,本书既可以作为入门指引,又可以作为日常开发的参考书。
——王文睿博士(Roger Wang),node-webkit 和 NW.js 项目创始人和维护者,因特尔软件架构师
]]>清晰记得手写的第一个 Node.js C++ 扩展模块,在 Node.js 0.6.9 跑通的那种愉悦感。随着应用升级到 Node.js 0.8,依赖的 C++ 扩展模块无法安装编译成功,最后发现是 V8 的 API 变化导致不兼容,从此对 C++ 扩展模块产生抗拒。后来看到《Node.js:来一打 C++ 扩展》,从实现原理,到 V8 基础概念的一系列介绍,让我重新对 C++ 扩展模块产生兴趣。参考书里的实战例子,以及 NAN 的辅助下,现在编写一个跨 Node.js 版本的 C++ 扩展已经不是什么困难的事情。通过最后一章节,可以了解到 Node.js 官方的 N-API 计划,让 C++ 扩展不仅仅能跨版本复用,还能跨操作系统(平台)复用。
——袁锋(fengmk2),Node.js 技术专家
如下图所示,在 10x10 的区域中,随机生成面积为 6 的单连通区域,该「随机」包括「位置随机」以及「形状随机」。
注意:
其中点 2 可以分采用和不采用周期性结构分别讨论。
这个问题,我不知道原题提问者想要做什么事。但是就这题本身而言,我们可以拿它去生成一个随机地图,例如:
建造、等待的沙盒类手游,游戏中有一个空岛,玩家能在上面建造自己的建筑然后等待各种事件完成。空岛形状随机生成,并且都联通且面积一定,这样每个玩家进去的时候就能得到不同地形。
在得知了问题原题之后,我们就可以照着题目的意思开始解决了。
其实这么一个问题一出现,脑子里面就瞬间涌出几个词汇:DFS、Flood fill、并查集等等。
那么其实这最粗暴的办法相当于你假想有一个连通区域,然后你去 Flood fill 它——至于墙在哪,在递归的每一个节点的时候随机一下搜索方向的顺序就可以了。
我们先实现一个类的框架吧(我是 Node.js 开发者,自然用 JavaScript 进行 Demo 的输出)。
const INIT = Symbol("init"); |
有了架子之后,我们就可以实现递归函数 fill
了,整理一下流程如下:
fill(x, y)
进入递归搜索:this[INIT]()
;this.count++
,表示填充区域面积加了 1
,并在数组中将该位置填充为 x
;this.count
是否等于所需要的面积:x
、y
轴的值按当前方向走一个算出新的坐标值 newX
和 newY
;fill(newX, newY)
得到结果,若有结果则返回;在这里「状态还原」表示把
this.count--
还原回当前坐标填充前的状态,并且把当前填充的'x'
给还原回'.'
。
照着上面的流程很快就能得出代码结论:
const _ = require("lodash"); |
这么一来,类就写好了。接下去我们只要实现一些交互的代码,就可以看效果了。
点我进入 JSFiddle 看效果。
如果懒得进入 JSFiddle 看,也可以看看下面的几个截图:
10x10 填 50 效果图
10x10 填 6 效果图
50x50 填 50 效果图
其实原题说了一个条件,那就是采用周期性结构,超出右边移到最左边,以此类推。
而我们前文的代码其实是照着非周期性结构来实现的。不过如果我们要将其改成周期性实现也很简单,只需要把前文代码中边界判断的那一句代码改为周期性计算的代码即可,也就是说要把这段代码:
// 判断边界 |
改为:
// 周期性计算 |
这个时候出来的效果就是这样的了:
10x10 填 50 周期性效果图
至此为止 DFS 的代码基本上完成了。不过目前来说,当然这个算法的一个缺陷就是,当需要面积与总面积比例比较大的时候,有可能陷入搜索的死循环(或者说效率特别低),因为要不断复盘。
所以我们可以做点改造——由于我们不是真的为了搜索到某个状态,而只是为了填充我们的小点点,那么 DFS 中比较经典的「状态还原」就不需要了,也就是说:
this.count--; |
这两行代码可以删掉了,用删掉上面两行代码的代码跑一下,我用 50x50 填充 800 格子的效果:
继续之前的 50x50 填充 50:
上面 DFS 的方法,由于每次都要走完一条路,虽然会转弯导致黏连,但在填充区域很小的情况下,很容易生成“瘦瘦的区域”。
这里再给出另一个方法,一个 for
搞定的,思路如下:
给出代码 Demo:
function random(max) { |
注意:上面的代码是我一溜烟写出来的,所以并没有后续优化代码简洁度,其实很多地方的代码可以抽象并复用的,懒得改了,能看就好了。用的时候就跟之前 DFS 代码一样
new
一个Filler2
出来并fill
就好了。效果依然可以去 JSFiddle 看。
或者也可以直接看效果图:
显而易见,跟之前 DFS 生成出来的奇形怪状相比,这种算法生成的连通区域更像是一块 Mainland,而前者则更像是一个洼地沼泽或者丛林。
前面两种算法,一个是生成瘦瘦的稀奇古怪的面积,一个是生成胖胖的区域。有没有办法说在生成胖胖的区域的情况下允许一定的稀奇古怪的形状呢?
其实将两种算法结合一下就好了。结合的做法有很多,这里举一个例子,大家可以自己再去想一些出来。
for
生成胖胖的区域;注意:为了保证每次 DFS 一开始的时候都能取到最新的边界坐标,在 DFS 流程中的时候每标一个区域填充也必须走一遍边界坐标更新的逻辑。
具体代码就不放文章里面解析了,大家也可以到 JSFiddle 中去观看。
或者也可以直接看效果图:
结合了两种算法,我们得到了一个我认为可能会更好看一点的区域。
此外,我们还能继续「丧心病狂」一点,例如两种方式交替出现,流程如下:
for
,偶数次用 DFS;for
则随机一个 Math.min(剩余面积, 总面积 / 4)
的数字;Math.min(剩余面积, 总面积 / 10)
的数字;依旧是给出 JSFiddle 的预览。
或者也可以直接看效果图:
注意:这里只给出思路,具体配比和详细流程大家可以继续优化。
最后,这里给出几张 10x10 填 50 的效果图放一起对比一下。
以及,几张 50x50 填充 800 面积的效果图对比。
之所以多出一节来,是因为我在写回答以及这篇文章的时候脑抽了一下,迷迷糊糊想成了连通区域,感谢评论区童鞋的提醒。实际上单连通区域要稍微复杂一些。
在拓扑学中,单连通是拓扑空间的一种性质。直观地说,单连通空间中所有闭曲线都能连续地搜索至一点。此性质可以由空间的基本群刻画。
这个空间不是单连通的,它有三个洞
对于非周期性的区域来说,生成一个单连通区域只要在上面的方法里面加点料就可以了。即在一个位置填充的时候,判断一下将它填充进去之后是否会出现所谓的「洞」。而这一点在非周期性区域中,由于在填充当前坐标前,已存在的区域已经是一个单连通区域,所以枚举一下几种情况即可排除非单连通区域的情况:
而对于周期性的区域来说,暂时我还没想到很好的办法。
对于情况一而言,如果处于对面的两接壤坐标都有填充,且再多一个接壤面的话,原小区域内只有可能是「匚」型,那么填充进去只会形成一个 2x3 的实心区域,而如果只有处于对面的两个接壤坐标有填充的话,说明原小区域有两个面对面隔空的区域,它们形成单连通区域的大前提就是从其它地方绕出去将它们连起来,若这个时候将它们闭合的话,势必会形成一个空洞,如下图所示:
对于情况二而言,如果只有一面有填充,但是对面的顶点有共享的话,可以类比为情况一,举例如下:
对于情况三而言,其实就是情况二加一条边有填充,如果在情况二的情况下,在上图“原”的区域下方的空若已有填充,那么在“新”的位置填充进去,就形不成空洞了。毕竟如果“空”的位置已有填充的话,若先前状态生成没有洞的连通区域,则“空”下方也必定不是一个空洞的区域。
在解析完三种情况后,算法就明朗起来——在上面的 DFS 算法每次执行填充操作的时候,都判断一下当前填充是否符合刚才列举的三种情况,若符合,则不填充该点。
所以只需对 DFS 的那个代码做一下修改就好了,首先把状态还原两行代码删掉,然后在之前
if (newX < 0 || newX >= this.length || newY < 0 || newY >= this.length) continue; |
这句代码之下加一个条件判断就好了:
if(this.willBreak(newX, newY)) { |
剩下的就是去实现 this.willBreak()
函数:
class Filler { |
进 JSFiddle 看完整代码。
然后是 50x50 填充 800 的效果:
以及 10x10 填充 50:
注意:左下角的洞看起来是洞,实际上是处于边界了,而填充区域无法与边界合成闭合区域,实际上将地图往外想想空一格就可以知道它并不是一个洞了。当然如果读者执意不允许这种情况发生,那么只需要在
willBreak()
函数判断的时候做点手脚就可以了,至于怎么做手脚大家自己想吧。
这种情况生成的地图比较像迷宫,哪怕是针对「胖胖的区域」做这个改进,JSFiddle 出来的也是下面的效果:
所以呢,继续优化——我们知道有三种情况是会生成非单连通区域的,所以当我们探测到这种情况的时候,去 BFS 它内外区域,看看究竟是哪个区域被封闭出一个空洞来,探测出来之后再看看我们目前还需要填充的区域面积跟这个空洞的面积是否够用,若够用则将空洞补起来,不够用则当前一步重新来过——即再随机一个坐标看看行不行。
思想说出来了,具体的实现还是看看我写在 JSFiddle 里面的代码吧。
50x50 填充 800 的效果如下:
这么一来,我们很容易能跟 DFS 的算法结合起来,即之前说过的更丧病的算法。结合方法很简单,分别把改进过的 DFS 和胖胖区域的算法一起融合进之前丧病算法的代码中就好了。老样子我还是把代码更新到了 JSFiddle 里面。大家看看 50x50 填充 800 的效果吧:
最后,由于一开始文章写的概念性错误给大家带来的不变表示非常抱歉,好在最后我还是补全了一下文章。
本文主要还是讲了,如何随机生成一个指定面积的单连通区域。从一开始拍脑袋就能想到 DFS 开始,延伸到胖胖的区域,然后从个人认为「图不好看」开始,想办法如何结合一下两种算法使其变得更自然。
针对同一件事的算法们并非一成不变或者不可结合的。不是说该 DFS 就只能 DFS,该 for
就只能 for
,稍微结合一下也许食用效果更佳哦。
哦对了,在这之前还有一个例子就是我在三年多前写的主题色提取的文章《图片主题色提取算法小结》,其中就讲到我最后的方法就是结合了八叉树算法和最小差值法,使其在提取比较贴近的颜色同时又能够规范化提取出来的颜色。
总之就是多想想,与诸君共勉。
]]>今年的足迹并没有去年多,大多都是杭州周边随便游玩,没有什么特别的地方。值得一提的是以度假的形式终于出境游了一次,以后也能说是去过美帝的人了。其次就是参加了两场演唱会,其中终于是还了欠了十五六年的两张周董的演唱会门票。
周末么大多都是在商圈逛,感觉去最多的就是西溪印象城了,同以往一样,依旧抓了好多娃娃。平均每次去不抓个十几只就不收手的感觉。最近西溪银泰城也开了,也去抓了一次,感觉手感依旧。
这次的沪 JSConf 是一个契机,促成了我为了去 Code + Learn Workshop 而刷 PR,从而成为了 Node.js Collaborator 这件事。
还有一件感觉特对不起老东家的一件事,就是公费参加 QCon 没多久,我就离职了,至今觉得愧疚。
买车是去年年终总结时候对于今年的一个 TODO 项,结果还真做了;农药主要还是在跟前同事在玩,当时疯狂到基本上中午都要结队出去吃饭,然后在饭桌上都要开个一两局;而练字基本上就是吃完饭后的午休时间随便写几下的——没有午睡习惯的我;Pentax 是一个偶然机会发现分期乐上面可以薅,于是有了二话不说下单的一幕,最终在快递被召回之前开车赶往快递站勇夺快递——真市井;双十一的时候纠结了好久是要买吉他还是买电钢,最终媳妇还是选择了电钢,然后就偶尔下班回家学一会儿——由于没有老师只能自己野路子乱学;
今年的工作比较坎坷。
年初的时候大搜车的研发发生了比较大的一个组织架构调整。空降而来的怀叔重整了研发部门,车牛业务交出,Node.js 团队开始做 BFF 的基础建设。
很高兴怀叔和头哥信任我,将 Node.js 团队交给我来带,如果没在将近年底的时候离职,我现在也是一个 9 人团队的负责人了。
在组织架构调整后,团队开始了一轮新的定位摸索,我们先后推出、负责、整理了一些项目,如大搜车的网关,这是一个基于 OpenResty + Node.js 进行开发的动态网关系统,被我命名为帕秋丽(Patchouli)。
现在已经在公司几个比较大流量的项目中应用起来了,也算是我留给公司的一份礼物吧——非常感谢小伙伴们一起努力把它最终给造了出来。Patchouli 的项目介绍和简单的技术分享我曾在沪 JSConf 的 After Party 中分享过,可以参考一下我在知乎上的回答「参加 JSConf CN 2017 是个什么样的体验?」。
另一个在公司中比较重要的项目就是新版的开放平台,分为对内的开放平台(Izayoi-Coffee)和对外的开放平台(Izayoi-Darts)。该项目是在公司宣布将要平台化的时候立项进行的,主要作为 API Hub 对内对外进行接洽,例如与各资金方、银行进行对接等。
感觉在进行这些开发的时候,也开始履行了去年总结时候的一句话——不再像以前一样只无脑关心技术本身,而是更多地去思考技术之外的事,对待旧的代码更宽容了。
剩下的就是继续迭代我以前负责和主导的一些项目,如短链接平台(Hata no Kokoro)、计算密集型任务集中处理系统(Youmu)和大搜车商学院(Yuuka)等。
本来坐拥十人左右的小团队,加上能主导整个团队的技术方向,十分开心。但是在年底的时候,还是决定再出去看一看——并不是因为大搜车不好,毕竟我在它最困难的 2015 年底 2016 年初都没有走,相反我认为它离上市已经不远了,我只不过是觉得年轻,有机会还是得尝试下。大家若是对大搜车有什么兴趣的话,也可以找我私聊。
最后在这里非常感谢大搜车的小伙伴们一年来的陪伴,有你们才有团队今天做到的成绩。
记得是八九月份的时候苏千来找的我,他挥动了他的小锄头跟我说有 HC 了,可以去试试看。我抱着去试试看的心理——反正面了也不亏,经过了几个月的心理挣扎,于今年 11 月 13 日正式入职蚂蚁,告别了伴我成长两年的大搜车。
进来之后其实感觉还是比较迷的,前驱体系太庞大了,现在还处于摸索阶段。总之我进来之后是做 Node.js 基础中间件和基础平台相关的一些事。
由于入职的时间不是很长,所以也没有太多的东西可以总结。总之在新的环境中遇到了很多大佬,包括团队中的小伙伴们也都是各种大佬——反而我是团队中最水的,这也是我从大搜车离职的原因之一,毕竟目前为止在那边我是我们团队的天花板了。
今年在成长方面也发生了挺多事。正如上面说的,曾经都已经开始带领九人小团队了。除了负责公司的几个项目架构之外,转折点还是在于沪 JSConf。
怎么说呢,就是一开始只是为了成为沪 JSConf 的 Code+Learn Workshop Mentor,开始刷各种 Node.js 的陈年老 Bug。最后无心插柳柳成荫,成为 Node.js Core Collaborator 之一,算是本年度最值得吹逼的事情了吧。
也正是这个事情,让我有动力去深入解读和剖析 Node.js 源码,以了解更底层相关的东西。
其余的,就是参加了几场圈子里面的会议,面了个基。还有一件觉得挺对不起五花肉的事的,那个时候我成为了她负责的阿里云 MVP,并且去分享了一场关于阿里云 ONS(现 MQ)Node.js SDK 的 Topic,但是最终由于我入职了蚂蚁金服,从而无法再继续以阿里云 MVP 的身份活跃在社区了,算是辜负了一番她当时做的工作。
与头哥一起举办了几场 Node Party,其中认识了好多杭州 Node.js 圈子里面的大佬,以及杭州周边的大佬们——包括贺老也来参加了几次。头哥还搞了一个 Node Open-Source Foundation,募集了有小几万吧,用于每次 Node Party 的开销,包括非杭州讲师的食宿等等。
在将近年底的时候,偶然一次机会看到了一场 OpenResty Con 2017。由于我今年的网关就是基于 OpenResty + Node.js 完成的,心血来潮召集了学长学弟(都是又拍云的中流砥柱)主办了一趴 Hangzhou OpenResty Meetup,还请了女装大佬闪总过来帮忙主持,本来《OpenResty & Node.js 开发网关》这个 Topic 是由我提供的,不过由于最终的开办日期是在我离职后,所以不方便继续提供,转而由我在大搜车的小伙伴 @duanpengfei 呈现,而我从台前走到了幕后。
在此之前,我也应闪总之邀,去参加了一次 Girls Coding Day,客串了一次教练。
Girls Coding Day 是由社会企业 Coding Girls Club 联合众多性别友好的公司和程序员为促进性别平等而举办的公益编程工作坊。
去年这种形式的知识分享非常火热,我也开了几场,分别在知乎和 GitChat。
这件事是 2016 年年终总结时候留下的一个新年愿望,居然真的达成了。
由于市面上 Node.js 相关的书已经够多了,而且这个 Runtime 本身也没有什么太多很深的东西,所以我最后找了一个比较刁钻的角度开始写。
这里要感谢头哥帮我牵头博文视点的编辑,让我有机会能与出版社接触。书大概从 2 月份开始写,为时半年,终于在 8 月份将书稿交予出版社。截止写总结的目前,出版社第一次排版结束,我跟编辑一起做一次审校。
哦,对了,书名是《Node.js:来一打 C++ 扩展》。
Node.js作为近几年新兴的一门编程运行时,托V8引擎的福,在作为后端服务时有比较高的运行效率,很多场景下在我们日常开发的时候足够用了。不过,它还为开发者开了个使用C++开发Node.js原生扩展的口子,让开发者有了更多的可能性来对其项目进行开发。
本书以Chrome V8的知识作为基础,配合上GYP的一些内容,将教会大家如何使用Node.js提供的一些API来编写其C++的原生扩展。此外,在后续的提高章节中,还会介绍原生抽象NAN,以及异步相关的libuv相关知识,最后辅以几个实例来进阶学习。不过,在学习本书内容之前,笔者推荐读者已经有了初步的Node.js以及C++基础。
总之,买了这一本书,相当于读者拥有了Chrome V8开发、libuv开发以及Node.js的原生C++扩展开发三本书,非常值当。
这里列出 2016 年总结中对 2017 年的一些希冀。
基本上都完成了。去了塞班、周末经常开车出去、车子也买了、书也写了,游戏也通了些许,薅了个相机,技术还要继续努力,正在赚钱的路上——换了个新的工作环境。
最后列出一些明年想做的事情。
Ask me anything: https://github.com/xadillax/ama
]]>首先声明,我在“Bug”字眼上加了引号,自然是为了说明它并非一个真 Bug。
昨天有个童鞋在看后台监控的时候,突然发现了一个错误:
[error] 000001#0: ... upstream prematurely closed connection while reading response header from upstream. |
大概意思就是说:一台服务器通过 HTTP 协议去请求另一台服务器的时候,单方面被对方服务器断开了连接——并且并没有任何返回。
其实这次请求的一些猫腻很容易就能发现——在 URL 中有空格。所以我们能简化出一条最简单的 CURL 指令:
$ curl "http://foo/bar baz" -v |
注意:不带任何转义。
好的,那么接下去开始写相应的最简单的 Node.js HTTP 服务端源码。
; |
大功告成,启动这段 Node.js 代码,开始试试看上面的指令吧。
如果你也正在跟着尝试这件事情的话,你就会发现 Node.js 的命令行没有输出任何信息,尤其是嘲讽的 '🌚'
,而在 CURL 的结果中,你将会看见:
$ curl 'http://127.0.0.1:5555/d d' -v |
瞧,Empty reply from server。
发现了问题之后,就有另一个问题值得思考了:就 Node.js 会出现这种情况呢,还是其它一些 HTTP 服务器也会有这种情况呢。
于是拿小白鼠 Nginx 做了个实验。我写了这么一个配置:
server { |
接着也执行一遍 CURL,得到了如下的结果:
$ curl 'http://127.0.0.1:5555/d d' -v |
于是乎,理所当然,我暂时将这个事件定性为 Node.js 的一个 Bug。
认定了它是个 Bug 之后,我就开始了一贯的看源码环节——由于这个 Bug 的复现条件比较明显,我暂时将其定性为“Node.js HTTP 服务端模块在接到请求后解析 HTTP 数据包的时候解析 URI 时出了问题”。
源码以 Node.js 8.9.2 为准。
这里先预留一下我们能马上想到的 node_http_parser.cc,而先讲这几个文件,是有原因的——这涉及到最后的一个应对方式。
首先看看 lib/http.js 的相应源码:
... |
那么,马上进入 lib/_http_server.js 看吧。
首先是创建一个 HttpParser 并绑上监听获取到 HTTP 数据包后解析结果的回调函数的代码:
const { |
从源码中文我们能看到,当一个 HTTP 请求过来的时候,监听函数 connectionListener()
会拿着 Socket 对象加上一个 data
事件监听——一旦有请求连接过来,就去执行 socketOnData()
函数。
而在 socketOnData()
函数中,做的主要事情就是 parser.execute(d)
来解析 HTTP 数据包,在解析完成后执行一下回调函数 onParserExecuteCommon()
。
至于这个 parser
,我们能看到它是从 lib/_http_common.js 中来的。
var parsers = new FreeList('parsers', 1000, function() { |
能看出来 parsers
是 HTTPParser
的一条 Free List(效果类似于最简易的动态内存池),每个 Parser 在初始化的时候绑定上了各种回调函数。具体的一些回调函数就不细讲了,有兴趣的童鞋可自行翻阅。
这么一来,链路就比较明晰了:
请求进来的时候,Server 对象会为该次请求的 Socket 分配一个 HttpParser
对象,并调用其 execute()
函数进行解析,在解析完成后调用 onParserExecuteCommon()
函数。
我们在 lib/_http_common.js 中能发现,HTTPParser
的实现存在于 src/node_http_parser.cc 中:
const binding = process.binding('http_parser'); |
至于为什么
const binding = process.binding('http_parser')
就是对应到 src/node_http_parser.cc 文件,以及这一小节中下面的一些 C++ 源码相关分析,不明白且有兴趣的童鞋可自行去阅读更深一层的源码,或者网上搜索答案,或者我提前无耻硬广一下我快要上市的书《Node.js:来一打 C++ 扩展》——里面也有说明,以及我的有一场知乎 Live《深入理解 Node.js 包与模块机制》。
总而言之,我们接下去要看的就是 src/node_http_parser.cc 了。
env->SetProtoMethod(t, "close", Parser::Close); |
如代码片段所示,前文中 parser.execute()
所对应的函数就是 Parser::Execute()
了。
class Parser : public AsyncWrap { |
首先进入 Parser
的静态 Execute()
函数,我们看到它把传进来的 Buffer
转化为 C++ 下的 char*
指针,并记录其数据长度,同时去执行当前调用的 parser
对象所对应的 Execute()
函数。
在这个 Execute()
函数中,有个最重要的代码,就是:
size_t nparsed = |
这段代码是调用真正解析 HTTP 数据包的函数,它是 Node.js 这个项目的一个自研依赖,叫 http-parser。它独立的项目地址在 https://github.com/nodejs/http-parser,我们本文中用的是 Node.js v8.9.2 中所依赖的源码,应该会有偏差。
如果你已经对 HTTP 包体了解了,可以略过这一节。
HTTP 的 Request 数据包其实是文本格式的,在 Raw 的状态下,大概是以这样的形式存在:
方法 URI HTTP/版本 |
简单起见,这里就写出最基础的一些内容,至于 Body 什么的大家自己找资料看吧。
上面的是什么意思呢?我们看看 CURL 的结果就知道了,实际上对应 curl ... -v
的中间输出:
GET /test HTTP/1.1 |
所以实际上大家平时在文章中、浏览器调试工具中看到的什么请求头啊什么的,都是以文本形式存在的,以换行符分割。
而——重点来了,导致我们本文所述“Bug”出现的请求,它的请求包如下:
GET /foo bar HTTP/1.1 |
重点在第一行:
GET /foo bar HTTP/1.1 |
话不多少,我们之间前往 http-parser 的 http_parser.c 看 http_parser_execute ()
函数中的状态机变化。
从源码中文我们能看到,http-parser 的流程是从头到尾以 O(n) 的时间复杂度对字符串逐字扫描,并且不后退也不往前跳。
那么扫描到每个字符的时候,都有属于当前的一个状态,如“正在扫描处理 uri”、“正在扫描处理 HTTP 协议并且处理到了 H”、“正在扫描处理 HTTP 协议并且处理到了 HT”、“正在扫描处理 HTTP 协议并且处理到了 HTT”、“正在扫描处理 HTTP 协议并且处理到了 HTTP”、……
憋笑,这是真的,我们看看代码就知道了:
case s_req_server: |
在扫描的时候,如果当前状态是 URI 相关的(如 s_req_path
、s_req_query_string
等),则执行一个子 switch
,里面的处理如下:
s_req_http_start
并认为 URI 已经解析好了,通过宏 CALLBACK_DATA()
触发 URI 解析好的事件;0.9
,并修改当前状态,最后认为 URI 已经解析好了,通过宏 CALLBACK_DATA()
触发 URI 解析好的事件;parse_url_char()
函数来解析一些东西并更新当前状态。(因为哪怕是在解析 URI 状态中,也还有各种不同的细分,如 s_req_path
、s_req_query_string
)这里的重点还是当状态为解析 URI 的时候遇到了空格的处理,上面也解释过了,一旦遇到这种情况,则会认为 URI 已经解析好了,并且将状态修改为 s_req_http_start
。也就是说,有“Bug”的那个数据包
GET /foo bar HTTP/1.1
在解析到 foo
后面的空格的时候它就将状态改为 s_req_http_start
并且认为 URI 已经解析结束了。
好的,接下来我们看看 s_req_http_start
怎么处理:
case s_req_http_start: |
如代码所见,若当前状态为 s_req_http_start
,则先判断当前字符是不是合标。因为就 HTTP 请求包体的格式来看,如果 URI 解析结束的话,理应出现类似 HTTP/1.1
的这么一个版本申明。所以这个时候 http-parser 会直接判断当前字符是否为 H
。
H
,则将状态改为 s_req_http_H
并继续扫描循环的下一位,同理在 s_req_http_H
下若合法状态就会变成 s_req_http_HT
,以此类推;H
,那么好了,http-parser 直接认为你的请求包不合法,将你本次的解析设置错误 HPE_INVALID_CONSTANT
并 goto
到 error
代码块。至此,我们基本上已经明白了原因了:
http-parser 认为在 HTTP 请求包体中,第一行的 URI 解析阶段一旦出现了空格,就会认为 URI 解析完成,继而解析 HTTP 协议版本。但若此时紧跟着的不是 HTTP 协议版本的标准格式,http-parser 就会认为你这是一个 HPE_INVALID_CONSTANT
的数据包。
不过,我们还是继续看看它的 error
代码块吧:
error: |
这段代码中首先判断一下当跳到这段代码的时候有没有设置错误,若没有设置错误则将错误设置为未知错误(HPE_UNKNOWN
),然后返回已解析的数据包长度。
p
是当前解析字符指针,data
是这个数据包的起始指针,所以p - data
就是已解析的数据长度。如果成功解析完,这个数据包理论上是等于这个数据包的完整长度,若不等则理论上说明肯定是中途出错提前返回。
看完了 http-parser 的原理后,很多地方茅塞顿开。现在我们回到它的调用地 node_http_parser.cc 继续阅读吧。
Local<Value> Execute(char* data, size_t len) { |
从调用处我们能看见,在执行完 http_parser_execute()
后有一个判断,若当前请求不是 upgrade
请求(即请求头中有说明 Upgrade
,通常用于 WebSocket),并且解析长度不等于原数据包长度(前文说了这种情况属于出错了)的话,那么进入中间的错误代码块。
在错误代码块中,先 HTTP_PARSER_ERRNO(&parser_)
拿到错误码,然后通过 Exception::Error()
生成错误对象,将错误信息塞进错误对象中,最后返回错误对象。
如果没错,则返回解析长度(nparsed_obj
即 nparsed
)。
在这个文件中,眼尖的童鞋可能发现了,执行
Execute()
有好多处,这是因为实际上一个 HTTP 请求可能是流式的,所以有时候可能会只拿到部分数据包。所以最后有一个结束符需要被确认。这也是为什么 http-parser 在解析的时候只能逐字解析而不能跳跃或者后退了。
我们把 Parser::Execute()
也就是 JavaScript 代码中的 parser.execute()
给搞清楚后,我们就能回到 _http_server.js 看代码了。
前文说了,socketOnData
在解析完数据包后会执行 onParserExecuteCommon
函数,现在就来看看这个 onParserExecuteCommon()
函数。
function onParserExecuteCommon(server, socket, parser, state, ret, d) { |
长长的一个函数被我精简成这么几句话,重点很明显。ret
就是从 socketOnData
传进来已解析的数据长度,但是在 C++ 代码中我们也看到了它还有可能是一个错误对象。所以在这个函数中一开始就做了一个判断,判断解析的结果是不是一个错误对象,如果是错误对象则调用 socketOnError()
。
function socketOnError(e) { |
我们看到,如果真的不小心走到这一步的话,HTTP Server 对象会触发一个 clientError
事件。
整个事情串联起来了:
GET /foo bar HTTP/1.1
会被解析出错并返回一个错误对象;if (ret instanceof Error)
条件分支并调用 socketOnError()
函数;socketOnError()
函数中会对服务器触发一个 clientError
事件;(this.server.emit('clientError', e, this)
)function(req, resp)
中去,所以不会有任何的数据被返回就结束了,也就解答了一开始的问题——收不到任何数据就请求结束。这就是我要逐级进来看代码,而不是直达 http-parser 的原因了——clientError
是一个关键。
要解决这个“Bug”其实不难,直接监听 clientError
事件并做一些处理即可。
; |
注意:由于运行到
clientError
事件时,并没有任何 Request 和 Response 的封装,你能拿到的是一个 Node.js 中原始的 Socket 对象,所以当你要返回数据的时候需要自己按照 HTTP 返回数据包的格式来输出。
这个时候再挥起你的小手试一下 CURL 吧:
$ curl 'http://127.0.0.1:5555/d d' -v |
如愿以偿地输出了 400 状态码。
接下来我们要引申讨论的一个点是,为什么这货不是一个真正意义上的 Bug。
首先我们看看 Nginx 这么实现这个黑科技的吧。
打开 Nginx 源码的相应位置。
我们能看到它的状态机对于 URI 和 HTTP 协议声明中间多了一个中间状态,叫 sw_check_uri_http_09
,专门处理 URI 后面的空格。
在各种 URI 解析状态中,基本上都能找到这么一句话,表示若当前状态正则解析 URI 的各种状态并且遇到空格的话,则将状态改为 sw_check_uri_http_09
。
case sw_check_uri: |
然后在 sw_check_uri_http_09
状态时会做一些检查:
case sw_check_uri_http_09: |
例如:
H
才修改状态为 sw_http_H
认为接下去开始 HTTP 版本扫描;sw_check_uri
,然后倒退回一格以 sw_check_uri
继续扫描当前的空格。在理解了这个“黑科技”后,我们很快能找到一个很好玩的点,开启你的 Nginx 并用 CURL 请求以下面的例子一下它看看吧:
$ curl 'http://xcoder.in:5555/d H' -v |
怎么样?是不是发现结果跟之前的不一样了——它居然也返回了 400 Bad Request。
原因为何就交给童鞋们自己考虑吧。
那么,为什么即使在 Nginx 支持空格 URI 的情况下,我还说 Node.js 这个不算 Bug,并且指明 Nginx 是“黑科技”呢?
后来我去看了 HTTP 协议 RFC。
原因在于 Network Working Group 的 RFC 2616,关于 HTTP 协议的规范。
在 RFC 2616 的 3.2.1 节中做了一些说明,它说了在 HTTP 协议中关于 URI 的文法和语义参照了 RFC 2396。
URIs in HTTP can be represented in absolute form or relative to some known base URI, depending upon the context of their use. The two forms are differentiated by the fact that absolute URIs always begin with a scheme name followed by a colon. For definitive information on URL syntax and semantics, see “Uniform Resource Identifiers (URI): Generic Syntax and Semantics,” RFC 2396 (which replaces RFCs 1738 and RFC 1808). This specification adopts the definitions of “URI-reference”, “absoluteURI”, “relativeURI”, “port”, “host”,”abs_path”, “rel_path”, and “authority” from that specification.
而在 RFC 2396 中,我们同样找到了它的 2.4.3 节。里面对于 Disallow 的 US-ASCII 字符做了解释,其中有:
控制符,指 ASCII 码在 0x00-0x1F 范围内以及 0x7F;
控制符通常不可见;
空格,指 0x20;
空格不可控,如经由一些排版软件转录后可能会有变化,而到了 HTTP 协议这层时,反正空格不推荐使用了,所以就索性用空格作为首行分隔符了;
分隔符,"<"
、">"
、"#"
、"%"
、"\""
。
如 #
将用于浏览器地址栏的 Hash;而 %
则会与 URI 转义一同使用,所以不应单独出现在 URI 中。
于是乎,HTTP 请求中,包体的 URI 似乎本就不应该出现空格,而 Nginx 是一个黑魔法的姿势。
嚯,写得累死了。本次的一个探索基于了一个有空格非正常的 URI 通过 CURL 或者其它一些客户端请求时,Node.js 出现的 Bug 状态。
实际上发现这个 Bug 的时候,客户端请求似乎是因为那边的开发者手抖,不小心将不应该拼接进来的内容给拼接到了 URL 中,类似于
$ rm -rf /
。
一开始我以为这是 Node.js 的 Bug,在探寻之后发现是因为我们自己没用 Node.js HTTP Server 提供的 clientError
事件做正确的处理。而 Nginx 的正常请求则是它的黑科技。这些答案都能从 RFC 中寻找——再次体现了遇到问题看源码看规范的重要性。
另,我本打算给 http-parser 也加上黑魔法,后来我快写好的时候发现它是流式的,很多状态没法在现有的体系中保留下来,最后放弃了,反正这也不算 Bug。不过在以后有时间的时候,感觉还是可以好好整理一下代码,好好修改一下给提个 PR 上去,以此自勉。
最后,求 fafa。
本文由我首发于 GitChat 中。
在 Node.js 开发领域中,原生 C++ 模块的开发一直是一个被人冷落的角落。但是实际上在必要的时候,用 C++ 进行 Node.js 的原生模块开发能有意想不到的好处。
本文将从早期的 Node.js 开始,逐渐披露 Node.js 原生 C++ 模块开发方式的变迁。一直到最后,会比较详细地对 Node.js v8.x 新出的原生模块开发接口 N-API 做一次初步的尝试和解析,使得大家对 Node.js 原生 C++ 模块开发的固有印象(认为特别麻烦)有一个比较好的改观,让大家都来尝试一下 Node.js 原生 C++ 模块的开发。
虽然 Node.js 原生 C++ 模块开发方式有了很大的改变,但是有一些内容是不变的,至少到现在来说都是基本上没什么 Breaking 的变化。
这就要从 Node.js 最本质的 C++ 模块开发讲起了。举个例子,我们在 Linux 下有一个合法的原生模块 ons.node,它其实是一个二进制文件,使用文本编辑器无法正常地看出什么鬼,直到我们遇到了二进制文件查看器。
眼尖的同学会看到它的 Magic Number[^1] 是 0x7F454C46
,其按位的 ASCII 码代表的字符串是 ELF
。于是答案呼之欲出,这就是一个 Linux 下的动态链接库文件。
事实上,不只是在 Linux 中。当一个 Node.js 的 C++ 模块在 OSX 下编译会得到一个后缀是 *.node 本质上是 *.dylib 的动态链接库;而在 Windows 下则会得到一个后缀是 *.node 本质上是 *.dll 的动态链接库。
这么一个模块在 Node.js 中被 require
的时候,是通过 process.dlopen()
对其进行引入的。我们来看一下 Node.js v6.9.4 的 DLOpen
[^2] 函数吧:
void DLOpen(const FunctionCallbackInfo<Value>& args) { |
按照逻辑来讲,这个加载过程其实就是下面这样的。
uv_dlopen
加载链接库。mp->nm_register_func()
初始化这个模块,并得到该有的 module
和 module.exports
。流程走下来就跟这个流程图差不多。
这货是 Node.js 中编译原生模块用的。自从 Node.js v0.8 之后,它就跟 Node.js 黏上了,在此之前它的默认编译帮助包是 node-waf[^3],对于老 Noder 来说应该不会陌生的。
node-gyp 是基于 GYP[^4] 的。它会识别包或者项目中的 binding.gyp[^5] 文件,然后根据该配置文件生成各系统下能进行编译的项目,如 Windows 下生成 Visual Studio 项目文件(*.sln 等),Unix 下生成 Makefile。在生成这些项目文件之后,node-gyp 还能调用各系统的编译工具(如 GCC)来将项目进行编译,得到最后的动态链接库 *.node 文件。
从上面的描述中大家可以看到,Windows 下编译 C++ 原生模块是依赖 Visual Studio 的,这就是为什么大家在安装一些 Node.js 包的时候会需要你事先安装好 Vusual Studio 了。
事实上,对于并没有 Visual Studio 需求的同学们来说,它不是必须的,毕竟 node-gyp 只依赖它的编译器,而不是 IDE。想要精简化安装的同学可以直接访问 http://landinghub.visualstudio.com/visual-cpp-build-tools 下载 Visual CPP Build Tools 安装,或者通过
$ npm install --global --production windows-build-tools
命令行的方式安装,就能得到你该得到的编译工具了。
说了那么多,让大家见识一下 binding.gyp 的基本结构吧。
# binding.gyp |
这段配置讲述了这么一个故事:
关于 GYP 配置文件的更多内容,大家可自行去官方文档观摩,在脚注中有 GYP 的链接。
node-gyp 除了自身是基于 GYP 的之外,它还做了一些额外的事情。首先,在我们编译一个 C++ 原生扩展的时候,它会去指定目录下(通常是 ~/.node-gyp 目录下)搜我们当前 Node.js 版本的头文件和静态连接库文件,若不存在,它就会火急火燎跑去 Node.js 官网下载。
这是一个 Windows 下 node-gyp 下载的指定版本 Node.js 头文件和库文件的目录结构。
这个头文件目录会在 node-gyp 进行编译时,以 "include_dirs"
字段的形式合并进我们事先写好的 binding.gyp 中,总而言之,这里面的所有头文件能被直接 #include <>
。
node-gyp 是一个命令行的程序,在安装好后能通过 $ node-gyp
直接运行它。它有一些子命令供大家使用。
$ node-gyp configure
:通过当前目录的 binding.gyp 生成项目文件,如 Makefile 等;$ node-gyp build
:将当前项目进行构建编译,前置操作必须先 configure
;$ node-gyp clean
:清理生成的构建文件以及输出目录,说白了就是把目录清理了;$ node-gyp rebuild
:相当于依次执行了 clean
、configure
和 build
;$ node-gyp install
:手动下载当前版本的 Node.js 的头文件和库文件到对应目录。第 N 套国际 Node.js 开发者原生 C++ 模块开发方式,时代在召唤。
除去前文中讲的一些不变的内容,还有很多内容是一直在变化的,虽然说用老旧的开发方式也是可以开发出能用的 C++ 原生模块,但是旧不如新。
而且,其实目前来说 node-gyp 的地位也有可能在未来进行变化。因为当年 Chromium 是通过 GYP 来管理它的构建配置的,现如今已经步入了 GN[^6] 的殿堂,是否也意味着 node-gyp 有一天也会被可能叫做 node-gn 的东西给取代呢?
话不多说,先来看看沧海桑田的故事吧。
在 Node.js 0.8 之前,通常在开发 C++ 原生模块的时候,是通过 node-waf 构建的。当然彼 node-waf 不是现在在 NPM 仓库上能搜到的 node-waf 了,当年那个 node-waf 早就年久失修了。
这个东西使用一种叫 wscript 的文件来配置。自 Node.js 升上 0.8 之后,就自带了 node-gyp 的支持,从此就不再需要 wscript 了。
不过就是因为有这个青黄交接的时候,那段时间的各种使用 C++ 来开发 Node.js 原生扩展的包为了兼容 0.8 前后版本的 Node.js,通常都是 binding.gyp 和 wscript 共存的。
大家可以来看一下 node-mysql-libmysqlclient 这个包在当年相应时间段的时候的仓库文件。为了支持 node-gyp,有一个 binding.gyp 文件,然后还存留着 wscript 配置文件。
在早期的时候,Node.js 原生 C++ 模块开发方式是非常暴力的,直接使用其提供的原生模块开发头文件。
开发者直接深入到 Node.js 的各种 API,以及 Google V8 的 API。
举个最简单的例子,在几年前,你的 Node.js C++ 原生扩展代码可能是长这样的。
Handle<Value> Echo(const Arguments& args) |
这是一个最简单的 echo
函数,返回传进来的参数。写作 JavaScript 相当于是这样的。
exports.echo = function() { |
遗憾的是,这样的代码如果发成一个包,你现在是无论如何无法安装的,除非你用的是 0.10.x 的 Node.js 版本。
为什么这么说呢,这段代码的确是在 Node.js 0.10.x 的时候可以用的。但是再往上升 Google V8 的大版本,这段代码就无法适用了,讲粗暴点就是没办法再编译通过了。
就拿 Node.js 6.x 版本的 Google V8 来说,函数声明的对比是这样的:
Handle<Value> Echo(const Arguments& args); // 0.10.x |
事实上,根本不需要等到 6.x。上面的代码到 0.12 就已经无法再编译通过了。不只是函数声明的变化,连句柄作用域[^7]的声明方式都变了。
如果要让它在 Node.js 6.x 下能编译,就需要改代码,就像这样。
void Echo(const FunctionCallbackInfo<Value>& args) |
也就是说,以黑暗时代的方式进行 Node.js 原生模块开发的时候,一个版本只能支持特定几个版本的 Node.js,一旦 Node.js 的底层 API 以及 Google V8 的 API 发生变化,而这些原生模块又依赖了变化了的 API 的话,包就作废了。除非包的维护者去支持新版的 API,不过这样依赖,老版 Node.js 下就又无法编译通过新版的包了。
这就很尴尬了。
在经历了黑暗时代的尴尬局面之后,2013 年年中,一个救世主突然现世。
它的名字叫作 NAN,全称 Native Abstractions for Node.js,即 Node.js 原生模块抽象接口。
NAN 由 Rod Vagg 和 Benjamin Byholm 两手带大,记名在 GitHub 的 Rod Vagg 账号下。并且在 Node.js 与 io.js 黑历史的年代,这个在 GitHub 上面项目移到了 io.js 的组织下面;后来由于两家又重归于好,NAN 最终归属到了 nodejs 这个组织下面。
总之在 NAN 出现之后,Node.js 的原生开发方式进入了城堡时代,并且一直持续到现在,甚至可能会持续到好久之后。
说 NAN 是 Node.js 原生模块抽象接口可能还是有点抽象,那么讲明白点,它就是一堆宏判断。比如声明一个函数的时候,只需要通过下面的一个宏就可以了:
NAN_METHOD(Echo) |
NAN 的宏会判断当前编译时候的 Node.js 版本,根据不同版本的 Node.js 来展开不同的结果。这会儿就又会提到先前的两个函数声明对比了。
Handle<Value> Echo(const Arguments& args); // 0.10.x |
NAN_METHOD
将会在不同版本的 Node.js 下被 NAN 展开成上面两个这样。
而且 NAN 可不只是提供了 NAN_METHOD
一个宏,它还有一坨一坨数不清的宏供开发者使用。
比如声明句柄作用域的 Nan::HandleScope
、能黑盒调起 libuv[^8] 进行事件循环上的异步操作的 Nan::AsyncWorker
等。
于是,在城堡时代,大家的 C++ 原生模块代码都差不多长这样。
NAN_METHOD(Echo) |
这样做的好处就是,代码只需要随着 NAN 的升级做改变就好,它会帮你兼容各不同 Node.js 版本,使其在任意版本都能被编译使用。
即使是 NAN 这样的好物,也有自己的一个使命,使命之外的东西会被逐渐剥离。比如 0.10.x 和 0.12.x 等版本就应该要退出历史舞台了,NAN 会逐渐放弃对它们的兼容和支持。
自从前几天 Node.js v8.0.0 发布之后,Node.js 推出了全新的用于开发 C++ 原生模块的接口,N-API。
据官方文档所述,它的发音就是一个单独的
N
,加上 API,即四个英文字母单独发音。
这东西相较于先前三个时代有什么不同呢?为什么会是更进一步的帝国时代呢?
首先,我们知道,即使是在 NAN 的开发方式下,一次编写好的代码在不同版本的 Node.js 下也需要重新编译,否则版本不符的话 Node.js 无法正常载入一个 C++ 扩展。即一次编写,到处编译。
而 N-API 相较于 NAPI 来说,它把 Node.js 的所有底层数据结构全部黑盒化,抽象成 N-API 当中的接口。
不同版本的 Node.js 使用同样的接口,这些接口是稳定地 ABI 化的,即应用二进制接口(Application Binary Interface)。这使得在不同 Node.js 下,只要 ABI 的版本号一致,编译好的 C++ 扩展就可以直接使用,而不需要重新编译。事实上,在支持 N-API 接口的 Node.js 中,的确就指定了当前 Node.js 所使用的 ABI 版本。
为了使得以后的 C++ 扩展开发、维护更方便,N-API 致力于以下的几个目标:
而这些 API 主要就是用来创建和操作 JavaScript 的值了,我们就再也不用直接使用 Google V8 提供的数据类型了。毕竟在 NAN 中,就算我们有时候看不到 Google V8 的影子,实际上在宏展开后还是无数的 Google V8 数据结构。
为了达成上述隐藏的目标,N-API 的姿势就变成了这样:
napi_status
枚举,来表示这次调用成功与否;napi_status
占坑了,所以真实返回值由传入的参数来继承,如传入一个指针让函数操作;napi_value
封装,不再是类似于 v8::Object
、v8::Number
等类型;napi_get_last_error_info
函数来获取最后一次出错的信息。注意:哪怕是现在的 Node.js v8.x 版本,N-API 仍处于一个实验状态,个人认为还有非常长的一段路要走,所以大家在生产环境中还不必太过于激进,不过 N-API 依然是大势所趋;不过对于使用老版本的 Node.js 开发者来说,大家也不要着急,即使 N-API 是在 v8.x 才正式集成进 Node.js,在其它旧版本的 Node.js 中依然可以将 N-API 作为外挂式的头文件9中使用,只不过无法做到跨版本的特性,这只是它做的向后兼容的一个事情而已。
关于 N-API 一系列的函数可以访问它的文档了解更多详情,现在我们来点料儿让大家对 N-API 的印象不是那么抽象。
在封建时代和 NAN 所处的,模块的初始化是交给 Node.js 提供的宏来实现的。
NODE_MODULE(addon, Init) |
而到了当前的 N-API,它就变成了 N-API 的一个宏了。
NAPI_MODULE(addon, Init) |
相应地,这个初始化函数 Init
的写法也会有所改变。比如这是封建时代和 NAN 时代的两种不同写法:
// 暴力写法 |
而到了 N-API 的时候,这个 Init
函数就该是这样的了。
void Init(napi_env env, napi_value exports, napi_value module, void* priv) |
napi_property_descriptor
是用于设置对象属性的描述结构体,它的声明如下:
typedef struct {
const char* utf8name;
napi_callback method;
napi_callback getter;
napi_callback setter;
napi_value value;
napi_property_attributes attributes;
void* data;
} napi_property_descriptor;那么上面
Init
函数中的desc
意思就是,即将被挂在的对象下会挂一个叫"echo"
的东西,它的函数是Echo
,其它的getter
、setter
等全是空指针,而属性则是napi_default
。
napi_property_attributes
除了napi_default
之外,还有诸如只读、是否可枚举等属性。
还记得之前的两种函数声明吗?第三次再搬过来。
Handle<Value> Echo(const Arguments& args); // 0.10.x |
在 N-API 中,你不用再被告知需要有 C++ 基础,C 即可。因为在 N-API 里面,声明一个 Echo
是这样的:
napi_value Echo(napi_env env, napi_callback_info info) |
重要:目前 8.0.0 和 8.1.0 版本的 Node.js 官方文档中,关于 N-API 的各种接口文档错误颇多,所以还是要以能使用的接口为准。
而且现在大家也有很多人正在帮忙一起修复文档。例如现在的 JavaScript 函数声明返回值其实是
napi_value
,而官方文档上还是老旧的void
。又比如 ``napi_property_descriptor_desc结构体中,在
utf8name之后还有一个
napi_value` 的变量,而文档中却是没有的。这也是为什么我前面强调目前来说 N-API 还处于试验阶段。毕竟 API 并没有完全稳定下来,还处于一个快速迭代的步伐中,文档的更新并未跟上代码的更新。至少在笔者写作的当前是这样的(现在日期 2017 年 6 月 9 日)。
上面代码分步解析。
napi_get_cb_info
获取当次函数请求的参数信息,包括参数数量和参数体(参数体以 napi_value
的数组形式体现);status
不等于 napi_ok
)或者看看参数数量是否小于 1;napi_throw_type_error
在 JavaScript 层抛出一个错误对象,并返回;argv[0]
,即第一个参数。这里放上这个 Echo 样例的完整代码,大家可以拿回家试试看。
{ |
|
在完成了代码之后,大家赶紧试一下代码吧。
首先在 Node.js v8.x 下进行试验,把这两段代码分别放到同一个目录下,命名好后,执行这样的终端命令:
$ node-gyp rebuild |
注意:还是因为试验特性,目前在 Node.js v8.x 要加载和执行 N-API 的 C++ 扩展的话,在启动
node
的时候需要加上--napi-modules
参数,表示这次执行要启用 N-API 特性。
效果显而易见,在刚启动 Node.js REPL 的时候,你会得到一个警告。
(node:52264) Warning: N-API is an experimental feature and could change at any time
表示它目前还不是特别稳定,但是值得我们展望未来。然后在我们 require()
扩展的时候,我们就得到了一个拥有 echo
函数的对象了。
我们尝试了三种调用方式。第一次是规规矩矩传入一个参数,echo
如期返回我们传入的参数 "2333"
;第二次传入两个参数,echo
返回了第一个参数 "蛋花汤🐶"
;最后一次我们没传任何参数,这个时候就走到了 C++ 扩展中判断函数参数数量失败的条件分支,就抛出了一个 Wrong number of arguments
的错误对象。
总之,它按照我们的预期跑起来了。并且代码里面并没有任何 Node.js 非 N-API 所暴露出来的数据结构和 V8 的数据结构——版本差异消除了。
接下来激动人心的时刻到了,如果读者是使用 nvm
来管理自己的 Node.js 版本的话,可以尝试着安装一个 8.1.0 的 Node.js 版本。
$ nvm install 8.1.0 |
在安装成功切换版本成功后,尝试着直接打开 Node.js RELP,忘掉再次编译刚才编译好的扩展这一步。(不过别忘了 --napi-module
参数)
把刚才用于测试的几句 JavaScript 代码再重复地输入——N-API 诚不我欺,居然还是能输出结果。这对于以前的暴力做法和 NAN 做法来说,无疑是非常大的一个进步。
至此,我希望大家还没有忘记 N-API 是自 Node.js 8.0 之后出的特性。所以之前 Demo 的代码并不能在 Node.js 8.0 之前的版本如期编译和运行。
辛辛苦苦写好的包,居然不能在 Node.js 6.x 下面跑,搞什么。
先别急着摔。文中之前也说了,有一个外挂式头文件的包,其包名是 node-addon-api
。
我们就试着通过它来进行向下兼容吧。首先在我们刚才的源码目录把这个包给安装上。
$ npm install --save node-addon-api |
还是由于快速迭代的原因,我不能保证这个包当前版本的时效性,不过我相信大家都有探索精神,在未来版本不符导致的 API 不符的问题应该都能解决。
然后,给我们的 binding.gyp 函数加点料,加两个字段,里面是两个指令展开。
"include_dirs": [ "<!@(node -p \"require('node-addon-api').include\")" ], |
<!@
和 <!
开头的字符串在 GYP 中代表指令,表示它的值是后面的指令的执行结果。上面两条指令的返回结果分别是外挂式头文件的头文件搜索路径,以及外挂式 N-API 这个包编译成静态连接库供我们自己的包使用的依赖声明。
有了这两个字段后,就表示我们依赖了外挂式 N-API 头文件。而且它内部自带判断,如果版本已经达到了有 N-API 的要求,它的依赖就会是一个空依赖,即不依赖外挂式 N-API 编译的静态连接库。
也就是说,用了外挂式的 N-API,能自动适配 Node.js 8.x 和低版本。
于是这个 binding.gyp 现在看起来是这样子的。
{ |
至于源码层面,我们就不需要作任何修改。在 Node.js v6.x 下面试试看吧。同样是使用 node-gyp rebuild 进行编译。然后通过 Node.js REPL 进去测试。
具体的终端输出这里就不放出来了,相信经过实验的大家都得到了自己想要的结果。
本次内容主要讲解了在 Node.js 领域中原生 C++ 模块开发的方式变迁。
目前的中坚力量仍然是 NAN 的开发方式,甚至我猜测是否未来有可能 NAN 会提供关于 N-API 的各种宏封装,使其彻底消除版本差异,包括 ABI 版本上的差异。当然这种 ABI 版本差异导致的需要多次编译问题应该还是存在的,这里指的是一次编码的差异。
在大家跟着本文对 N-API 进行了一次浅尝辄止的尝试之后,希望能对当下仍然处于实验状态的 N-API 充满了希冀,并对现在存在的各种坑处以包容的心态。
毕竟,Node.js loves you all。
[^1]: 用于定义某种文件类型的特殊标识,详见 https://en.wikipedia.org/wiki/Magic_number_(programming)
[^2]: 代码参见 https://github.com/nodejs/node/blob/v6.9.4/src/node.cc#L2427-L2502
[^3]: 年久失修,当前 NPM 上搜索到的 node-waf 已经不是当年的了,不过这个是 Waf 的官方仓库 https://github.com/waf-project/waf。
[^4]: 全称 Generate Your Projects,是谷歌开发的一套构建系统,未尽事宜详询 https://gyp.gsrc.io。
[^5]: GYP 的配置文件的后缀就是 *.gyp 或者 *.gypi 等,是个类 JSON 文件。
[^6]: GN 是谷歌开发的相较于 GYP 更新更快的一套构建工具,可以参考 https://chromium.googlesource.com/chromium/src/tools/gn/+/HEAD/docs/quick_start.md
[^7]: 让垃圾回收机制来管理 JavaScript 对象生命周期的一种类,即 HandleScope,在我的新书中将会有详解。
[^8]: Node.js 的异步事件循环支撑者,详询 http://www.libuv.org/
[^9]: 详情请查看 node-api 这个包,https://github.com/nodejs/node-api
先上 Repo 地址:https://github.com/XadillaX/byakuren。
图像主题色是从一张图像中提取出来最能代表这张图片主色调的多种颜色。 也就是说在一幅色彩斑斓的图片里面,各种不同颜色的数量就对应着该颜色在图 片中的比例,程序可以通过计算图片中不同颜色的像素数来算出主题色。
提取的算法在我之前的博客中有说明。在 Byakuren 中其实用的就是之前博客中讲的一些算法。
除去上面两种算法,Byakuren 还提供了将这两种算法结合起来的 Mix 算法。
Byakuren 是我前两年写的一个主题色提取库,也是继 thmclrx 之后的 C 版实现,个人认为代码质量比旧版的 thmclrx 要高。并且它实际上经过了企业级的验证,在某相关的公司已经欢快跑了有些年头了。
在经过相关人员的同意下,我也算把这雪藏了好久的代码给开源出来了。
聖 白蓮(ひじり びゃくれん,Hiziri Byakuren)是系列作品《东方 project》中的角色,首次登场于《东方星莲船》。
- 种族:魔法使
- 能力:使用魔法程度的能力(酣畅增强身体能力的)
- 危险度:不明
- 人类友好度:中
- 主要活动场所:命莲寺之类
- 命莲寺的住持。虽然原本是人类,不过由于常年的修行已经完全超越了人类。现在已经属于人们常说的魔法使了。
虽然已经入了佛门,但是不知道什么原因却被妖怪敬仰着。她从来没有像童话故事中的魔法使那样,念诵着咒语治退妖怪。使用的力量完全是邪恶的,一点都不像是圣人,虽然并没有人目击到她与人类为敌,但其实已彻底成为妖怪的同伴了。
好吧,总之本人是个东方控,所以基本上项目名都跟东方有关。
如文章题目所说,它是一个 C 实现的开源主题色提取的库,大家可以把它编译成链接库使用。
不过目前暂时只支持 Makefile 的形式来编译,大家如果有兴趣也可以自己建个 Windows 下的 Visual Studio 项目等,也欢迎提类似于 CMake 之类的 PR。
其实详细的使用方法在文档中就有说明。
不过这里还是简单介绍一下吧。
先把代码给下下来,你也可以把它 Clone 下来。总之仓库地址是:https://github.com/XadillaX/byakuren。
然后跑到目录下执行 make
。
$ make byakuren |
你将得到一个 byakuren.a
的静态链接库。
这个时候你只要拿着这个静态链接库,然后顺便在你的项目中把头文件引进来就可以了。
我们假设你有 bkr_rgb* rgb
的图片像素信息,以及 uint32_t color_count
的图片像素总数两个变量,下面分别给出三个样例。具体的 API 解析还请去文档观摩。
bkr_color_stats stats[256]; |
colors
就是主题色数量了,传进去的stats
就会接收到主题色的具体信息了。
bkr_color_stats stats[256]; |
colors
和stats
如上所述。
bkr_color_stats stats[256]; |
colors
和stats
如上所述。
可能有人想看看效果,我这里就放个效果图给大家看看吧。
其中 Octree 和 Mix 两个算法的主题色最大数量参数传的都是 16。
你可以拿它来写一些主题色提取的东西。
你也可以拿它来完成其它语言的主题色提取库的封装,如 Python、Lua 等等。欢迎反馈给我。
把一份自己觉得还不错的代码开源出来的感觉特爽,尤其是这种重见天日的感觉。ヘ|・∀・|ノ*~●
]]>从 2015 年 9 月入职现东家大搜车已经一年多了,经历了公司 C 轮亿级刀的融资和新产品的发布,在今年也有几件令自己比较自豪和挫败的事。
这一块是在年底借着公司新产品「弹个车」的东风才开始真正落地开来。其实这一块已经落后大厂好多个年代了——2014 年淘宝的中途岛方案以及 2015 年死马分享的天猫 Wormhole 方案。
一是因为在未分离的阶段我们公司前端还是以传统的 Java 方式开发,二是在分离的阶段直接上了 Vue 等框架,所以直接就跳过那一步了。
但是其实很多情况下都是 Vue 力所不能及的——比如既要考虑到首屏就渲染好的展示页但是又不想继续在传统 Web 开发的阴影下进行工作的时候。我相信就算在阿里也是为了解决这个问题的——**让前端更专注前端,让后端更专注后端,让浏览器得到的还是传统的结果。**
项目在内部唤作 Vanille,第一个上线版本花了两周的时间,目前已正常服役于第一个项目「弹个车」。这算是我今年在公司做的最自豪的一件事之一了,**终于推动了一次内部技术的发展。**
从去年以项目经理的身份接了这个项目,经历了从无到有到三不管再到现在的平稳状态。今年年初的时候该项目经历了一段时间研发,成为了公司内第一个接入内部支付平台的项目,只不过后来运营的头儿走了后 O2O 的支付功能一直被雪藏了——一股挫败感油然而生。
到了快年底的时候终于来了一波小需求,接入了内部的搜索引擎和加了一波小需求。不过这段时间在忙别的事情,主要的开发任务交给了同事开发,而我就做一些 Code Review 和部署的工作。
期间也想把商学院给升格成一个公司内部统一的 CMS,后来因为一直没有落地实施和一直被调遣做其它的任务,所以到后面不了了之了——那段时间公司的 Noder 着实不够用。
一开始不要把所有事情都想得面面俱到,很有可能是浪费你的精力和感情,在迭代速度非常快的互联网时代,快速出产品才是最重要的事。还要练就强大的内心来拥抱各种变化。
这是今年做的一些业务相关的事,挑重要的总结一下感想。
做得最久的就是两大期的订阅相关的需求了,其实有点像花瓣网的瀑布流关注,稍加改造即可。不过由于做了接盘侠,接手代码的时候看到的是一坨比较晦涩的诡异实现方法,经历了三期的全盘复刻及 Promise 替换,四期从头开始再实现,还是没能将其迁移成我心目中的那一套方法——*技术债的还债成本总是大的,以及在线上跑了那么久的代码至少是稳定的,实现方案的改造的开发成本远大于代码层面重构的开发成本。**综上所述,除了得出不要轻易重构旧代码的结论,还得出了「宁可一开始花更多的时间和设计去搞定一套东西,也不要在代码稳定线上跑了好久之后再重构它的实现方式——哪怕旧的方式是一坨屎」的结论。***
今年对 Toshihiko 主要只是修修补补。但是在 Node Party 第一次 Speak 之后,下定决心重构了一次 Toshihiko 的代码并且使其理论上支持了多适配器层。
这个项目源自于去年我们 Node.js 团队要接其他团队的消息队列,而他们使用的是阿里云的 ONS。他们没有 Node.js 的 SDK,我们只能自给自足。
一开始用了一个比较脏的办法,使用他们的 PHP SDK 然后在项目中启动子进程与主进程进行通讯,逻辑放在主进程处理,而收发消息的任务则交给 PHP。
后来我毛了,于是自给自足,基于他们的 C++ SDK 自己封装了一个 Node.js 版本的 SDK。经过了几个大版本的迭代和性能优化,目前该 SDK 已经稳稳当当服役于大搜车半年多了,一直很稳定。
虽然阿里云一直说在出 Node.js 的 SDK,但是迟迟未见产出,我姑且也自豪一番吧。
这个项目是为了当时即将去「蘑菇鸡」的小龙童鞋写的,虽然他由于身体原因最后没去成。
主要用法就是能根据你的一些设定(比如金木水火土)然后随机一堆花名,供你在阿里等厂用——面向各种起名困难户。
除了上述的一些事情外,还收获了最重要的一点——**不再像以前一样只无脑关心技术本身,而是更多地去思考技术之外的事,对待旧的代码更宽容了。**
不过纯技术方面来说,感觉还是到达一定瓶颈了。
最后列出一些明年想做的事情。
在之前《跟我一起部署和定制 CNPM——基础部署》中提到过,CNPM 配置项里面有一项配置 nfs
,它所对应的是一个 NFS 对象。
在同步 package 的时候,CNPM 会把源站的包下载到本地,然后传给 NFS 对象相应的函数交予去处理,由 NFS 对象返回处理结束之后该包在我们自己部署的 CNPM 对应的包下载链接。
上面的这一套流程就给我们自定义包存储提供了可能,比如我们可以把包同步到又拍云存储、阿里云 OSS 等地方去,也可以以二进制的形式存入我们自己的数据库(不推荐),甚至可以什么都不用做直接放在本地,然后把本地文件对外网暴露即可。
NFS 的接口是实现定义好的,我们如果要写一个自己的 NFS 类,只需要按照约定的接口实现他们的逻辑即可。
虽然我自己不喜欢,但是 NFS 的所有函数需要在菊花函数中被实现。
下面给出接口的定义:
function* upload(filepath, options)
filepath
:文件路径。options
key
:待上传文件的标识size
:待上传文件大小function* uploadBuffer(fileBuffer, options)
fileBuffer
:待上传文件的 Bufferoptions
key
:待上传文件的标识size
:待上传文件的大小function* remove(key)
key
: 文件标识function* download(key, savePath, options)
(可选实现)key
:文件标识savePath
:保存路径options
timeout
:超时时间function* createDownloadStream(key, options)
(可选实现)key
: 文件标识options
timeout
:超时时间ReadStream
function[*] url(key)
(可选实现,可以不是菊花函数)key
: 文件标识这里拿出一个 NFS 的官方实现阿里云 OSS 版来作为解析。它的 Repo 是 https://github.com/cnpm/oss-cnpm。
打开 index.js 我们能看到,的确 OssWrapper
实现了上面的一些接口。
在 function OssWrapper
里面我们看到它 new
了 ali-oss 对象。
if (options.cluster) { |
也就是说在各种上传等函数里面都是以这个 client
为主体做的事情的。
首先我们看看 upload
函数,从外部传进来文件的 key
,NFS 对象将该文件以 key
为名传到 OSS 去,并返回该文件上传之后在 OSS 上的地址。
proto.upload = function* (filePath, options) { |
uploadBuffer
其实也一样,参数第一个 fileBuffer
是一个文件二进制 Buffer 对象,而 ali-oss
包的 put
函数第二个参数既可以传一个文件路径,也可以传一个 Buffer,所以相当于把 upload
这个函数直接拿过来就能用了,于是就有了:
proto.uploadBuffer = proto.upload; |
这两个函数实际上也是直接调用了 ali-oss
的函数,并没有什么好讲的,大家自己看看就好了。
这个函数无非就是判断下有没有自定义的 CDN 域名什么的,根据不同的返回不同的网址而已。
把 key
里面带的最前面的斜杠去掉。
上面一节解析了 oss-cnpm
这个包的代码,如果官方出的几个 NFS 包不能满足,大家也能自己去写一个 CNPM 存储层的包了。
我们公司的包是直接在 OSS 上面的,所以用 oss-cnpm
并没有什么不妥。
不过对于阿里系本身的公司门来说,OSS 并不是什么大事儿,对于我们来说,OSS 的 bucket 资源还是蛮稀缺的,上次就达到上限了。所以我们目前的 NPM 包跟公司别的测试业务用的是同一个 bucket。
那么问题来了:
oss-cnpm
直接把所有文件放在根目录下建文件夹,太乱了,而且的确是有小可能冲突的。而这个包又不能让人自定义前缀什么什么的。
于是我就自己 Fork 小小改装了一下这个包,让它适合我们公司自己。
改装很简单,在上传的目录中加一个文件夹前缀。
动的是 trimKey
函数:
function trimKey(key) { |
这下所有在我们内部 CNPM 里面的包的链接都多了个 _snpm_/
的前缀了。
上面解析了接口之后,我们来扒一扒什么时候会调用上面实现的接口们吧,这样就知道 CNPM 对于 NFS 使用的工作原理了。
对于包下载来说,它的路由是:
/{package}/download/{package}-{version}.tgz |
然后在里面判断一下如果 NFS 对象有实现 url()
函数的话,先用 url()
函数生成对该包而言的真实下载链接。
读出这个包的 registry 信息,里面如果没有 dist
等参数的话直接 302 到刚生成的地址去。
if (typeof nfs.url === 'function') { |
接下去是涉及到上一章没有提到过的一个配置参数,叫 downloadRedirectToNFS
,默认为 false
。如果该值为 true
的话并且刚才由 url()
函数生成了下载链接的话,也是直接 302 到真实下载链接去。
if (config.downloadRedirectToNFS && url) { |
不过如果本身 registry 里面就没 key
这个选项的话也会直接用 url()
生成的链接给跳过去。如果没有 url()
的链接,那么直接用 registry 里面的 tarball
字段。
var dist = row.package.dist; |
上面如果都跳过去了,那么说明要开始调用事先写好的 download
那两个函数了,把文件读到 Buffer 里面,然后把 Buffer 放到 Response 里面传回去。
对于删除包来说,除了把包从数据库删掉之外,还要循环遍历一遍这个包的所有版本,把所有版本的这个包都从 NFS 里面删除。
try { |
这里就调用了你事先写好的 remove
了。当然你不实现也没关系,最多是包的压缩文件不删除而已。
这里跟上一小节差不多,之前是删除整个包,这里是删除包的某一个版本,所以就不用循环删除了。
try { |
然后就是用户 $ npm publish
用的路由了,在一堆判断之后,发布传过来的包被放在二进制 Buffer 内存里面:
var tarballBuffer; |
接下去又判断来判断去,最后交由 NFS 的 uploadBuffer
来上传并得到结果。
var uploadResult = yield nfs.uploadBuffer(tarballBuffer, options); |
看到没有,就是这里记录的它到底是 key
还是 tarball
了。
如果你的 upload
函数返回的是 { url: 'FOO' }
,那么就是 tarball
设置成该值,在下载的时候会直接 302 到 tarball
所指的地址去;如果返回的是 { key: 'key' }
的话,会在 dist
里面存个 key
,下载的时候判断如果有 key
的话会把它传进你的 createDownloadStream
或者 download
函数去交由你的函数生成包 Buffer 并传回 Response。
这个文件是从源端同步相关的一些逻辑了,这里面有两个操作。
一个是 unpublish
,调用的就是 NFS 的 remove
,不作详谈了。
另一个就是同步了。同步包会被打散成同步一个版本,然后把每个版本同步过来。在同步版本的时候先把包文件下载到本地文件 filepath
里面去。
var r = yield urllib.request(downurl, options); |
urllib 是苏千死马他们自己写的比较方便和适合他们自己的一个 http 请求库。
上面的代码 options
里面有一个文件流,链接到 filepath
目录的这个文件去,相当于这一步就是把源端的包下载到本地 filepath
去了。
经过一堆 blahblah 的判断(比如 SHASUM)之后,这个这个函数就会调用 NFS 的 upload
函数将本地文件名对应的文件上传到你所需要的地方去了。
try { |
其结果到底是
key
还是url
对于下载的影响跟前一小节一个道理。
本章讲了如何使用和自己定制一个 CNPM 的 NFS 层,让包的走向跟着你的心走。在描述了开发规范和出示了样例代码和改造小例子之后,又解析了这个 NFS 是如何在 CNPM 里面工作的,上面已经提到了 2.12.2 版本中所有用到 NFS 的地方。
看了上面的解析之后会对 NFS 的工作流程有更深一层的了解,然后就不会有写 NFS 层的时候有种心慌慌摸不着底的情况了。
]]>该文章所对应的 cnpm 目标版本为 v2.12.2,上下浮动一些兼容的版本问题也都不是特别大。
想要部署 CNPM,你需要做以下的一些准备。
首先在本地选择一个目录,比如我将它选择在 /usr/app
,然后预想 CNPM 的目录为 /usr/app/cnpm
,那么需要在终端 $ cd /usr/app
。
接下去执行 Git 指令将 CNPM 克隆到相应目录。
$ git clone https://github.com/cnpm/cnpmjs.org.git |
Windows 用户也可以用类似 Cygwin、MinGW、Powershell 甚至直接是 Command 等来运行 Git。
当然也可以直接下载一些 GUI 工具来克隆,如 SourceTree。
跑到 CNPM 的 Release 页面,选择相应的版本下载,比如这里会选择 v2.12.2 版。
下载完毕后将文件夹解压到相应目录即可。
安装依赖其实就是一个 npm install
,不过 CNPM 把该指令已经写到 Makefile 里面了,所以直接执行下面的命令就好了。
$ make install |
当然万一你是 Windows 用户或者不会 make
,那么还是要用 npm install
。
$ npm install --build-from-source --registry=https://registry.npm.taobao.org --disturl=https://npm.taobao.org/mirrors/node |
新建一份 config/config.js
文件,并且写入如下的骨架:
; |
在这里面输入你需要的键值对。
这里将会列举一些常用的配置项,其余的一些配置项请自行参考 config/index.js 文件。
enableCluster
:是否启用 cluster-worker 模式启动服务,默认 false
,生产环节推荐为 true
;registryPort
:API 专用的 registry 服务端口,默认 7001
;webPort
:Web 服务端口,默认 7002
;bindingHost
:监听绑定的 Host,默认为 127.0.0.1
,如果外面架了一层本地的 Nginx 反向代理或者 Apache 反向代理的话推荐不用改;sessionSecret
:session 用的盐;logdir
:日志目录;uploadDir
:临时上传文件目录;viewCache
:视图模板缓存是否开启,默认为 false
;enableCompress
:是否开启 gzip 压缩,默认为 false
;admins
:管理员们,这是一个 JSON Object
,对应各键名为各管理员的用户名,键值为其邮箱,默认为 { fengmk2: 'fengmk2@gmail.com', admin: 'admin@cnpmjs.org', dead_horse: 'dead_horse@qq.com' }
;logoURL
:Logo 地址,不过对于我这个已经把 CNPM 前端改得面目全非的人来说已经忽略了这个配置了;adBanner
:广告 Banner 的地址;customReadmeFile
:实际上我们看到的 cnpmjs.org 首页中间一大堆冗长的介绍是一个 Markdown 文件转化而成的,你可以设置该项来自行替换这个文件;customFooter
:自定义页脚模板;npmClientName
:默认为 cnpm
,如果你有自己开发或者 fork 的 npm 客户端的话请改成自己的 CLI 命令,这个应该会在一些页面的说明处替换成你所写的;backupFilePrefix
:备份目录;database
:数据库相关配置,为一个对象,默认如果不配置将会是一个 ~/.cnpmjs.org/data.sqlite
的 SQLite;db
:数据的库名;username
:数据库用户名;password
:数据库密码;dialect
:数据库适配器,可选 "mysql"
、"sqlite"
、"postgres"
、"mariadb"
,默认为 "sqlite"
;hsot
:数据库地址;port
:数据库端口;pool
:数据库连接池相关配置,为一个对象;maxConnections
:最大连接数,默认为 10
;minConnections
:最小连接数,默认为 0
;maxIdleTime
:单条链接最大空闲时间,默认为 30000
毫秒;storege
:仅对 SQLite 配置有效,数据库地址,默认为 ~/.cnpmjs/data.sqlite
;nfs
:包文件系统处理对象,为一个 Node.js 对象,默认是 fs-cnpm 这个包,并且配置在 ~/.cnpmjs/nfs
目录下,也就是说默认所有同步的包都会被放在这个目录下;开发者可以使用别的一些文件系统插件(如上传到又拍云等),又或者自己去按接口开发一个逻辑层,这些都是后话了;registryHost
:暂时还未试过,我猜是用于 Web 页面显示用的,默认为 r.cnpmjs.org
;enablePrivate
:是否开启私有模式,默认为 false
;scopes
:非管理员发布包的时候只能用以 scopes
里面列举的命名空间为前缀来发布,如果没设置则无法发布,也就是说这是一个必填项,默认为 [ '@cnpm', '@cnpmtest', '@cnpm-test' ]
,据苏千大大解释是为了便于管理以及让公司的员工自觉按需发布;更多关于 NPM scope 的说明请参见 npm-scope;privatePackages
:就如该配置项的注释所述,出于历史包袱的原因,有些已经存在的私有包(可能之前是用 Git 的方式安装的)并没有以命名空间的形式来命名,而这种包本来是无法上传到 CNPM 的,这个配置项数组就是用来加这些例外白名单的,默认为一个空数组;sourceNpmRegistry
:更新源 NPM 的 registry 地址,默认为 https://registry.npm.taobao.org
;sourceNpmRegistryIsCNpm
:源 registry 是否为 CNPM,默认为 true
,如果你使用的源是官方 NPM 源,请将其设为 false
;syncByInstall
:如果安装包的时候发现包不存在,则尝试从更新源同步,默认为 true
;syncModel
:更新模式(不过我觉得是个 typo
),有下面几种模式可以选择,默认为 "none"
;"none"
:永不同步,只管理私有用户上传的包,其它源包会直接从源站获取;"exist"
:定时同步已经存在于数据库的包;"all"
:定时同步所有源站的包;syncInterval
:同步间隔,默认为 "10m"
即十分钟;syncDevDependencies
:是否同步每个包里面的 devDependencies
包们,默认为 false
;badgeSubject
:包的 badge 显示的名字,默认为 cnpm
;userService
:用户验证接口,默认为 null
,即无用户相关功能也就是无法有用户去上传包,该部分需要自己实现接口功能并配置,如与公司的 Gitlab 相对接,这也是后话了;alwaysAuth
:是否始终需要用户验证,即便是 $ cnpm install
等命令;httpProxy
:代理地址设置,用于你在墙内源站在墙外的情况。下面给出一个样例配置:
module.exports = { |
上面的配置包文件系统层用的是 upyun-cnpm 插件,需要在 CNPM 源码根目录执行
$ npm install --save -d upyun-cnpm |
这个时候你的
package.json
就有更改与源 Repo 不一致了,如果是 Git 克隆的用户在以后升级更新系统的时候稍稍注意一下可能的冲突即可。
下面给出几个官方的 NFS 插件:
以后官方如果有一些新的插件进来,这里可能不会更新了,请自行去 NFS Storage Wrappers 获取最新的 NFS 插件们。
如果你使用的是 SQLite 的话,数据库是自动就好了的,可以忽略该步。
其它数据库需要自行导入初始数据库结构。
初始数据库脚本在 docs/db.sql 里面,你可以用一些 GUI 工具将数据导入,也可以直接进入命令行导入。
比如你用的是 MySQL,就可以在本机操作 MySQL。
$ mysql -u yourname -p |
搞好配置之后就可以直接启动服务了。
最简单的办法也是我现在正在用的方法就是直接用 node
执行一下入口文件就好了。
$ node dispatch.js |
其实我是在 tmux 里面执行上面的指令的。
官方的其它一些指令,比如你可以用 NPM 的 script 来运行。
$ npm run start |
在 CNPM 里面,npm script 还有下面几种指令
npm run dev
:调试模式启动;npm run test
:跑测试;npm run start
:启动 CNPM;npm run status
:查看 CNPM 启动状态;npm run stop
:停止 CNPM。
本文介绍了一些 CNPM 基础的部署方法,基本上能达到最小可用状态。
如果想要进阶定制一些 CNPM 的功能,请期待后续吧。ξ( ✿>◡❛)
以及一些写得不好和不对的地方,请多多指正哦。
]]>cnpm 是 Node.js 中国社区成员主导的一个私有 NPM 开源项目,可以用于部署私有 NPM、公共 NPM 镜像等。
你可以对本系列文章进行勘误或者更新,直接提交 PR 或者在博客文章后方留言。
]]>起因是我一个叫『小龙』的好基友由于某些原因离职去了一家跟阿里一样有着『花名文化』的公司,于是开始为花名犯愁。
结合之前妹纸『弍纾』在起花名的时候也遇到了同样的困扰,于是决定用 Node.js 写个『一本正经乱起花名』的程序。
首先起花名的原理就是胡乱随机一串字出来胡乱拼。
于是准备应该有 chinese-random-name,一个随机生成中文名的包。
使用它很简单,先把它 require
进来:
const randomName = require("chinese-random-name"); |
如果你需要随机生成一个名字只需要 randomName.generate()
就可以了;如果你要随机一个姓那么就 randomName.surnames.getOne()
;如果你只需要获得名字,这里面就有点门道了。
你可以随机生成一个名(不带姓的) randomName.names.get()
;你也可以指定名字的字数,一二三:
randomName.names.get1(); |
或者!
又或者!
然后或者!
你可以指定每个字的五行哦!
什么意思呢?比如你想给孩子起个名,然后孩子命里五行缺金,那么就可以:
randomName.names.get2("金金"); |
然后你就可能得到一个『紫铨』,两个字都是属金的。那么如果你孩子姓李,就叫李紫铨;如果孩子姓王,就叫王紫铨;如果姓爱新觉罗,那么就叫爱新觉罗·紫铨。
有木有想给我装得这个逼打个 82 分呢?剩下的就交给 666 吧。(ง •̀_•́)ง
这个包是用来解析命令行参数的。虽然市面上有挺多别的的,比如 commander 等,不过我还是最习惯 nomnom,用称手了就不想换了。
虽然它的 GitHub Repo 下面有这么一段话。
Nomnom is deprecated. Check out https://github.com/tj/commander.js, which should have most, if not all of the capability that nomnom had. Thank you!
不过再怎么说 nomnom 也是当年 substack 大神推荐的啊。(ಡωಡ)
这个包是用来上色的。
毕竟五行是有颜色的哇。
const color = require("colorful"); |
那么在你的终端就好看到一个红色的 (*/∇\*)
。
用来判断是不是中文的包。
作为一个起名的命令行程序,你总得好好传参才行吧,总不能你随便传个咸鸭蛋🐣我也好好处理吧。
于是就用 is-chinese 来判断某个字符串是不是纯中文。
这个包是由前阿里小伙伴,CNode 站长唐少写的。
用起来也很简单,只要 isChinese("什么你要判断什么")
就可以了。
$ npm install --save -d chinese-random-name |
其结果在这里。
首先效果是这样的:
$ hua --help |
使用者可以自己想一个前缀或者后缀,然后自定义(或者也可以不指定)两个字的五行,以及指定一次性生成多少个花名。
比如想要生成以 龙
字为前缀的花名,就可以 $ hua --prefix 龙
,得到结果可以是这样的:
* 龙幼 |
如果想两个字分别所属金和谁,就可以 $ hua --five-elements 金水
来起花名:
* 倩娥 |
想要得到这样一个命令行参数,我们可以用 nomnom
来解决。
var opts = require("nomnom") |
上面的这段代码分别指定了脚本名为 hua
,然后指定了 prefix
/ suffix
/ five-elements
和 count
四个参数,并把解析好的参数赋值给 opts
变量。
由于我希望这个包在通常的 Node.js 下都可以跑,所以没有用
let
之类的东西。
接下去要写一个花名类,这个类不只是可以在 CLI 之中使用,也可以让别人作为一个包来引入。然后实际上这个类就是要对 chinese-random-name
进行一个封装。
var Hua = function(options) { |
首先这个 options
就是之前由 nomnom
解析出来的参数,当然有些参数是可选的。
接下去我们要在构造函数也就是 Hua
里面格式化前缀或者后缀(如果有的话),将他们弄成只有一个汉字的格式。
if(options.prefix) { |
前后缀弄好之后要对五行进行分析了。
如果有前后缀那么忽略五行参数。
if(options.prefix && options.suffix) { |
如果有前缀,那么忽略传进来的五行的第一个五行;如果有后缀那么忽略第二个字的五行。
var wuxing = "金木水火土"; |
如果前后缀都没有,那么要格式化一下该参数,使其仅剩两个有效的五行汉字。
} else { |
以上的这些逻辑都写在构造函数里面,如果想要完整的构造函数可以看 hua
的 hua.js 文件。
生成一个花名其实就是调用 randomName.names.get
系列函数们了。
get1
。get1
加后缀。get2
。注意:以上情况都会传进(哪怕是
undefined
)五行参数。
所以 generateOne
函数长这样:
Hua.prototype.generateOne = function() { |
还记得 CLI 的 count
参数么?因为为了方便,我们可以批量生成花名,所以就需要生成 Count 个花名的函数了。
实际上就是一个循环调用 generateOne
的函数而已。
Hua.prototype.generate = function(count) { |
刚刚那个 nomnom
解析就在这个文件里面,然后接下去就是实例化一个 Hua
对象,然后生成 count
个花名。
var hua = new Hua(opts); |
最后把花名输出来就好了。
for(var i = 0; i < result.length; i++) { |
说好的五行颜色呢?!
好像是的哦,我们要在输出之前给 result
上个色儿。
遍历 result
里面的花名每个字,获取它的五行属性,然后涮上色儿。
chinese-random-name
暴露了字典中每个字的五行属性,只需要赋值一下就好了。
var dict = require("chinese-random-name").names.dict; |
然后逐一对比。最后对应金木水火土的颜色值分别为:
var definedColors = [ |
220 为黄色,代表金;83 为绿色,代表木;26 蓝色代表水;197 红色代表火;59 灰色代表土。
如果那个字无法找到属性,则不上色,保持默认。
result = result.map(function(name) { |
至此我们的 CLI 就写好了,最后别忘了在 hua 这个 CLI 文件顶部加上一句话。
这代表到时候如果要 ./hua
的时候这个脚本是用 Node.js 来跑的。
本来想好好写篇起花名的牢骚,结果不知不觉写成了给初心者看的初级教程了,泪目 ( •̥́ ˍ •̀ू )
不嫌弃的就这么看看吧。
最后这里给出我写好的这个 hua
程序。
$ [sudo] npm install -g huaming |
然后就能在命令行下面跑了,跑法上面几章有介绍过。它的 Repo 在这里。
最后希望这个包在你们起花名的时候还真有那么一丢丢的用处。
]]>我们还是先回顾下原题吧。
var a = 2; |
上题由我们亲爱的小龙童鞋发现并在我们的 901 群里提问的。
不过在上面一篇文章中,我们讲的是在 REPL 和 vm
中有什么事情,但是并没有解释为什么在文件模块的载入形式下,var
并不会挂载到全局变量去。
其实原因很简单,大家应该也都明白,在 Node.js 中,每个文件相当于是一个闭包,在 require
的时候被编译包了起来。
但是具体是怎么样的呢?虽然网上也有很多答案,我还是决定在这里按上一篇文章的尿性稍微解释一下。
首先我们还是回到上一篇文章的《Node REPL 启动的沙箱》一节,里面说了当启动 Node.js 的时候是以 src/node.js 为入口的。
如果以 REPL 为途径启动的话是直接启动一个 vm
,而此时的所有根级变量都在最顶级的作用域下,所以一个 var
自然会绑定到 global
下面了。
而如果是以文件,即 $ node foo.js
形式启动的话,它就会执行 src/node.js 里面的另一坨条件分支了。
// ... |
从上面的代码看出,只要是以 $ node foo.js
形式启动的,都会经历 startup.preloadModules()
和 Module.runMain()
两个函数。
我们来看看这个函数。
startup.preloadModules = function() { |
实际上就是执行的 lib/module.js 里面的 _preloadModules
函数,并且把这个 process._preload_modules
给传进去。当然,前提是有这个 process._preload_modules
。
这个 process._preload_modules
指的就是当你在使用 Node.js 的时候,命令行里面的 --require
参数。
-r, --require module to preload (option can be repeated) |
代码在 src/node.cc 里面可考。
// ... |
如果遇到了 --require
这个参数,则对静态变量 local_preload_modules
和 preload_module_count
做处理,把这个预加载模块路径加进去。
待到要生成 process
这个变量的时候,再把预加载模块的信息放到 process._preload_modules
里面去。
void SetupProcessObject(Environment* env, |
最重要的就是这句
READONLY_PROPERTY(process, |
上面我们讲了这个 process._preload_modules
,然后现在我们说说是如何把 $ node --require bar.js foo.js
给预加载进去的。
接下去我们就要移步到 lib/module.js 文件里面去了。
在第 496 行左右的地方有这个函数。
Module._preloadModules = function(requests) { |
大概我们能看到,就是以 internal/preload
为 ID 的 Module 对象来载入这些预加载模块。
var parent = new Module('internal/preload', null); |
根据这个函数的注释说明,这个 Module 对象是一个虚拟的 Module 对象,主要是跟非预加载的那些模块给隔离或者区别开来,并且提供一个模块搜索路径。
看完上面的说明,我们接下去看看 Module.runMain()
函数。
这个函数还是位于 lib/module.js 文件里面。
Module.runMain = function() { |
我们看到了就是在这句话中,Module 载入了 process.argv[1]
也就是文件名,自此一发不可收拾。
这个函数相信很多人都知道它的用处了,无非就是载入文件,并加载到一个闭包里面。
这样一来在文件里面 var
出来的变量就不在根作用域下面了,所以不会粘到 global
里面去。它的 this
就是包起来的这个闭包了。
Module._load = function(request, parent, isMain) { |
上面的代码首先是根据传入的文件名找到真的文件地址,就是所谓的搜索路径了。比如 require("foo")
就会分别从 node_modules
路径等依次查找下来。
我经常 Hack 这个 _resolveFilename
函数来简化 require
函数,比如我希望我用 require("controller/foo")
就能直接拿到 ./src/controller/foo.js 文件。有兴趣讨论一下这个用法的童鞋可以转到我的 Gist 上查看 Hack 的一个 Demo。
第二步就是我们常说的缓存了。如果这个模块之前加载过,那么在 Module._cache
下面会有个缓存,直接去取就是了。
第三步就是看看是不是 NativeModule
。
if (NativeModule.nonInternalExists(filename)) { |
之前的代码里面其实也没少出现这个 NativeModule
。那这个 NativeModule
到底是个 shenmegui 呢?
其实它还是在 Node.js 的入口 src/node.js 里面。
它主要用来加载 Node.js 的一些原生模块,比如说 NativeModule.require("child_process")
等,也用于一些 internal
模块的载入,比如 NativeModule.require("internal/repl")
。
之前代码的这个判断就是说如果判断要载入的文件是一个原生模块,那么就使用 NativeModule.require
来载入。
NativeModule.require = function(id) { |
先看看是否是本身,再看看是否被缓存,然后看看是否合法。接下去就是填充 process.moduleLoadList
,最后载入这个原生模块、缓存、编译并返回。
有兴趣的同学可以在 Node.js 中输出
process.moduleLoadList
看看。
这个 compile
很重要。
在 NativeModule
编译的过程中,大概的步骤是获取代码、包裹(Wrap)代码,把包裹的代码 runInContext
一遍得到包裹好的函数,然后执行一遍就算载入好了。
NativeModule.prototype.compile = function() { |
我们往这个 src/node.js 文件这个函数的上面几行看一下,就知道包裹代码是怎么回事了。
NativeModule.wrap = function(script) { |
根据上面的代码,我们能知道的就是比如我们一个内置模块的代码是:
var foo = require("foo"); |
那么包裹好的代码将会是这样子的:
(function (exports, require, module, __filename, __dirname) { |
这样一看就明白了这些 require
、module
、exports
、__filename
和 __dirname
是怎么来了吧。
当我们通过 var fn = runInThisContext(source, { filename: this.filename });
得到了这个包裹好的函数之后,我们就把相应的参数传进这个闭包函数去执行。
fn(this.exports, NativeModule.require, this, this.filename); |
这个 this
就是对应的这个 module
,自然这个 module
里面就有它的 exports
;require
函数就是 NativeModule.require
。
所以我们看到的在 lib/*.js
文件里面的那些 require
函数,实际上就是包裹好之后的代码的 NativeModule.require
了。
所以说实际上这些内置模块内部的根作用域下的 var
再怎么样高级也都是在包裹好的闭包里面 var
,怎么的也跟 global
搭不着边。
通过上面的追溯我们知道了,如果我们在代码里面使用 require
的话,会先看看这个模块是不是原生模块。
不过回过头看一下它的这个判断条件:
if (NativeModule.nonInternalExists(filename)) { |
如果是原生模块并且不是原生内部模块的话。
那是怎么区分原生模块和内部原生模块呢?
我们再来看看这个 NativeModule.nonInternalExists(filename)
函数。
NativeModule.nonInternalExists = function(id) { |
上面的代码是去除各种杂七杂八的条件之后的一种情况,别的情况还请各位童鞋自行看 Node.js 源码。
也就是说我们在我们自己的代码里面是请求不到 Node.js 源码里面 lib/internal/*.js
这些文件的——因为它们被上面的这个条件分支给过滤了。(比如 require("internal/module")
在自己的代码里面是无法运行的)
注意: 不过有一个例外,那就是
require("internal/repl")
。详情可以参考这个 Issue 和这段代码。
解释完了上面的 NativeModule
之后,我们要就上面 Module._load
里面的下一步 module.load
也就是 Module.prototype.load
做解析了。
Module.prototype.load = function(filename) { |
做了一系列操作之后得到了真·文件名,然后判断一下后缀。如果是 ".js"
的话执行 Module._extensions[".js"]
这个函数去编译代码,如果是 ".json"
则是 Module._extensions[".json"]
。
这里我们略过 JSON 和 C++ Addon,直奔 Module._extensions[".js"]
。
Module._extensions['.js'] = function(module, filename) { |
它也很简单,就是奔着 _compile
去的。
先上代码。
Module.prototype._compile = function(content, filename) { |
感觉流程上跟 NativeModule
的编译相似,不过这里是事先准备好要在载入的文件里面用的 require
函数,以及一些 require
的周边。
接下去就是用 Module.wrap
来包裹代码了,包裹完之后把得到的函数用参数 self.exports, require, self, filename, dirname
去执行一遍,就算是文件载入完毕了。
最后回到之前载入代码的那一刻,把载入完毕得到的 module.exports
再 return
出去就好了。
这个就不用说了。
在 lib/module.js 的最顶端附近有这么几行代码。
Module.wrapper = NativeModule.wrapper; |
一切豁然开朗了吧。
连 NativeModule
的代码都逃不开被之前说的闭包所包裹,那么你自己写的 JS 文件当然也会被 NativeModule.wrap
所包裹。
那么你在代码根作用域申明的函数实际上在运行时里面已经被一个闭包给包住了。
以前可能很多同学只知道是被闭包包住了,但是包的方法、流程今天算是解析了一遍了。
(function (exports, require, module, __filename, __dirname) { |
这个 var a
怎么也不可能绑到 global
去啊。
虽然我们上面讲得差不多了,可能很多童鞋也厌烦了。
不过该讲完的还是得讲完。
我们在我们自己文件中用的 require
在上一节里面有提到过,传到我们闭包里面的 require
实际上是长这样的:
function require(path) { |
所以实际上就是个 Module.prototype.require
。
我们再看看这个函数。
Module.prototype.require = function(path) { |
一下子又绕回到了我们一开始的 Module._load
。
所以基本上就差不多到这过了。
最后我们再点一下,或者说回顾一下吧。
REPL 启动的时候 Node.js 是开了个 vm
直接让你跑,并没有把代码包在一个闭包里面,所以再根作用域下的变量会 Biu
一下贴到 global
中去。
而文件启动的时候,会做本文中说的一系列事情,然后就会把各文件都包到一个闭包去,所以变量就无法通过这种方式来贴到 global
去了。
不过这种二义性会在 "use strict";
中戛然而止。
珍爱生命,use strict
。
本文可能很多童鞋看完后悔觉得很坑——JS 为什么有那么多二义性那么坑呢。
其实不然,主要是可能很多人对 Node.js 执行的机制不是很了解。
本文从小龙抛出的一个简单问题进入,然后浅入浅出 Node.js 的一些执行机制什么的,希望对大家还是有点帮助,更何况我在意的不是问题本身,而是分析的这个过程。
]]>以下均为臆想。
小龙: 喂喂喂,我就问一个简单的小破题目,你至于嘛!
题目是这样的。
var a = 2; |
上题由我们亲爱的小龙童鞋发现并在我们的 901 群里提问的。
然后有下面的小对话。
小龙:你们猜这个输出什么?
弍纾:2
力叔:2 啊
死月·丝卡蕾特:2
力叔:有什么问题么?
小龙:输出 undefind。
死月·丝卡蕾特:你确定?
小龙:是不是我电脑坏了
力叔:你确定?
弍纾:你确定?
小龙:为什么我 node 文件名跑出来的是 undefined?
郑昱:-.- 一样阿。undefined
以上就是刚见到这个题目的时候群里的一个小讨论。
后来我就觉得奇怪,既然小龙验证过了,说明他也不是随地大小便,无的放矢什么的。
于是我也验证了一下,不过由于偷懒,没有跟他们一样写在文件里面,而是直接 node 开了个 REPL 来输入上述代码。
结果是 2!
结果是 2!
结果是 2!
于是这就出现了一个很奇怪的问题。
尼玛为毛我是 2
他们俩是 undefined
啊!
不过马上我就反应过来了——我们几个的环境不同,他们是 $ node foo.js
而我是直接 node 开了个 REPL,所以有一定的区别。
而力叔本身就是前端大神,我估计是以 Chrome 的调试工具下为基础出的答案。
其实上述的问题,需要解释的问题大概就是 a
到底挂在哪了。
因为细细一想,在 function
当中,this
指向的目标是 global
或者 window
。
还无法理解上面这句话的童鞋需要先补一下基础。
那么最终需要解释的就是 a
到底有没有挂在全局变量上面。
这么一想就有点细思恐极的味道了——如果在 node 线上运行环境里面的源代码文件里面随便 var
一个变量就挂到了全局变量里面那是有多恐怖!
于是就有些释然了。
但究竟是什么原因导致 REPL 和文件执行方式不一样的呢?
首先是弍纾找出了阮老师 ES6 系列文章中的全局对象属性一节。
全局对象是最顶层的对象,在浏览器环境指的是 window 象,在 Node.js 指的是 global 对象。ES5 之中,全局对象的属性与全局变量是等价的。
window.a = 1;
a // 1
a = 2;
window.a // 2上面代码中,全局对象的属性赋值与全局变量的赋值,是同一件事。(对于Node来说,这一条只对REPL环境适用,模块环境之中,全局变量必须显式声明成global对象的属性。)
有了阮老师的文章验证了这个猜想,我可以放心大胆继续看下去了。
知道了上文的内容之后,感觉首要查看的就是 Node.js 源码中的 repl.js 了。
先是结合了一下自己以前用自定义 REPL 的情况,一般的步骤先是获取 REPL 的上下文,然后在上下文里面贴上各种自己需要的东西。
var r = relp.start(" ➜ "); |
关于自定义 REPL 的一些使用方式可以参考下老雷写的《Node.js 定制 REPL 的妙用》。
有了之前写 REPL 的经验,大致明白了 REPL 里面有个上下文的东西,那么在 repl.js 里面我们也找到了类似的代码。
REPLServer.prototype.createContext = function() { |
看到了关键字 vm
。我们暂时先不管 vm
,光从上面的代码可以看出,context
要么等于 global
,要么就是把 global
上面的所有东西都粘过来。
然后顺带着把必须的两个不在 global
里的两个东西 require
和 module
给弄过来。
下面的东西就不需要那么关心了。
接下去我们来讲讲 vm
。
VM 是 node 中的一个内置模块,可以在文档中看到说明和使用方法。
大致就是将代码运行在一个沙箱之内,并且事先赋予其一些 global
变量。
而真正起到上述 var
和 global
区别的就是这个 vm
了。
vm
之中在根作用域(也就是最外层作用域)中使用 var
应该是跟在浏览器中一样,会把变量粘到 global
(浏览器中是 window
)中去。
我们可以试试这样的代码:
var vm = require('vm'); |
其输出结果是:
localVar: initial value |
如文档中所说,vm
的一系列函数中跑脚本都无法对当前的局部变量进行访问。各函数能访问自己的 global
,而 runInThisContext
的 global
与当前上下文的 global
是一样的,所以能访问当前的全局变量。
所以出现上述结果也是理所当然的了。
所以在 vm
中跑我们一开始抛出的问题,答案自然就是 2
了。
var vm = require("vm"); |
最后我们再只需要验证一件事就能真相大白了。
平时我们自定义一个 repl.js
然后执行 $ node repl.js
的话是会启动一个 REPL,而这个 REPL 会去调 vm
,所以会出现 2
的答案;或者我们自己在代码里面写一个 vm
然后跑之前的代码,也是理所当然出现 2
。
那么我们就输入 $ node
来进入的 REPL 跟我们之前讲的 REPL 是不是同一个东西呢?
如果是的话,一切就释然了。
首先我们进入到 Node 的入口文件——C++ 的 int main()
。
它在 Node.js 源码 src/node_main.cc 之中。
int main(int argc, char *argv[]) { |
就在主函数中执行了 node::Start
。而这个 node::Start
又存在 src/node.cc 里面。
然后在 node::Start
里面又调用 StartNodeInstance
,在这里面是 LoadEnvironment
函数。
最后在 LoadEnvironment
中看到了几句关键的语句:
Local<String> script_name = FIXED_ONE_BYTE_STRING(env->isolate(), "node.js"); |
还有这么一段关键的注释。
// Now we call 'f' with the 'process' variable that we've built up with |
也就是说,启动 node
的时候,在做了一些准备之后是开始载入执行 src 文件夹下面的 node.js 文件。
在 92 行附近有针对 $ node foo.js
和 $ node
的判断启动不同的逻辑。
// ... |
在上述节选代码的第一个 else if
中,就是对 $ node foo.js
这种情况进行处理了,再做完各种初始化之后,使用 Module.runMain();
来运行入口代码。
第二个 else if
里面就是 $ node
这种情况了。
我们在终端中打开 $ node
的时候,TTY 通常是关连着的,所以 require('tty').isatty(0)
为 true
,也就是说会进到条件分支并且执行里面的 cliRepl
相关代码。
我们进入到 lib/module.js 看看这个 Module.requireRepl
是什么东西。
Module.requireRepl = function() { |
所以我们还是得转入 lib/internal/repl.js 来一探究竟。
上面在 node.js
里面我们看到它执行了这个 cliRepl
的 createInternalRepl
函数,它的实现大概是这样的:
function createRepl(env, opts, cb) { |
转头一看这个 lib/internal/repl.js 顶端的模块引入,赫然看到一句话:
const REPL = require('repl'); |
真相大白。
最后再梳理一遍。
在于 Node.js 的 vm
里面,顶级作用域下的 var
会把变量贴到 global
下面。而 REPL 使用了 vm
。然后 $ node
进入的一个模式就是一个特定参数下面启动的一个 REPL
。
所以我们一开始提出的问题里面在 $ node foo.js
模式下执行是 undefined
,因为不在全局变量上,但是启用 $ node
这种 REPL 模式的时候得到的结果是 2
。
小龙:我用 node test.js 跑出来是
a: undefined
;那我应该怎么修改“环境”,来让他跑出:a: 2
呢?
于是有了上面写的那段代码。
var vm = require("vm"); |
本来这里不应该出现这一节的,因为实际上大家应该都知道什么是哈希。不过有时候为了文章的完整性,我这里就稍微教条性地说明一下吧。ヽ(́◕◞౪◟◕‵)ノ
散列(英语:Hashing),通常音译作哈希,是电脑科学中一种对资料的处理方法,通过某种特定的函数、算法将要检索的项与用来检索的索引关联起来,生成一种便于搜索的数据结构。也译为散列。
-- From 散列, Wikipedia
实际上通俗的说法就是把某种状态或者资料给映射到某个值上的操作。
本酱大概就解释到这里了,至于哈希的进一步认知包括冲突的产生和解决等,如果米娜桑不了解的话还请自行学习咕。థ౪థ
这个不是我在实践中遇到的问题,而是当年去某不作恶的大厂面试时候遇到的问题,觉得比较经典,所以就拿出来了。ᕙ༼ຈل͜ຈ༽ᕗ
给定一棵二叉树,假设每个节点的数据只有左右子节点,自身并不存储数据。请找出两两完全相等的子树们。
有兴趣的童鞋可以自己先思考一下。₍₍◝(・’ω’・)◟⁾⁾
实际上我也不知道自己的做法是不是正确做法,不过既然通过了那一轮面试,想来也不会偏差到哪去喵。ლ(╹ε╹ლ)
做法大概如下:
至于哈希值怎么算,方法有很多。最简单的就是设叶子节点一个哈希值,比如是 md5("")
,然后每次非叶子节点的哈希值就用 md5(LEFT_HASH + RIGHT_HASH)
来计算。大家也可以自己随便想一种方法来做就好了。
很多人可能不解了,明明是用 md5
,这篇文章是讲哈希,有毛线关系。(╯°O°)╯┻━┻
实际上 md5
就是一种哈希算法,而且是非常经典的哈希算法。
典型的哈希算法包括 MD2、MD4、MD5 和 SHA-1 等。当然不局限于这些,对于数字来说,取模也算是哈希算法,对于字符串状态转整数状态哈希来说还有诸如 BKDR、ELF 等等。
如果大家想多了解一些字符串转数字哈希的算法,可以参考一下 BYVoid 的这篇《各种字符串Hash函数比较》,或者想直接在 Node.js 里面使用的小伙伴们可以光顾下这个包——bling-hashes。
初步的轮廓已经明晰了,说白了就是将每个节点的哈希全算出来,如果是父亲节点就用子节点的哈希拼接起来再哈希一遍。σ`∀´)σ
把这些哈希算出来之后放在一个散列表里面待查。如果一个算出来的哈希跟之前已有的哈希值相等,那么就是说这个节点跟那个节点为根节点的子树有可能完全相等。
注意:有可能完全相等。
注意:只是有可能完全相等。
注意:重要的事情说三遍,只是有可能完全相等。
哈希是存在着一定的冲突概率的,所以说两个相等的哈希所检索到的源不一定一样,所以我们根据这些计算到的哈希建立哈希表,然后把表中同哈希值的子树再两两同时遍历一遍以检验是否相等。
false
。至此为止,我们可以看出大概是两大步——计算各子树的哈希值和验证各同哈希子树的相等性。不过稍微变通一下,我们就可以在计算出哈希值的时候就去跟以前的对比了。
实际上上面的做法还有一个优化的方案,不过跟哈希相关性已经基本上很小了。不过还是跟解决冲突有一丢丢的关系的,没兴趣的童鞋也可以直接跳过了。(๑•́ ₃ •̀๑)
由于子树哈希值是存在一定的冲突概率的,所以两个同哈希的子树不一定相同。那么我们如果能一眼看出这样的两棵子树是不相等的,就可以省略验证这一个递归的步骤了。
这里有一种最显而易见的情况我们是可以忽略省略步骤的,那就是深度。
如果两棵子树两两完全相等,那么说明这俩基佬的深度(或者说高度)是一样的,如果连深度都不一样了还如何愉快搞基——所以说如果有两个相等哈希值的子树的深度不一样的话可以直接略过验证步骤了。
那么就可以这么做:
0
,然后每往上一层加一。以上步骤在遍历计算哈希的时候顺便也做了,这样就多了一个验证标记了。
所以差不多就这样了,浅尝辄止。( ˘・з・)
就上述的场景来说,哈希非常好地将一个非常复杂的状态转化成一个可以检索的状态。本来毫无头绪的一个问题使用了哈希之后就完全变成了一个检索加验证的过程了。
这个问题就是我在大搜车中确实遇到的场景了。大家也不需要知道什么是报告图,就当它是一个代号了。
要做的事情大概就是说给定一个报告,我们根据报告的各个细节选定各种图层然后揉成一团叠加在一起形成最后一个结果图。
其实本来就有个系统在做这件事情的——每来一个报告就生成一张图,然后存储好之后给前端使用。
我做的事情是将逻辑迁移到另一套计算密集型任务集中处理系统中去。(´艸`)
其实生成这样一张图片的逻辑是 CPU 计算密集型的逻辑,所以比较耗费资源和时间的,那么我们就能在这上面做点手脚优化一下。
首先我们要知道的是,有哪些图层是固定的,所以其实这算半个排列组合的问题了。
不过我们也知道排列组合的增长性非常快,更何况我这里有约 100 个图层选择,所以可能性非常多,一下子全生成好不可能。
那么就可以用哈希和懒惰的思想来实现了。(ˇωˇ人)
虽然报告是有无限种可能的,但是把报告转成图层数据之后,拥有完全一样的图层数据的报告就可以用同一张图片了,这样就可以大大节省空间和时间了。
其实大概的步骤非常简单:
md5
计算一下)如果大家想知道“按某种算法重新生成哈希”里面“某种算法”的话可以看看下面的瞎狗眼的说明了。(ノ◕ヮ◕)ノ*:・゚✧
其实很简单,把图层数据的这个字符串加某个固定字符当小尾巴,如果哈希还是冲突则继续加这个小尾巴,直到计算出来的哈希不冲突为止。
比如我就用了这字符当小尾巴——🀣(麻将牌中的兰)。(♛‿♛)
在这种场景中,我把哈希拿来作检索某种报告图是否已经生成的用途。如果没有生成则生成一张,如果已经生成则直接拿已有的报告图去用。
至少比原来的来一张报告就生成一张图片来得快,并且省空间——相当于作冗余处理了。
事实上在很多的网盘系统中也有作冗余处理的。你以为你有多少多少 T 的空间,实际上相同的文件最终在网盘系统里面只存一份(不过排除备份的那些),而我相信做这些冗余判断的原理就是哈希了,SHA-1 也好 MD5 也好,反正就是这样。
上面网盘的冗余处理原理也只是我的猜测,我没在那些厂子里面工作过所以不能说就是就是这样子的。欢迎指正。。゚ヽ(゚´Д`)ノ゚。
这是我来这边工作后的另一个小插曲了,遇到一个主键生成的小需求。
有一个数据要插入到数据库,所以要给它生成一个主键,但是需求比较奇葩,可能是历史遗留问题吧。(눈‸눈)
10
、11
、12
等。如果是 前缀 + 随机数
的冲突概率会比较大的,所以还是用哈希来搞。
非常简单。首先前缀是固定的,我们就不管了,然后我根据这次进来的数据拼接成字符串(数据不会完全一样的),加上一点随机盐,然后用字符串哈希计算一遍,加上前导零,加上当前时间戳的后几位拼接起来,最后接上前缀就好了。
这个 generate
函数看起来就像这样子:
var bling = require("bling-hashes"); |
注意:这里的
bling
就是上面提到过的那个 bling-hashes,采用了BKDR
算法来计算哈希。以及Number.prototype.pad
函数是我邪恶得使用了 SugarJs 里面的函数,就是加上前导零的意思。如果受“千万不要修改原型链”影响较深地童鞋别学我哦。bodyParamStr
是前端传过来的 Raw Form Data,它看起来像"data1=1&data2=2&..."
。
最后得到的这个字符串是我们所要的主键了。。:.゚ヽ(*´∀`)ノ゚.:。
不过要注意的是,这个主键仍然又冲突的可能性,所以一旦冲突了(无论是自己检测到的还是插入数据库的时候疼了)就需要再生产一遍。就目前来说再生成的时候毫秒时间戳后三位会不一样,所以问题不大,允许存在的误差——毕竟不是那种分分钟集千万条的数据,肯定在 int
范围内。如果到时候真出问题了再改进。
这里的哈希是用在生成基本上没有碰撞的主键身上,感觉效果也是非常不错的——前提是你也有这种奇葩需求。
本文大致介绍了哈希的几种用途,有可能是大家熟知的用途,也有可能是巧用,总之就是说了为什么我要用哈希。
在编程中,无论是实际用途还是自己玩玩的题目,多动动脑子就会出来一些“奇技淫巧”。哈希也好,别的东西也罢,反正都是为了解决问题的——千万别因为实际开发中通常性的“并没有什么卵用”而去忽视它们,虽然哈希已经是够常用的了。(๑•ૅω•´๑)
]]>目前的一个架构导致的结果就是时间越久,数据本体与搜索引擎索引中的数据越不同步,相差甚大。
新的一个架构打算从 MySQL 的 Binlog 中读取数据更新、删除、新增等历史记录,并把相应信息提取出来丢到队列中慢慢去同步。
所以我就在这里小小去了解一下 Binlog。
MySQL Server 有四种类型的日志——Error Log、General Query Log、Binary Log 和 Slow Query Log。
第一个是错误日志,记录 mysqld 的一些错误。第二个是一般查询日志,记录 mysqld 正在做的事情,比如客户端的连接和断开、来自客户端每条 Sql Statement 记录信息;如果你想准确知道客户端到底传了什么瞎 [哔哔] 玩意儿给服务端,这个日志就非常管用了,不过它非常影响性能。第四个是慢查询日志,记录一些查询比较慢的 SQL 语句——这种日志非常常用,主要是给开发者调优用的。
剩下的第三种就是 Binlog 了,包含了一些事件,这些事件描述了数据库的改动,如建表、数据改动等,也包括一些潜在改动,比如 DELETE FROM ran WHERE bing = luan
,然而一条数据都没被删掉的这种情况。除非使用 Row-based logging,否则会包含所有改动数据的 SQL Statement。
那么 Binlog 就有了两个重要的用途——复制和恢复。比如主从表的复制,和备份恢复什么的。
通常情况 MySQL 是默认关闭 Binlog 的,所以你得配置一下以启用它。
启用的过程就是修改配置文件 my.cnf
了。
至于 my.cnf
位置请自行寻找。例如通过 OSX 的 brew
安装的 mysql
默认配置目录通常在
/usr/local/Cellar/mysql/$VERSION/support-files/my-default.cnf
这个时候需要将它拷贝到 /etc/my.cnf
下面。
紧接着配置 log-bin
和 log-bin-index
的值,如果没有则自行加上去。
log-bin=master-bin |
这里的 log-bin
是指以后生成各 Binlog 文件的前缀,比如上述使用 master-bin
,那么文件就将会是 master-bin.000001
、master-bin.000002
等。而这里的 log-bin-index
则指 binlog index 文件的名称,这里我们设置为 master-bin.index
。
如果上述工作做完之后重启 MySQL 服务,你可以进入你的 MySQL CLI 验证一下是否真的启用了。
$ mysql -u $USERNAME ... |
然后在终端里面输入下面一句 SQL 语句:
SHOW VARIABLES LIKE '%log_bin%'; |
如果结果里面出来这样类似的话就表示成功了:
+---------------------------------+---------------------------------------+ |
更多的一些相关配置可以参考这篇《MySQL 的 binary log 初探》。
然后你就可以随便去执行一些数据变动的 SQL 语句了。当你执行了一堆语句之后就可以看到你的 Binlog 里面有内容了。
如上表所示,log_bin_basename
的值是 /usr/local/var/mysql/master-bin
就是 Binlog 的基础文件名了。
那我们进去看,比如我的这边就有这么几个文件:
很容易发现,里面有 master-bin.index
和 master-bin.000001
两个文件,这两个文件在上文中有提到过了。
我们打开那个 master-bin.index
文件,会发现这个索引文件就是一个普通的文本文件,然后列举了各 binlog 的文件名。而 master-bin.000001
文件就是一堆乱码了——毕竟人家是二进制文件。
索引文件就是上文中的 master-bin.index
文件,是一个普通的文本文件,以换行为间隔,一行一个文件名。比如它可能是:
master-bin.000001 |
然后对应的每行文件就是一个 Binlog 实体文件了。
Binlog 的文件结构大致由如下几个方面组成。
文件头由一个四字节 Magic Number,其值为 1852400382
,在内存中就是 "\xfe\x62\x69\x6e"
,参考 MySQL 源码的 log_event.h,也就是 '\0xfe' 'b' 'i' 'n'
。
与平常二进制一样,通常都有一个 Magic Number 进行文件识别,如果 Magic Number 不吻合上述的值那么这个文件就不是一个正常的 Binlog。
在文件头之后,跟随的是一个一个事件依次排列。每个事件都由一个事件头和事件体组成。
事件头里面的内容包含了这个事件的类型(如新增、删除等)、事件执行时间以及是哪个服务器执行的事件等信息。
第一个事件是一个事件描述符,描述了这个 Binlog 文件格式的版本。接下去的一堆事件将会按照第一个事件描述符所描述的结构版本进行解读。最后一个事件是一个衔接事件,指定了下一个 Binlog 文件名——有点类似于链表里面的 next
指针。
根据《[High-Level Binary Log Structure and Contents](High-Level Binary Log Structure and Contents)》所述,不同版本的 Binlog 格式不一定一样,所以也没有一个定性。在我写这篇文章的时候,目前有三种版本的格式。
实际上还有一个 v2 版本,不过只在早期 4.0.x 的 MySQL 版本中使用过,但是 v2 已经过于陈旧并且不再被 MySQL 官方支持了。
通常我们现在用的 MySQL 都是在 5.0 以上的了,所以就略过 v1 ~ v3 版本的 Binlog,如果需要了解 v1 ~ v3 版本的 Binlog 可以自行前往上述的《High-level…》文章查看。
一个事件头有 19 字节,依次排列为四字节的时间戳、一字节的当前事件类型、四字节的服务端 ID、四字节的当前事件长度描述、四字节的下个事件位置(方便跳转)以及两字节的标识。
用 ASCII Diagram 表示如下:
+---------+---------+---------+------------+-------------+-------+ |
也可以字节编造一个结构体来解读这个头:
struct BinlogEventHeader |
如果你要直接用这个结构体来读取数据的话,需要加点手脚。
因为默认情况下 GCC 或者 G++ 编译器会对结构体进行字节对齐,这样读进来的数据就不对了,因为 Binlog 并不是对齐的。为了统一我们需要取消这个结构体的字节对齐,一个方法是使用
#pragma pack(n)
,一个方法是使用__attribute__((__packed__))
,还有一种情况是在编译器编译的时候强制把所有的结构体对其取消,即在编译的时候使用fpack-struct
参数,如:
$ g++ temp.cpp -o a -fpack-struct=1
根据上述的结构我们可以明确得到各变量在结构体里面的偏移量,所以在 MySQL 源码里面(libbinlogevents/include/binlog_event.h)有下面几个常量以快速标记偏移:
而具体有哪些事件则在 libbinlogevents/include/binlog_event.h#L245 里面被定义。如有个 FORMAT_DESCRIPTION_EVENT
事件的 type_code
是 15、UPDATE_ROWS_EVENT
的 type_code
是 31。
还有那个 next_position
,在 v4 版本中代表从 Binlog 一开始到下一个事件开始的偏移量,比如到第一个事件的 next_position
就是 4,因为文件头有一个字节的长度。然后接下去对于事件 n 和事件 n + 1 来说,他们有这样的关系:
next_position(n + 1) = next_position(n) + event_length(n)
关于 flags 暂时不需要了解太多,如果真的想了解的话可以看看 MySQL 的相关官方文档。
事实上在 Binlog 事件中应该是有三个部分组成,header
、post-header
和 payload
,不过通常情况下我们把 post-header
和 payload
都归结为事件体,实际上这个 post-header
里面放的是一些定长的数据,只不过有时候我们不需要特别地关心。想要深入了解可以去查看 MySQL 的官方文档。
所以实际上一个真正的事件体由两部分组成,用 ASCII Diagram 表示就像这样:
+=====================================+ |
而这个 post-header
对于不同类型的事件来说长度是不一样的,同种类型来说是一样的,而这个长度的预先规定将会在一个“格式描述事件”中定好。
在上文我们有提到过,在 Magic Number 之后跟着的是一个格式描述事件(Format Description Event),其实这只是在 v4 版本中的称呼,在以前的版本里面叫起始事件(Start Event)。
在 v4 版本中这个事件的结构如下面的 ASCII Diagram 所示。
+=====================================+ |
这个事件的 type_code
是 15,然后 event_length
是大于等于 91 的值的,这个主要取决于所有事件类型数。
因为从第 76 字节开始后面的二进制就代表一个字节类型的数组了,一个字节代表一个事件类型的 post-header
长度,即每个事件类型固定数据的长度。
那么按照上述的一些线索来看,我们能非常快地写出一个简单的解读 Binlog 格式描述事件的代码。
如上文所述,如果需要正常解读 Binlog 文件的话,下面的代码编译时候需要加上
-fpack-struct=1
这个参数。
|
这个时候你得到的结果有可能就是这样的了:
1852400382 - �binpz� |
一共会输出 40 种类型(从 1 到 40),如官方文档所说,这个数组从 START_EVENT_V3
事件开始(type_code
是 1)。
跳转事件即 ROTATE_EVENT
,其 type_code
是 4,其 post-header
长度为 8。
当一个 Binlog 文件大小已经差不多要分割了,它就会在末尾被写入一个 ROTATE_EVENT
——用于指出这个 Binlog 的下一个文件。
它的 post-header
是 8 字节的一个东西,内容通常就是一个整数 4
,用于表示下一个 Binlog 文件中的第一个事件起始偏移量。我们从上文就能得出在一般情况下这个数字只可能是四,就偏移了一个魔法数字。当然我们讲的是在 v4 这个 Binlog 版本下的情况。
然后在 payload
位置是一个字符串,即下一个 Binlog 文件的文件名。
由于篇幅原因这里就不详细举例其它普通的不同事件体了,具体的详解在 MySQL 文档中一样有介绍,用到什么类型的事件体就可以自己去查询。
本文大概介绍了 Binlog 的一些情况,以及 Binlog 的内部二进制解析结构。方便大家造轮子用——不然老用别人的轮子,只知其然而不知其所以然多没劲。
好了要下班了,就写到这里过吧。
不过前几天有一个小需求的东西可以提出来写一点点小干货儿跟大家分享分享。米娜桑会的就可以忽略了,反正我也是随便写的;如果觉得本文对你有用的话还请多多支持喵。(●´ω`●)ゞ
本文所说的定时任务或者说计划任务并不是很多人想象中的那样,比如说每天凌晨三点自动运行起来跑一个脚本。这种都已经烂大街了,随便一个 Crontab 就能搞定了。
这里所说的定时任务可以说是计时器任务,比如说用户触发了某个动作,那么从这个点开始过二十四小时我们要对这个动作做点什么。那么如果有 1000 个用户触发了这个动作,就会有 1000 个定时任务。于是这就不是 Cron 范畴里面的内容了。
举个最简单的例子,一个用户推荐了另一个用户,我们定一个二十四小时之后的任务,看看被推荐的用户有没有来注册,如果没注册就给他搞一条短信过去。Σ>―(〃°ω°〃)♡→
一开始我是想把这个计时器做在内存里面直接调用的。
考虑到 Node.js 的定时并不是那么准确(无论是 setTimeout
还是 setInterval
),所以本来打算自己维护这个定时器队列。
又考虑到 Node.js 原生对象比较耗内存。之前我用 JSON
对象存了一本字典,约十二万多的词条,原文件大概也就五六兆,用 Node.js 的原生对象一存居然有五六百兆的内存占用——所以打算这个定时器队列用 C++ 来写 addon。
考虑到任何时候插入的任务都有可能在已有的任务之前或者之后,所以本来想用 C++ 来写一个小根堆。每次用户来一个任务的时候就将这个任务插入到堆中。
如果按照上述方法的话,再加上对时间要求掐得也不是那么紧,于是就是一个不断的 process.nextTick()
的过程。
在 process.nextTick()
当中执行这么一个函数:
process.nextTick()
来让下一个 tick 执行步骤 1 中的流程。 所以最后就是一边往小根堆插入任务,另一边通过不断 process.nextTick()
消费任务的这么一个过程。
最后,为了考虑到程序重启的时候内存数据会丢失,还应该做一个持久化的事情——在每次插入任务的时候顺便往持久化中间件中插一条副本,比如 MySQL、MongoDB、Redis、Riak 等等任何三方依赖。消费任务的时候顺便把中间件中的这条任务数据给删除。
也就是说中间件中永远存的就是当前尚未完成的任务。每当程序重启的时候都先从中间件中把所有任务读取进来重建一下堆,然后就能继续工作了。
如果当时我没有发现 Redis 的这个妙用的话,上述的流程将会是我实现我们定时任务的流程了。
在 Redis 的 2.8.0 版本之后,其推出了一个新的特性——键空间消息(Redis Keyspace Notifications),它配合 2.0.0 版本之后的 SUBSCRIBE
就能完成这个定时任务的操作了,不过定时的单位是秒。
Redis 在 2.0.0 之后推出了 Pub / Sub 的指令,大致就是说一边给 Redis 的特定频道发送消息,另一边从 Redis 的特定频道取值——形成了一个简易的消息队列
比如我们可以往 foo
频道推一个消息 bar
,那么就可以直接:
PUBLISH foo bar |
另一边我们在客户端订阅 foo
频道就能接受到这个消息了。
举个例子,如果在 Node.js 里面使用 ioredis 这个包那么看起来就会像这样:
var Redis = require("ioredis"); |
在 Redis 里面有一些事件,比如键到期、键被删除等。然后我们可以通过配置一些东西来让 Redis 一旦触发这些事件的时候就往特定的 Channel 推一条消息。
本文所涉及到的需求的话我们所需要关心的事件是 EXPIRE
即过期事件。
大致的流程就是我们给 Redis 的某一个 db 设置过期事件,使其键一旦过期就会往特定频道推消息,我在自己的客户端这边就一直消费这个频道就好了。
以后一来一条定时任务,我们就把这个任务状态压缩成一个键,并且过期时间为距这个任务执行的时间差。那么当键一旦到期,就到了任务该执行的时间,Redis 自然会把过期消息推去,我们的客户端就能接收到了。这样一来就起到了定时任务的作用。
当达到一定条件后,有两种类型的这种消息会被触发,用哪个需要自己选了。举个例子,我们删除了在 db 0 中一个叫 foo
的键,那么系统会往两个频道推消息,一个是 del
事件频道推 foo
消息,另一个是 foo
频道推 del
消息,它们小俩口被系统推送的指令分别等价于:
PUBLISH __keyspace@0__:foo del |
其中往 foo
推送 del
的频道名为 __keyspace@0__:foo
,即是 "__keyspace@" + DB_NUMBER + "__:" + KEY_NAME
;而 del
的频道名为 "__keyevent@" + DB_NUMBER + "__:" + EVENT_NAME
。
即使你的 Redis 版本达标了,但是 Redis 默认是关闭这个功能的,你需要修改配置文件来打开它,或者直接在 CLI 里面通过指令修改。这里就说说配置文件的修改吧。
如果不想看我在这里罗里吧嗦的,也可以直接去看 Redis 的相关文档。
首先打开 Redis 的配置文件,在不同的系统和安装方式下文件位置可能不同,比如通过 brew
安装的 MacOS 下可能是在 /usr/local/etc/redis.conf
下面,通过 apt-get
安装的 Ubuntu 下可能是在 /etc/redis/redis.conf
下,总之找到配置文件。或者自己写一个配置文件,启动的时候指定配置文件地址就好。
然后找到一项叫 notify-keyspace-events
的地方,如果找不到则自行添加,其值可以是 Ex
、Klg
等等。这些字母的具体含义如下所示:
keyspace
事件,有这个字母表示会往 __keyspace@<db>__
频道推消息。keyevent
事件,有这个字母表示会往 __keyevent@<db>__
频道推消息。DEL
、EXPIRE
、RENAME
等等。EXPIRE
不同的是,g 的 EXPIRE
是指执行 EXPIRE key ttl
这条指令的时候顺便触发的事件,而这里是指那个 key
刚好过期的这个时间点触发的事件。key
由于内存上限而被驱逐的时候会触发的事件。g$lshzxe
的别名。也就是说 AKE
的意思就代表了所有的事件。 结合上述列表我们就能拼凑出自己所需要的事件支持字符串了,在我的需求中我只需要 Ex
就可以满足了,所以配置项就是这样的:
notify-keyspace-events Ex |
然后保存配置文件,启动 Redis 就启用了过期事件的支持了。
我们先说任务的创造者吧。由于这里 Redis 的事件只会传键名,并不会传键值,而过期事件触发的时候那个键已经没了,你也无法获取键值,加上我的主系统和任务系统是分布式的,所以就把所有需要的信息往键名塞。
一个最简单的键名设计就是 任务类型 + ":" + JSON.stringify 化后的参数数组
;更有甚者可以直接把任务类型替换成所需的函数路径,比如需要执行这个任务的函数在 task/foo/bar
文件下面的 baz
函数,参数 arguments
数组为 [ 1, 2 ]
,那么键名的设计可以是 task/foo/bar.baz:[1,2]
,反正我们只需要触发这个键,用不着去查询这个键。等到真正过期了任务系统接收到这个键名的时候再一一解析,得到需要执行 task/foo/bar.baz
这个消息,并且网函数里面传入 [1,2]
这个 arguments
。
所以当接收到一个定时任务的时候,我们得到消息、函数名、过期时间参数,这个函数可以如下设计:
/** 我们假设 redis 是一个 ioredis 的对象 */ |
Ioredis 的稳定可以点此查看。
然后在任务系统里面的一开始监听这个过期频道:
// assign 是 sugarjs 里面的函数 |
注意: 我们这里选择 db 1 是因为一旦开启过期事件监听,那么这个 db 的所有过期事件都会被发送。为了不跟正常使用的 redis 过期键混淆,我们为这个事情专门用一个新的 db。比如我们在自己正常使用的 db 0 里面监听了,那么不是我们任务触发的过期事件也会传过来,这个时候我们解析的键名就不对了。
最后就是我们的 sampleOnExpired
函数了。
var sampleOnExpired = function(channel, key) { |
这个简易的架子搭好后,你只需要去写一堆任务执行函数,然后在生成任务的时候把相应参数传给 sampleTaskMaker
就好了。Redis 会自动过期并且触发事件给你的 sampleOnExpired
函数,然后就会去执行相应的任务处理函数了。
其实这个需求在我们项目目前就是给用户定时发提醒短信用的。如果没有发现 Redis 的这个妙用,我还是会去用第二节里面的方法来写的。其实这期间也有考虑过用 RabbitMQ,不过貌似它的定时消息需要做一些 Hack,比较麻烦,最后就放弃了。
Redis 的这个方法其实是我在谷歌搜出来的,别人在 StackOverflow 回答的答案。我参考了之后用我自己的方法实现了出来,并且把代码的关键部分提取出来整理成这篇小文,还希望能给各位看官一些用吧,望打赏。
如果没有什么用也憋喷我,毕竟我是个蒟蒻。有更好的方法希望留个言,望告知。谢谢。(´,,•ω•,,)♡
]]>其实这是花瓣的一个入库系统结构图,蕾米莉亚是这个项目的名字。
设计得不好,纯属做归档。
其中 SanaeHDCS 是另一套系统,给 RemiliaHDPS 提供数据的。
主要分为 Bathtub,Dryer,Vampire 三个部分。
由 SanaeHDCS 提供的数据,存储在 MongoDB 当中。
将 Bathtub 出来的湿数据变成干货的解析器,根据不同的数据用不同的规则进行解析。
全名其实是 Vampire Coffin,只不过把这个写到项目里面看着貌似不是很吉利,于是取了前半部分。吹轰机处理好的干活会存储在这边,实际上也是
MongoDB 里面。然后 Vampire 提供给外部接口,让其能够用正确的姿势获取正确的干货数据。
一个视窗。
假的红魔馆,里面一堆 Puppet。
每个 Puppet 都有自己的属性、人格、作息时间和生活。
重新设计了 Remilia 结构图。
好吧还是我的脑洞太大了。我知道你们看着这货不知所云。
好吧忘了这个东西吧,我只是无聊发一篇而已。
]]>由于某些原因,我写了个很搓的内存池(C 版本的)。
然后我想到了把之前写的一个 Node.js 包 thmclrx 的更挫的“伪·内存池”用新写的内存池去替换掉。(❛◡❛✿)
然后问题就来了,我貌似不能控制 node-gyp 去用 G++ 编译 *.c
文件,这样的话所有文件编译好之后链接 *.o
文件会出问题。虽然链接的时候没报错,但是使用的时候就会报这么个错 (;´༎ຶД༎ຶ`):
➜ thmclrx git:(master) ✗ node test/test.js |
大致意思就是说在我编译好链接好的 thmclrx.node
中找不到 __Z16xmem_create_poolj
这个符号,也就是说 xmempool.o
这个用 C 编译出来的文件并没有正确被链接。
一开始我想找的是“如何在 node-gyp 中手动选择编译器”,即不让机器自动选择 GCC 去编译 *.c
文件。后来无果。ル||☛_☚|リ
再后来我想开了,于是决定让编译的时候去识别我在跟 C 说话还是跟 C++ 说话。(ノ◕ヮ◕)ノ*:・゚✧
于是我找到了这么个帖子:http://grokbase.com/t/gg/nodejs/14amregx72/linking-c-sources-files-in-cc-files
他貌似也遇到了跟我相似的问题。下面这个提问者自己提出了这样的回答:
Nevermind, found my own answer after finally hitting the right google search terms.
Added
extern "C" {
//... source code here...
}
So that the CPP compiler would know I was talking C and not CPP :)
答案的大意就是在你的 C 头文件中添加上面 blahblah 一大段宏,好让 C++ 的编译器知道它是在跟 C 的中间文件交流而不是 C++,这样的话链接的时候就能正常接轨了。于是我在我的新版 xmempool 的头文件里面就已经添加上了这两段话了。
其实以前我也老在别的项目里面看到这个 #ifdef __cplusplus
的宏定义,只不过以前不知道什么意思。
今天通过这么一件事情终于知道了它的用途了,新技能 get √。
ε(*´・∀・`)з゙
]]>在主题色提取的过程中,要把颜色加入搜索引擎。但是如果是真彩色任意值加进去的话,对于搜索的时候来说无疑是一个复杂的操作。搜索条件要各种计算距离什么的。
于是一个妥协的做法就是提供一套调色板,保证所有颜色都被吸纳到调色板中的某一色值当中。
那么这个时候调色板的覆盖率以及距离什么的就比较重要了。本文就讲如何生成一套看起来还不错的自用“标准色板”。
一开始我用了一套 256 色的色板,不知道哪里搞来的 Windows 色板。
由于颜色太多,不好贴代码,我就直接把链接贴过来了:
这一套色板大致的效果如下:
我指的更好并不一定真的比之前找到的 256 色要好,毕竟上面那个是人家智慧和劳动的结晶。我指的更好是颜色更多,但是偏差又不会太大。
理论上我们能按照那种规则生成比真彩色少的任意种数的色板。
这里有必要重新普及下 N 多种色彩模式中的其中两种,也就是我们今天生成一个色板所用到的两种模式。
这个大家都已经耳熟能详了,无非是 RGB 通道中的分量结合起来生成的一种颜色。
RGB 色彩模式是工业界的一种颜色标准,是通过对红 (R)、绿 (G)、蓝 (B)三个颜色通道的变化以及它们相互之间的叠加来得到各式各样的颜色的,RGB 即是代表红、绿、蓝三个通道的颜色,这个标准几乎包括了人类视力所能感知的所有颜色,是目前运用最广的颜色系统之一。
使用 RGB 模型为图像中每一个像素的 RGB 分量分配一个 0 ~ 255 范围内的强度值。RGB 图像只使用三种颜色,就可以使它们按照不同的比例混合,在屏幕上呈现 16777216 (
256 * 256 * 256
) 种颜色。
HSL 色彩模式是工业界的一种颜色标准,是通过对色相 (H)、饱和度 (S)、明度 (L) 三个颜色通道的变化以及它们相互之间的叠加来得到各式各样的颜色的,HSL 即是代表色相,饱和度,明度三个通道的颜色,这个标准几乎包括了人类视力所能感知的所有颜色,是目前运用最广的颜色系统之一。
HSL 色彩模式就是今天的主角了。我们将会用 HSL 生成一张类似下图的色板,而色板的粒度将会与你决定色板的颜色数量相关:
为了简化代码,我们暂时不考虑饱和度,也就是说所有颜色让它饱和度都为 **100%**。
而且实际上色相是在一个圆里面的 0° ~ 360°,那么也就是说我们只需要做两步就是了:
在这里我定了一个步长:色相以 10° 为一个步长,明度以 5% 为一个步长。并且剔除 RGB 相等的黑白灰色。
当然这里步长完全可以按照自己的喜好来。
我们以前端的 Javascript 为例,能想到下面的一段代码:
var count = 0; |
这里需要注意的是,实际上我明度的步长是 (100 / 22)
。然后 0
和 100
这两个明度我们另外拎出来,所以只取了 1 ~ 21 的明度。
剩下的就是跟刚才说的一样,各色相的各明度生成一个 HSL 颜色赋值给 background-color
。
接下去我们生成一个灰色条的色板,专治灰黑白。这个时候实际上我们可以直接用 RGB 搞定:
$("#palette").append("<br />"); |
最后把颜色输出到一个数组就好了。
这里有一点 happy 的是,就算你是用 HSL 来搞的背景色,用 jQuery 的
$(foo).css("background-color")
获取到的仍然是 RGB 值。
var colors = []; |
所以最后我们还需要初始的 HTML 了:
<textarea></textarea> |
效果的话这里能看到:
用 HSL 生成的色彩空间(色板)一个是表现力好,相对于 RGB 来说,好像更好知道如何去生成分部比较 OK 的一个色彩空间。
但是也有一个缺点,当我们不去管饱和度的时候,实际上我们还是丢失了一部分的颜色。好在本身我们生成色板也只是为了合并颜色,可以通过 k-D 树来快速寻找某个颜色在色板中是属于哪种色块的。当然,目前我们就是这么做的。
虽然里面水题居多,不过在上班比较空闲的档口 #带薪刷题# 的感觉还是蛮不错的。
高中的时候就跟 @MatRush 发现了一个名字超级好玩的编程语言叫 BrainF**k,它比较搞脑筋,因为所有的编程操作都是集合在操作符里面,然后控制指针偏移和内存值的修改来进行一系列操作。
这与后面发现的 HVM(Hack Virtual Machine)有异曲同工之妙。其实之前也出过一个“实现一个简易 HVM 解释器”的题目,所以在 CodeWars 看到这个题目的时候还感觉蛮亲切的。
问题很简单,就是让你实现一个函数来解释一句 BrainF**k 的语句,并且根据输入数据来输出相应的内容。
至于这题所需的 BrainF**k 的语法,大致如下:
>
: 指针右移一位。<
: 指针左移一位。+
: 当前指针所指的内存值加一,以 255 为界,溢出为 0,即 255 + 1 = 0
。-
: 当前指针所指的内存值减一,以 0 为界,溢出为 255,即 0 - 1 = 255
。.
: 输出当前指针所指的值,即输出该值 ASCII 码所对应的字符。,
: 从输入取一个字符转为 ASCII 码存入当前指针所指的内存。[
: 若当前指针所指的值为 0,则命令跳到该 [
匹配的结束 ]
符号位置的下一位置的指令。]
: 若当前指针所指的值不为 0,则指令向前跳到该 ]
匹配到的 [
符号位置的下一位置的指令。举个例子:
,+[-.,+] |
上面的句子大致就是说:
[
后面的位置——即第四步。说白了,就是不断获取输入的值,如果输入的值是 255,那么就跳出循环,否则原样输出。
明白了上面的题意之后就可以开始实现了,步骤大致上就是逐位遍历指令,然后一个 switch
来处理各种指令即可。
CodeWars 给了你一个函数原型,你在里面实现代码就好了:
function brainLuck(code, input){ |
在开始之前,我们做一些初始化工作,比如申明几个变量什么的:
[ 0 ]
。return
的字符串,即输出的值。所以接下去我们要把架子填成这样:
function brainLuck(code, input) { |
上面的 getMatchingBra
就是我们要实现的一个括号匹配函数了,思想就是用栈。
碰到前括号就把这个前括号的下标入栈;碰到后括号,就把栈顶元素即前括号的下标推出,这个时候括号匹配数组的这个前括号下标的值就是当前后括号的下标,而后括号下标的值就是前括号的下标了。
/** |
有了这个数组就可以随便跳了,如果指令第 i
位是一个括号(不管前括号还是后括号),那么它的匹配括号下标就是 matching[i]
了。
要处理指令的话实际上就是一个 while
语句不断循环指令,然后判断当前指令是什么然后做相应的事,最后指令位置加一就好了:
while(commandPos < code.length) { |
指针右移的话就把指针位置加一,如果内存数组还没当前指针位置的值的话 push
一个 0
就好了:
case '>': { |
左移就是减一,如果位置小于 0,那么内存数组从前推入一个值,并让指针等于 0。
case '<': { |
没什么好说的,内存加一就好了。
case '+': { |
减一。
case '-': { |
输出的话直接往 output
字符串里面加上当前指针的值就好了,注意要 ASCII 转变之后的字符。
case '.': { |
输入的话就让 input
当前位置的值变成 ASCII 存进当前指针,然后输入位置加一就好了。
case ',': { |
由于之前已经做好了匹配数组,所以我们只需要判断当前指针是不是 0,然后如果是就跳到匹配括号处。
case '[': { |
同上,只不过条件改一下而已。
case ']': { |
上面的函数体完成之后,我们只需要在最后把 output
给返回就好了:
return output; |
完成了上面七零八落的肢体之后,我们要把五马分尸的代码给凑回去,所以最后就长这个样子了:
function getMatchingBra(code) { |
艾玛,忘了放题目链接了:http://www.codewars.com/kata/526156943dfe7ce06200063e。以及大家如果有兴趣的话也可以去试试看写个 HVM 看看。
实际上本文实现的东西实用性几乎没有,只不过是抛砖引玉,让大家在做一些模拟题逻辑(或者说是简单模拟逻辑)的时候理清思路、按部就班,切忌自己乱了思路和逻辑。
]]> 题意大致就是你需要实现一个 Singleton
也就是单件模式的类,让其下面代码执行成功:
var obj1 = new Singleton(); |
并且还有很重要的一点就是 Singleton
的对象的 instanceof
也得的确是 Singleton
才行。
我们猜想 new Singleton()
的结果,如果 Singleton
函数也就是这个类的构造函数没返回值的话,直接会返回 this
,有返回值的话,那么就是等于其返回值了。
我们码下面的代码看一下:
var Singleton = function() { |
跑一遍之后我们的确发现了输出的值就是:
{ foo: "bar" } |
于是我这么做:
var foo = {}; |
结果上面的几个条件都符合了,不信大家可以自己输出一遍看看。
但是——
这东西不是一个 Singleton
的实例,它只是一个简单的 JSON
对象,所以还是无法通过。
答案有很多,CodeWar 上面每个人的解法都不一样,但是归根结底本质还是大同小异的。
就是第一次的时候先直接返回 this
,并且把 this
放在某个地方。以后每次来这里创建的时候返回之前存好的 this
即可:
var Singleton = function() { |
写法很多,我这里随意挑几个别人的答案吧。
/** |
/** |
一开始我在移动的宽带中。那个时候虽然还不是完全的局域网,但是电信网络访问不了我的外网 IP。又因为我需要一个 DDNS 服务来维持我的 kacaka.ca(目前暂失效)。
为了解决让电信网络也能访问我的 Web,于是我想到了免费 CDN 当中比较有名的 CloudFlare。而且它也有提供 API 让开发者自己开发通过他的服务解析域名的服务。
再然后,去年的九月份,我的早期 Node.js 作品 dloucflare 发布了。所以就有了这个帖子。
现在,我已经搬到电信了,然后旧版的貌似不能用了,因为 CloudFlare 貌似 API 都迁移到了 https
上面。然后我为了我的小伙伴们能访问我出租屋里的旧电脑,又重构了一遍这个项目。
首先安装最新的 dlouc-flare
包:
$ npm install dlouc-flare |
然后去创建一个 DF 对象:
var DloucFlare = require("dlouc-flare"); |
CloudFlare 如何使用的话这里就不多做解释了,至于 API KEY 的话,可以在这里获取到。
然后调用 df.dynamicDomains
函数去把你这个域名下面的一些子域名加入你这个脚本的动态域名范畴当中:
df.dynamicDomains([ "@", "www", "子域名3", "子域名4", ... ], 检测时间间隔); |
其中
"@"
代表的是域名没有www
前缀的本身。检测时间间隔以毫秒为单位。
事实上,你也可以自定义一个检测你当前主机的 IP 地址的函数(如果你不喜欢用包内的默认检测 IP 函数)。
只要你写一个函数:
function checkIp(callback) { |
然后覆盖掉默认的 IP 检测函数即可:
df.getIpFunction = checkIp; |
最后保存退出并用 node
执行你的程序就好了,程序就会开始欢快地跑了。
其实要完全自己写也是很简单的——无非就是调用一下 CloudFlare 的 API 而已。
我们定位明确就是要做 DDNS,所以没必要关系其它很多不相关的 API,只需要最基础的几个就够了。
所有 API 的基础 URI 都为:https://www.cloudflare.com/api_json.html。
根据 CloudFlare 文档所说,所有的提交都要黏上验证信息给 POST 过去。而验证的字段如下:
其操作名为 rec_load_all
,我们不关心其它不重要的参数,只需要再传一个 z
字段代表其域名就好了,举个例子:
var self = this; |
上述代码就是把 param
数据给 POST 到 API 的 RESTful 里面去。然后根据返回值进行解析。
关于
DNSRecordObject
的代码可以自行翻阅这里。以及 spidex 的文档在这里。
其操作名为 rec_edit
,如文档所说,除了固有的几个参数之外,我们还需要有如下参数:
z:
域名。id:
域名记录编号,从 rec_load_all
中获取。type:
记录类型。如 A
/ CNAME
等等。name:
子域名名,如果无前缀子域名则与域名相同。content:
值。如果我们只是做动态域名的话,这里的值就是 IP。service_mode:
服务类型,填原值即可。ttl
: TTL,填原值即可。上面参数的解说只是对于我们要做 DDNS 脚本而言的解释。
所以说在 dnsrecordobject.js 中我是这么做的:
var param = { |
上面的代码就能将你某个域名(
this.domain
)下的子域名this.name
的 IP 给修改成ip
了。
这种 API 网上就多了去了。
举个简单的例子,我的 dlouc-flare
的获取 IP 的 API 就是从
来的。
请求上面的地址之后,输出的内容(注意有换行符)就是你当前机子所在的网络的公网 IP 了。
类似的 API 还有很多:
有了上面的仨 API,一切都好说了,流程很简单:
CloudFlare
解析的域名下的子域名。有了上面的几个步骤,加上之前我们讲的几个 API,大家就能轻松加愉快地完成自己的 DDNS 脚本了。
当然,如果自己懒的话也可以用本文一开始的方法,使用 dlouc-flare
这个包,通过简单的编码就能实现自己的 DDNS 动态域名脚本了。
这里的定时器时间自己按需而定,就我自己而言,我是给设置了
1000 * 60
毫秒的间隔。
最早与动态域名结缘的时候是初中的时候,大概七八年前了吧,那个时候花生壳什么的,但是最终用的是 3322.org
。
其实基本的动态域名的原理很简单,无非就是本地开一个脚本,不停去探测本机 IP,一旦有变化就去解析服务器修改。
本人在这里抛砖引玉。如果哪里有别的解析商的 API,大家自己也可以举一反三,写什么 DNSPod 的动态域名,写什么 jiasule 的动态域名等等等等。
喵~ଘ(੭ˊᵕˋ)੭* ੈ✩‧₊˚
]]>所谓主题色提取,就是对于一张图片,近似地提取出一个调色板,使得调色板里面的颜色能组成这张图片的主色调。
以上定义为我个人胡诌的。
大家不要太把我的东西当成严谨的文章来看,很多东西什么的都是我用我自己的理解去做,并没有做多少考证。
解析中都会以 Node.js 来写一些小 Demo。
写该文章主要是为了对我这几天对于『主题色提取』算法研究进行一个小结。
花瓣网需要做一件事,就是把图片的主题色提取出来加入到花瓣网搜索引擎的索引当中,以供用户搜索。
于是有了一个需求:提取出图片中在某个规定调色板中的颜色,加入到搜索引擎。
接下去就开始解析两种不同的算法以及在这种业务场景当中的应用。
这个算法大家可以忽略,可能是我使用的姿势不对,总之提取出来(也许它根本就不是这么用的)的东西错误很大。
不过看一下也好开阔下眼界,尤其是我这种想做游戏又不小心掉进互联网的坑里的蒟蒻来说。
首先该算法我是从这里找到的。想当年我还是经常逛 GameRes 的。ヾ(;゚;Д;゚;)ノ゙
然后辗转反侧最终发现这段代码是提取自 Allegro 游戏引擎。
具体我也就不讲了,毕竟找不到资料,只是粗粗瞄了眼代码里面有几个魔法数字(在游戏和算法领域魔法数字倒是非常常见的),也没时间深入解读这段代码。
我把它翻译成了 Node.js,然后放在了 Demo 当中。大家有兴趣可以自己去看看。
这个算法在颜色量化中比较常见的。
该算法最早见于 1988 年,*M. Gervautz*** 和 *W. Purgathofer*** 发表的论文**《A Simple Method for Color Quantization: Octree Quantization》**当中。其时间复杂度和空间复杂度都有很大的优势,并且保真度也是非常的高。
大致的思路就是对于某一个像素点的颜色 R / G / B 分开来之后,用二进制逐行写下。
如 #FF7800
,其中 R 通道为 0xFF
,也就是 255
,G 为 0x78
也就是 120
,B 为 0x00
也就是 0
。
接下去我们把它们写成二进制逐行放下,那么就是:
R: 1111 1111 |
RGB 通道逐列黏合之后的值就是其在某一层节点的子节点编号了。每一列一共是三位,那么取值范围就是 0 ~ 7
也就是一共有八种情况。这就是为什么这种算法要开八叉树来计算的原因了。
举个例子,上述颜色的第一位黏合起来是 100(2)
,转化为十进制就是 4,所以这个颜色在第一层是放在根节点的第五个子节点当中;第二位是 110(2)
也就是 6,那么它就是根节点的第五个儿子的第七个儿子。
于是我们有了这样的一个节点结构:
var OctreeNode = function() { |
isLeaf
: 表明该节点是否为叶子节点。pixelCount
: 在该节点的颜色一共插入了几次。red
: 该节点 R 通道累加值。green
: G 累加值。blue
: B 累加值。children
: 八个子节点指针。next
: reducible 链表的下一个节点指针,后面会作详细解释,目前可以先忽略。根据上面的理论,我们大致就知道了往八叉树插入一个像素点颜色的步骤了。
就是每一位 RGB 通道黏合的值就是它在树的那一层的子节点的编号。
大致可以看下图:
图片来源:http://www.microsoft.com/msj/archive/S3F1.aspx
由此可以推断,在没有任何颜色合并的情况下,插入一种颜色最坏的情况下是进行 64 次检索。
注意:我们将会把该颜色的 RGB 分量分别累加到该节点的各分量值中,以便最终求平均数。
大致的流程就是从根节点开始 DFS,如果到达的节点是叶子节点,那么分量、颜色总数累加;否则就根据层数和该颜色的第层数位颜色黏合值得到其子节点序号。若该子节点不存在就创建一个子节点并与该父节点关联,否则就直接搜下一层去。
创建的时候根据层数来确定它是不是叶子节点,如果是的话需要标记一下,并且全局的叶子节点数要加一。
还有一点需要注意的就是如果这个节点不是叶子节点,就将其丢到 reducible 相应层数的链表当中去,以供之后颜色合并的时候用。关于颜色合并的内容后面会进行解释。
下面是创建节点的代码:
function createNode(idx, level) { |
以及下面是插入某种颜色的代码:
function addColor(node, color, level) { |
这一步就是八叉树的空间复杂度低和保真度高的另一个原因了。
勿忘初心。
我们用这个算法做的是颜色量化,或者说我要拿它提取主题色、调色板,所以肯定是提取几个有代表性的颜色就够了,否则茫茫世界中 RRGGBB 一共有 419430400 种颜色,怎么归纳?
我们可以让指定一棵八叉树不超过多少多少叶子节点(也就是最后能归纳出来的主题色数),比如 8,比如 16、64 或者 256 等等。
所以当叶子节点数超过我们规定的叶子节点数的时候,我们就要合并其中一个节点,将其所有子节点的数据都合并到它身上去。
举个例子,我们有一个节点有八个子节点,并且都是叶子节点,那么我们把八个叶子节点的通道分量全累加到该节点中,颜色总数也累加到该节点中,然后删除八个叶子节点并把该节点设置为叶子节点。那么一下子我们就合并了八个节点有木有!
为什么这些节点可以被合并呢?
我们来看看某个节点下的子节点颜色都有神马相似点吧——它们的三个分量前七位(或者说如果已经不是最底层的节点的话那就是前几位)是相同的,那么比如说刚才的 FF7800
,跟它同级并且拥有相同父节点(也就是它的兄弟节点)的颜色都是什么呢:
R: 1111 111(0,1) |
整合出来一看:
FE7800 |
怎么样?是不是确实很相近?这就是八叉树的精髓了,所有的兄弟节点肯定是在一个相近的颜色范围内。
所以说我们要合并就先去最底层的 reducible 链表中寻找一个可以合并的节点,把它从链表中删除之后合并叶子节点并且删除其叶子节点就好了:
function reduceTree() { |
这样一来,就合并了一个最深层次的节点了,如果满打满算的话,一次合并最多会烧掉 7 个节点(我大 FFF 团壮哉)。
上面的函数都有了,我们可以开始建树了。
实际上建树的过程就是遍历一遍传入的像素颜色信息,对于每个颜色都插入到八叉树当中;并且每一次插入之后都判断下叶子节点数有没有溢出,如果满出来的话需要及时合并。
function buildOctree(pixels, maxColors) { |
整棵树建好之后,我们应该得到了最多有 maxColors
个叶子节点的高保真八叉树。其根节点为 root
。
有了这么一棵八叉树之后我们就可以从里面提取我们想要的东西了。
主题色提取实际上就是把八叉树当中剩下的叶子节点 RGB 通道分量求平均,求出来的就是近似的主题色了。(也许有更好的,不是求平均的方法能获得更好的主题色结果,但是我没有深入去研究,欢迎大家一起来指正 (❀╹◡╹))
于是我们深度遍历这棵树,每遇到叶子节点,就求出颜色加入到我们所存结果的数组或者任意数据结构当中了:
function colorsStats(node, object) { |
八叉树主题色提取算法提取出来的主题色是一个无固定调色板(Non-palette)的颜色群,它有着对原图的尽量保真性,但是由于没有固定的调色板,有时候对于搜索或者某种需要固定值来解释的场景中还是欠了点火候。但是活灵活现非它莫属了。比如某种图片格式里面预先存调色板然后存各像素的情况下,我们就可以用八叉树提取出来的颜色作为该图片调色板,能很大程度上对这张图片进行保真,并且图片颜色也减到一定的量。
该算法的完整 Demo 大家可以在我的 Github 当中找到。
这是一个非常简单又实用的算法。
大致的思想就是给定一个调色板,过来一个颜色就跟调色板中的颜色一一对比,取最小差值的那个调色板里的颜色作为这个颜色的代表。
对比的过程就是分别将 R / G / B 通道的值两两相减取绝对值,将三个差相加,得到的这个值就是颜色差值了。
反正最后就是调色板中哪个颜色跟对比的颜色差值最小,就拿过来就是了。
var best = 0; |
八叉树的缺点我在之前的八叉树小结中提到过了,就是颜色不固定。对于需要有一定固定值范围的主题色提取需求来说不是那么合人意。
而最小差值法的话又太古板了。
于是我的做法是将这两种算法都过一遍。
比如我要将一张图片提取出少于 256 种颜色,我会用八叉树过滤一遍得出保证的两百多种颜色,然后拿着这批颜色和其数量再扔到最小插值法里面将颜色规范化一遍,得出的最终结果可能就是我想要的结果了。
这期间第二步的效率可以忽略不计,毕竟如果是上面的需求的话第一步的结果也就那么两百多种颜色。
这个方法我已经实现并且用在我自己的颜色提取包 thmclrx 里了。大致的代码可以看这里。
在这几天的辛勤劳作下,总算完成了某种意义上我的第一个 Node.js C++ Addon。
跟算法相关(八叉树、最小差值)的计算全放在了 C++ 层进行计算。大家有兴趣的可以去读一下并且帮忙指出各种各样的缺点,算是抛砖引玉了。
这个包的 Repo 在 Github 上面:
文档自认为还算完整吧。并且也可以通过
$ npm install thmclrx |
进行安装。
进花瓣两个月了,这一次终于如愿以偿地碰触到了一点点的『算法相关』的活。(我不会告诉你这不是我的任务,是我从别人手中抢来的 2333333 ଘ(੭ˊᵕˋ)੭* ੈ✩‧₊˚
总之几种算法和实现在上方介绍了,具体需要怎么用还是要看大家自己了。我反正大致找到了我使用的途径,那你们呢。( ´・・)ノ(._.`)
]]>最近闲着蛋疼实现了两个库。
我当然不会说去用各种人工智能去实现一个强大的的解析器然后生成,也不会说用一个非常庞大如搜狗拼音的姓名库来随机获取——我只是偶然间知道蘑菇街小侠节一个混战 PK 的 Demo 编写比赛,闲来无聊随便写写,然而这其中我需要随机给 Bot 起名以及技能起名而用到的库。
不需要有多少正确性——这两个库的结果经常出现非常奇葩的名字,让人哭笑不得,但是我要的就是这种效果。
就两个库,我各生成一批名字以示效果。
阙造 |
地永心法 |
实际上无论是起名还是技能名,都用了一个相同的起名字库和一段差不多的复用代码(虽然没有真正意义上的复用,只是复制粘贴而已,谁让他们是两个库呢,已经很简单了,我总不能再给他们搞一个依赖出来吧?)
关于 chinese-random-name
中的姓氏,我找了一个中国百家姓(包括复姓)比较全的词库。
比较幸运,我找到的时候已经是这么分段分好了。我也没有详细做研究,随便给了不同的段不同的权值,当然越前面的段权值越高,被随机到的可能性越大。
首先用 split
来分割不同段:
dict = dict.split("\n\n"); |
对于每一段来说通过 Array.reduce
(详见 SugarJs) 来分割成行再成字。
看字典一共有 6 大段,每段的权值分别为:
const weights = [ 100, 70, 10, 5, 1, 1 ]; |
然后每个字都有一个其权值区间,是累加上去的。
最后获取姓的时候随机生成一个在总区间内的数字,然后看看数字在哪个姓的区间内,就返回这个姓。
关于 chinese-random-skill
中的技能后缀,我偷懒了。因为那个时候 Demo 就快 Deadline 了,所以随便糊弄了一下——直接把印象里面比较熟的后缀写上去了事,也不给权值了。
var suffix = [ |
名字主体为两个包的共用部分。
实际上他们依赖于一个特定款式的字库——我也就网上随便那么一搜。
它每一行的结构一样:
Number UniChar UniChar:String |
其中第一个数字我目测是繁体的笔画数,比如 899 行的 书
繁体就是 书
,数一下的确是 10 划。
第二个就是字本体,第三个是该字的五行属性,最后是这个字在什么什么命数(请不要迷信)描述。
为了让名字看起来稍微正常点(只是稍微而已),我尽可能让同属性的字在一块儿,于是有了以下组合:
这些字凑在一起的权值为 100。
然后隔一个属性的话是相克的,我不懂什么起名大法什么的,只是用膝盖想了下相克的属性不好起名吧(猜错了不要怨我),于是给了 20 的权值。
至于隔壁属性,是相生吧?于是给了 50 权值。
对于三个字的起名来说,也是用了类似的方法给权值,具体可以参考代码。
总之就是根据其两两之间的五行关系来起名的,听起来还是有那么点道理的。
哈哈,权当玩的,认真你就输了。
上面的分步做完了,然后真·生成名字的步骤是:
随机生成一个姓(或者技能后缀),然后按照某个权值随机生成一个数字代表剩下的名字的长度,然后随机生成一串该长度的名字即可。
最后拼接上去就 OK 了。
最后还是贴一下两个包的 repo 地址吧:
以及安装的话照下去弄就好了
$ npm install chinese-random-name |
README
文件两个包都有。
Storm 中的 Bolt 都是通过 Nimbus 这个服务将序列化好的 Bolt 断章取义地发到各个 worker 中。所以,任何在 bolt 之外你自认为加载期间初始化计算好的上下文环境并不会被打包上去,Java 我不懂也不知道,但是至少在 Clojure 这个类的概念被淡化的 LIST 方言中,你要做的就是把所有跟 bolt 初始化计算相关的代码放到其 prepare
的代码当中去。
你想一下,当你在文件加载的时候初始化了一个 MongoDB 链接,这个链接总不能被序列化到远程去吧?所以说办法就是把 bolt 搞上去之后,bolt 自动去初始化一个链接——这就是 prepare
的作用了。
说白了,这个还是我们在 Suwako 当中踩到的坑。
大致的骨架如下:
(defbolt bolt [...] {:prepare true} |
首先就是 {:prepare true}
代表了它是一个需要初始化的 Bolt。
然后在 (bolt)
的作用域之内有两个 form——prepare
和 execute
。
其中 prepare
就是你要初始化的语句了。举个例子,我们让这里面初始化一个 Monger,于是我们要在 let
里面定义一个用于链接的 atom {}
。
(defbolt bolt ["..."] {:prepare true} |
这样一来,当 Bolt 被 Nimbus 打包传到各个 worker 之后,Bolt 执行起来的时候会自动执行 prepare
当中的代码,即初始化 MongoDB 的链接,并且将其赋值给 conn
和 db
两个 atom。
那么,我们就能在本体 execute
当中使用 @conn
和 @db
来使唤 MongoDB 了。
可能很多人不解,不是说尽量保持 LISP 语系当中值的不变性的么?
其实不变性只是为了提高程序在运行时的效率——而事实上是,上面那段代码并没有在运行时去做变量。
虽然说这么说有点牵强,但是的确就是这个意思——因为我们是在程序执行真正有用的好逻辑的时候没有去改变一些值,相反只是在 Bolt 启动的时候做一些变量的操作。
换句话说,虽然严谨的讲那个时候是算运行时,但是在运行时里面我们却可以把它归类为预处理——这一类东西反正程序还没真正开始跑有用的东西,效率慢一点无所谓,而且就初始化这么屁大点事儿能有多少影响?
效率和效果之间权衡上面的还是要仁者见仁智者见智了。
本以为 Suwako
终于可以暂时告一段落了,紧要关头居然还是阻塞了。
说多都是泪,不说了,找 Bug 去了。
]]>说来话长,自从入了花瓣,整个人就掉进连环坑了。
后端元数据采集是用 Storm 来走拓扑流程的,又因为 @Zola 不是很喜欢 Java,所以退而求其次选择了 Clojure,所以正在苦逼地学习 Clojure 和 Storm 中。
目前来说外面的 Storm 拓扑的 Spout 是从 Kafka 中流入数据的。但是我们要给 Kafka 发送测试数据的时候,就需要跑到 Kafka 的测试服务器打开它的一个发送脚本进去发送,非常蛋疼;要么就是直接通过特定的发送业务逻辑代码测试,没有一个稍微泛一点的测试用发数据工具,于是 Mikasa 诞生了。
讲到 Mikasa 名字的来源,实际上看过『巨人』都知道,八块腹肌的三爷。
这里小爆料一下,又拍云和花瓣(都是同宗)的项目名很大部分都是以海贼王的角色命名的——尤其是又拍云更是丧心病狂。不过这让我这个伪·二次元的小伙伴异常欣喜,因为我也能用各种啪啪啪来命名我的角色了。比如我的第一个 Storm 相关的项目就叫 Suwako,即诹访子大人,因为脑子需要各种跳,于是就对诹访子大人这位青蛙之神各种膜拜。
至于这个发射器为什么要用三爷呢?因为三爷相当于先锋军哇!
这里的 Kafka 依赖用了搜狐小伙伴 @Crzidea 他们团队写的模块。
于是,话也不多说,直接上 repo 吧。在公司内网的 gitlab 里面有一份,还有一个 repo 在 GitHub 上。
如果要直接下载的话就用这个链接:
如果要克隆的话就:
git clone https://github.com/XadillaX/mikasa.git |
直接安装一下依赖:
npm install |
接下去就是简单的配置一下了,其实就是配置下配置文件。由于是快速开发,直接用了自己之前的 Exframess 框架,所以很多无用代码也懒得删了。
这里其实别的也不用动,主要是修改下端口即可。
这里修改一下 Kafka 的 Connection String
就好了。
最后启动服务即可。
node app.js |
最后的效果是这样的:
只要在 Topics 栏里面输入你要发送的 Topic,然后再下面的消息栏里面输入你要传的消息(字符串),最后点击 Send
即可将你的测试消息发进 Kafka 中去了。
]]>托大家的福,今天我的 Suwako 整个逻辑终于跑通了,撒花!ε٩(๑> ₃ <)۶з
昨天凌晨花了仨小时通关了这个游戏,在这里就粗粗做一下题解吧,好几题都是 Hack 过去的。(不要脸,( ゚Д゚)σ
这有点像教学关吧,总之先拿到那台电脑你就能操作了。拿到电脑后你就能修改地图内部黑色底色的代码了。
这个时候你只需要把中间设置墙的代码去掉就可以了,或者注释掉:
//for(y = 3; y <= map.getHeight() - 10; y++) { |
然后 <ctrl-5>
重新执行——哒哒~墙就消失了,赶紧到蓝色的出口处吧。
代码大致是给你创建了一个迷宫,并且出口处四面用围墙围起来。
我用了一个比较 Hack 的方法,在第一个黑色区域的最上方把 maze.create
重定向到自己的一个空函数,这样下面调用创建迷宫的函数就不会被执行,这个时候再执行的话迷宫就不见了:
maze.create = function() {}; |
迷宫不见了还不靠谱,因为还有一个出口四周有墙——那就自己再建一个出口呗,在第二个黑色区域写上建立一个新出口的代码即可:
map.placeObject(0, 0, "exit"); |
勇敢的少年啊,快去创造奇迹!
这题的要求是在还存在着一定量『壁』的情况下你能到达出口,也就是说纯粹地删除它加『壁』的代码是不行的,那我们做点改动就 OK 了。把『壁』往外移动,直到把人和出口都是在『壁』内。
那一天,人类终于回想起曾经一度被他们所支配的恐怖,还有囚禁于鸟笼中的那份屈辱。
for(y = 0; y <= map.getHeight() - 3; y++) { |
嘛嘛,这是第二关的简化版——直接再搞一个出口就 OK 了。
map.placeObject(20, 10, 'exit'); |
这是一个雷区,你不碰雷就好。从代码里面看出来有个 map.setSquareColor
函数可以设置某个格子的颜色。那好办,我们在设置一个地雷后把它用别的颜色标记出来就好了,然后重新执行只要你不是色盲都能安全通过。
map.setSquareColor(x, y, "#ff7800"); |
这题大概就是说有个痴汉会跟你靠近,然后把你先奸后杀。
但是痴汉很笨,在他的必经之路用墙堵住他就不会继续动了。
map.placeObject(30, 12, 'block'); |
这个是那个卖相不错的电话机的教学关卡。所以大致的意思是设置了打电话的回调函数即可。ε٩(๑> ₃ <)۶з
分析代码可知,要通过那几个长得跟菊花一样的带色儿的墙你就要跟那个菊花颜色一样。所以电话机的回调函数大致是让你自己变色就好了。
按照顺序所见,如果人是绿色的通过之后要变成红色,然后再变成黄色再绿色。于是写以下的变色过程就可以了:
var player = map.getPlayer(); |
重新执行捡起电话机,然后通过绿菊花之后按 Q
使用电话机让自己变色儿就好了。
“哎呀,天!他是惦记弟弟了。……可我还不知道呢!那么这是他老人家的狗?很高兴。……你把它带去吧。……这条小狗怪不错的。……挺伶俐。……一口就把这家伙的手指咬破了!哈哈哈哈!……咦,你干吗发抖?呜呜,……呜呜。……它生气了,小坏蛋,……好一条小狗……”
森林里面有树和墙,我也懒得想或者写代码了。(明明是自己想不出来#゚Å゚)⊂彡☆))゚Д゚)・∵
总之我是尽可能向出口靠近,然后到死路了赶紧打电话让森林重新生成一遍,如此循环往复直到出口。
23333333333333!做这题的时候差点没把自己浏览器卡死。
大致的意思是河的上面有一条船,你直接遇水会死,要上船。但是船貌似不跟你走啊 QAQ。
而且设定写着只能有一条 raft
。
咱就来个偷天换日,自己造诺亚方舟铺满整条河(因为懒得计算)。
首先定义诺亚方舟的类型:
map.defineObject("noah", { |
然后呢把它铺满整条大河吧:
for(var x = 0; x < map.getWidth(); x++) { |
一条大河,两岸宽,风吹稻花香两岸。(喂喂喂,小心卡死(┐「ε:)
后来我去 Untrusted
的 repo 去看题解,发现他们都是去驱使这群痴汉干嘛干嘛。我感觉我的最简单暴力了——直接废了他们。
其实呢只要把碰撞函数重写一遍,这堆痴汉马上就变得人畜无害,你走过去人家还行礼呢233333333333
仔细看一下我们要完成的部分在 behavior
里面,所以在这里面用 this
是妥妥生效的。
this.onCollision = function() {}; |
看我碎蛋大粉拳!(忽然觉得下身一阵疼痛 |Д`)ノ⌒●~*
你走一步机器人走一步,也是教学关卡。
机器人能往下走就往下走,能往右走就往右走就拿到钥匙了,最后你再追上机器人把钥匙抢过来就好了。因为机器人是可以穿过紫翔色的那扇门的。
if(me.canMove("down")) me.move("down"); |
站住,保护费。你不装 X 我们还是好朋友。
我居然无聊到自己把路线数出来了。
var road = "ddddrrrrrrrrrrrrrrrrrrrrrrrrrrrrrruurrrrrrrrrrrrrrrrrddddddd"; |
好吧作者早就想到了有人会无聊地去数。
嘛嘛,就如作者所愿写个最基础的 DFS 了事吧。
var direct = { |
红魔馆的地下室一样呢。反正是机器人多走几步路没事,没必要用 BFS 求最优解2333333333
刚才那仨 2B 机器人引领你拿到了仨颜色的钥匙在这边派上用场了。
钻红菊花你需要有红钥匙,并且用了之后会少掉。其它颜色也一样。最终你要拿到 A
所代表的 theAlgorithm
走到出口。
等等!啊咧?绿钥匙的通过判定有个地方可以修改?就是你通过绿菊花的时候需要有绿钥匙并且你可以选择你丢弃的东西。丢什么好呢?电脑?不行不行,过关还靠它呢。电话机?以后肯定要用到。其它颜色钥匙?那你肯定会被锁在某个地方出不来。那就只有丢弃
theAlgorithm
了——反正只要拿到theAlgorithm
之后不再通过绿菊花就没事了。
于是只要把绿菊花的通过判断函数里面可修改的区域改成 theAlgorithm
就好了。
最后走的顺序大概是:
进左上角的门拿到黄药屎和蓝药屎出来。然后右上角把红和蓝拿出来。然后向下直捣黄龙,左黄菊花进拿到
theAlgorithm
蓝菊花通过拿到黄药屎然后再黄菊花出。大功告成!走向胜利的出口吧!
自古红蓝出 CP!
又是过河啊,这次你只能是死了,因为你的编辑区域只有在 player.killedBy()
里面。
《订制死神》:这个时候让死神笑就可以了。
让我们一起来玩坏它吧!在里面填上 ) = (0
就好了。什么什么看不懂?你填进去看一下整句话就知道了:
player.killedBy() = (0); |
然后死神就会被你玩坏了。你走过去的时候这句话执行出错了2333333
有很多隐藏线,你人必须要跟隐藏线的颜色一致才能通过,然后目前所有线都用白色给画出来。
目测作者的意思是让你把硬编码的白色改成隐藏线的颜色,这样就能把线的颜色给标记出来,然后再给电话机写个函数就是让你自己的人变色。
不过我还是用了个 Hack 的方法——
第一条线他要画就画,咱不碰它就好了,只不过在第一条线画完的后面我们把这个画线函数给 Hack 掉:
// using canvas to draw the line |
接下去是在第二片区域写下自己的画线函数吧,这题最下方检测了线的数量不能少于 25 条。么事,爷高兴画 100 条都么问题,因为我都把它缩在左上角了 2333333
function abc() { |
有好多传送门,每次执行随机生成传送位置,有些传送门会把你传到二小姐的地下室然后被吃掉。
我也懒得多动脑筋或者画线什么的,直接对两个都是传送门的 CP 标记一样的随机颜色就好了,最后跟着颜色走到出口去(有个坑就是有时候这个地图本身就是死局,所以得多试几次重新执行 இдஇ
var dict = "0123456789ABCDEF"; |
好吧本意是让你设置一个 timer
然后一直跳啊跳的。
不过呢,定一个新方块给自己搭一座桥就是了:
map.defineObject("❤", { |
你只要打个电话桥就会出现的。
好神奇!好奇葩!我键盘 hjkl
乱按一通就过了。
打 Boss 了。
好吧我承认我 Cheat 了——原谅我用了 console.log
。
因为当我打开控制台的时候下面的语句出现在我的眼里:
If you can read this, you are cheating!
But really, you don’t need this console to play the game. Walk around using arrow keys (or Vim keys), and pick up the computer (⌘). Then the fun begins!
嘛嘛,无论如何,过关了就好。
这题呢是要让所有的 boss
给毁灭掉即可—— 当所有的 boss
毁灭之后会爆出任务道具 theAlgorithm
然后就能通关了。
后来我发现可以让子弹消灭 boss
。但是我当时没这么做。
我先弄了堵墙把子弹挡住先:
map.defineObject("保命的", { |
这下你就能捡到电话机了——然后给电话机写回调函数。
怎么说呢,当你每用一次电话机,我就把当前存在于屏幕的 boss
和 bullet
给分开罗列,然后把 boss
的 _destroy
(警察叔叔,就是这个函数是我 console.log
出来的)给嫁接到 bullet
的 _destroy
去。
这样会出现什么样的结果呢?——当子弹碰到墙的时候就会销毁,这个时候会触发 _destroy
函数,但是这个时候的 _destroy
函数已经会变成了 boss
的了,也就是说这个时候子弹不会被销毁反而是某一个 boss
的 _destroy
函数被调用然后被销毁了。
再怎么说这都是 Hack 的办法,所以并不会触发 boss
的 onDestroy
函数也就是说即使所有 boss
都没了也不会出现 theAlgorithm
这玩意儿。
自己动手丰衣足食!
敌人不给我们我们就自己造呗!反正通关判定是——boss
数量为 0
且你有 theAlgorithm
这个道具。
所以说当所有 boss
都被销毁之后,我们自己去 map.replaceObject
一个 theAlgorithm
道具即可。
map.getPlayer().setPhoneCallback(function() { |
以上代码写完后就开始打 boss
吧!赶紧去拿到电话机,然后你会发现打一个电话 boss
就少一堆,那感觉倍爽儿!
马上要通关了。这里是个坑,开始我还以为这里就是真·通关了 QAQ。
随后看看后面还是有关卡啊。但是我突然发现 <ctrl+0>
跳出来的 menu 左边多出了文件夹!然后进去随意翻看了。
最后发现原来是要修改 scripts/objects.js
文件→_→。
好吧,分析通关验证来看,这一关的 map.finalLevel
为 true
。所以我们只需要把 scripts/objects.js
文件里面的:
if(!game.map.finalLevel) { |
给改成如果是 finalLevel
就跑到下一关去就可以了:
if(game.map.finalLevel) { |
由于事先文章结构没有写好,就接这关的坑位来小结吧 0. 0。(反正人家只是序幕章了
好的,其实也什么总结的,但是总觉得得有这么个小结才对。
找工作啊找工作——有想要我的请联系我 2333333333
联系资料在 CV 里面。
]]>READEME.md
里面就有!(´≖◞౪◟≖) 另,在搭建环境的时候,最好保证你在墙外。以及我默认觉得大家已经有了 Python
环境和 JDK
。
先去 cocos2d-x 官网下压缩包,放到一个只有神知道的世界里面。
接下去需要安装仨东西:
这东西真尼玛大啊!我家的小水管真吃不起。
然后把 adt-bundle-…zip 这个包压缩到任意木有中文和空格的路径下面。
这小伙伴也不小啊。都是 500M 的主儿啊(٩(ŏ﹏ŏ、)۶
也解压到一个地方不用管它。
据说这货是阿帕奇出的?总之下载地址在这里。
哦对了你还得有个 Python 路径,这里就不累述了。接下去在命令行里面执行 Cocos2d 的 setup.py
文件即可:
py setup.py |
接下去终端会停在下面一行:
Please enter the path of NDK_ROOT (or press Enter to skip): |
在后面输入你放好的 NDK 目录即可。
如果下面又出现了:
Please enter the path of ANDROID_SDK_ROOT (or press Enter to skip): |
你只需在里面输入你刚放好的 Android SDK 的目录即可。(注意是要刚才的 SDK 压缩包解压出来的 sdk 路径)
再如果下面还出现:
Please enter the path of ANT_ROOT (or press Enter to skip): |
那么再把 Ant 的路径搞上去就好了。(又得注意这里得是 Ant 的 bin 目录)
最后确保终端(或者说命令行)里面出现如下字样:
Please restart the terminal or restart computer to make added system variables take effect |
然后你把终端关了再开一个就好了。至此,大致就安装完毕了。
随意跑到一个目录下面执行下面的命令:
cocos new FirstGame -p in.xcoder.firstgame -l cpp -d FirstGame |
大致意思就是说创建一个新的项目路径,叫
FirstGame
,其包名叫in.xcoder.firstgame
,然后语言是cpp
,最后-d
是路径。
命令详情帮助可以看 cocos --help
。
读标题,是 Win 篇。所以我们跑到项目路径下面的 proj.win32
目录下面用 M$ VS 打开 FirstGame.sln
就可以打开刚创建的模板项目了。
无论如何先编译看看吧!~
如何?跑起来了吧?
这里就讲讲如何打包安卓的版本吧:
跑到你的项目目录下面(即有 .cocos-project.json
文件的目录),然后执行下面的命令:
cocos run -p android |
等工具编译打包完成就 OK 了。(记得要查安卓手机并且调试模式哦~)
如果要上传到 Google Play 之类的地方,需要有签名。所以发布 Release 版本之前,你先得搞好自己的签名。
在终端跑到你的项目路径下面,然后执行:
keytool -genkey -v -keystore FirstGame.keystore -alias FirstGame -keyalg RSA -keysize 2048 -validaty 10000 |
照着命令行给的提示完成创建密钥即可。
生成之后啊就直接执行编译命令了:
cocos run -p android -m release |
在里面呢最后会让你输入 .keystore
文件的路径。
我们输入相对路径,由于我们刚才把这个文件搞在项目根目录,所以我们只需要输入 ../FirstGame.keystore
即可。接下去他会让你输入密码、别名和别名信息的密码。你都正确输入一遍他就会安安分分跑在你的手机里面了。
上面都弄好之后,你的仨版本 *.apk
文件也就生成了。很多人可能很困惑,为什么是仨版本。因为其中 Release 版本还分带签名和没签名版本。
总之那个路径在 publish/android
下面,里面有仨 *.apk
文件,你拿出来发布就可以了。
其实也没什么结不结的,这些东西你们自己去看看官方文档就好了。总之就这样了吧,以上。
]]>之所以想写这篇文章,目的有三个,
所以,本文不会面面俱到,只是对 TCP 协议、算法和原理的科普。
我本来只想写一个篇幅的文章的,但是 TCP 真 TMD 的复杂,比 C++ 复杂多了,这 30 多年来,各种优化变种争论和修改。所以,写着写着就发现只有砍成两篇。
废话少说,首先,我们需要知道 TCP 在网络 OSI 的七层模型中的第四层 —— 传输层(Transport),IP 在第三层 —— 网络层(Network),ARP 在第二层 —— 数据链路层(Data Link),在第二层上的数据,我们叫 Frame,在第三层上的数据叫 Packet,第四层的数据叫 Segment。
首先,我们需要知道,我们程序的数据首先会打到 TCP 的 Segment 中,然后 TCP 的 Segment 会打到 IP 的 Packet 中,然后再打到以太网 Ethernet 的 Frame 中,传到对端后,各个层解析自己的协议,然后把数据交给更高层的协议处理。
接下来,我们来看一下 TCP 头的格式
你需要注意这么几点:
src_ip
, src_port
, dst_ip
, dst_port
)准确说是五元组,还有一个是协议。但因为这里只是说TCP协议,所以,这里我只说四元组。关于其它的东西,可以参看下面的图示
其实,网络上的传输是没有连接的,包括 TCP 也是一样的。而 TCP 所谓的“连接”,其实只不过是在通讯的双方维护一个“连接状态”,让它看上去好像有连接一样。所以,TCP 的状态变换是非常重要的。
下面是:“TCP 协议的状态机”(图片来源) 和 “TCP 建链接”、“TCP 断链接”、“传数据” 的对照图,我把两个图并排放在一起,这样方便在你对照着看。另外,下面这两个图非常非常的重要,你一定要记牢。(吐个槽:看到这样复杂的状态机,就知道这个协议有多复杂,复杂的东西总是有很多坑爹的事情,所以 TCP 协议其实也挺坑爹的)
很多人会问,为什么建链接要 3 次握手,断链接需要 4 次挥手?
另外,有几个事情需要注意一下:
tcp_syncookies
的参数来应对这个事 —— 当 SYN 队列满了后,TCP 会通过源地址端口、目标地址端口和时间戳打造出一个特别的 Sequence Number 发回去(又叫 cookie),如果是攻击者则不会有响应,如果是正常连接,则会把这个 SYN Cookie 发回来,然后服务端可以通过 cookie 建连接(即使你不在 SYN 队列中)。请注意,请先千万别用 tcp_syncookies
来处理正常的大负载的连接的情况。因为,synccookies
是妥协版的 TCP 协议,并不严谨。对于正常的请求,你应该调整三个 TCP 参数可供你选择,第一个是:tcp_synack_retries
可以用他来减少重试次数;第二个是:tcp_max_syn_backlog
,可以增大 SYN 连接数;第三个是:tcp_abort_on_overflow
处理不过来干脆就直接拒绝连接了。tcp_tw_reuse
,另一个叫 tcp_tw_recycle
的参数,这两个参数默认值都是被关闭的,后者 recyle 比前者 resue 更为激进,resue 要温柔一些。另外,如果使用 tcp_tw_reuse
,必需设置 tcp_timestamps = 1
,否则无效。这里,你一定要注意,打开这两个参数会有比较大的坑 —— 可能会让 TCP 连接出一些诡异的问题(因为如上述一样,如果不等待超时重用连接的话,新的连接可能会建不上。正如官方文档上说的一样“**It should not be changed without advice/request of technical experts**”)。tcp_tw_reuse
**。官方文档上说 tcp_tw_reuse
加上 tcp_timestamps
(又叫 PAWS, for Protection Against Wrapped Sequence Numbers)可以保证协议的角度上的安全,但是你需要 tcp_timestamps
在两边都被打开(你可以读一下 tcp_twsk_unique
的源码 )。我个人估计还是有一些场景会有问题。tcp_tw_recycle
**。如果是 tcp_tw_recycle
被打开了话,会假设对端开启了 tcp_timestamps
,然后会去比较时间戳,如果时间戳变大了,就可以重用。但是,如果对端是一个 NAT 网络的话(如:一个公司只用一个 IP 出公网)或是对端的 IP 被另一台重用了,这个事就复杂了。建链接的 SYN 可能就被直接丢掉了(你可能会看到 connection time out 的错误)(如果你想观摩一下 Linux 的内核代码,请参看源码 tcp_timewait_state_process
)。tcp_max_tw_buckets
**。这个是控制并发的 TIME_WAIT 的数量,默认值是 180000,如果超限,那么,系统会把多的给 destory 掉,然后在日志里打一个警告(如:time wait bucket table overflow),官网文档说这个参数是用来对抗 DDoS 攻击的。也说的默认值 180000 并不小。这个还是需要根据实际情况考虑。Again,使用
tcp_tw_reuse
和tcp_tw_recycle
来解决 TIME_WAIT 的问题是非常非常危险的,因为这两个参数违反了TCP协议(RFC 1122) 。
下图是我从 Wireshark 中截了个我在访问 coolshell.cn 时的有数据传输的图给你看一下,SeqNum 是怎么变的。(使用 Wireshark 菜单中的 Statistics -> Flow Graph…
)
你可以看到,SeqNum 的增加是和传输的字节数相关的。上图中,三次握手后,来了两个 Len:1440 的包,而第二个包的 SeqNum 就成了 1441。然后第一个 ACK 回的是 1441,表示第一个 1440 收到了。
注意:如果你用 Wireshark 抓包程序看 3 次握手,你会发现 SeqNum 总是为0,不是这样的,Wireshark 为了显示更友好,使用了 Relative SeqNum —— 相对序号,你只要在右键菜单中的 protocol preference 中取消掉就可以看到“Absolute SeqNum”了。
TCP 要保证所有的数据包都可以到达,所以,必需要有重传机制。
注意,接收端给发送端的 Ack 确认只会确认最后一个连续的包,比如,发送端发了 1,2,3,4,5 一共五份数据,接收端收到了 1,2,于是回 ack 3,然后收到了 4(注意此时 3 没收到),此时的 TCP 会怎么办?我们要知道,因为正如前面所说的,SeqNum 和 Ack 是以字节数为单位,所以 ack 的时候,不能跳着确认,只能确认最大的连续收到的包,不然,发送端就以为之前的都收到了。
一种是不回 ack,死等 3,当发送方发现收不到 3 的 ack 超时后,会重传 3。一旦接收方收到 3 后,会 ack 回 4 —— 意味着 3 和 4 都收到了。
但是,这种方式会有比较严重的问题,那就是因为要死等 3,所以会导致 4 和 5 即便已经收到了,而发送方也完全不知道发生了什么事,因为没有收到 Ack,所以,发送方可能会悲观地认为也丢了,所以有可能也会导致 4 和 5 的重传。
对此有两种选择:
这两种方式有好也有不好。第一种会节省带宽,但是慢,第二种会快一点,但是会浪费带宽,也可能会有无用功。但总体来说都不好。因为都在等 timeout,timeout 可能会很长(在下篇会说 TCP 是怎么动态地计算出 timeout 的)
于是,TCP 引入了一种叫 Fast Retransmit 的算法,不以时间驱动,而以数据驱动重传。也就是说,如果,包没有连续到达,就 ack 最后那个可能被丢了的包,如果发送方连续收到 3 次相同的 ack,就重传。**Fast Retransmit** 的好处是不用等 timeout 了再重传。
比如:如果发送方发出了 1,2,3,4,5 份数据,第一份先到送了,于是就 ack 回 2,结果 2 因为某些原因没收到,3 到达了,于是还是 ack 回 2,后面的 4 和 5 都到了,但是还是 ack 回 2,因为 2 还是没有收到,于是发送端收到了三个 ack = 2 的确认,知道了 2 还没有到,于是就马上重转 2。然后,接收端收到了 2,此时因为 3,4,5 都收到了,于是 ack 回 6。示意图如下:
Fast Retransmit 只解决了一个问题,就是 timeout 的问题,它依然面临一个艰难的选择,就是重转之前的一个还是重装所有的问题。对于上面的示例来说,是重传 #2 呢还是重传 #2,#3,#4,#5 呢?因为发送端并不清楚这连续的 3 个 ack(2) 是谁传回来的?也许发送端发了 20 份数据,是 #6,#10,#20 传来的呢。这样,发送端很有可能要重传从 2 到 20 的这堆数据(这就是某些 TCP 的实际的实现)。可见,这是一把双刃剑。
另外一种更好的方式叫:**Selective Acknowledgment (SACK)**(参看 RFC 2018),这种方式需要在 TCP 头里加一个 SACK 的东西,ACK 还是 Fast Retransmit 的 ACK,SACK 则是汇报收到的数据碎版。参看下图:
这样,在发送端就可以根据回传的 SACK 来知道哪些数据到了,哪些没有到。于是就优化了 Fast Retransmit 的算法。当然,这个协议需要两边都支持。在 Linux 下,可以通过 tcp_sack
参数打开这个功能(Linux 2.4 后默认打开)。
这里还需要注意一个问题 —— 接收方 Reneging,所谓 Reneging 的意思就是接收方有权把已经报给发送端 SACK 里的数据给丢了。这样干是不被鼓励的,因为这个事会把问题复杂化了,但是,接收方这么做可能会有些极端情况,比如要把内存给别的更重要的东西。所以,发送方也不能完全依赖 SACK,还是要依赖 ACK,并维护 Time-Out,如果后续的 ACK 没有增长,那么还是要把 SACK 的东西重传,另外,接收端这边永远不能把 SACK 的包标记为 Ack。
注意:SACK 会消费发送方的资源,试想,如果一个攻击者给数据发送方发一堆 SACK 的选项,这会导致发送方开始要重传甚至遍历已经发出的数据,这会消耗很多发送端的资源。详细的东西请参看《TCP SACK的性能权衡》
Duplicate SACK 又称 D-SACK,其主要使用了 SACK 来告诉发送方有哪些数据被重复接收了。RFC-2833 里有详细描述和示例。下面举几个例子(来源于 RFC-2833)
D-SACK使用了SACK的第一个段来做标志,
下面的示例中,丢了两个 ACK,所以,发送端重传了第一个数据包(3000 - 3499),于是接收端发现重复收到,于是回了一个 SACK = 3000 - 3500,因为 ACK 都到了 4000 意味着收到了 4000 之前的所有数据,所以这个 SACK 就是 D-SACK —— 旨在告诉发送端我收到了重复的数据,而且我们的发送端还知道,数据包没有丢,丢的是 ACK 包。
Transmitted Received ACK Sent |
下面的示例中,网络包(1000 - 1499)被网络给延误了,导致发送方没有收到 ACK,而后面到达的三个包触发了“Fast Retransmit 算法”,所以重传,但重传时,被延误的包又到了,所以,回了一个 SACK = 1000 - 1500,因为 ACK 已到了 3000,所以,这个 SACK 是 D-SACK —— 标识收到了重复的包。
这个案例下,发送端知道之前因为“Fast Retransmit 算法”触发的重传不是因为发出去的包丢了,也不是因为回应的 ACK 包丢了,而是因为网络延时了。
Transmitted Received ACK Sent |
可见,引入了 D-SACK,有这么几个好处:
知道这些东西可以很好得帮助TCP了解网络情况,从而可以更好的做网络上的流控。
Linux 下的 tcp_dsack
参数用于开启这个功能(Linux 2.4 后默认打开)
好了,上篇就到这里结束了。如果你觉得我写得还比较浅显易懂,那么,欢迎移步看下篇《TCP的那些事(下)》
]]>hypnotized
,于是红心了。过了几天我再去听——发现这首歌变了。
最后经过多方面求证,我大概得出结果就是应该有人传错了歌,然后后来有人重新传了一遍,导致我听的不是原来那首歌了。那我那天听的那首歌到底叫什么名字呢?
然后大致看了一下,虽然歌被重新传了,但是这里显示的这首歌的时间没变!还是11分钟,目测是数据库没更新。
于是我就想了个笨办法,去爬收音机里面所有 tag 为 东方project
的专辑,然后跑到专辑页看歌曲的长度。
问题来了,如果我直接爬,然后爬完 callback
之后又直接爬,没有任何间隔,就相当于我在 DDOS
它的站子。或者即使没那么严重——反正最后到一定程度并发太大我就访问不了了。
于是我就想到了做一个任务队列的 module。该 module 的作用就是把一堆任务扔到队列中,完成一个才开始下一个。
然后如果同时执行一个也太慢,module 还允许你开多几个子队列同时执行。
模块的 repo 在 GitHub 上面。名字叫 Scarlet Task
的原因一是我本身就喜欢二小姐,二是为了纪念这次事件我是为了找有关二小姐的歌。
要安装也很简单:
$ npm install scarlet-task |
然后 repo 的 README.md
里面有使用方法的——大致就是实例化一个对象,然后定义好某个任务的任务标识(可以是字符串,可以是 json 对象,可以是任何类型的数据),然后再定义好处理这个任务的函数,将这个数据推倒队列中即可。然后在处理函数中任务处理完的时候执行以下任务完成的函数即可。
markdown
如何高亮之类的问题还请自行谷歌。 然后请打开你自己的 .vimrc
文件。
首先定义一个变量——你自己的 hexo
目录,如果要跨平台可以做个判断之类的,如下:
if has("win32") |
这个函数大致就是让你进入你自己的 Hexo
路径:
fun! OpenHexoProjPath() |
接下去就是一个打开 Post
的函数了:
function! OpenHexoPost(...) |
解析:上面的代码大意就是进入 Hexo 路径,然后设定好文件名,最后执行
:e filename
即可打开文件了。
新建的流程跟打开相似,只不过首先要在 Hexo
目录下执行一遍 hexo new FOO
的命令而已,命令执行完毕之后再打开即可。
function! NewHexoPost(...) |
函数写好后我们最后把函数映射成类似于 :e
, :w
之类的后面能跟着参数的指令即可。
以前木有接触过的同学可以参考一下这里的文档。
command -nargs=+ HexoOpen :call OpenHexoPost("<args>") |
command -nargs=+ HexoNew :call NewHexoPost("<args>") |
当你做完以上步骤的时候,你就可以无论在什么目录下在 VIM 里面通过下面的指令进行新建一篇日志了:
:HexoNew artical-name |
以及下面的指令来打开一篇已存在的日志:
:HexoOpen artical-name |
相信看到这里之后,大家也能自己写出一个生成的指令了,这里就不累述了,无非就是:
:!hexo generate |
好了废话不多说,还是直接上题解吧。
题意大概就是说一排岩浆和水,你要拿一桶水和岩浆,并且水的下标小于岩浆。
为了更便于理解,我们从后往前做。首先将序列读进来之后从后往前遍历——若是岩浆,那么岩浆数加一,如果是水,那么这桶水能选择后面岩浆的任意一桶,也就是说答案加上当前的岩浆数即可。
注意用
__int64
。
|
一堵墙,每单位高度不定。你需要选择其中任意连续的墙,使得你选择的墙每单位的高度都是唯一的——问有多少种选法。
先求出总的种数,然后求不满足的数量,最后用总数减去不满足数即为答案。
|
注:备份到这篇日志的时候,感觉眼睛进了什么奇怪的东西。(才……才不是眼泪呢,那一定是沙子!(;´༎ຶД༎ຶ`)
主要引起感伤的还是这首轩辕剑的 BGM 吧。因为 Hero Snake 的BGM就是这个。
这篇文章原文是在 2011年5月1日 发的。游戏是跟 MatRush 在2011年寒假一起写的,虽然是我边教边写的。呵呵,转眼间三年过去了。
想当年我还是那么执着于游戏行业,现在纠结于到底要从事游戏行业还是互联网呢?半年真的能改变很多,要是我当时没有去汽族网实习,也许现在并不会有那么大的改变吧。
自从被 @朴大 刷了之后,我又开始着重考虑了。是不是我玩互联网只是觉得新鲜好玩而已呢?毕竟我是半路出家的,虽然有着十来年的 Web经历
,但那都是小打小闹哇。还记得小学的时候买的第一本电脑书——《在网上安个家》,到现在还记得那个时候捧着书的激动心情。
我是不是有点偏离了自己本身的轨道呢?总之还是在这两块领域犹豫不决。
废话有点扯远了,还是把文章从 Capture 备份回来再说吧。
这是一款正宗基于HGE的小游戏,算是我做的游戏中自己比较满意的小游戏吧。
幕后故事是这样的:MatRush寒假找我一起做游戏,因为他们学校弄了一个蛋疼的游戏制作比赛。然后命题是贪吃蛇,于是我们加了一点自己的小创意,给他讲解了一些HGE的基础之后,就写了这个游戏了。然后因为我们都比较喜欢轩辕剑,于是BGM就是轩辕剑的《永远的三个人的快乐时光》,由于MatRush比较喜欢MapleStory,便有了素材是那些像素画面。
首先这个游戏有两种模式:单人模式和双人模式。
关於单人模式,这是一个闯关型模式,大家在每一关必须通过吃道具获得一定的分数以及吃圈圈获得一定圈圈数才能开启通往下一关的门,默认一共20关,可以自己编辑关卡,这是后话。下面是几种道具的解说:
开山斧:捡到这货随机获得1~3个斧头并且附赠100分数。斧头的作用是能破开木桶,安全通过,一个斧头用一次。斧头数在左下角的Axe(s)后面。
命运之剪:捡到这个之后获得100分并且给你断掉一个尾巴以降低难度。
降速器:因为你每吃一个圈圈会增加一定速度,而这个降速器是降低你的速度让你容易些。并且附赠100分。
药水:药水是装饰变色用的,其实是送分的。前四种药水100分,紫色的300分。
骷髅头:想死的话就碰碰它试试。
传送门:遇到这货就说明你功德圆满了。恭喜,可以通过它前往下一关。
接下去是双人模式。双人模式因为当初设想有些问题,所以实现起来仅仅是简单的双人走啊走,看谁碰到谁谁就输。在双人模式中,先要选择一张对战地图,然后开始双人走啊走啊走。
然后Rank是排行榜,Option是游戏的一些选项,Introduction是游戏介绍,这个介绍有些蛋疼,最后不用说Exit就是退出了。
下面是关于地图编辑器的说明:
首先在游戏目录小有一个config.ini文件,是一些游戏设置。其中levelnum是游戏关卡数。注意这个数字必须要跟地图数量一致或者小于它,否则会因为找不到之后的地图而出错。地图就存在data里,命名方式是mapX.txt,从0开始。地图编辑器则在MapEditor文件夹下。操作很简单,说明都在编辑器下方的文字上,就几个快捷键。可以用鼠标操作也可以用上下左右控制方向。
最后,预祝大家玩得愉快。附上下载地址和几张预览图吧:
C++
中实现 Node.js
调用时传参数和调用回调函数,并且我自己也心血来潮写了个小 Demo 供大家参考。今天我们就不复习了,直捣黄龙吧。
]]>C++
中类成员函数 inline
修饰符的一个坑。 这个坑是我在尝试着写我的第一个 Node.js
扩展 simpleini
时候遇到的。
因为只是尝试着写,所以懒得自己实现,于是网上找了个开源的 C++
阅读 ini 文件的项目,名不见经传,叫 miniini。
好了,问题来了,当我写好我的源文件的时候,然后写好了我的 binding.gyp
,总之一切大功告成开始编译的时候—— Windows
下没问题,MacOS
下也可以正常运行,但是在 Linux
下就出问题了:
node: symbol lookup err: .../simpleIni.node: undefined symbol: _ZNK10INISection10ReadStringEPKcRS1_ |
大致的意思呢就是说找不到 INISection
的 ReadString
函数符号。
又是怀着崇敬的心情去 SO 求解了。
最后的解答大概如下:
内联成员函数的声明看起来像一个非内联函数的声明:
class Fred {
public:
void f(int i, char c);
};
但是你的内敛成员函数定义前面又加了
inline
这个关键字时,你必须把这个定义放到头文件中:
inline
void Fred::f(int i, char c)
{
// ...
}
这么做的原因就是为了避免链接器
unresolved external
的发生。如果你不这么做,这个错误就将会在你从另外一个.cpp
文件中调用它时出现。
好嘛,原来是原作者自己写的代码有问题啊。但是不得不说一下又涨姿势了。C++还真是有千奇百怪的坑和错误啊。
最后的解决方案大致就是把函数定义放到头文件中去,或者在函数声明前面也加上 inline
关键字。
我的第一个 C++
模块,叫 simpleini
,其实只是抱着试试看 Node.j
的 C++
模块是不是这么写的而已,并没有多大实际用处。Repo 在 Github 上。
然后用法很简单,先安装:
$ npm install simpleini |
然后下面的代码就是例子了:
var simpleIni = require("simpleini"); |
读取配置的时候第一个参数是 Section
,第二个参数是 Key
,第三个参数是取不到该值时的默认值。
首先请大家记住这个 V8 的在线手册——http://izs.me/v8-docs/main.html。
还记得上次的 building.gyp
文件吗?
{ |
就像这样,举一反三,如果多几个 *.cc
文件的话就是这样的:
"sources": [ "addon.cc", "myexample.cc" ] |
上次我们把俩步骤分开了,实际上配置和编译可以放在一起的:
$ node-gyp configure build |
复习完了吗?没?!
好的,那我们继续吧。
现在我们终于要讲参数了呢。
让我们设想有这样一个函数 add(a, b)
代表把 a
和 b
相加返回结果,所以先把函数外框写好:
|
这个就是函数的参数了。我们不妨先看看 v8 的官方手册参考。
int Length() const
Local<Value> operator[](int i) const
其它的我们咱不关心,这两个可重要了!一个代表传入函数的参数个数,另一个中括号就是通过下标索引来访问第 n
个参数的。
所以如上的需求,我们大致就可以理解为 args.Length()
为 2
,args[0]
代表 a
以及 args[1]
代表 b
了。并且我们要判断这两个数的类型必须得是 Number
。
注意到没,中括号的索引操作符返回结果是一个 Local<Value>
也就是 Node.js
的所有类型基类。所以传进来的参数类型不定的,我们必须得自己判断是什么参数。这就关系到了这个 Value
类型的一些函数了。
IsArray()
IsBoolean()
IsDate()
IsFunction()
IsInt32()
IsNativeError()
IsNull()
IsNumber()
IsRegExp()
IsString()
我就不一一列举了,剩下的自己看文档。。:.゚ヽ(*´∀`)ノ゚.:。
这个是我们等下要用到的一个函数。具体在 v8 文档中可以找到。
顾名思义,就是抛出错误啦。执行这个语句之后,相当于在 Node.js
本地文件中执行了一条 throw()
语句一样。比如说:
ThrowException(Exception::TypeError(String::New("Wrong number of arguments"))); |
就相当于执行了一条 Node.js
的:
throw new TypeError("Wrong number of arguments"); |
这个函数呢也在文档里面。
具体就是一个空值,因为有些函数并不需要返回什么具体的值,或者说没有返回值,这个时候就需要用 Undefined()
来代替了。
在理解了以上的几个要点之后,我相信你们很快就能写出 a + b
的逻辑了,我就把 Node.js
官方手册的代码抄过来给你们过一遍就算完事了:
|
函数大功告成!
最后把尾部的导出函数给写好就 OK 了。
void Init(Handle<Object> exports) |
等你编译好之后,我们就能这样用了:
var addon = require('./build/Release/addon'); |
你会看到一个 2b
!✧。٩(ˊᗜˋ)و✧*。
上一章我们只讲了个 Hello world
,这一章阿婆主就良心发现一下,再来个回调函数的写法。
惯例我们先写好框架:
|
然后我们决定它的用法是这样的:
func(function(msg) { |
即它会给回调函数传入一个参数,我们设想它是一个字符串,然后我们可以 console.log()
出来看。
废话不多说,先给它一个字符串喂饱了再说吧。(√ ζ ε:)
不过我们得让这个字符串是通用类型的,因为 Node.js
代码是弱类型的。
Local<Value>::New(String::New("hello world")); |
什么?你问我什么是 Local<Value>
?
如文档所示,Local<T>
实际上继承自 Handle<T>
,我记得上一章已经讲过 Handle<T>
这个东西了。
然后下面就是讲 Local 了。
Handle 有两种类型, Local Handle 和 Persistent Handle ,类型分别是
Local<T> : Handle<T>
和Persistent<T> : Handle<T>
,前者和Handle<T>
没有区别生存周期都在 scope 内。而后者的生命周期脱离 scope ,你需要手动调用Persistent::Dispose
结束其生命周期。也就是说 Local Handle 相当于在 C++`在栈上分配对象而 Persistent Handle 相当于 C++ 在堆上分配对象。
终端命令行调用 C/C++ 之后怎么取命令行参数?
|
对了,这里的 argc
就是命令行参数个数,argv[]
就是各个参数了。那么调用 Node.js
的回调函数,v8
也采用了类似的方法:
V8EXPORT Local<Value> v8::Function::Call(Handle<Object>recv, |
QAQ 卡在了Handle<Object> recv
了!!!明天继续写。
好吧,新的一天开始了我感觉我充满了力量。(∩^o^)⊃━☆゚.*・。
经过我多方面求证(SegmentFault和StackOverflow以及一个扣扣群),终于解决了上面这个函数仨参数的意思。
后面两个参数就不多说了,一个是参数个数,另一个就是一个参数的数组了。至于第一个参数 Handle<Object> recv
,StackOverflow 仁兄的解释是这样的:
It is the same as apply in JS. In JS, you do
var context = ...;
cb.apply(context, [ ...args...]);The object passed as the first argument becomes this within the function scope. More documentation on MDN. If you don’t know JS well, you can read more about JS’s this here: http://unschooled.org/2012/03/understanding-javascript-this/
—— 摘自 [StackOverflow](http://stackoverflow.com/questions/22842908/what-does-the-first-argument-of-functioncall-in-v8-engine-mean/22848601?noredirect=1#22848601)
总之其作用就是指定了被调用函数的 this
指针。这个 Call
的用法就跟 JavaScript 中的 bind()
、call()
、apply()
类似。
所以我们要做的事情就是先把参数表建好,然后传入这个 Call
函数供其执行。
第一步,显示转换函数,因为本来是 Object
类型:
Local<Function> cb = Local<Function>::Cast(args[0]); |
第二步,建立参数表(数组):
Local<Value> argv[argc] = { Local<Value>::New(String::New("hello world")) }; |
调用 cb
,把参数传进去:
cb->Call(Context::GetCurrent()->Global(), 1, argv); |
这里第一个参数 Context::GetCurrent()->Global()
所代表的意思就是获取全局上下文作为函数的 this
;第二个参数就是参数表中的个数(毕竟虽然 Node.js
的数组是有长度属性的,但是 C++
里面数组的长度实际上系统是不知道的,还得你自己传进一个数来说明数组长度);最后一个参数就是刚才我们建立好的参数表了。
相信这一步大家已经轻车熟路了吧,就是把函数写好,然后放进导出函数里面,最后申明一下。
我就直接放出代码吧,或者直接跑去 Node.js
的文档看也行。
|
Well done! 最后剩下的步骤就自己去吧。至于 Js
里面这么调用这个函数,我在之前已经提到过了。
嘛嘛,我感觉我的学习笔记写得越来越奔放了求破~
今天就先写到这里吧,写学习笔记的过程中我又涨姿势了,比如说那个 Call
函数的参数意义。
如果你们觉得本系列学习笔记对你们还有帮助的话,就来和我一起搞基吧么么哒~Σ>―(〃°ω°〃)♡→
]]>总之我们现在要做的其实简而言之就是——用C/C++来实现 Node.js 的模块。
工欲善其事,必先耍流氓利其器。
首先你需要一个 node-gyp
模块。
在任意角落,执行:
$ npm install node-gyp -g |
在进行一系列的 blahblah
之后,你就安装好了。
然后你需要有个 python
环境。
自己去官网搞一个来。
注意: 根据
node-gyp
的GitHub显示,请务必保证你的python
版本介于2.5.0
和3.0.0
之间。
嘛嘛,我就偷懒点不细写了,还请自己移步到 node-gyp 去看编译器的需求。并且倒腾好。
我就拿官网的入门 Hello World说事儿了。
请准备一个 C++
文件,比如就叫 sb.cc hello.cc。
然后我们一步步来,先往里面搞出头文件和定义好命名空间:
#include <node.h> |
接下去我们写一个函数,其返回值是 Handle<Value>
。
Handle<Value> Hello(const Arguments& args) |
然后我来粗粗解析一下这些东西:
V8 里使用 Handle 类型来托管 JavaScript 对象,与 C++ 的 std::sharedpointer 类似,Handle 类型间的赋值均是直接传递对象引用,但不同的是,V8 使用自己的 GC 来管理对象生命周期,而不是智能指针常用的引用计数。
JavaScript 类型在 C++ 中均有对应的自定义类型,如 String 、 Integer 、 Object 、 Date 、 Array 等,严格遵守在 JavaScript 中的继承关系。 C++ 中使用这些类型时,必须使用 Handle 托管,以使用 GC 来管理它们的生命周期,而不使用原生栈和堆。
而这个所谓的 Value ,从 V8 引擎的头文件 v8.h 中的各种继承关系中可以看出来,其实就是 JavaScript 中各种对象的基类。
在了解了这件事之后,我们大致能明白上面那段函数的申明的意思就是说,我们写一个 Hello
函数,其返回的是一个不定类型的值。
注意: 我们只能返回特定的类型,即在 Handle 托管下的 String 啊 Integer 啊等等等等。
这个就是传入这个函数的参数了。我们都知道在 Node.js
中,参数个数是乱来的。而这些参数传进去到 C++
中的时候,就转变成了这个 Arguments
类型的对象了。
具体的用法我们在后面再说,在这里只需要明白这个是个什么东西就好。(为毛要卖关子?因为 Node.js
官方文档中的例子就是分开来讲的,我现在只是讲第一个 Hello World
的例子而已( ´థ౪థ)σ
接下去我们就开始添砖加瓦了。就最简单的两句话:
Handle<Value> Hello(const Arguments& args) |
这两句话是什么意思呢?大致的意思就是返回一个 Node.js
中的字符串 "world"
。
同参考自这里。
Handle 的生命周期和 C++ 智能指针不同,并不是在 C++ 语义的 scope 内生存(即{} 包围的部分),而需要通过 HandleScope 手动指定。HandleScope 只能分配在栈上,HandleScope 对象声明后,其后建立的 Handle 都由 HandleScope 来管理生命周期,HandleScope 对象析构后,其管理的 Handle 将由 GC 判断是否回收。
所以呢,我们得在需要管理他的生命周期的时候申明这个 Scope
。好的,那么为什么我们的代码不这么写呢?
Handle<Value> Hello(const Arguments& args) |
因为当函数返回时,scope
会被析构,其管理的Handle也都将被回收,所以这个 String
就会变得没有意义。
所以呢 V8 就想出了个神奇的点子——HandleScope::Close(Handle<T> Value)
函数!这个函数的用处就是关闭这个 Scope 并且把里面的参数转交给上一个 Scope 管理,也就是进入这个函数前的 Scope。
于是就有了我们之前的代码 scope.Close(String::New("world"));
。
这个 String
类所对应的就是 Node.js
中原生的字符串类。继承自 Value
类。与此类似,还有:
这些东西有些是继承自 Value
,有些是二次继承。我们这里就不多做研究,自己可以看看 V8 的代码(至少是头文件)研究研究或者看看这个手册。
而这个 New
呢?这里可以看的。就是新建一个 String
对象。
至此,这个主要函数我们就解析完毕了。
我们来温习一下,如果是在 Node.js
里面写的话,我们怎么导出函数或者对象什么的呢?
exports.hello = function() {} |
那么,在 C++
中我们该如何做到这一步呢?
首先,我们写个初始化函数:
void init(Handle<Object> exports) |
这是龟腚!函数名什么的无所谓,但是传入的参数一定是一个 Handle<Object>
,代表我们下面将要在这货上导出东西。
然后,我们就在这里面写上导出的东西了:
void init(Handle<Object> exports) |
大致的意思就是说,为这个 exports
对象添加一个字段叫 hello
,所对应的东西是一个函数,而这个函数就是我们亲爱的 Hello
函数了。
用伪代码写直白点就是:
void init(Handle<Object> exports) |
大功告成!
(大功告成你妹啊!闭嘴( ‘д‘⊂彡☆))Д´)
这才是最后一步,我们最后要申明,这个就是导出的入口,所以我们在代码的末尾加上这一行:
NODE_MODULE(hello, init) |
纳了个尼?!这又是什么东西?
别着急,这个 NODE_MODULE
是一个宏,它的意思呢就是说我们采用 init
这个初始化函数来把要导出的东西导出到 hello
中。那么这个 hello
哪来呢?
它来自文件名!对,没错,它来自文件名。你并不需要事先申明它,你也不必担心不能用,总之你的这个最终编译好的二进制文件名叫什么,这里的 hello
你就填什么,当然要除去后缀名了。
详见官方文档。
Note that all Node addons must export an initialization function:
cpp
void Initialize (Handle<Object> exports);
NODE_MODULE(module_name, Initialize)There is no semi-colon after NODE_MODULE as it’s not a function (see node.h).
The module_name needs to match the filename of the final binary (minus the .node suffix).
来吧,让我们一起编译吧!
我们再新建一个类似于 Makefile
的归档文件吧——binding.gyp
。
并且在里面添加这样的代码:
{ |
为什么这么写呢?可以参考 node-gyp
的官方文档。
在文件搞好之后,我们要在这个目录下面执行这个命令了:
$ node-gyp configure |
如果一切正常的话,应该会生成一个 build
的目录,然后里面有相关文件,也许是 M$ Visual Studio 的 vcxproj
文件等,也许是 Makefile
,视平台而定。
Makefile
也生成好之后,我们就开始构造编译了:
$ node-gyp build |
等到一切编译完成,才算是真正的大功告成了!不信你去看看 build/Release
目录,下面是不是有一个 hello.node
文件了?没错,这个就是 C++ 等下要给 Node.js 捡的肥皂!
我们在刚才那个目录下新建一个文件 jianfeizao.js
:
var addon = require("./build/Release/hello"); |
看到没!看到没!出来了出来了!Node.js 和 C++ 搞基的结果!这个 addon.hello()
就是我们之前在 C++ 代码中写的 Handle<Value> Hello(const Arguments& args)
了,我们现在就已经把它返回的值给输出了。
时间不早了,今天就写到这里了,至此为止大家都能搞出最基础的 Hello world 的 C++ 扩展了吧。下一次写的应该会更深入一点,至于下一次是什么时候,我也不知道啦其实。
(喂喂喂,撸主怎么可以这么不负责!(o゚ロ゚)┌┛Σ(ノ´ω`)ノ
Expressjs
的时候写的。当时还随便搞了一下 backbone.js
,但是没有深入,勿笑。关于深入构架 Expressjs
方面也没做,只是粗粗写了下最基础的路由,所以整个文件结构也不是很规范。但是应该能比较适合刚学 Node.js
以及刚接触 Expressjs
的人吧。 Repo地址在我的Github上。Demo地址在 http://dang.kacaka.ca/,由于个人电脑的不稳定性,所以不保证你们随时可以访问,保不定哪天就失效了,所以最好的办法还是自己 clone
下来啪啪啪。
它所需要的东西大致就是 Expressjs
+ Redis
+ Backbone
了。不过都是最最基础的代码。
把部署写在最前面是为了能让你们自己电脑上有一个能跑的环境啦。公众档所在我自己这边的环境里面是由三台电脑组成的。
nginx
,然后对内部做反向代理。redis
罢了,也没做与其它数据库的持久化,只是用了他内部自带的持久化。如果你们装一台机子上,那么就是:
将 repo
给 clone 到自己的机子上。
$ git clone https://github.com/XadillaX/public-file-house |
装好 redis,并根据需要修改 redis.conf
文件。
执行 redis.sh
文件开启数据库。如果你自己本身已经开启数据库或者用其它方法开启了,请忽略上面数据库相关步骤。
然后打开 commonConst.js
文件进行编辑,把相关的一些信息改成自己所需要的。
哦对了,还有一个“洁癖相关”的步骤。我以前年轻不懂事,把 node_modules
文件夹也给加到版本库中了,而且也在里面居然自己加了两个没有弄到 nmp
去的模块(而且这两个模块本来就不应该放在这个文件夹下,但是不要在意这些细节,反正我现在肯定不会做这么傻的事了)。
至于为什么不要这么做,就跟 node_modules
文件夹的意义相关了。而且里面有可能有一些在我本机编译好的模块,所以最好还是清理下自己重新装一遍为佳。
具体呢大致就是把 node_modules
文件夹里面的 alphaRandomer.js
文件和 smpEncoder.js
文件拷贝出来备份到任意文件夹,然后删除整个 node_module
文件夹。接下去跑到项目根目录执行:
$ npm install |
把三方模块重新装好之后,把刚才拷出去的俩文件放回这个目录下。(但是以后你们自己写别的项目的话千万别学我这个坏样子啊,以前年轻不懂事 QAQ)
最后跑起来就行啦:
$ node pfh.js |
接下去就是要剖析这小破东西了。
这个文件其实是 Expressjs
自动生成的,以前不是很懂他,所以也没怎么动,基本上是保持原封不动的。
这个是路由定义的文件。比较丑陋的一种方法,把需要定义的所有路由都写进两个 json
对象中,一个 POST
和一个 GET
。
看过 Expressjs
文档的人或者教程的人都知道,最基础的路由注册写法其实就是:
app.get(KEY, FUNCTION); |
或者:
app.post(KEY, FUNCTION); |
所以我下面有一个函数:
exports.setRouter = function(app) { |
其大致意思就是把之前我们定义好的两个路由对象里的内容一一给注册到系统的路由当中去。这个是我最初最简陋的思想,不过后来我把它稍稍完善了一下写到别的地方去了。
这个就是模型层了,主要就是 redis
的一些操作了。在这里我用的是 redis
这个模块,具体的用法大家可以看它 repo
的 README.md
文件。
大致就三个函数:
fileModel.prototype.keyExists
: 判断某个提取码存在与否。fileModel.prototype.get
: 获取某个验证码的文件信息。fileModel.prototype.addFile
: 添加一个文件信息。不过有个坏样子大家不要学,
Node.js
大家都约定俗成的回调函数参数一般都是callback(err, data, blahblah...)
的,第一个参数都是错误,如果没错误都是null
或者是undefined
的。但是以前也没这种意识,所以回调函数的参数也都是比较乱的。
这是一些基础控制器。
纯粹的首页显示。
文件下载控制器。由代码可知,首先获取 token
和 code
。 token
是验证 URL 的有效性而 code
即提取码了。
期间我们验证了下 token
:
if(!functions.verifyBlahblah(token)) { |
而这个 verifyBlahblah
函数就在这个文件里面。
exports.verifyBlahblah = function(blahblah) { |
大体意思就是把其打散到数组里面,其中时间戳是最后一位。然后解密。最后验证解密后的 token
是否等于系统的 token
以及时间戳有没有过期。
大家通过截取 Chrome
或者 Firefox
的请求信息,不难发现有这么个地址:
Request URL:http://localhost/download?file=662ZE&token=65^97^74^68^106^125^88^115^65^96^66^105^127^114^87^123^123^114^84^124^114^125^120^121^99^116^100^118^116^98^124^120^109^98^120^100^80^119^120^87^119^105^116^8^1395904110 |
而这一坨 65^97^74^68^106^125^88^115^65^96^66^105^127^114^87^123^123^114^84...^1395904110
便是所谓的 token
了。而且本来就是个demo,这个 token
也就是随便做做样子罢了。
接下去通过验证之后,便可以从数据库中读取文件信息了。如果有文件,那么通过 resp.download
函数呈现给用户。
var fileModel = new FileModel(); |
这个函数就是生产一个有效的 token
用的。在前端是通过 ajax 来获取的。
var encoder = require("smpEncoder"); |
大体呢就是根据目前的时间戳和系统 token
一起加密生产一个有效的 token
。
通过自己的飞信给自己发送提取码以备忘。
这里的话用了一个 fetion-sender
的模块。Repo
在这里。
这个文件里面其实就一个 exports.upload
函数,另一个是生成提取码用的。
生成提取码。我们假设最多尝试10次,若尝试10次还没有生成唯一的验证码就输出错误让用户重试。所以就有了:
function genAlphaKey(time, callback) { |
不断地生成定长的提取码,然后通过模型的 keyExists
函数来确定这个提取码是否存在,如果存在了就递归调用重新生成,否则就直接回调。
上传文件的页面了。
if(req.files.files.length !== 1) { |
前面一堆话大致就是做下有效性判断而已。然后调用函数来生成有效的提取码:
genAlphaKey(1, function(status, msg, filename) { |
如果生成成功的话就往数据库中添加文件信息:
var fileModel = new FileModel(); |
如果添加也成功了的话,那么把刚上传到临时文件夹的文件给移动到上传文件储存目录中,以便以后可以被下载:
fs.rename(fileInfo.path, uploadDir + filename, function(err) { |
如果移动也成功了的话,那么返回一个成功的json信息:
result.status = true; |
这里视图就一个 index.ejs
。然后通过 backbone.js
来调用不同的页内模板和逻辑来实现的类似于 SPA (Solus Par Agula) (Single Page Application) 的效果。
像类似于下面的这种就是 backbone.js
的模板概念了:
<script type="text/template" id="faq-template"> |
到时候就可以通过 backbone.js
中的函数来填充到页面实体当中去。
在拥有了所有的前端js依赖之后,这个文件就是这个 SPA
的入口了。
逻辑很简单:
var workspace = null; |
新建一个 Workscpace
,然后对 backbone
进行一点配置。
To indicate that you’d like to use HTML5 pushState support in your application, use Backbone.history.start({pushState: true}). If you’d like to use pushState, but have browsers that don’t support it natively use full page refreshes instead, you can add {hashChange: false} to the options.
——摘自 [backbonejs.org](http://backbonejs.org/#History-start)
然后这个 Workspace
即这个 SPA
的本体了。
这里定义了几个路由,即什么路由要用哪个类去处理。这样才能在 URL
当中各种跳转。其实无非就是把待渲染元素渲染成页内模板,然后把页面的各种事件响应逻辑改掉即可。对于 Backbone
我其实只用过两次,现在也忘不大多了,怕误人子弟,所以一些具体的函数啊用法啊还是去参考下官网比较好来着。
就是各路由所对应的视图了。
比如说 uploadView.js
文件当中,执行渲染函数:
..., |
就是用页内模板来渲染:
$(this.el).html(Mustache.to_html( |
而这个 this.el
是在 Workspace
中定义的:
..., |
如你所见,就是这个 #main-template-container
了。
这个渲染完毕之后,然后把 #uploadfile
给变成上传按钮(用了 jquery.fileupload.js)。再然后把渲染好的页面给 show
出来。
然后这个 uploadView.js
中还定义了两个响应事件:
..., |
即在按下 .upbutton
的时候会执行 upload
函数,在按下“去下载”的按钮时会执行 goDownload
函数。
..., |
执行上传函数的时候,实际上是自动触动了 #uploadfile
按钮的 click
事件。这个时候就会按照之前定义好的 $("#uploadfile").fileupload(...)
去处理了。
这个是获取文件的视图。
渲染时会获取 code
。这个 code
同样是 Workspace
传入的:
..., |
上面关于 get
的路由是 get/:code
之类的,所以这个 code
会作为一个路由参数传给 get
函数。
有了这个 code
之后就可以把页面渲染出来了。这就是为什么我们地址输入 http://localhost/get/XXXXX
的时候输入框里面就有提取码了。把这个渲染出来之后,我们对“二维码”的两张图片做下响应:鼠标移动上去会显示出来。再然后我们要获取二维码了(this.genQRCode()
):
..., |
无非就是调用谷歌的 API 然后生成图片地址放上去罢了。一个地址就是当前页面地址,另一个就是加上 token
之后的直接下载地址。
如你所见,获取token是通过ajax往服务器请求的:
..., |
然后事件的话:
..., |
按了“去上传”按钮会跑去上传。如果按下“下载”按钮就下载文件了。然后输入框里面弹起键盘的话,会导致输入框文字变化,这个时候就要更新二维码以及URL了。
..., |
每当输入框变化之后,地址栏就要变成新的 get/:code
(workspace.navigate("get/" + code)
) 了,然后重新获取一遍二维码。
下载按钮的逻辑代码如下:
..., |
反正就是根据 code
来生成地址,然后从获取token的地址中把token拿出来拼接成下载地址之后再访问(window.location.href = url
)就好了。
这个视图是上传成功视图。功能很简单,就是现实下提取码,然后飞信能发送一下,以及能复制验证码罢了。
..., |
通过判断有没有 code
来判断是否上传成功。这个 code
的来源是 uploadView.js
中的 uploaded (done: this.uploaded)
函数:
..., |
e
和 data
这两个参数哪来?首先这个 uploaded
函数是在之前渲染的时候定义成 jquery.fileupload
的上传结束回调函数的,所以这两个参数自然是 jquery.fileupload
传过来的。详见这里。
总之就是上次成功之后,这个upload函数会获取一个 code
,然后它就会拿这个 code
存到 store
中。这个 store.js
是一个 localStorage
的封装。它的代码和文档在这里。
存好之后让 Workspace
给导航到 uploaded
视图中。
而这个 uploaded
视图的初始化函数里面有这样的代码:
..., |
就是初始化的时候,从 localStorage
中把 code
给取出来。
代码量少,用到的东西也是基础;不过以前的代码由于不了解 Node.js
啊 Expressjs
啊等等的,所以导致代码杂乱无章、脏乱无比,所以一定程度上阻碍了可读性的存在。
希望本文能给各位看官稍稍理清思路。我也不必写得面面俱到,只是在某个程度上点题一下而已。更多的大家自己看代码即可了。不过希望还不要把大家给误导了就好,毕竟这代码我自己现在看觉得好丢脸啊 QAQ。大家就去其糟粕取其精华吧。(喂喂喂,我去年买了个表,哪有什么精华啊!
]]>XPlan 是一个“基于校园强关系的社交应用”的开发代号。其中有一个功能是从学校网站上通过网络爬虫(Web Crawler)形式将学校新闻抓取到XPlan自身的数据库当中。
而这里出现的一个问题就是学校网站上面的文章是通过类似于 KindEditor
、UEditor
这类在线富文本编辑器生成的代码。
这类代码有几个共性:
代码有大量冗余、多重无用嵌套。
非常低的代码可读性。
在PC浏览器中表现力不错,往往能以低效的代码实现预期排版。
所以这些富文本编辑器可以在PC各大内核浏览器中表现良好,但是不便人工修改代码。
而 XPlan 确是一个由智能手机主导的应用,新闻将会通过一个 WebView 体现出来。所以就需要一定的方法将这些脏乱的代码适配成手机屏幕下表现力良好的代码。
在这里,我们将新闻的代码锁定在新闻内容排版,而排除了其它类似于新闻标题、新闻作者等其它信息。
以我们浙江大学软件学院为例,我们爬取的新闻内容代码将如下:
<div class="vid_wz"> |
所有内容将被包括在这个类型为 vid_wz
的 div
当中。
这时,我们将其包括在一个自己实现定义好的模板当中。该模板与新闻内容将会形成一个完整的网页,包括完整的 html
、head
、body
等标签。
<!DOCTYPE html> |
这里需要注意的一点的就是其中的一个 meta
标签:
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" /> |
它的意思是定义 viewport
的一些属性,实现了初步的手机网页适配。
手机浏览器是把页面放在一个虚拟窗口(即 viewport
)中,用户可以通过平移和缩放来看网页的不同部分。
通过 viewport
我们能对页面的一些缩放进行手机适配。
我们所需要做的仅是在 head
中插入一个 meta
标签,命名为 viewport
,然后定义好其 content
。
content
的语法如下:
控制 viewport
的宽度,可以指定一个值或者特殊的值,如 device-width
为设备宽度。
与 width
相对应,指定高度
初始缩放,即页面初始缩放程度。这是一个浮点值,是页面大小的一个乘数。例如,如果你设置初始缩放为 1.0
,那么页面在展现的时候就会以分辨率的1:1来展现。如果你设置为2.0
,那么这个页面就会放大为2倍。
最大放大倍数。
用户调整缩放,即用户是否能改变页面缩放程度。如果为 yes
即为可以, no
为不可以。
由于 XPlan 的后端是基于 node.js
构架的,所以 cheerio 模块是一个 node.js
专有的模块。
它的作用是将一段HTML代码转换为一棵DOM元素树。
在其官网上是这么诠释的:为服务端定制的快速、灵活、轻量级实现的 jQuery 内核。通常熟悉 jQuery 使用的开发者应该会对其使用方法比较熟悉。
所以在我们做接下去适配修改的之前,我们需要将我们刚才生成的完整HTML代码 转换为一棵我们可以操作的DOM元素树。
var cheerio = require("cheerio"); |
这时我们便能以熟悉的jQuery模式对其进行操作了,如:
$("p").html("hello foo!"); |
Bootstrap是Twitter推出的一个开源的用于前端开发的工具包。它有一个非常好的响应式的页面风格,使其在个尺寸屏幕上表现良好。
为了能更好适应屏幕,我们决定采用其自带的栅格系统,于是刚才的页面模板就有了新的变化:
<!DOCTYPE html> |
首先最外框的 container
,用其包裹的元素将实现居中对齐。在不同的媒体查询阈值范围内都为 container
设置了 width
,用以匹配栅格系统。
row
是一行栅格系统的外包元素。一行可以有12个栅格。
以 .col-md-
开头的栅格的最大 container
宽度为970,最大列宽为78,并能自适应屏幕。
在完成了以上操作以后,我们将对各元素进行微调处理。
好在在手机浏览器或者 WebView
中,对各种字体的设置不是非常敏感,所以我们仍然可以不处理一些关于字体变更的设置,以减轻开发量。
这里距几个微调的例子。
在新闻当中,图片充当的基本上是新闻照片的角色,在手机当中以单行出现为佳。
而 Bootstrap 当中本身就有元素类型来让图片元素响应屏幕宽度,并可以加上圆角边框。
所以我们需要做的就是为所有图片加上响应的类型:
$("img").addClass("img-thumbnail"); |
注意: 最后的一个移除
img
元素自带的style
属性是因为在文章发布的时候,有可能会被富文本编辑器自动加上一些宽高、边框等信息。为了统一所有图片风格以及让响应式生效,需要将其style
属性全部移除。
下面是是适配前与适配后的对比:
对于 table
元素也需要对它进行自适配,不然很有可能会溢出屏幕,使其多出了一个横向的滚动条。
$("table").removeAttr("style"); |
上面两句是移除 table
的原有的一些风格信息以及属性。后面是为其加上 Bootstrap 特有的 table
类型。
当然,更多的 table
元素还需要其它更多操作。不过就目前为止,XPlan 还没有着手关于 table
的更深一层容错处理。不过这里可以提供一个思路。
比如说 这篇文章中,不知道是谁给的在线富文本编辑器勇气,使其下面几张图片都各自被一个 table
及其子元素所包含。更有甚者,有一篇文章的一个段落被一个 table
所包容,并且在其左侧还有一个看不见的 td
元素。
我们可以提供的思路就是如果一个 table
只有一行一列就直接将其内容取出并删除该 table
。
超链接元素是一个新闻与用户互动的比较重要的元素之一。我们需要保持其美观性。
举几个例子来说,我们可以将超链接以一个类按钮的形式出现:
$("a").removeAttr("style"); |
然后我们甚至可以对其做一些细微的词汇修改。
比如当新闻发布者上传了一个附件然后不负责任地直接将文件名贴上的时候,我们可以贴心地将其显示文字改为“下载附件”。
再比如发布者直接以URL形式显示一个超链接的时候,我们可以贴心地将其改变为“打开链接”等等。
$("a").each(function(idx, elem) { |
然后我们再处理几个由于误操作而增加的错误链接,如在经上面操作后,还存在着url与显示内容相关的超链接可以直接取消,如这类:
让我们荡<a href="起双桨">起双桨</a> |
至此,当下版本的 XPlan 的新闻爬虫手机屏幕适配基本完成。其中当然还存在着一些细节处理和显示错误处理的不足,但是已经定下了基本的适配思路。
我们还在探索更好的适配方法,而当下的适配形式暂时已经可以满足了我们项目的需求。
]]>随便进去扯了一套最新的 SRM 来搞,全跑完之后才发现原来这场比赛还处于 System Running 阶段。于是知道了比赛一结束还在 Running 的时候你就已经可以自己拉出来做了。小绿名大家不要笑。
这次 DIV 2 的难度一般,一道签到题加两道普通的 DP。
题意很简单,就是给你一个字符串,问你最少改变多少字符让字符串所有字符都一样。
签到题,找最多的字符跟总长度一减就OK了。
#define SIZE(x) ((int)(x.size())) |
有 N 个齿轮围成一圈,相邻两个齿轮要反方向转才能正常运转不卡到其它轮子。你要从中间拿掉几个齿轮(留空了就不影响其左边的左边的齿轮),问最少拿掉几个使得所有齿轮能正常转。
我们建两个二维 dp 数组,或者一个三维 dp 数组:
dp[i][0|1][0|1] |
第一维 i
代表当前是第 i
个齿轮。第二维若是 0
则表示这个齿轮拿走,若是 1
代表留下。第三维若是 0
则代表第一个齿轮拿走,1
代表第一个齿轮留下。整个数组的每个元素就代表该齿轮留下或者拿走且第一个齿轮是留下或者拿走的情况下的最少拿走齿轮数。
所以我们能得到几个状态转移方程:
第一个齿轮
dp[i][0][0] = 1; |
第二个齿轮
如果与第一个同向那么就有了一留一走或者两个都走的情况。否则就是四种情况都可以。
if(与第一个齿轮同向) |
之后的所有齿轮
若该齿轮与前一个齿轮方向相同 ,那么该齿轮留下的时候,前一个齿轮必须得走,那么就是
dp[i - 1][0][?]
;该齿轮走的时候,前一个齿轮可走可留,就是dp[i - 1][0|1][?] + 1
的稍微小一点那个。若方向不相同 ,那么就是该齿轮留下的时候,前一个齿轮也可以留下。
if(与前一个齿轮同向) |
最后若最后一个齿轮与第一个齿轮同向,那么在 dp[i - 1][0][0]
、dp[i - 1][0][1]
、dp[i - 1][1][0]
中挑一个。若不同向,那么多了个 dp[i - 1][1][1]
这个选择。
下面就是代码了:
class GearsDiv2 |
给你一个 01串 与一个正整数 M。01串 有如下三种操作:
k * M
位反转。k 可以是任何正整数。k * M
位反转。k 可以是任何正整数。问最少需要几步将整个字符串变成都是 1
。
这又是一个 DP 的题目。
我们先设有 G 组,一组 M 个 01字符
。那么就能有
dp1[i][0|1] |
其中 i
代表第 i
组,第二维如果是 0
就代表这一组采用一位位反转的操作将这组全变成 1
,如果是 1
则将整组全部反转再采用一位位反转的操作将这组全变成 1
。至于 dp1
和 dp2
则代表从头到尾和从尾到头。
由于只有 0
和 1
反转,那么一组反转两次就能还原原状——这是一个非常重要的性质。
如果某一组采用整组反转的操作,若前一组也是整组反转,那么就相当于操作次数不变,只是将前一组的反转范围延续到这一组;若前一组是非整组反转,那么就相当于从头到这一组反转之后,前面的所有组再反转回去——相当于是多了两次操作。于是就有了(先只拿 dp1
作为例子):
dp1[i][1] = min( |
如果某一组采用非整组反转,那么操作次数就是前一组的整组反转或者非整组反转的操作次数加上这一组 0
的数量:
dp1[i][0] = min( |
用上面的转移方程把正反向都求了一遍之后,我们就可以求总答案了,总答案就是我们枚举中间只有操作1的段的首尾,加上该中间段前部分的 dp 答案和其后部分的 dp 答案,取出最小值就是了。
class FlippingBitsDiv2 |
不同于 PHP 的是,PHP 就是是有了新变量也无需申明,而 JavaScript 则还是需要 var
来申明一下的。而这个 var
涵盖了 C++ 中的int
、string
、char
等一切类型的含义,甚至是 function
。
本篇以及后篇的所有内容都是在 Linux 或者 Cygwin 下用 vim 进行编辑(若不是则请自行转变成你自己的方法),然后在命令行下进行查看结果的。
在 C/C++ 中,我们这么声明变量的:
void foo() {} |
而在 Node.js 中则是这样的:
function foo() {} |
所以,无论是什么类型的变量,在 Node.js 中都是以一个 var
来解决的。
这个循环语句基本上跟 C/C++ 一样,都是
for(int i = 0; i < foo; i++) |
而鉴于 Node.js 是弱类型,所以只需要:
for(var i = 0; i < foo; i++) { |
这是一种后有型的循环语句,类似于 PHP 的 foreach
。
比如我们有一个 JSON对象 如下:
var foo = { |
这个时候我们就可以用 for...in
来循环遍历了:
for(var key in foo) { |
我们如果在命令行中打入下面的命令:
$ node foo.js |
屏幕上就会显示下面的内容了:
hello: world |
提示:由上可知,
for...in
语句是用来遍历 JSON对象、数组、对象的键名的,而不提供键值的遍历。如果要获取键值,只能通过
foo[<当前键名>] |
的形式来获取。这个跟 PHP 的 foreach
还是有一定区别的。
这个就不多做解释了,跟其它语言没什么大的区别,无非就是如果有变量声明的话,需要用 var
就够了。
这几个运算符也就这样,要注意的是 +
。它既可以作用于字符串,也可以作用于数值运算。弱类型语言虽然说类型是弱的,数字有时候可以以字符串的形态出现,字符串有时候可以用数值的形态出现,但是在必要的时候也还是要说一下它是什么类型的,我们可以用下面的代码去看看结果:
var a = "1"; |
这里的
parseInt
是 Node.js 的一个内置函数,作用是将一个字符串解析成int
类型的变量。
上面的代码执行结果是
12 |
第一个 console.log
结果是 12
,由于 a
是字符串,所以 b
也被系统以字符串的姿态进行加操作,结果就是将两个字符串黏连在一起就变成了 12
。而第二个 console.log
结果是 3
,是因为我们将第一个 a
转变为了 int
类型,两个 int
型的变量相加即数值相加,结果当然就是 3
了。
这里有一点要解释,当这个逻辑运算符长度为 2
的时候(==
, !=
),只是判断外在的值是不是一样的,而不会判断类型。如
var a = 1, b = "1"; |
它输出的结果就是 true
。但是如果我们在中间判断的时候再加上一个等号,那么就是严格判断了,需要类型和值都一样的时候才会是 true
,否则就是 false
。也就是说
var a = 1, b = "1"; |
的时候,返回的结果就是 false
了,因为 a
是 int
型的,而 b
则是字符串。
顺带着就把条件语句讲了吧,其实这里的
if
跟别的语言没什么两样,就是几个逻辑运算符两个等号三个等号的问题。所以就不多做累述了。
这里我姑且把它当成是一个运算符而不是函数了。
这个运算符的作用是判断一个变量的类型,会返回一个字符串,即类型名,具体的执行下面的代码就知道了:
function foo() {} |
这里的执行结果就将会是:
number |
在 JavaScript 中,有三个特殊的值,如标题所示。其中第一个大家可能都比较熟悉吧,C/C++ 里面也有,不过是大写的,其本质就是一个
#define NULL 0 |
而在 JavaScript 中,这三个值所代表的意义都不同。
null
是一种特殊的 object,大致的意思就是空。比如说:
var a = null; |
大家都能看懂,就不多做解释了。但是跟 C/C++ 不同的是,这个 null
跟 0
不相等。
这个东西的意思就是说这个变量未声明。为了能够更好地区分 null
,我们的样例代码如下写:
var a = { |
上面的代码中,我们让 a["foo"]
的值为空,即 null
。而压根没有声明 a["bar"]
这个东西,它连空都不是。输出的结果大家都差不多应该猜到了:
null |
这是一个空的数值,是一个特殊的 number
。它的全称是 Not a Number
。有点奇怪,大家可以理解为 不是数字形态,或者数值出错的 number
类型变量。
多在浮点型数值运算错误(如被0除)的情况下出现,甚至可以是用户自己让一个变量等于 NaN
以便返回一个错误值让大家知道这个函数运算出错了云云。
其它剩余的语句也跟已存在的其它语言差不多,比如说 break
啊、switch
啊、continue
啊等等等等。
这一节主要讲的是 JavaScript 对象,其它类型差不多一带而过吧。
Node.js 包含的基础类型差不多有如下几个:
其中前三种类型可以直接赋值,而 array
的赋值只是一个引用赋值而已,在新变量中改变某个值的话旧变量的值也会改变,直接可以试试下面的代码:
var foo = [ 1, 2, 3 ]; |
它得出的结果是:
[ 3, 2, 3 ] |
也就是说 array
要是复制出一个新的数组的话,不能用直接赋值的方法,而必须“**深拷贝**”。
这里有必要讲一下 array
的三种创建方法。
第一种:
var dog = new Array(); |
第二种:
var dog = new Array( "嘘~", "蛋花汤", "在睡觉" ); |
第四种:
var dog = [ |
我个人比较喜欢第三种写法,比较简洁。
这里我把 JSON对象 单独拎出来而不是把它归类为 JavaScript对象,如果觉得我有点误人子弟就可以直接跳过这一节了。
本人对于 JSON对象 和 JavaScript 对象的区分放在 是否只用来存储数据,而并非是一个类的实例化。其实 JSON 的本质便是 JavaScript Object Notation。
更多有关 JSON 的信息请自行百科。
在 Node.js 中声明一个 JSON对象 非常简单:
var dog = { |
有两种方式能得到 JSON对象 中的某个键名的键值,第一种是用点连接,第二种是用中括号:
dog.pre; |
试试看:现在你自己动手试试看,用
for...in
的形式遍历一遍上面的JSON对象
。别忘了用上typeof
喵~
严格意义上来讲,Node.js 的类不能算是类,其实它只是一个函数的集合体,加一些成员变量。它的本质其实是一个函数。
不过为了通俗地讲,我们接下去以及以后都将其称为“类”,实例化的叫“对象”。
因为类有着很多 函数 的特性,或者说它的本质就是一个 函数,所以这里面我们可能一不留神就顺带着把函数基础给讲了。
声明一个类非常简单,大家不要笑:
function foo() { |
好了,我们已经写好了一个 foo
类了。
真的假的?!真的。
不信?不信你可以接下去打一段代码看看:
var bar = new foo(); |
别看它是一个函数,如果以这样的形式(new
)写出来,它就是这个类的实例化。
而这个所谓的 foo()
其实就是这个 foo()
类的构造函数。
成员变量有好两种方法。
第一种就是在类的构造函数或者任何构造函数中使用 this.<变量名>
。你可以在任何时候声明一个成员变量,在外部不影响使用,反正就算在还未声明的时候使用它,也会有一个 undefined
来撑着。所以说这就是第一种方法:
function foo() { |
注意:只有在加了
this
的时候才是调用类的成员变量,否则只是函数内的一个局部变量而已。要分清楚有没有this
的时候变量的作用范围。
第二种方法就是在构造函数或者任何成员函数外部声明,其格式是 <类名>.prototype.<变量名>
:
function foo() { |
无聊上面哪种方法都是对成员变量的声明,我们可以看看效果:
var bar = new foo(); |
甚至你可以这么修改这个类:
function foo() { |
然后再用上面的代码输出。
想想看为什么输出的还是
world
而不是蛋花汤
。
我们之前说过了这个 foo()
实际上是一个 构造函数。那么显然我们可以给构造函数传参数,所以就有了下面的代码:
// 代码2.1 |
我们看到上面有一个奇葩的判断 if(hello === undefined)
,这个判断有什么用呢?第一种可能,就是开发者很蛋疼地特意传进去一个 undefined
进去,这个时候它是 undefined
无可厚非。
还有一种情况。我们一开始就说了 JavaScript 是一门弱类型语言,其实不仅仅是弱类型,它的传参数也非常不严谨。你可以多传或者少传(只要保证你多传或者少传的时候可以保证程序不出错,或者逻辑不出错),原则上都是可以的。多传的参数会被自动忽略,而少传的参数会以 undefined
补足。
看看下面的代码就明白了:
// 上接代码2.1 |
请自行输出一下两个 bar
的 hello
变量,会发现一个是 world 一个是 蛋花汤。显而易见,我们的第一个 bar1
在声明的时候,被 Node.js 自动看成了:
var bar1 = new foo(undefined); |
所以就有了它是 world 一说。
还有就是在这个构造函数中,我们看到了传进去的参数是 hello
而这个类中本来就有个成员变量就是 this.hello
。不过我们之前说过了有 this
和没 this
的时候作用域不同,那个参数只是作用于构造函数中,而加了 this
的那个则是成员变量。用一个 this
就马上区分开来他们了,所以即使同名也没关系。
成员函数的声明跟成员变量的第二种声明方法差不多,即 <类名>.prototype.<函数名> = <函数>;
// 上接代码2.1 |
上面这段代码显而易见,我们实现了 foo
类的 setHello
函数,能通过它修改 foo.hello
的值。
但是这么写是不是有点麻烦?接下去我要讲一个 JavaScript 函数重要的特性了。
很多时候我们的某些函数只在一个地方被引用或者调用,那么我们为这个函数起一个名字就太不值了,没必要,所以我们可以临时写好这个函数,直接让引用它的人引用它,调用它的人调用它。所以函数可以省略函数名,如:
function(hello) { |
至于怎么引用或者调用呢?如果是上面的那个类需要引用的话,就是写成这样的:
foo.prototype.setHello = function(hello) { |
这样的写法跟 2.3.3.1. 成员函数声明 是一个效果的,而且省了很多的代码量。而且实际上,基本上的类成员函数的声明都是采用这种匿名函数的方式来声明的。
至于说怎么样让匿名函数被调用呢?这通常用于传入一个只被某个函数调用的函数时这样写。
比如我们有一个函数的原型是:
/** |
比如我们有两个版本的输出函数,一个是中文输出,一个是英文输出,那么如果不用匿名函数时候是这么写的:
function zh(a, b, sum) { |
执行一遍这段代码,输出的结果将会是:
1 + 2 的值是:3 |
这样的代码如果采用匿名函数的形式则将会是:
sumab(1, 2, function(a, b, sum) { |
这种形式通常使用于回调函数。回调机制算是 Node.js 或者说 JavaScript 的精髓。在以后的篇章会做介绍。
虽然上一节讲过了,不过还是再讲一遍吧。
通常我们声明类的成员函数时候都是用匿名函数来声明的,因为反正那个函数也就是这个类的一个成员函数而已,不会在其它地方被单独引用或者调用,所以就有了下面的代码:
// 上接代码2.1 |
这样我们就使得 foo
类有了 setHello
这个函数了。
这个又是我胡扯的。所谓类的随意性即 JavaScript 中你可以在任何地方修改你的类,这跟 Ruby 有着一定的相似之处。
比如说 string
,它其实也是一个类,有着诸如 length
这样的成员变量,也有 indexOf
、substr
等成员函数。但是万一我们觉得这个 string
有些地方不完善,想加自己的方法,那么可以在你想要的地方给它增加一个函数,比如:
String.prototype.sb = function() { |
这个函数的意思就是填充一个字符串,使其变成 sb
的化身。
我们来测试一下:
var str = "嘘~蛋花汤在睡觉。"; |
你将会得到这样的结果:
sbsbsbsbs |
你跟你的电脑说“嘘~蛋花汤在睡觉。”,你的电脑会骂你四次半傻逼。(赶快砸了它)
所谓深拷贝就是自己新建一个数组或者对象,把源数组或者对象中的基础类型变量值一个个手动拷过去,而不是只把源数组或者对象的引用拿过来。所以这就涉及到了一个递归的调用什么的。
下面是我实现的一个深拷贝函数,大家可以写一个自己的然后加入到自己的 Node.js 知识库中。
function cloneObject(src) { |
其实[Windows](#windows-环境)下也不算麻烦,但是这里会讲一定量的别的环境的搭建。
讲到这个就很简单了,跟着下面的 bash 操作即可:
$ cd /usr/local/bin |
其中将上方的 v0.00.00 替换成 Node.js 最新的版本号,把 x00 替换成你自己电脑的位数。
也可以直接去官网 http://nodejs.org/download/ 找到相应的地址。
最后将其的连接加入到 /usr/bin
下即可。
$ cd bin |
注意: 该用
sudo
的地方就用sudo
或者su
。
至此,Linux 下的 Node.js 环境基本搭建完毕。
Cygwin 是一个在 Windows 平台上运行的 Unix 模拟环境。对于学习 Unix/Linux 操作环境,或者从 Unix 到 Windows 的应用程序移植,或者进行某些特殊的开发工作,尤其是使用 GNU工具集 在 Windows 上进行嵌入式系统开发,非常有用。
我们先跑到 Cygwin 的官网上去把东西下来:
http://cygwin.com/install.html
注意,最好下 x86 的包,因为我们之后要讲一个
cyg-apt
的脚本插件,这是一个能让 Cygwin 能跟 Linux 一样通过脚本从源安装软件包的脚本。为了方便修改,我们将其下成 x86 的版本。
然后就是安装步骤了。
到 [图2.1] 这个步骤的时候,选择默认的 Install from Internet
即可。
在 [图2.2] 的时候选一个安装路径。
注意:尽可能让这个安装路径简单,而不要是类似于
c:\Program Files\blahblah
这样的文件路径。
[图2.3] 的时候选一个本地包的路径,我这里选的是 e:\cygwin\tmp
。
[图2.4] 选择直接连接。
我们国内的用户源还是选择 163
的速度比较快。所以在 [图2.5] 这一步的时候就直接选用默认的 163
的源了。如果不是默认的话,请选中它。
在 Select Package 也就是选择预安装的软件的时候,把下列表中的软件包勾选起来:
- wget: 在 Utils 中
- vim: 在 Editors 中
- gcc: 在 Devel 中
- gcc-g++: 在 Devel 中
- make: 在 Devel 中
- cmake: 在 Devel 中
若是这些选项已经被选起来了就不用再选了,如果没有选起来则把它选中。
勾选好了之后就可以下一步安装了,直至安装完毕,你就可以打开你的 Cygwin 了。
提示:你可以点击窗口左上角的小图片,然后里面的 Options 中,你可以调整你自己的 Cygwin 外观。
上一步我们已经选中了 vim ,也就是说我们已经在 Cygwin 中装上了 vim。但是由于这里的 vim 默认配置非常蛋疼,所以我们得改一下。
在你的 Cygwin 中一句句输入下面的命令:
$ cd /home/<你自己的用户名> |
这样你的 vim 就用上了上面的那个地址的配置文件,当然你也可以编辑你自己的配置文件或者说从网上下别的配置文件以满足你的个性化需求。
vim 配置以及使用请参照:https://wiki.archlinux.org/index.php/Vim
事无巨细问 ArchWiki。
*-- [kalxd](https://github.com/kalxd)*
apt-cyg is a command-line installer for Cygwin which cooperates with Cygwin Setup and uses the same repository. The syntax is similar to apt-get.
*-- From apt-cyg googlecode page*
总之意思就是说 apt-cyg
是类似于 Linux 中的 apt-get
, yum
, zypper
等命令行软件包安装器一样,可以通过
apt-cyg install <package names>
来安装软件包apt-cyg remove <package names>
来移除软件包apt-cyg update
来更新 setup.iniapt-cyg show
来列出已安装的软件包apt-cyg find <pattern(s)>
来查找符合条件的软件包apt-cyg describe <pattern(s)>
来描述符合条件的软件包apt-cyg packageof <commands or files>
来定位其父软件包其实也不能说是安装,纯粹是把脚本从网络上拷到自己的 Cygwin 的环境目录中。
在你的 Cygwin 中输入以下命令:
$ cd /usr/local/bin |
这样你就“安装”好了 apt-cyg 了。不过这里用的是默认的源,所有东西都是默认的。
如果你现在已经心安理得或者不想折腾了可以跳过 **2.1.3.2. apt-cyg 修改**,如果你想把源换成 163
的话那么稍微看一下吧。
接下去我们要对 apt-cyg 做一些编辑。
你有下面两个选择:
vim apt-cyg
来进行编辑。大约是 68
行上下吧,有一句是:
mirror=ftp://mirror.mcs.anl.gov/pub/cygwin |
将其改成:
mirror=http://mirrors.163.com/cygwin |
还有就是大概在 98
行和 105
行左右:
wget -N $mirror/setup.bz2 |
修改成:
wget -N $mirror/x86/setup.bz2 |
至此,你的 Cygwin 环境基本完成,以后可以再慢慢完善。
这个就很简单了,打开 Node.js 官网下载安装即可。
选择 Windows Installer (.msi) 或者 **Windows Binary (.exe)**。
安装好后就能直接在 Cygwin 里面使用了。
现在,无论你是 Linux 用户还是 Windows 用户,都可以用一样的步骤来完成下面的 Hello World
了。
随便跑一个目录里面新建一个文件并且用 vim 编辑:
$ vim hello.js |
在里面输入下面的东西:
console.log("Hello world!"); |
然后退出 vim 执行:
$ node hello.js |
终于,真·Hello world 出现在了你的眼前,而不需要借助 IDEOne 了。
To be continued…
]]>由于那帮人大多还处于使用 M$ Windows 的令人不愉快的阶段,所以本教程将会退而求其次,使其在 Cygwin 中模拟 linux 的命令(Windows的bat脚本实在是让人不敢恭维)。以及在这里会讲述一些 Git 操作的初步。当然,如果你已经在使用 linux 进行开发的话,可以跳过前面一堆令人感到厌烦的环境配置章节。或者你在使用 M$ Windows 但却不想改变自己的脚本习惯的话,也可以选择性地跳过一些章节和步骤。
很多人都知道JS是一门语言,而且是一门脚本语言,其全称就是 JavaScript,而且与所谓的 Java 没有一个屁的关系。
在好多年前,JavaScript 是网页的一个寄生虫,它必须依赖于网页的浏览器中才能执行,并且作为网页的一部分,以
<script type="text/javascript"> |
标签进行包含,这样才能提供其上下文环境。或者说将其单独写入一个 *.js
文件中,并且在网页里以
<script src="foo/bar.js" type="text/javascript"></script> |
的形式将其包含进来。
但总而言之,JavaScript 只是寄生在网页里面的一只小小可怜虫罢了。它的作用无非就是使网页的交互性更强,页面效果更多而已。
后来,这帮不甘寂寞的人类将 JavaScript 从网页(或者说前端)的帝国中独立了出来(小心快递),于是就出现了 CommonJS。
CommonJS 其实不是一门新的语言,甚至都不能说它是一个新的解释器——实际上它只是一个概念或者是一个规范。
在这个规范中,它定义了很多 API ,讲通俗点或者直截了当点就是函数啊类啊什么的,而这些 API 是为那些普通应用程序(Native App)而非浏览器应用使用。它的终极目标就是提供一个类似于 Python、Ruby 之类的脚本一样的标准库,开发者可以用这样的东西一样来做到 Python、Ruby 能做到的事,而非仅仅局限于网页中的效果或者功能实现,它也可以跑在本地。
所以说下面的事情对于 JavaScript 来说不再是梦:
那么,它具体弥补了 前端JavaScript 的哪些空白呢?其实这也涉及了很多 前端JavaScript 所没有涉及的东西,如二进制、编码、IO、文件、系统、断言测试、套接字、事件队列、Worker、控制台等等。
关于 CommonJS 的更进一步了解可以翻阅一下其 **Wiki**。
上面讲了那么多,却始终停留在“规范”这个层面上。而 Node.js 的出现便是让 CommonJS 成为了现实。
这里要大家明确的一点的就是 Node.js 并不是一门新的语言,它的语言还是 JavaScript ,硬要说是一门新的语言那也应该是 Common JavaScript。Node.js 只是 CommonJS 的一个解释器罢了。
它是基于 Google 的 V8虚拟机(Chrome浏览器所使用的JavaScript执行环境) 的一个解释器。
很多人印象中的概念还是没能摆脱 前端JavaScript 的阴霾,认为 JavaScript 就是做网站的, Node.js 也是如此。
包括本人在 cnodejs.org 中看到的帖子大多也都是讲 Node.js 如何如何做网站(服务端)云云,如何如何使用 Express 模块来搭建一个网站云云。
这是一个误区。
PHP 还能用 PHP-CLI 来写个脚本放本地跑呢,Node.js 更是可以写任何程序。虽然这么讲有些夸大了,但是我这么说的理由是希望大家能摆脱这么一个误区。
举个简单的例子吧,大家都是搞过 ACM 的孩子了,总对终端窗口的输入输出有一定感觉了吧。现在给我以最快速度码一个 A + B Problem 给我看看。
轻车熟路,我知道。但是你们现在做的事用 Node.js 同样能做到。
process.stdin.resume(); |
由于我们学校我的前任学校OJ不支持 Node.js,所以请你们移步到 AIZU OJ 去把上面的代码交过去看看结果看。
注意:语言要选择 JavaScript。
怎么样,同样能过题的对吧?
上面对这些东西做了个简单的介绍,我需要你们知道的东西很简单:
有个码畜老了,想学学书法来修身养性。当他展开宣纸,犹豫了半天之后,终于挥毫泼墨,在纸上龙飞凤舞写下几个大字:
Hello World
虽然这一篇文章没有讲到任何 Node.js 的语法,但是还是可以让你们练练书法的。
C语言 的标准输出函数是 printf
,而 Node.js 的标准输出则是:
console.log("blahblah..."); |
好的,即使没有装上 Node.js 环境也阻止不了我们向世界问好。
打开 IDEOne**,将你的 Hello World
贴到编辑框中,然后在左侧的语言栏里面选中 **Node.js ,点击送出,你就能看到你的第一个 Node.js 程序的运行结果了。
To be continued…
]]>众所周知JS中的一个精髓就是异步回调。
所以在我自己写的框架中也经常会出现类似于下面的代码:
foo.bar(a, b, function(){}); |
总而言之就是写一个函数,这个函数将会调用一个回调函数。
但是问题出现了:在那个回调函数 function
中,你如果使用了一个 this
指针的话,它将会指向根,而不是 foo
的本体。
那么如果我们想在 function
中也用 this
来指代这个 foo
对象该怎么办呢?
结果还是IRC有用。本人跑 Node.JS 的 IRC 上问了这个问题,结果有人就这样回复我了:
13:07 <shama> xadillax: foo(a, b callback.bind(foo))
13:10 <olalonde> foo (a, b fn) { fn = fn.bind(this); …. }
然后还很热心地给了我个网址:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind
总之最后得出的结论就是说:
你只要给你的 callback
函数指定一个 this
指针即可。
如:
var cb = callback.bind(foo); |
这样就能在回调函数中使用foo来作为其this指针了。
]]>SevenzJS
也已经被遗弃最近在做公司项目的一个模块,主要用于 JSON Api 的传输,所以开发环境的目标就锁定在了 Node.js。而这一块的登陆用户又是存在 MongoDB 里面的,所以就有了如下的问题。
我们试想一下,如果我们有几句MongoDB的查询之类的,用node-mongodb-native来写的话是这样的:
var client = new Db('test', new Server("127.0.0.1", 27017, {})); |
各种嵌套回调有木有!这不是我们想要的,尤其是我的那个框架,因为我的框架是流式的。
所以我就想有这样的一种方案:
var client = mongodb.connect(); |
使得这样就能找出dbname表下的foo为bar值的记录了。
出于这样的想法,我在网上找遍了大江南北,除了 CNode 社区有人问到了类似的问题以外,再也找不到音信了,而且那里也没有一个好的回答。
不过这也正常,因为 Node.js 官方本身就不推荐这么做——他们认为异步非阻塞是非常优雅的一件事情。
包括我在 Node.js 的 IRC 聊天室里面问了这个问题,也有人是这么回答我的:
You can’t use a car as a boat. If you want a boat, use a boat.
言简意赅,直截了当地说明 Node.js 是不支持这样的,如果你想这样做,就用 python 或者 ruby 去吧。
不过好在后来 IRC 里面有人推荐了我一个模块:fibers。
有了这个模块好啊,直接能用了有木有!
接下来就来讲一下如何使用吧:
function find(collection, selector, callback) { |
这就是一个非常简单的同步查询 MongoDB 的例子了,实际上本质还是一个异步,注意到没有,其实 Fiber()
内部的那个 function
本质上还是一个回调函数,只不过在这个回调函数里面,里面的所有 callback
都可以被同步了。不过我们只需要小动一些手脚就能加上这个外壳了。具体请参见 sRouter.js 约 121 行的外壳以及 sMongoSync.js 的实现,加上 index.js 中的查询 demo。
所以说当我们做不到某件事的时候,多去IRC看看,多去社区混混,也多去找找模块,要真没有的话就只能自己丰衣足食了(我还没到那水平,笑)。总之这次Fibers帮了我一个大忙。
最后,SevenzJS 欢迎 Fork。
]]>废话不多说,直接切入正题吧——
我要做得就是让下面一段代码生效:
$("#yourid").stop().animate({ "backgroundColor" : "#rrggbb", "color" : "#rrggbb" }, "fast"); |
但是,很遗憾,一点也没有动。本来效果应该跟这个版本的xcoder博客的天头导航条一样有个动态效果(只不过xcoder的导航条是透明度变化,而项目中我想让它背景色变化)。
原因是什么呢?死月上网查了很久,找到的东西都很简单地说明了一下,貌似都可以。嘛,也许是jQuery新版本不支持这个特性了吧。
最后,死月在jQuery的官方文档中找到了下面这段话——
All animated properties should be animated to a single numeric value, except as noted below; most properties that are non-numeric cannot be animated using basic jQuery functionality (For example, width, height, or left can be animated but background-color cannot be, unless the jQuery.Color() plugin is used). Property values are treated as a number of pixels unless otherwise specified. The units em and % can be specified where applicable.
—— [jQuery官方文档 .animate()](http://api.jquery.com/animate/)
大致的意思就是说所有动画属性都必须是一个单数字值,所以说大多数非数字的属性是不能被动画化的。例如高度、宽度等可以被动画化,但是背景色就不信了。除非你用了jQuery.Color()插件。
所以说问题找到了,我们必须得用一个jQuery.Color()插件来对一些颜色进行动画操作。
话不多说,我们去下一个jQuery.Color()插件。把它加在我们的页面中,然后就可以用如下方式来进行动画操作了:
$(this).stop().animate({ |
我最先用到 ScopeLock 模式是在自己开发 XAE引擎 的时候。在里面用到挺多的线程函数,那么如何解决临界区就成了一个重要的课题。可能大家想,不就一个线程锁临界区什么的么,一个 EnterCriticalSection
和一个 LeaveCriticalSection
不就解决了么?
其实不然。在 M$ 中,最常用的当然就是 CRITICAL_SECTION
了,但是如果临界区上锁却木有解锁呢?这就会发生死锁现象。对于一个粗心的程序猿来说这样的错误还是有机率发生的。就算你足够细心,还是有时候会一失足成千古恨。
所以就有了这么一种方法来杜绝这种死锁的产生—— ScopeLock
。
那么什么叫 ScopeLock
?
我们试想一下如果有这么一个类——在构造的时候,你传进去一个 CRITICAL_SECTION
的引用并且将其 EnterCriticalSection
进入到临界区。当它析构的时候,我们直接 LeaveCriticalSection
就好了。
也许你会问,这样的一个类会有什么用呢?
那么我下面演示一段简单的 ScopeLock 代码先吧:
struct ScopeLock |
我们可以发现,当我们刚进入 ScopeLockTest
函数的时候,声明了一个 oLock
对象,这个时候运行 oLock
的构造函数,也就是进入了 cs
这个临界区。而当 ScopeLockTest
函数运行完毕要退出这个函数的时候,oLock
对象的生命周期也就走到了尽头,对应的,它将会执行析构函数,那么就自然而然地退出了 cs
临界区。
其实无论 ScopeLockTest
这个函数怎么写,哪怕是中间有一些 if
判断直接 return
掉,只要是 ScopeLockTest
这个函数执行完毕,oLock
就会自动析构,从而达到了解锁过程。那么不管粗心还是细心的童鞋们都不用为忘记退出临界区而烦恼了。
而且 ScopeLock
模式只是一种思想,并不是对于 M$ 的临界区的一种专用性物品。例如在QT里,我们一样可以用 ScopeLock
来对线程的一些 MutexLock
之类的东西进行操作。
上面所写的例子只是思路的一种形成,并不是一个完整的ScopeLock类(结构体),虽然说它现在已经可以用了。你可以在上面完善,加上自己的东西,使其能确确实实在项目中使用。由于代码的关联性,我单单发出我的 ScopeLock
的话会缺少很多关联的东西,所以咱就不发了,思路在这里,相信谁都能写出自己的一个 ScopeLock
吧。
题意就是说,CF有两题,每题初始分A和B,然后每题在每分钟会扣DA和DB分。给你比赛总时间T,问你某个人可不可能拿到X分。(注意可能做出一题、两题或者一题也没做出)
(0 ≤ x ≤ 600; 1 ≤ t, a, b, da, db ≤ 300且保证及时比赛时间到了各题的分数也不会小于0)
有一个 NN 方格纸。在上面的方格里一格格涂黑。每一步涂一格一共涂 m 次,给定 xi 和 yi。问最少涂几步方格纸里会出现一个 33 的正方形。
(1 ≤ n ≤ 1000, 1 ≤ m ≤ min(n· n, 105))
照相内存卡里有d容量。其中高质量照片占a容量、低质量占b容量。然后有n个顾客,每个顾客需要xi张高质量照片和yi张低质量照片。摄影师如果给一个人拍照了,就应该满足他所有要求(即给xi张高质量照片和yi张低质量照片)。问摄影师最多能给几个人拍照。
(1 ≤ n ≤ 105, 1 ≤ d ≤ 109, 1 ≤ a ≤ b ≤ 104, 0 ≤ xi, yi ≤ 105)
封闭房间里,从房间的一头最底下的中间以某个方向踢球(一定是网对面踢),问踢到另一头的墙上的时候,x、z各是多少。
(各座标以及向量都是小于等于100的正整数)
还没看。
这题只要注意几个trick就行了:可以做出0题、1题或者2题。直接两个for枚举各题在几分钟内做出来,然后做一下0题、1题的特殊判断就好了。
在每次涂的时候,以当前涂的点位中心,设它为九宫格的其中一个位置(一共九种位置),对于每种位置,都判断其对应的九宫格是不是 3*3 的黑色就好了。(我做的时候在设位置的时候 x - 1, y - 1 手贱敲成了 x - 1, y - 2,lock 之后才发现。悲剧)
贪心。对于每个人将其所需的总容量算出来再进行递增排序。最后求的时候推荐累减的方式判断,因为我累加然后用 int 最后爆范围了。
首先拿出空间几何的线面相交模板。然后来一个 while
,每次循环的时候判断当前所在的点与方向适量形成的直线与 (X, 0, Z) 面的交点在不在终点墙壁大小的范围内。若不是则说明中途撞墙了判断方向向量:x < 0则线面相交判断是不是撞左墙,若是则 x 正负值变一下;x > 0 则线面相交判断是不是撞右墙,若是则 x 正负值变一下。z < 0则判断是不是以求抢地,若是则 z 正负变一下。最后 z > 0 则判断是不是撞天花板,若是则z正负值变一下。然后以球撞击的点为新的起点,与新的方向向量形成新的直线,继续下一次循环。因为房间大小最大是 100 * 100 * 100,而方向向量各方向是 1 到 100 的整数,不是小数,则撞击次数不会很多,直接 while
撞击也不会超。
|
|
|
|
前几天在HGE的群里看到有人突然问到如何判断鼠标有没有点到人(点到纹理的透明区域不算),从而引申出了碰撞检测问题。
他的问题相对好实现,只要算出纹理所按的点是不是透明即可。
接下来我得做下碰撞检测的笔记:
碰撞检测最常用一个方法就是关节设置(当然我并没有做过),关节设置的话因为只是判断多边形的重叠状况,算法的复杂度低、效率高,虽然做工有点粗,但总体效果还是性价比比较高的一种方法。当然,这样的方法需要对每一帧的纹理都设置一个关节,对于人工的代价就稍微大了一些了,并且还要写个关节编辑器啊神马的,于是乎代码量又增加了。我这次是和同寝室木有一点基础的童鞋一起练手的,所以并没有打算引进这个方法。
于是我就用了另一种稍微“非主流”一些的方法了——逐像素判断。
但是逐像素判断还是有问题的——如果你的一个“效果”因为“温度过高”而不需要显示,直接隐藏,但又算伤害,这时纹理的逐像素就失去了意义。于是又有了个“臃肿”的办法,为需要“额外附加像素”的纹理另做一张图片,这张图片上有两种区域——热点区和非热点区。我们把需要“当做空气”的那些区域一律用某一种极其不常用的颜色覆盖,如 ff00ff
这种变态的粉色,然后其它区域的颜色就随你怎么搞了。我们载入的时候两张纹理一起载入,显示的时候显示正常的纹理,而在碰撞检测的时候用“热点图片”来进行逐像素检测。
与上面的关节设置法比较的话,人工的工作量我个人认为是大大地减少了,至于对于机器的执行能力来说,把时间复杂度提到了 O(mn)
,平方级的复杂度了,即纹理相交区域的宽和高。
我们来看一下这种碰撞检测的大体流程吧:
false
。lock
) 当然以上的流程我们还可以优化一下,省去拷贝的那一段时间。我们可以直接 hge->Texture_Lock()
来进行得到两个纹理的像素信息的首指针,如果两个纹理其实只是一个纹理的话,则只需 hge->Texture_Lock()
一次,而另一个指针也只想 hge->Texture_Lock
即可,然后直接开始判断。
下面献上我这个函数的实现以及测试代码和素材:
/** |
拿出来分享一下吧。
其实方法很简单,HTEXTURE
是纹理句柄,当你用 Texture_Lock
这个函数锁定这个纹理的时候,它的返回值就是这个纹理在内存中的首地址。也就是说接下来的 width * height 个地址中就是这个纹理的每一个像素了。既然要设置透明色,只要对于每个像素判断一下与运算一下就好了。
HTEXTURE SetTransColor(HTEXTURE hTex, DWORD dwColor) |
嘛,这样一来,就透明了~
]]>我的任务基本完成——将原本单屏的游戏改成三屏,完善整个 GUI系统 以及“劫持”了原游戏中的一些逻辑,比如滚轴的排列可以任意控制等。由于原代码中的GUI系统没有文本编辑框,我还得自己写一个。然而我对于IME的操作、GDI和DX的结合不是非常熟悉,所以还是参照了一下 ShowLong 所修改的微妙的平衡给HGE写的中文解决方案。
完成了以上的任务之后,由于我的考试以及签证问题以及我本身的任务差不多了,就把这个摊子就扔回公司去了。在交接的时候,老大给我派了一个任务,让我来写这个游戏资源包的代码。
原版游戏代码中有资源包代码,但是写得非常乱,于是需要我来写一个新的文件结构、新的加密算法,然后仍然是“劫持”掉原代码中的资源包加载函数。
在此之前,我拜读了云风的《游戏资源的压缩、打包与补丁更新》,有了点灵感。
最主要的就是其删除这一块。为了让用户在更新的时候减少大量的文件IO操作,做法就是减少文件内容的大幅度移动。
而我便是参考了云风大大的这个思想来写我的文件包。首先因为在游戏中需要实时读取,所以文件没有压缩,只是做了两层加密处理,密钥也是通过哈希得到的,所以每个文件的密钥是不同的。
然后在文件索引的时候,我这里是分了两种索引:文件索引以及空块索引。
所谓空块索引就是:在文件包中删除某一个文件的时候,不把后面的文件内容全部往前挪以覆盖这一块的内容、导致整个文件包在删除文件之后的信息全部往前挪而产生的大量IO操作,而是对这一块内容不作任何处理、把这一块内容的索引从文件索引中移除并附加到空块索引中以供以后新文件加入时所用,这样就只产生了一点对于索引的文件IO更新,不过索引的更新充其量也就那么点,相对于文件的操作来说只是九牛一毛,当然前提是这个索引是在整个文件包的最后。而在有新文件插入的时候,先在空块索引中找有比新文件大的空块,如果有的话就直接把这个文件插入到那个空块中,然后更新一下文件索引以及空块索引即可,这里又少掉了一些IO操作。
正如云风所说:
如果新增加的文件较之小,就重复利用这个空间。如果利用不上,就浪费在那里。这有点像内存管理算法,时间久了,资源包内会有一些空洞,但也是可以接受的。
接着就是资源包在游戏中的使用了。在原先的游戏代码中是有判断重复加载的代码,也就是说把已加载的资源存到node里,在之后再次需要加载这个资源(通过文件名判断)就直接从node中去,这样就少了很多内存开销,尤其是当我把单屏改为三屏之后,这样的优化效果更为明显(否则相同的资源要加载三次,等于消耗了三倍的内存)。不得赞一下这05年开发的代码,虽然是棒子。不过原游戏代码中的高耦合度让我蛋疼。
想到以后这个资源包类要用到以后的一些项目,于是我自己也写了一个Cache机制。就是在一个包中,当加载某个资源的时候,顺便把这个资源的Buffer加到一个Cache中,当下次再需要用到这个资源的时候就直接从Cache中取就好了,实际上这就还是之前的代码实现的功能,只不过我自己在这基础上精简了一下。最后写一个ClearCache的函数能清除Cache,我这个资源包类就算完成了。
还有在获取资源的时候,为了防止内存突增,我的Buffer是一段一段获取的,类似于Socket中的获取消息一直到消息结束为止。当然,每一段Buffer的大小是可以自己传进去的。我这种以时间换空间的做法还没自己实际测试过效果如何,只不过是自己想想可能会比较优罢了,因为最近实在是太慢,这篇日志还是考完了概率然后摸着黑地写的。
我对文件系统本身不是非常了解,操作系统还没考呢。所以我现在仅仅做到的是云风九年前的一种设计,然后加上了原先代码有的Cache机制而已。不过写下这篇日志来记录我自己成长的足迹罢了。
]]>首先我的代码是通过宏定义的“#”来进行输出表达式,然后期间一坨让人火星的自加自减运算。凑合着看一下吧:
|
Kalxd
的对话。刚忙完邀请赛,蹭了块铜。刚才在逛别人博客的时候看别人的文章,突然心血来潮想记一些东西。
连连看是我学HGE做的第一个小游戏,素材用的是QQ的。时间大概是去年国庆吧。好吧,废话不多说,就讲讲连连看怎么找到能消的两块吧。
首先来回顾一下消方块的规则,一共有三种可能性:
嗯,接下去我们就针对每种可能性开始写代码。
首先讲讲一些定义:
座标结构体,这个结构体包含了x、y的值以及一些座标中常用的函数。
/** |
然后是关于地图数组的定义:
int Map[MAP_HEIGHT][MAP_WIDTH]; |
接着是路径结构体:
/** |
接着可以正式开始了。首先我们来想一下,哪些条件各符合上面三种情况的哪一种。对于一条直线的,显然是x相等或者y相等;对于有一个转折点的话,我们只需要判断起点横向画线(或者纵向),然后终点纵向画线(或者横向),然后从起点到交点以及从交点到终点各可行不;对于两个转折点,其中一个转折点的x或者y跟起点的x或者y相等,另一个转折点跟终点的x或者y相等。于是这两个转折点就根据这样的性质进行枚举。因为连连看的地图比较小,所以这种O(n^2)的时间复杂度不碍事。
为了方便,我们写一个 Abled(CoorType, CoorType, bool, bool);
函数来进行判断两个点(当然两点是在同一直线上的)是否有同路(即中间没有东西挡着)。我们先放着这个Abled不管,先实现寻路过程吧。
我是用一个CMapSearch类来实现的,声明如下:
/** |
然后我这个分享里所讲的算法就是DoSearch和Abled函数了,因为其它函数就是用于“提示”道具的。在DoSearch中我们先定义两个临时变量,一个是返回值(一个PointPath),四个座标变量:
PointPath ans; |
其中a、b表示起点和终点,c、d表示可能用到的两个转折点。
首先我们先来判断直线情况吧,这种情况比较简单:
//如果是直线 |
对于这种情况,我们只需直接判断a、b直接有没有通路就好,如果有通路我们就将结果记录到ans中并返回即可。
而有一个转折点、两个转折点的情况以及Abled函数将在下一篇文章中小分享一下。
然后下面就是在这篇文章里面我跟 Kalxd
的对话了,想想现在真是沧海桑田啊。
CSS 样式早已经不在了,截图里面是一篇白板
]]>开发过程中学到了不少底层的东西。我的任务是先将游戏的单屏给弄成三份,以三个逻辑来实现。由于工程耦合性太高,原程序员是 C 语言出生的,造成了我修改工程一定程度上的麻烦。先自己写了个 Wrapper 类来包含三个 SceneGAME,然后一次性将三个 SceneGAME 渲染到目标上。这样改了一下之后,有两百多个错误。不知道其它游戏是不是也有这样的特点,因为这是我开发的第一款游戏,所以也没经验。
不过幸好最后还是让我给完成了这第一个 quest。接着是添加 GUI,就是现在正在做的。还有就是完全分离三块分屏的逻辑代码。让我这个大二学生兴奋的是,这个游戏完成之后,是会有奖金的。这个可是让我这个穷孩子两眼放光莫。YY 一下到底会有多少呢,我本身也只是过去见识一下真正的工程而已,压根没想过 money 这事。
今天,额好吧,以现在的时间来说应该是昨天了,一个韩国人以及另一个中国公司的人过来验收我们现在的进度。我打开自己电脑里编译好的可执行文件,心中那种自豪感油然而生啊,毕竟我还是 newbie,当然是有些小兴奋啊。期间那个中国公司的人说要赶在韩国人那边任务完成之前完成,这样好体现我们自己程序员不比他们差。无形之间的较量啊,表示鸭梨很大。据说他们那边现在遇到小问题了,我们超前的可能性还是蛮大的,接下去我就是要卯足劲来分离逻辑以及设置 GUI 了。柯总那边已经把网络通讯方面差不多完成了,我这一块做好,一对接,差不多能在五月初交工了。
最近还是比较忙啊,要学车,有个宁工基金会网站、港城关系研究所网站以及国际港口与物流中心网站,然后团区委那边的志愿者网站还有点小尾巴,浙江能源集团那个用 C# 写的员工墙完工了还不知道那边怎么说,接着应该还有一个姜老师那边的站子。都堆在一坨了。不过最近最要紧的还是这个游戏以及基金会网站以及去西班牙实习的一些相关事宜了。不管了,先睡觉。
]]>从 090 为 QQ 的旧版表情,从 100204 是新版表情(这两套表情不稀奇)。300337 是 QQ 空间签到的星星表情 I,从 500514 是星星表情 II。最后从 1000~1005 是手机表情。
预览如下:
事情是这样的:
我觉得 QQ 空间里的这套星星表情挺不错的,就想扒下来,先观察了一下表情的地址:
http://cnc.qzs.qq.com/qzone/em/e300.gif
…
很显然,这是连续的,但我想想不甘心,于是从 0 开始找,那么就是旧版表情。
好的,找到规律之后就可以开工了,开始我用渣雷批量载(999 个),发现载不下,于是就删掉了。
接下去的方案是自己写个页面(后来我发现从 1000 开始还有表情):
|
保存在我本机 Apache 根目录下的 emo.php
文件然后打开,悲催的一幕发生了:
如果你用我上面的方法也出现这样的情况,那么恭喜你,你也中了防盗链机制。
我就琢磨 TX 的防盗链应该是按域名来的吧,那么这就好办了:
第一步,ping:
C:\Users\死月>ping cnc.qzs.qq.com |
接下去打开 hosts 表,加上下面一句话
127.0.0.1cnc.qzs.qq.com |
最后,把上面的 PHP 代码改一下:
|
然后打开“你的” http://cnc.qzs.qq.com/emo.php
完成!你会发现 QQ 的各表情就列在你眼前了。接下去干什么呢?
哟西,就是保存啦!我用的是 Chrome,然后右键保存网页(当然是全部网页内容啦)。好的,这下表情就被你扒到手啦!
我打个包:
]]>点击下载(自然是已经找不到了)
通常情况下,大家都是用 bat 文件加命令行进行编译的。但这里不好控制时间。如果用一个线程去监控这个编译进程,进程结束就下一步的话,未免有点小题大做了。
我采用的是下面一种“文件锁”的方法:
在创建 bat 文件的同时创建一个“文件锁”文件,如 .lock
。
然后在 bat 文件的最后一行加入一句 del .lock
即可。而在程序中你只要在运行 bat 之后来一句
while(0 == access(“.lock”, 0)); |
这样就可以做到延时了。等到文件锁被删除之后,就表示文件编译完成。
当然,有可能编译时间过久,那这里也可以从 while
下文章,加一个条件,如果时间到了,则删掉这个进程即可。
下面是部分的实现代码:
/** 这段代码就是生成bat文件的代码 */ |
磨蹭了半年,项目是五月份上去的,现在都12月份了,终于要开始动工我的学生项目——Online Judge System了。
现在在写评测内核,用的是 VC++。我的环境就是 Win7 + VS2008。有这么个构思,评测内核用 dll 来写,然后写个评测服务用 .NET,调用这个 dll。服务负责从数据库中读取待评测数据,编译、运行,然后向数据库提交结果。
C/S 端用 PHP 编写,决定了用 ThinkPHP,不过鉴于我的 PHP 初级水平,接下来要开始劳烦 KonaKona 了,不耻下问。她一个美女 PHPer,而且是个高手喔。
欢迎同做 OJ 的同学们一起讨论,大家多多交流哈。联系我扣扣:8…..5。
]]>用于批量生成同一个父类的不同子类的对象时用到。
本学习笔记基于Singleton(单件模式)基础上进行扩展。
看《C++单件模式:Singleton学习笔记》请点击链接。
对于工厂模式,网上有很多不同的实现方法。我这里是一个HGE的RPG Demo中所用的,这段代码本身写的非常的好,开始好些语句没看懂,虽然就这么几句话。花了一点时间去研究了其代码,并自己重新实现了一遍,加上了通俗易懂的注释。
工厂类以模板形式实现,基于Singleton:
/**-------------------------------- |
以上就是基於单件模式而实现的工厂模式了。
在样例中,我建立了一个基类Base,然后用A和B来继承它。
在一个for循环中,交替建立了A对象和B对象。这只是一个Demo,看不出有什么方便的,感觉用一个if来各自生成就好了,就像
if(type == "A") p = new A(); |
当然,上面也是一种方法。但是,试想一下,我们将要创建的A、B、C、D、E、F、G类放到一个配置文件中,然后我们从配置文件中读取这些数据并创建相应的对象,并且这些对象的顺序是打乱的,你就要有n个if来判断了,而且扩展性不高。用一个对象工厂进行封装的话,俨然形成了一个静而有序的生产工厂,有秩序地管理着不同的对象车间,不觉得这是一件非常美妙的事情么?
好了,话不多说,直接上Demo。
|
在小小地学习了一下C++的单件模式之后,突然联想到PHP的ThinkPHP的MVC框架,觉得这就是一个单件模式的很好实例吧?
我上次在PUDN上载了一个HGE的RPG Demo,里面就用了Singleton模式还有ObjectFactory模式写的。没看懂,于是问了谷歌。
Singleton可以说是《Design Pattern》中最简单也最实用的一个设计模式。那么,什么是Singleton?
顾名思义,Singleton就是确保一个类只有唯一的一个实例。Singleton主要用于对象的创建,这意味着,如果某个类采用了Singleton模式,则在这个类被创建后,它将有且仅有一个实例可供访问。很多时候我们都会需要Singleton模式,最常见的比如我们希望整个应用程序中只有一个连接数据库的Connection实例;又比如要求一个应用程序中只存在某个用户数据结构的唯一实例。我们都可以通过应用Singleton模式达到目的。一眼看去,Singleton似乎有些像全局对象。但是实际上,并不能用全局对象代替Singleton模式,这是因为:其一,大量使用全局对象会使得程序质量降低,而且有些编程语言例如C#,根本就不支持全局变量。其二,全局对象的方法并不能阻止人们将一个类实例化多次:除了类的全局实例外,开发人员仍然可以通过类的构造函数创建类的多个局部实例。而Singleton模式则通过从根本上控制类的创建,将”保证只有一个实例”这个任务交给了类本身,开发人员不可能再有其它途径得到类的多个实例。这一点是全局对象方法与Singleton模式的根本区别。
——摘自百度百科(我不是有意在谷歌找百度的)
我在看了这个RPG Demo的Pattern里的Singleton之后,仿照着自己写了一个最简单的Singleton模板实例。
思想就是,在Singleton中建立一个静态对象,然后以后就用 Singleton::Instance()
来调用这个静态对象。
而作为模板就是可以 class A : public Singleton
来让A继承Singleton的属性,那么我们就可以直接用A::Instance()
来访问A这个静态对象了。这个就是这段Singleton代码的主要思想了。
先是建立了一个空工程,往里面放了:
Singleton.h
|
TestSingleton.h
|
TestSingleton.cpp
|
main.cpp
|
conn.asp
之后,发现无法连接数据库。也就是说数据库无法打开。一直从十二点搞到四点,一直以为是我的进程出问题了,不断调试。后来越来越觉得不对劲,最后我把网站指向以前机子里的一些 ASP 进程。最后发现,原来所有页面都无法连上数据库。我想也许是权限问题,所以我把一些文档夹的权限弄来弄去,结果还是不行。
接下来就是去网上搜了。开始也是怎么也搜不到,因为我忽略了一个非常重要的问题:我用的是 64 位的系统!
好吧,在IIS里,应用进程池貌似 64 位的不能连数据库。于是有这幺一个方案。
打开IIS里的应用进程池:
如图所示,选中 Classic .NET AppPool,然后是到右边栏里选择“设置应用进程池默认设置”。
最后在“常规”里将“激活32位应用进程”设置为True即可正常访问Access数据库了。
]]>这次学校的材料所找他重新设计站子,但没有写后台的人,于是他找到了我。
服务器是学校的。我听到这个之后就感觉困难加大了挺多。这以为着,我不得不抛弃 PHP 而使用 ASP 了。对于 ASP 项目,我没开发过,以前也就修改修改一些小进程而已。今天接到这么个项目心里还是有点没底啊。不过好在,界面什么的他都设计好了(PSD 的),我写代码,不用管图片设计了。
ASP 有一点不好,没有 MVC 框架。用原生态的话,开发周期有点长。虽然我知道时间不短了,不过要下个月 11 号之前做好。据说是材料所的老师快要出国了,他想在出国之前能看一眼新站子好安心。
没 MVC 就没吧,我去网上搜索了一阵,幸好发现了两个应该会比较好用的类库、框架:EasyIDE 和 EasyASP。一个是框架(Framework),其函数、语法变得跟 PHP 有点接近了,有点小亲切。另一个是类库,其中包括了数据库操作类之类的。这样的话写起来应该会方便很多吧。
老师挺热情的,找我说明情况的时候选择到了圣巴里,而且开的报酬也挺好的,¥1500,和学校其它的项目比起来,我非常满足了。
好吧,接下来就是开始做了。
顺便发下老师给我的方案:
]]>本笔记只有思路,而且是最最入门级的思路。笔记最后附启蒙我的 PPT 下载=。=
老规矩,大牛们可以一笑而过或者选择围观,有兴趣的童鞋们可以一起研究研究。按下面按钮以阅读全文。
所谓最大流,就是水一样哗哗哗地流过若干管道。而每个管道都有其自身的最大流量。然后你要找到从起点到终点时可以流过的最大流量。
直接给出模型:
设定有向图 G = (V, E)
,每条边都有给定的容量 Cv, u
,其中:s
为发点,t
为收点。对于每一点边 (v, w)
,最多可以有 Cv, u
个单位的流量通过。除发点和收点外,每个顶点的进入的流量必须等于发出的流量。
最大流量问题就是确定从 s
到 t
的最大流及流图。
接下来是一个贪心(or 深搜?)的算法。即:
s
到 t
的通路);这样就得到了“最大流”。
很重要的一点:不好意思,以上说的只是一种思路,而不是“正确”思路。
贪心策略不能保证最优!!!
很显然,如果上述的第一条通路换成如下的通路,错误性就显而易见了:
这样一来,就阻断了 s-b-d-t
的这样一条通路,所以贪心显然是错误的。但是不能贪心,也不能随机取啊,怎幺知道要先通哪条呢?
事实上,我们只要在上面的算法中动点小手脚就好了。
在每次找到一条通路之后,删边或者减小流量之后,在这条通路的反方向加上相应的可用流量即可。就像这样:
然后就可以再找通路了,接下来的一条通路就是(蓝边):
然后我们再开始删边、加反方向可用流量,一直到没有通路为止。
为什幺要加一条回路呢?我开始不是很理解。于是我请教了 StarVae,下面是他的话:
Star VAE! 12:36:15
哦 反向边 是为了能够有后悔的机会 直接流一次 不一定就是最大流的
也就是说,反向边是为了可以让流往回流再继续下一次流的一个“过渡边”。Star VAE! 12:38:45
流量 是加上去了的 我们算流量 不是算a->b这段路上的流量 算的是s->t这个流量 按照你的意思 如果还需要减去的话 那应该是叫做 费用 而不是流量} **.死月| 12:39:05
喔} **.死月| 12:39:09
就是说 如果后悔了} **.死月| 12:39:18
是因为有更大的通过同一条路的流量} **.死月| 12:39:28
所以 只要加上去补差量就好了 是幺Star VAE! 12:39:43
恩 差不多吧
好吧,本小菜是看着一个《数据结构第 19 讲:第 7 章(4)最短距离,网络流》的 ppt 学的,所以本文中的图是出自于那个 ppt,以及思路也是从那里灌输过来的。
有兴趣的童鞋们可以下载。可能比我自己写的学习笔记更易懂吧。
]]>飞速下载 PPT(原下载链接已无法寻回,但是可以自行去网上搜索)