拓展Node.js从宏观来说,有几种方式,包括直接修改Node.js内核重新编译分发、提供npm包。npm包又可以分为JS和C拓展。本章主要是介绍修改Node.js内核和写C插件。
20.1 修改Node.js内核
修改Node.js内核的方式也有很多种,我们可以修改JS层、C++、C语言层的代码,也可以新增一些功能或模块。本节分别介绍如何新增一个Node.js的C++模块和修改Node.js内核。相比修改Node.js内核代码,新增一个Node.js内置模块需要了解更多的知识。
20.1.1 新增一个内置C++模块
1.首先在src文件夹下新增两个文件。
cyb.h
#ifndef SRC_CYB_H_#define SRC_CYB_H_#include "v8.h"namespace node {class Environment;class Cyb {public:static void Initialize(v8::Local<v8::Object> target,v8::Local<v8::Value> unused,v8::Local<v8::Context> context,void* priv);private:static void Console(const v8::FunctionCallbackInfo<v8::Value>& args);};} // namespace node#endif
cyb.cc
#include "cyb.h"#include "env-inl.h"#include "util-inl.h"#include "node_internals.h"namespace node {using v8::Context;using v8::Function;using v8::FunctionCallbackInfo;using v8::FunctionTemplate;using v8::Local;using v8::Object;using v8::String;using v8::Value;void Cyb::Initialize(Local<Object> target,Local<Value> unused,Local<Context> context,void* priv) {Environment* env = Environment::GetCurrent(context);// 申请一个函数模块,模板函数是ConsoleLocal<FunctionTemplate> t = env->NewFunctionTemplate(Console);// 申请一个字符串Local<String> str = FIXED_ONE_BYTE_STRING(env->isolate(),"console");// 设置函数名t->SetClassName(str);// 导出函数,target即exportstarget->Set(env->context(),str,t->GetFunction(env->context()).ToLocalChecked()).Check();}void Cyb::Console(const FunctionCallbackInfo<Value>& args) {v8::Isolate* isolate = args.GetIsolate();v8::Local<String> str = String::NewFromUtf8(isolate,"hello world");args.GetReturnValue().Set(str);}} // namespace node// 声明该模块NODE_MODULE_CONTEXT_AWARE_INTERNAL(cyb_wrap, node::Cyb::Initialize)
我们新定义一个模块,是不能自动添加到Node.js内核的,我们还需要额外的操作。
1 首先我们需要修改node.gyp文件。把我们新增的文件加到配置里,否则编译的时候,不会编译这个新增的模块。我们可以在node.gyp文件中找到src/tcp_wrap.cc,然后在它后面加入我们的文件就行。
src/cyb_wrap.ccsrc/cyb_wrap.h
这时候Node.js会编译我们的代码了。但是Node.js的内置模块有一定的机制,我们的代码加入了Node.js内核,不代表就可以使用了。Node.js在初始化的时候会调用RegisterBuiltinModules函数注册所有的内置C++模块。
void RegisterBuiltinModules() {#define V(modname) _register_##modname();NODE_BUILTIN_MODULES(V)#undef V}
我们看到该函数只有一个宏。我们看看这个宏。
void RegisterBuiltinModules() {#define V(modname) _register_##modname();NODE_BUILTIN_MODULES(V)#undef V}#define NODE_BUILTIN_MODULES(V) \NODE_BUILTIN_STANDARD_MODULES(V) \NODE_BUILTIN_OPENSSL_MODULES(V) \NODE_BUILTIN_ICU_MODULES(V) \NODE_BUILTIN_REPORT_MODULES(V) \NODE_BUILTIN_PROFILER_MODULES(V) \NODE_BUILTIN_DTRACE_MODULES(V)
宏里面又是一堆宏。我们要做的就是修改这个宏。因为我们是自定义的内置模块,所以我们可以增加一个宏。
#define NODE_BUILTIN_EXTEND_MODULES(V) \V(cyb_wrap)
然后把这个宏追加到那一堆宏后面。
#define NODE_BUILTIN_MODULES(V) \NODE_BUILTIN_STANDARD_MODULES(V) \NODE_BUILTIN_OPENSSL_MODULES(V) \NODE_BUILTIN_ICU_MODULES(V) \NODE_BUILTIN_REPORT_MODULES(V) \NODE_BUILTIN_PROFILER_MODULES(V) \NODE_BUILTIN_DTRACE_MODULES(V) \NODE_BUILTIN_EXTEND_MODULES(V)
这时候,Node.js不仅可以编译我们的代码,还会把我们代码中定义的模块注册到内置C模块里了,接下来就是如何使用C模块了。
2 在lib文件夹新建一个cyb.js,作为Node.js原生模块
const cyb = internalBinding('cyb_wrap');module.exports = cyb;
新增原生模块,我们也需要修改node.gyp文件,否则代码也不会被编译进node内核。我们找到node.gyp文件的lib/net.js,在后面追加lib/cyb.js。该配置下的文件是给js2c.py使用的,如果不修改,我们在require的时候,就会找不到该模块。最后我们在lib/internal/bootstrap/loader文件里找到internalBindingWhitelist变量,在数组最后增加cyb_wrap,这个配置是给process.binding函数使用的,如果不修改这个配置,通过process.binding就找不到我们的模块。process.binding是可以在用户JS里使用的。至此,我们完成了所有的修改工作,重新编译Node.js。然后编写测试程序。
3 新建一个测试文件testcyb.js
// const cyb = process.binding('cyb_wrap');const cyb = require('cyb');console.log(cyb.console())
可以看到,会输出hello world。
20.1.2 修改Node.js内核
本节介绍如何修改Node.js内核。修改的部分主要是为了完善Node.js的TCP keepalive功能。目前Node.js的keepalive只支持设置开关以及空闲多久后发送探测包。在新版Linux内核中,TCP keepalive包括以下配置。
1 多久没有通信数据包,则开始发送探测包。2 每隔多久,再次发送探测包。3 发送多少个探测包后,就认为连接断开。4 TCP_USER_TIMEOUT,发送了数据,多久没有收到ack后,认为连接断开。
Node.js只支持第一条,所以我们的目的是支持2,3,4。因为这个功能是操作系统提供的,所以首先需要修改Libuv的代码。
1 修改src/unix/tcp.c
在tcp.c加入以下代码
int uv_tcp_keepalive_ex(uv_tcp_t* handle,int on,unsigned int delay,unsigned int interval,unsigned int count) {int err;if (uv__stream_fd(handle) != -1) {err =uv__tcp_keepalive_ex(uv__stream_fd(handle),on,delay,interval,count);if (err)return err;}if (on)handle->flags |= UV_HANDLE_TCP_KEEPALIVE;elsehandle->flags &= ~UV_HANDLE_TCP_KEEPALIVE;return 0;}int uv_tcp_timeout(uv_tcp_t* handle, unsigned int timeout) {#ifdef TCP_USER_TIMEOUTint fd = uv__stream_fd(handle);if (fd != -1 && setsockopt(fd,IPPROTO_TCP,TCP_USER_TIMEOUT,&timeout,sizeof(timeout))) {return UV__ERR(errno);}#endifreturn 0;}int uv__tcp_keepalive_ex(int fd,int on,unsigned int delay,unsigned int interval,unsigned int count) {if (setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, &on, sizeof(on)))return UV__ERR(errno);#ifdef TCP_KEEPIDLEif (on && delay &&setsockopt(fd,IPPROTO_TCP,TCP_KEEPIDLE,&delay,sizeof(delay)))return UV__ERR(errno);#endif#ifdef TCP_KEEPINTVLif (on && interval && setsockopt(fd,IPPROTO_TCP,TCP_KEEPINTVL,&interval,sizeof(interval)))return UV__ERR(errno);#endif#ifdef TCP_KEEPCNTif (on && count && setsockopt(fd,IPPROTO_TCP,TCP_KEEPCNT,&count,sizeof(count)))return UV__ERR(errno);#endif/* Solaris/SmartOS, if you don't support keep-alive,* then don't advertise it in your system headers...*//* FIXME(bnoordhuis) That's possibly because sizeof(delay) should be 1. */#if defined(TCP_KEEPALIVE) && !defined(__sun)if (on && setsockopt(fd, IPPROTO_TCP, TCP_KEEPALIVE, &delay, sizeof(delay)))return UV__ERR(errno);#endifreturn 0;}
2 修改include/uv.h
把在tcp.c中加入的接口暴露出来。
UV_EXTERN int uv_tcp_keepalive_ex(uv_tcp_t* handle,int enable,unsigned int delay,unsigned int interval,unsigned int count);UV_EXTERN int uv_tcp_timeout(uv_tcp_t* handle, unsigned int timeout);
至此,我们就修改完Libuv的代码,也对外暴露了设置的接口,接着我们修改上层的C++和JS代码,使得我们可以在JS层使用该功能。
3 修改src/tcp_wrap.cc
修改TCPWrap::Initialize函数的代码。
env->SetProtoMethod(t, "setKeepAliveEx", SetKeepAliveEx);env->SetProtoMethod(t, "setKeepAliveTimeout", SetKeepAliveTimeout);
首先对JS层暴露两个新的API。我们看看这两个API的定义。
void TCPWrap::SetKeepAliveEx(const FunctionCallbackInfo<Value>& args) {TCPWrap* wrap;ASSIGN_OR_RETURN_UNWRAP(&wrap,args.Holder(),args.GetReturnValue().Set(UV_EBADF));Environment* env = wrap->env();int enable;if (!args[0]->Int32Value(env->context()).To(&enable)) return;unsigned int delay = static_cast<unsigned int>(args[1].As<Uint32>()->Value());unsigned int detal = static_cast<unsigned int>(args[2].As<Uint32>()->Value());unsigned int count = static_cast<unsigned int>(args[3].As<Uint32>()->Value());int err = uv_tcp_keepalive_ex(&wrap->handle_, enable, delay, detal, count);args.GetReturnValue().Set(err);}void TCPWrap::SetKeepAliveTimeout(const FunctionCallbackInfo<Value>& args) {TCPWrap* wrap;ASSIGN_OR_RETURN_UNWRAP(&wrap,args.Holder(),args.GetReturnValue().Set(UV_EBADF));unsigned int time = static_cast<unsigned int>(args[0].As<Uint32>()->Value());int err = uv_tcp_timeout(&wrap->handle_, time);args.GetReturnValue().Set(err);}
同时还需要在src/tcp_wrap.h中声明这两个函数。
static void SetKeepAliveEx(const v8::FunctionCallbackInfo<v8::Value>& args);static void SetKeepAliveTimeout(const v8::FunctionCallbackInfo<v8::Value>& args);
// 修改lib/net.jsSocket.prototype.setKeepAliveEx = function(setting,secs,interval,count) {if (!this._handle) {this.once('connect', () => this.setKeepAliveEx(setting,secs,interval,count));return this;}if (this._handle.setKeepAliveEx)this._handle.setKeepAliveEx(setting,~~secs > 0 ? ~~secs : 0,~~interval > 0 ? ~~interval : 0,~~count > 0 ? ~~count : 0);return this;};Socket.prototype.setKeepAliveTimeout = function(timeout) {if (!this._handle) {this.once('connect', () => this.setKeepAliveTimeout(timeout));return this;}if (this._handle.setKeepAliveTimeout)this._handle.setKeepAliveTimeout(~~timeout > 0 ? ~~timeout : 0);return this;};
重新编译Node.js,我们就可以使用这两个新的API更灵活地控制TCP的keepalive了。
const net = require('net');net.createServer((socket) => {socket.setKeepAliveEx(true, 1,2,3);// socket.setKeepAliveTimeout(4);}).listen(1101);
20.2 使用N-API编写C++插件
本小节介绍使用N_API编写C插件知识。Node.js C插件本质是一个动态链接库,写完编译后,生成一个.node文件。我们在Node.js里直接require使用,Node.js会为我们处理一切。
首先建立一个test.cc文件
// hello.cc using N-API#include <node_api.h>namespace demo {napi_value Method(napi_env env, napi_callback_info args) {napi_value greeting;napi_status status;status = napi_create_string_utf8(env, "world", NAPI_AUTO_LENGTH, &greeting);if (status != napi_ok) return nullptr;return greeting;}napi_value init(napi_env env, napi_value exports) {napi_status status;napi_value fn;status = napi_create_function(env, nullptr, 0, Method, nullptr, &fn);if (status != napi_ok) return nullptr;status = napi_set_named_property(env, exports, "hello", fn);if (status != napi_ok) return nullptr;return exports;}NAPI_MODULE(NODE_GYP_MODULE_NAME, init)} // namespace demo
我们不需要具体了解代码的意思,但是从代码中我们大致知道它做了什么事情。剩下的就是阅读N-API的API文档就可以。接着我们新建一个binding.gyp文件。gyp文件是node-gyp的配置文件。node-gyp可以帮助我们针对不同平台生产不同的编译配置文件。比如Linux下的makefile。
{"targets": [{"target_name": "test","sources": [ "./test.cc" ]}]}
语法和makefile有点像,就是定义我们编译后的目前文件名,依赖哪些源文件。然后我们安装node-gyp。
npm install node-gyp -g
Node.js源码中也有一个node-gyp,它是帮助npm安装拓展模块时,就地编译用的。我们安装的node-gyp是帮助我们生成配置文件并编译用的,具体可以参考Node.js文档。一切准备就绪。我们开始编译。直接执行
node-gyp configurenode-gyp build
在路径./build/Release/下生成了test.node文件。这就是我们的拓展模块。我们编写测试程序app.js。
var addon = require("./build/Release/test");console.log(addon.hello());
执行
node app.js
我们看到输出world。我们已经学会了如何编写一个Node.js的拓展模块。剩下的就是阅读N-API文档,根据自己的需求编写不同的模块。
