使用Nodejs实现一个简单的hello/hi程序
本文基于Nodejs14.0.0。分析的是node中的net模块,但是因为时间有限,只能对其中的某几个方法展开叙述。
nodejs是一个异步I/O框架,依赖于实现异步功能的框架libuv且运行于Chrome V8引擎之上,所以其中很大一部分代码是用C++编写的,为了实现能在JS端调用C++代码,Node实现了一对函数(宏)来实现C++代码的导入和导出:NODE_MODULE_CONTEXT_AWARE_INTERNAL和internalBinding。
- NODE_MODULE_CONTEXT_AWARE_INTERNAL是一个宏,其原型为NODE_MODULE_CONTEXT_AWARE_INTERNAL(modname, regfunc),其实现了将C++模块挂靠在一个全局链表上。modname为模块的名称,regfunc为注册模块时调用的方法。如:
NODE_MODULE_CONTEXT_AWARE_INTERNAL(tcp_wrap, node::TCPWrap::Initialize)
是将TCPWrap类中的某些函数导出,node::TCPWrap::Initialize函数定义了一些常量以及将函数暴露给JS端。
- internalBinding用于导入一个模块,参数为模块的名称。如:
const {TCP, TCPConnectWrap, constants: TCPConstants} = internalBinding('tcp_wrap');
Node中的net模块可用于实现一个简单的TCP服务器,它实现了两个主要的类:Server和Socket和一个函数createConnection来发送接收数据,下面是一个简单的hello/hi程序:
服务器端:
let net = require('net');
let server = net.createServer((socket) => {
socket.on('data', (data) => {
console.log(`sent from client:${data.toString()}`);
socket.write('hi!');
});
socket.on('end', () => {
console.log('a client disconnected');
});
});
server.listen(1234, () => {
console.log('server started');
});
就上面的代码,首先,调用createServer函数创建一个Server类的实例server,这个函数接收一个回调函数作为参数。这个回调函数是用户需要实现的逻辑参数是一个Socket类的实例。然后server调用listen函数开始监听,listen函数可接受许多可选参数,在这里只使用了端口号和回调函数两个。
客户端:
let net = require('net')
let client = net.createConnection({port: 1234}, ()=>{
console.log('connected to server');
client.write('hello!');
});
client.on('data', (data)=>{
console.log(`response from server:${data.toString()}`);
client.end();
});
client.on('end', ()=>{
console.log('disconnected from server');
});
createConnection接收两个的参数,一个是建立连接所需要的信息,如端口号,IP地址,地址类型等。另一个是回调函数。createConnection函数产生了一个Socket类型的实例,在这个实例上用户监听事件和处理数据。
接下来首先分析服务端的代码:
首先,是新建一个Server类的对象,构造函数的部分代码如下:
if (typeof options === 'function') { connectionListener = options; options = {}; this.on('connection', connectionListener); } else if (options == null || typeof options === 'object') { options = { ...options }; if (typeof connectionListener === 'function') { this.on('connection', connectionListener); } } else { throw new ERR_INVALID_ARG_TYPE('options', 'Object', options); }
构造函数主要做的是初始化成员变量和监听一个事件:'connection',这个事件被触发后将执行用户传入的回调函数。
接下来,转到Server.listen函数,在这个函数主要干了一件事:根据传入参数的不同来决定如何给listenIncluster函数传参,listenInCluster函数用于在cluster上执行监听。至于cluster是做什么的,参见Nodejs的cluster模块解读。另外在这个函数中也监听了一个事件:'listening',当该事件被触发后,将调用用户传入listen函数的回调函数。
在listenInCluster函数中会调用Server._listen2函数(指向setupListenHandle函数),这个函数主要做了五件事:创建一个句柄(这个句柄是一个TCP实例,是从C++模块中导入的),为句柄添加onconnection函数(这个函数的作用下面会提到),调用句柄的listen函数(开始涉及C++了),调用emitListeningNT函数(触发了'listening'事件)。另外,调用句柄的bind或者bind6函数,它们和listen一样,都是C++模块中的函数。
接着说上面的listen函数。开头说过,JS代码可以执行C++模块中的函数,listen正是tcp_wrap.cc文件中的TCPWRAP::Connect函数,函数的源码如下:
void TCPWrap::Listen(const FunctionCallbackInfo<Value>& args) { 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; int err = uv_listen(reinterpret_cast<uv_stream_t*>(&wrap->handle_), backlog, OnConnection); args.GetReturnValue().Set(err); }
这段代码中主要做的事是调用了uv_listen函数,uv_listen函数是libuv提供的函数,它将从上层传来的句柄和一个回调函数加入到一个队列中,其中可能会调用系统提供的listen函数(我猜的),当有客户端请求连接时,将调用OnConnection回调函数,这个函数在connection_warp.cc中,其代码如下:
1 template <typename WrapType, typename UVType> 2 void ConnectionWrap<WrapType, UVType>::OnConnection(uv_stream_t* handle, 3 int status) { 4 WrapType* wrap_data = static_cast<WrapType*>(handle->data); 5 CHECK_NOT_NULL(wrap_data); 6 CHECK_EQ(&wrap_data->handle_, reinterpret_cast<UVType*>(handle)); 7 8 Environment* env = wrap_data->env(); 9 HandleScope handle_scope(env->isolate()); 10 Context::Scope context_scope(env->context()); 11 12 // We should not be getting this callback if someone has already called 13 // uv_close() on the handle. 14 CHECK_EQ(wrap_data->persistent().IsEmpty(), false); 15 16 Local<Value> client_handle; 17 18 if (status == 0) { 19 // Instantiate the client javascript object and handle. 20 Local<Object> client_obj; 21 if (!WrapType::Instantiate(env, wrap_data, WrapType::SOCKET) 22 .ToLocal(&client_obj)) 23 return; 24 25 // Unwrap the client javascript object. 26 WrapType* wrap; 27 ASSIGN_OR_RETURN_UNWRAP(&wrap, client_obj); 28 uv_stream_t* client = reinterpret_cast<uv_stream_t*>(&wrap->handle_); 29 // uv_accept can fail if the new connection has already been closed, in 30 // which case an EAGAIN (resource temporarily unavailable) will be 31 // returned. 32 if (uv_accept(handle, client)) 33 return; 34 35 // Successful accept. Call the onconnection callback in JavaScript land. 36 client_handle = client_obj; 37 } else { 38 client_handle = Undefined(env->isolate()); 39 } 40 41 Local<Value> argv[] = { Integer::New(env->isolate(), status), client_handle }; 42 wrap_data->MakeCallback(env->onconnection_string(), arraysize(argv), argv); 43 }
在32行,执行uv_accept函数接受一个请求,如果成功,将调用句柄的onconnection函数(上面说的那个函数),在这个函数中,触发了'connection'事件,并将一个新建的socket对象传给监听器的回调函数。至此,在服务端,一个建立连接的过程结束。
一张简要描述这个过程的图:
接下来,是客户端的代码,在客户端使用createConnection(connect函数的别名)函数创建一个Socket实例并执行Socket.connect函数。在这个函数中,监听了一个事件:'connect',其回调函数是用户的代码。接下来,调用了internalConnect函数,在internalConnect函数中主要做了三件事:端口绑定,为句柄设置oncomplete属性(成功连接后要做的是,主要是触发'connect'事件),执行TCPWrap.Connect函数。在TCPWrap.Connect函数中,将执行uv_tcp_connect函数进行连接并将函数AfterConnect(在connection_wrap.cc文件中)作为它的回调,连接成功后将执行AfterConnect函数,AfterConnect执行了handle.oncomplete指向的函数:
req_wrap->MakeCallback(env->oncomplete_string(), arraysize(argv), argv);
即js模块中的afterConnect函数,在该函数中,触发了'connect'事件,开始执行传入createConnection的回调函数,连接过程结束。
只能写到这了,因为后面的看不懂了。。。

浙公网安备 33010602011771号