- Node.js:来一打 C++ 扩展
- 死月
- 5075字
- 2020-08-27 17:13:37
前言
写这本书是我在2016年底许下的愿望,希望在2018年初完成一本技术专著。
我于2012年加入Node.js开发的大军,现在也有幸成为Node.js这个项目的Core Collaborator之一。所以,我的意向就是为大家呈现一本Node.js领域相关的书。但是现在市面上相关的书籍其实有很多了,我再写一本日常开发类的图书就显得有些多余。反而是在Node.js的C++扩展开发方面,无论是在国内还是在国外,都是一块死角。就目前而言,国外市场我也只看到过一本电子书,并没有纸质图书出版,国内就更没有了。
Node.js作为近几年新兴的一种编程运行时,托Chrome V8引擎的福,在作为后端服务时有比较高的运行效率,在很多场景下对于我们的日常开发已经足够用了。不过,它也跟“PHP提供了C语言开发其原生扩展的方式”类似,为开发者开了一个使用C++开发Node.js原生扩展的口子,让开发者进行项目开发时有了更多的选择。
实际上,在Node.js的生态圈中,就有很多使用C++完成的包。如最近比较火的深度学习TensorFlow,其Node.js版本的封装就是基于官方的C++源码完成的。我自己就是在日常开发中有一些相应的需求,使用纯粹的Node.js来开发可能会使开发成本有点大,或者基本上做不到,又或者有性能上的要求。这时,我就会选择使用C++来实现它的一个扩展。在我写了一段时间C++扩展之后,想到可能在社区中有很多像我一样的人,苦于Node.js在底层操作时的一些局限性,如果他们也加入C++原生扩展开发阵营的话,兴许要再踩一遍我以前踩过的“坑”,找我之前找过的资料。因此,我就想把自己一路走来的经验分享给大家,让更多的人顺利地加入Node.js的C++原生扩展开发的大军中。
我的Node.js之路
我个人从小学开始接触静态网页的开发,直到高中开始参加信息学奥林匹克竞赛(Olympiad in Informatics,OI),才算正式踏入了编程之路。
在大学的时候我仍旧坚持参加大学生程序设计竞赛(ACM International Collegiate Programming Contest,ICPC),并且一直使用C++和PHP进行开发。也是那时我打下了C++基础,这样才有机会现在完成这本书的写作工作。
我接触Node.js其实并没有国内一些早期的布道者们早,相反还是有点迟的。在2012年底,我决心学习Node.js,从而完成自己的一个创业项目。我当时的学习方法特别简单,买了一本BYVoid的《Node.js开发指南》,就算正式踏入了Node.js领域。
在熟悉了Node.js之后,我开始为Node.js生态圈造轮子,如Toshihiko、ThmclrX、Huaming、mcnbt等。其实我个人认为,造轮子与写业务的一个不同点在于,造轮子可能会更容易遇到语言或是Node.js运行时本身的“坑”。所以,这就促使我去深究Node.js的文档,甚至源码。托我之前习得的C++基础的福,在阅读Node.js源码时并不觉得特别艰难。
我在老东家花瓣网的时候,就已经初步开始了Node.js的C++扩展开发。
后来我去了上一家就职的公司——大搜车,负责公司Node.js团队的建设。当时我就开始更深入地挖掘Node.js的一些内容了。甚至在2017年年中的时候,我通过给Node.js贡献源码,成为Node.js Core Collaborator之一。我在为Node.js贡献源码的时候,也为本书第3章和第6章的写作打下了基础。
原生扩展的一些示例
在Node.js早期的版本中,运行子进程是纯异步的,并没有像现在一样的各种spawnSync()等函数。我当时写了一个命令行工具,在其所用到的一个帮助类中实现一个参数校验的函数必须同步返回一个布尔类型的值;然而我在这个校验函数中所需要做的事情就是判断当前系统的Git版本。也就是说,我要通过子进程启动$ git-v,并得到它的结果看看版本是不是符合要求。当时Node.js中运行子进程是异步的,达不到我的要求,所以我自己使用C++封装了一个原生扩展,使其能在Node.js的事件循环中同步开启子进程并在其结束后获得它的终端输出——虽然Node.js天生异步,但是在我的一个命令行工具中用同步形式执行这些内容也是没有问题的。
再比如,在大搜车的时候,项目用了阿里云的消息队列服务,而这款产品当时只有闭源的Java、C++等SDK,而C++的SDK就只提供了几个动态链接库和一堆头文件,我们使用Node.js的开发者就完全没法使用其服务。如果一定要用Node.js进行开发,一个成本比较高的做法就是自己去逆向分析及研究消息队列服务的各种网络包的结构,自己解析,然后用Node.js实现一个同样功能的库。然而这个方法基本不可行——尤其是在我们的项目高速迭代的时候。那么另一个办法就是基于其闭源的C++SDK,使用本书中的各种开发方式,写一份Node.js的C++扩展。这样就能把它们的C++SDK集成到我们的Node.js项目中了。这是一个非常好的降低开发成本的方法。
当年我还在花瓣网的时候,有一个需求是提取一张图片的主题色。我当时翻阅了不少论文,最终采用了一种八叉树加最小差值法的结合体来完成这个需求。在数据结构和整型数字处理方面,我个人认为C++的开发效率和执行效率比Node.js要高,于是我自然而然地就使用了C++把核心算法部分完成了(现在我甚至使用C语言又重构了一套,开源在GitHub上面)。然后为了将其集成到我们的Node.js任务调度系统中,我又将其封装成了一个Node.js的C++的扩展。这样一来,主题色提取的任务就欢快地运行了——它也被开源在我的GitHub上面,就是前面提到过的ThmclrX。而且借这个包的“东风”,我的硕士毕业论文写的就是这么一套主题色提取的任务系统相关内容。
类似的案例还有很多。如计算字符串哈希值等,由于用JavaScript重写代码的时候,在整数的各种操作上会有很多“坑”,因此拿C++源码封装一下就非常简单了。甚至谷歌推出的CityHash这个算法只有一份冗长的C++源码,使用JavaScript重写的话将会是一个比较庞大的工作量;再比如解析MP4文件的时长,我个人不是多媒体相关领域的开发者,所以并不擅长。于是我弄了一份C++的源码,懒得转换——嘿,用C++扩展一包,直接就发布了;还有同步获取HTTP API的内容,写一个能继承的类似于ECMAScript 6中Proxy特性的拦截器;等等。
本书面向的读者
在阅读本书前,我希望你对Node.js比较熟悉,并且对于C++这门语言至少要有一个初步的认识。当然,如果你的C++基础并不是很好的话,也不要怕,可以多读几遍本节最后的一段话。
本书不仅仅讲实践,我还花了不少篇幅来讲解它的前驱知识,如Chrome V8引擎开发的一些基本概念,如句柄、句柄作用域等,以及各种API的初步介绍。另外,书中还介绍了libuv层面的内容,尤其是在异步方面,像libuv中的线程、同步原语,以及如何在Node.js的主时间循环中与你自身写的线程进行跨线程通信等。这么一算,Chrome V8、libuv,加上Node.js的C++扩展开发,你相当于一下子买了3本书,是不是觉得很超值?也就是说,你阅读本书的目的不一定是想要开发Node.js的C++扩展;如果你想学习Chrome V8,或者想学习libuv,也可以参考本书。
本书的最后还简单展望了一下Node.js 8.0之后出现的一个新特性,就是新一代Node.js C++原生扩展接口N-API。不过由于N-API还处于试验阶段,各种接口还不是很稳定,在未来随时会变,因此本书中并没有详细地介绍N-API,而只是简单讲解了它的思想,让大家在心中有一个思想准备。这样,哪一天N-API正式发布了,读者就可以比较快地上手了。不过,不要忘本,哪怕N-API真的出来了,我还是希望大家多了解一下底层的基础,比如像Chrome V8、libuv以及Node.js源码相关的内容。因为学习了这些基础知识,对大家肯定没有坏处(甚至对于Node.js,大家说不定会有一个新的认识)。
最后,奉上我在一次技术直播中说过的一句话:“当我们在学习Node.js的时候,我们其实就是在学编程。语言只是最表象的东西,思想才是核心内容。”如果还有部分读者由于本书需要有C++基础望而却步的话,多读几遍我刚才说的话,然后鼓起勇气入“坑”吧。
本书的结构
本书共分为9章。其中前两章描述了一些基础的前驱理论知识;第3章到第6章讲的是Node.js的C++扩展开发中用到的各种知识,并辅以简单的样例;第7章和第8章为实战章节,根据现实需求来完成相应的Node.js C++扩展;第9章为对未来的N-API的一个展望。
第1章讲述了我们在学习本书内容之前所需要了解的基础,如Node.js的模块机制与包机制,以及Node.js都是由什么三方依赖构成的。其中就提到了很重要的Chrome V8和libuv。本章的最后还讲述了要进行Node.js的C++扩展开发所需要做的准备工作,包括但不限于编辑器的挑选、开发环境的搭建等。
第2章主要讲述了什么是Node.js的C++扩展,它的本质是什么,并且什么情况下需要使用C++扩展,以及阐述了为什么在这些情况下要使用C++扩展。
第3章介绍了谷歌的Chrome V8引擎,从它与Node.js的关系讲到它的一些基本概念,例如V8的内存机制、基本对象等。在后续的章节中将开始介绍Chrome V8的各种类及其概念,以及它们的用法,如句柄、句柄作用域、模板和各种常用的数据类型等。
第4章相当于各种编程语言书籍中的“Hello World”,向读者介绍了binding.gyp这个重要的配置文件,以及GYP文件格式的基础,然后以几个最简单的例子向读者展示了Node.js的C++扩展最简单的一些代码,包括函数的参数、回调函数的用法、对象的返回、函数的返回等,以及如何将一个C++的类封装成Node.js中直接能用的类。
第5章为大家介绍了NAN(Native Abstractions for Node.js)这个非常实用的包,使大家能在不同的Node.js版本(本质上是各不兼容的Chrome V8版本)中使用同一份C++代码。
第6章讲解了如何使用libuv进行异步Node.js的C++扩展代码编程,首先介绍了libuv的一些基础概念,如句柄与请求等,然后讲述了如何使用libuv进行跨线程编程。
第7章就开始进入了实战环节。本章通过从零开始写一个基于C++的文件监视器扩展,讲述了要完成一个Node.js原生扩展的一些流程。本章所述的文件监视器源码地址在https://github.com/XadillaX/node-efsw。
第8章与第7章的实战不同,对两个现有的简单C++扩展包进行分析,从另一个角度剖析了一个Node.js的C++扩展包的源码。
第9章展望了如何使用Node.js的最新特性N-API进行原生扩展的开发。不过我估计等到本书正式上市的时候,第9章已经变成一个仅供参考的章节了。
阅读本书的注意事项
声明:我在编写本书之际,还在大搜车工作,所以书中的很多内容都是基于大搜车的角度来写的。比如8.2节中有一处内容是这样的:
在笔者所在公司的内部,用了一套基于Dubbo深度定制的RPC服务框架。Node.js要访问这些Java服务的RPC函数是通过定制的HTTP协议来完成的,所有的RPC服务节点都到Zookeeper进行注册。
这里指的公司就是大搜车。再比如2.1.2节中的一段话:
在官方的Node.js版本ONS SDK出来之前,笔者自己造了一个基于其官方C++版本的ONS SDK封装的轮子,用的当然是本书所讲的姿势——Node.js的C++扩展了。
由于编写本书时我还并未从大搜车离职,因此这仍然是站在就职于大搜车的角度写的。
我在编写本书之际,Node.js的8.x版本并未进入LTS阶段。于是我采用了Node.js 6.x作为样例进行了讲解,而Node.js 6.x距离LTS结束也还有一段时间。而且使用本书的方法进行Node.js的原生扩展开发,在Node.js 6.x、Node.js 8.x甚至是Node.js 9.x下都是通用的。本书中的样例都是基于Node.js v6.9.4进行讲解的,读者在参考的时候上调或者下调几个中、小版本号问题都不大。
至于N-API一章(第9章),我在该章中也曾谈道:
本章内容在书中将会一带而过,因为在笔者写书的时候,N-API还没有完全稳定下来,随时会改变。而且笔者个人认为,距离N-API能正式投入生产用途的时间还很长。所以本章内容在本书中仅以扩展阅读的形式存在,其实关于N-API的内容在5.1.2节中曾略微提及。
因此,该章内容仅供参考,具体内容应以官方文档为准。
另外,本书所有的随书代码均在macOS命令行下测试通过。理论上,它们也可以在Windows和UNIX上运行良好,但我并没有验证过。
最后,给出本书中经常用到的一些地址。
· 本书随书代码的Git仓库:https://github.com/XadillaX/nyaa-nodejs-demo
· Node.js v6.9.4代码仓库:https://github.com/nodejs/node/tree/v6.9.4
· Node.js v6.x所对应的Chrome V8文档:https://v8docs.nodesource.com/node-6.12/。若读者打开该地址,却发现页面不存在,可直接前往https://v8docs.nodesource.com/,并点击“6.x”字样的超链接进入(注意该地址经常换)。
· 作者的个人技术博客:https://xcoder.in
· 作者的GitHub地址:https://github.com/XadillaX
· Me:https://github.com/XadillaX/me
致谢
感谢我的妻子,她也是一位优秀的Node.js研发工程师。她的支持是对我的最大鼓励,如果不是她,这本书的问世也许会更晚。
感谢我的父母,在我的背后默默地支持我的事业。在我很小的时候,他们就一直支持我追寻自己的梦想,这才使我能够在编程领域一路走下来。
感谢我的老东家大搜车,它营造了良好的技术与实践氛围,同事(包括领导)给予了我不少帮助,如书中图示的优化、阅读体验的建议等。这些同事有段鹏飞、纪清华、刘佳楠、许波、王琦、袁小山……
感谢现实以及社区中的朋友们在本书创作的时候进行试读和探讨,并提供了一些其他帮助,他们包括但不限于Akagi201、ADoyle、David Cai、Hax(贺老)、贺星星、精子(jysperm)、孟德森、天然、五花肉、引证、张秋怡。
感谢为本书写序和推荐语的作者们:安娜·亨宁森(Anna Henningsen)、曹力(ShiningRay)、顾天骋(Timothy Gu)、桑世龙(狼叔)、雷宗民(老雷)、刘亚中(Yorkie)、迷渡(justjavac)、潘旻琦(pmq20)、田永强(朴灵)、袁锋(苏千)、孙信宇(芋头)、王文睿博士、响马,你们一直是我们的楷模与学习对象。
感谢我的高中计算机老师兼NOIP集训教练王震老师,王震老师是我在编程路上的启蒙老师,没有他就没有今天会写代码的我;也感谢当时陪我坚持走这一条路到毕业的好队友jiecchen和MatRush;感谢我的大学ACM教练宣江华老师和一直为集训队默默付出的陈萌老师;还要感谢我的研究生导师李启雷博士传道受业。
感谢博文视点的刘皎女士以及她的团队,是他们的努力使本书最终能与广大读者见面,他们提出的专业意见给了我很多帮助。
最后,还要特别感谢董伟明(《Python Web开发实战》作者)。在阅读了他的一篇文章《写一本技术书籍》后,我才有了写作本书的想法,并最终付诸实施。
死月(朱凯迪)
2018年3月于杭州