我们知道nodejs是由js、c++、c组成的。今天我们来看一下他们是如何分工和合作的。本文以net模块为例进行分析。我们可以通过以下方式使用net模块。
const net = require('net');
net模块是原生的js模块。对应nodejs源码的net.js。他是对tcp和pipe的封装,我们这里只讲tcp的功能。我们可以通过以下代码创建一个tcp服务器。
const net = require('net');
net.createServer((socket) => {}).listen(80);
我们知道js里是没有网络功能的,这就意味着,网络功能是由nodejs中的c++模块实现的。所以这时候nodejs就会创建一个c++对象。
const {
TCP,
} = internalBinding('tcp_wrap');
new TCP(TCPConstants.SERVER);
TCP对象封装了底层tcp的功能。他对应的是c++层的tcp_wrap模块。我们看看tcp_wrap模块的代码。在分析tcp_wrap之前我们先看看internalBinding做了什么事情。
let internalBinding;
{
const bindingObj = ObjectCreate(null);
internalBinding = function internalBinding(module) {
let mod = bindingObj[module];
if (typeof mod !== 'object') {
mod = bindingObj[module] = getInternalBinding(module);
}
return mod;
};
}
internalBinding是在getInternalBinding的基础上加了缓存处理。我们继续看getInternalBinding。
// 根据模块名查找对应的模块
void GetInternalBinding(const FunctionCallbackInfo& args) {
Environment* env = Environment::GetCurrent(args);
// 模块名
Local module = args[0].As();
node::Utf8Value module_v(env->isolate(), module);
Local
getInternalBinding通过模块名从模块链表里找到对应的节点。然后执行对应的初始化函数。
// 初始化一个模块,即执行他里面的注册函数
static Local
那么链表中的模块是从哪里来的呢?这时候我们就可以回头分析tcp_wrap了。tcp_wrap.cc的最后一句是(Initialize函数即上面的nm_context_register_func属性的值)
NODE_MODULE_CONTEXT_AWARE_INTERNAL(tcp_wrap, node::TCPWrap::Initialize)
NODE_MODULE_CONTEXT_AWARE_INTERNAL是一个宏,宏展开如下:
#define NODE_BUILTIN_MODULE_CONTEXT_AWARE(modname, regfunc) \
NODE_MODULE_CONTEXT_AWARE_CPP(modname, regfunc, nullptr, NM_F_BUILTIN)
#define NODE_MODULE_CONTEXT_AWARE_CPP(modname, regfunc, priv, flags) \
static node::node_module _module = { \
NODE_MODULE_VERSION, \
flags, \
nullptr, \
__FILE__, \
nullptr, \
(node::addon_context_register_func) (regfunc), \
NODE_STRINGIFY(modname), \
priv, \
nullptr \
}; \
void _register_ ## modname() { \
node_module_register(&_module); \
我们看到,宏展开后,首先定义了一个node_module 结构体。然后定义了一个_register_ xxx的函数,对应tcp模块就是_register_ tcp_wrap。这个函数在Nodejs初始化的时候会被执行
1. void RegisterBuiltinModules() {
2. // 宏展开后就是执行一系列的_register_xxx函数
3. #define V(modname) _register_##modname();
4. NODE_BUILTIN_MODULES(V)
5. #undef V
6. }
我们看到_register_ tcp_wrap函数被执行了,里面只有一句代码
node_module_register(&_module);
node_module_register定义如下
extern "C" void node_module_register(void* m) {
struct node_module* mp = reinterpret_cast(m);
mp->nm_link = modlist_builtin;
modlist_builtin = mp;
}
就是把一个node_module加入到链表中。完成了模块的注册。这就是我们刚才通过GetInternalBinding访问的那个链表。至此,我们已经了解了c++模块的注册和在js中是如何调用c++模块的。
本小节我们来看看,可以访问c++模块之后,又是如何使用c++模块的功能的。首先我们看看在js层执行new TCP的时候,c++做了什么事情。我们看一下TCP的定义。
void TCPWrap::Initialize(Local target,
Local unused,
Local context,
void* priv) {
Environment* env = Environment::GetCurrent(context);
// 以New为回调新建一个函数
Local t = env->NewFunctionTemplate(New);
Local tcpString = FIXED_ONE_BYTE_STRING(env->isolate(), "TCP");
// 函数名
t->SetClassName(tcpString);
t->InstanceTemplate()
->SetInternalFieldCount(StreamBase::kStreamBaseFieldCount);
// 设置t的原型方法,即TCP.prototype的属性
env->SetProtoMethod(t, "open", Open);
env->SetProtoMethod(t, "bind", Bind);
env->SetProtoMethod(t, "listen", Listen);
// target为导出的对象,设置对象的TCP属性
target->Set(env->context(),
tcpString,
t->GetFunction(env->context()).ToLocalChecked()).Check();
}
我们看以上代码似乎有点复杂,主要是v8的一些知识,翻译成js大概如下。
function FunctionTemplate(cb) {
function Tmp() {
Object.assign(this, map);
cb(this);
}
const map = {};
return {
PrototypeTemplate: function() {
return {
set: function(k, v) {
Tmp.prototype[k] = v;
}
}
},
InstanceTemplate: function() {
return {
set: function(k, v) {
map[k] = v;
}
}
},
GetFunction() {
return Tmp;
}
}
}
const TCPFunctionTemplate = FunctionTemplate((target) => { target[0] = new TCPWrap(); })
TCPFunctionTemplate.PrototypeTemplate().set('connect', TCPWrap.Connect);
TCPFunctionTemplate.InstanceTemplate().set('name', 'hi');
const TCP = TCPFunctionTemplate.GetFunction();
我们看到当在js层new TCP的时候,首先会new一个c++层的对象。然后执行一个函数,对应tcp_wrap.cc就是New。
void TCPWrap::New(const FunctionCallbackInfo& args) {
// 忽略一些c参数处理
new TCPWrap(env, args.This(), provider);
}
New中创建了一个TCPWrap对象。完整函数如下:
TCPWrap::TCPWrap(Environment* env, Local object, ProviderType provider)
: ConnectionWrap(env, object, provider) {
// 初始化一个tcp handle
int r = uv_tcp_init(env->event_loop(), &handle_);
}
这时候关系图如下。
这看起来很简单,但是其实有很多细节。这要从c++模块的基类说起。TCPWrap继承于BaseObject。BaseObject的构造函数里做了一些非常关键的操作(object就是刚才new TCP时对应的c++层对象,而不是new TCPWrap对应的对象,this对应的是new TCPWrap对象)。
// 把对象存储到persistent_handle_中,必要的时候通过object()取出来
BaseObject::BaseObject(Environment* env, v8::Local object)
: persistent_handle_(env->isolate(), object), env_(env) {
// 把this存到object中
object->SetAlignedPointerInInternalField(0, static_cast(this));
}
所以TCPWrap初始化后得到的关系图如下
这时候我们完成了new TCP的初始化工作,回到文章开始的代码,当我们创建一个tcp服务器的时候,会调用listen函数启动服务器。我们看看listen函数的调用过程。js层调用listen函数时,会执行c++层的Listen函数。
void TCPWrap::Listen(const FunctionCallbackInfo& args) {
TCPWrap* wrap;
// 把TCPWrap解包出来存到wrap中
ASSIGN_OR_RETURN_UNWRAP(&wrap,
args.Holder(),
args.GetReturnValue().Set(UV_EBADF));
Environment* env = wrap->env();
int backlog;
if (!args[0]->Int32Value(env->context()).To(&backlog)) return;
// OnConnection为有新建立的连接时触发的回调(已完成三次握手)
int err = uv_listen(reinterpret_cast(&wrap->handle_),
backlog,
OnConnection);
args.GetReturnValue().Set(err);
}
ASSIGN_OR_RETURN_UNWRAP这个是关键代码,new TCP的时候,我们已经知道js层和c++对象的关系。当js层调用listen函数时,他关联的对象是new TCP。ASSIGN_OR_RETURN_UNWRAP的作用就是把new TCP对应关联的new TCPWrap对象解包出来使用。这就是js层调用c++层功能的过程。
接下来我们分析c++层是如何调用libuv的。上一节分析到拿到了TCPWrap对象,然后会执行以下代码
int err = uv_listen(reinterpret_cast(&wrap->handle_),
backlog,
OnConnection);
那么&wrap->handle_是什么呢?我们来看看TCPWrap的定义。TCPWrap继承于ConnectionWrap。
class TCPWrap : public ConnectionWrap
ConnectionWrap是一个模板类。
// WrapType是c++层的类,UVType是libuv的类型
template
class ConnectionWrap : public LibuvStreamWrap {
public:
static void OnConnection(uv_stream_t* handle, int status);
static void AfterConnect(uv_connect_t* req, int status);
protected:
ConnectionWrap(Environment* env,
v8::Local object,
ProviderType provider);
UVType handle_;
};
我们看到&wrap->handle_的值是uv_tcp_t。该handle_在初始化的时候和TCPWrap对象会关联起来(TCPWrap继承HandleWrap)。
HandleWrap::HandleWrap(Environment* env,
Local object,
uv_handle_t* handle,
AsyncWrap::ProviderType provider)
: AsyncWrap(env, object, provider),
state_(kInitialized),
handle_(handle) {
// 保存Libuv handle和c++对象的关系
handle_->data = this;
}
后面我们会看到这个用处。这时候关系图如下
回调listen的代码
int err = uv_listen(reinterpret_cast(&wrap->handle_),
backlog,
OnConnection);
这时候我们就知道传给libuv的结构体是什么了。当listen结束后,就会回调OnConnection函数。
template
void ConnectionWrap::OnConnection(uv_stream_t* handle,
int status) {
// 拿到Libuv结构体对应的c++层TCPWrap对象
WrapType* wrap_data = static_cast(handle->data);
// 回调js,client_handle相当于在js层执行new TCP
Local argv[] = { Integer::New(env->isolate(), status), client_handle };
wrap_data->MakeCallback(env->onconnection_string(), arraysize(argv), argv);
}
我们看到在OnConnection里,首先根据通过handle->data拿到对应的c++层对象TCPWrap(这就是HandleWrap里关联这两个数据结构的用处,为了保持上下文)。然后调用TCPWrap的MakeCallback函数(onconnection_string是字符串“onconnection”)。MakeCallback在AsyncWrap中定义(TCPWrap继承AsyncWrap)。
inline v8::MaybeLocal AsyncWrap::MakeCallback(
const v8::Local symbol,
int argc,
v8::Local* argv) {
v8::Local cb_v;
// 根据字符串表示的属性值,从对象中取出该属性对应的值。是个函数
if (!object()->Get(env()->context(), symbol).ToLocal(&cb_v))
return v8::MaybeLocal();
// 要是个函数
if (!cb_v->IsFunction()) {
// TODO(addaleax): We should throw an error here to fulfill the
// `MaybeLocal<>` API contract.
return v8::MaybeLocal();
}
// 回调,见async_wrap.cc
return MakeCallback(cb_v.As(), argc, argv);
}
object()就是js层new TCP对应的对象,获取该对象的onconnection属性的值(该属性在js层设置),该值是一个函数,然后继续调用MakeCallback。
MaybeLocal AsyncWrap::MakeCallback(const Local cb,
int argc,
Local* argv) {
MaybeLocal ret = InternalMakeCallback(
env(), object(), cb, argc, argv, context);
return ret;
}
MakeCallback继续调用InternalMakeCallback,InternalMakeCallback中会调用v8的接口执行函数,从而回调js层。
ret = callback->Call(env->context(), recv, argc, argv)
这就是nodejs中js、c++、libuv的大致交互过程,也是通用的流程。