当前位置: 首页 > 科技观察

Node.js原理见No.js

时间:2023-03-13 18:24:33 科技观察

本文转载自微信公众号《编程杂技》,作者theanarkh。转载本文请联系编程杂技公众号。越来越多的同学在使用Node.js,每个人都不同程度地理解Node.js是什么。例如,Node.js由V8、Libuv和JS组成。Node.js的底层是C\C++。Node.js不是一种语言,而是一种运行时。本文通过实现一个类似Node.js的JS运行时No.js来理解Node.js的本质。No.js是我之前写的一个JS运行时,概念上的,但它不是真正的运行时,它只是一个demo,但它向你展示,如果你有兴趣,你也可以写一个Node.js。js。首先,让我们看一下V8的基本用法。#include#include#include#include"include/libplatform/libplatform.h"#include"include/v8.h"intmain(intargc,char*argv[]){//InitializeV8.v8::V8::InitializeICUDefaultLocation(argv[0]);v8::V8::InitializeExternalStartupData(argv[0]);std::unique_ptrplatform=v8::platform::NewDefaultPlatform();v8::V8::InitializePlatform(platform.get());v8::V8::Initialize();v8::Isolate::CreateParamscreate_params;create_params.array_buffer_allocator=v8::ArrayBuffer::Allocator::NewDefaultAllocator();//创建一个Isolate,代表一个隔离实例v8::Isolate*isolate=v8::Isolate::New(create_params);{v8::Isolate::Scopeisolate_scope(isolate);//定义AHandleScope下面管理句柄内存的分配和释放v8::HandleScopehandle_scope(isolate);//创建上下文,js中访问的东西来自contextv8::Localcontext=v8::Context::New(isolate);v8::Context::Scopecontext_scope(context);//定义我们要执行的代码v8::Localsource=v8::String::NewFromUtf8(隔离,"'Hello'+',World!'",v8::NewStringType::kNormal).ToLocalChecked();//编译脚本v8::Localscript=v8::Script::Compile(context,source).ToLocalChecked();//执行脚本v8::Localresult=script->Run(context).ToLocalChecked();//输出结果v8::String::Utf8Valueutf8(isolate,result);printf("%s\n",*utf8);}//DisposetheisolateandteardownV8.isolate->Dispose();v8::V8::Dispose();v8::V8::ShutdownPlatform();删除创建参数。array_buffer_allocator;return0;}我们看到了很多代码,但是大部分都是基于V8文档的。核心是上下文和脚本的定义。我们看到这里的上下文是V8提供的内容,然后执行的JS脚本也是普通的。接下来我们要做的是扩展这个上下文并向其中注入新的功能。相应的,在JS中也可以访问V8内置变量以外的变量。让我们看看如何去做。//获取一个全局变量,这是我们在js中对应的全局变量Localglobal=context->Global();//定义一个字符串对象Localkey=String::NewFromUtf8(isolate,"TCP",NewStringType::kNormal,strlen("TCP").ToLocalChecked();Localcbdata=String::NewFromUtf8(isolate,"dummy",NewStringType::kNormal,strlen("dummy")).ToLocalChecked();//定义一个函数Localfunc=Function::New(context,//func执行时会调用Invoke,cbdata为入参Invoke,cbdata).ToLocalChecked();//函数注册给全局变量,这样我们就可以在js中使用key函数了Maybeignore=global->Set(context,key,func);//打开文件intfd=open(argv[1],O_RDONLY);structstatinfo;//获取文件信息fstat(fd,&info);//分配内存保存文件内容char*ptr=(char*)malloc(info.st_size+1);//用ptr读取文件,Macos读取秒函数定义的参数是void*read(fd,(void*)ptr,info.st_size);//要执行的js代码Localsource=String::NewFromUtf8(isolate,ptr,NewStringType::kNormal,info.st_size).ToLocalChecked();上面的代码主要分为几部分。1从上下文中获取全局变量。2定义一个新的函数,并注入到一个全局变量中,这样我们就可以在JS中访问了。3打开一个文件并读入,交给V8编译执行。再来看重点,也就是我们自定义的函数。从注释中我们看到我们已经注入了一个TCP全局变量。它的值是一个函数。我们在JS中执行TCP函数的时候,会执行我们自定义的C++函数,并传入实参。我们定义的函数是Invoke,我们来看一下实现。staticvoidInvoke(constFunctionCallbackInfo&info){Isolate*isolate=info.GetIsolate();//新建一个函数模板,模板函数为TCPServer::NewTCPServerLocalServer=FunctionTemplate::New(isolate,TCPServer::NewTCPServer);LocaltcpServerString=String::NewFromUtf8(isolate,"TCPServer",NewStringType::kNormal,strlen("TCPServer")).ToLocalChecked();//函数名Server->SetClassName(tcpServerString);//pre留一个指针空间,用来保存一些自定义上下文Server->InstanceTemplate()->SetInternalFieldCount(1);//设置TCPServer的原型方法SetProtoMethod(isolate,Server,"socket",TCPServer::TCPServerSocket);SetProtoMethod(isolate,Server,"bind",TCPServer::TCPServerBind);SetProtoMethod(isolate,Server,"listen",TCPServer::TCPServerListen);SetProtoMethod(isolate,Server,"accept",TCPServer::TCPServerAccept);//设置原型方法(隔离,服务器,"setsockopt",TCPServer::TCPServerSetsockopt);info.GetReturnValue().Set(Server->GetFunction(isolate->GetCurrentContext()).ToLocalChecked());}上面的代码看起来很复杂,主要是针对V8API的使用,在V8中,我们自定义函数的格式如下staticvoidfunc(constFunctionCallbackInfo&info){info.GetReturnValue().Set(returnvalue);}入参为FunctionCallbackInfo,通过info.GetReturnValue().Set函数设置函数的返回值。也就是我们在JS层拿到的内容。上面的代码翻译成JS如下。functionInvoke(){returnServer;}//---functionServer(){TCPServer.NewTCPServer(this);}Server.prototype.socket=functionsocket(){returnthis[0].socket();}//---classTCPServer(){constructor(target){target[0]=this;this.persistent_handle_=target;}staticNewTCPServer(target){newTCPServer(target);}socket(){}bind(){}...}可以看出,执行Invoke后得到一个函数TCPServer。然后我们执行newTCPServer,JS代码如下(server.js)constServer=TCP();constserver=newServer('127.0.0.1',8989);server.socket();server.bind();server.listen();while(1){server.accept();}当我们执行新服务器时。V8会先创建一个对象obj,然后执行TCPServer.NewTCPServer。并传入obj对象。staticvoidNewTCPServer(constFunctionCallbackInfo&info){String::Utf8Valueip_address(info.GetIsolate(),info[0]);intport=info[1].As()->Value();//信息。this()isobjnewTCPServer(info.GetIsolate(),info.This(),*ip_address,port);}//在this中保存对象,并在析构C++对象时重置对象[0]TCPServer(Isolate*isolate,Localobject,char*ip,intport):_isolate(isolate),persistent_handle_(isolate,object),_ip(ip),_port(port){//obj[0]=thisobject->SetAlignedPointerInInternalField(0,static_cast(this));}NewTCPServer也创建了一个对象this,然后通过obj[0]=this关联起来。这就是核心逻辑,后面我们会看到它有什么用。接下来我们执行一系列的网络编程函数,但是原理是一样的,我们来分析server.socket()。因为服务器是一个服务器实例。所以server.socket()对应的函数就是Server.prototype.socket。该函数会从中取出真实对象(TCPServer实例)的socket函数。然后执行它。//执行真实对象的socket函数staticvoidTCPServerSocket(constFunctionCallbackInfo&info){GetTCPServer(info.Holder())->Socket();}//取出真实对象,即obj[0]staticTCPServer*GetTCPServer(Localobject){returnreinterpret_cast((*reinterpret_cast*>(&object))->GetAlignedPointerFromInternalField(0));}由此我们可以看出Server函数是一个透明的功能。主要用于适配V8协议。真正的逻辑是在它的关联对象中实现的。其余实现如下。intSocket(){listerFd=socket(AF_INET,SOCK_STREAM,0);returnlisterFd;}intBind(){structsockaddr_inserv_addr;memset(&serv_addr,0,sizeof(serv_addr));serv_addr.sin_family=AF_INET;serv_addr.sin_addr.s_addr=inet_addr(_ip);serv_addr.sin_port=htons(_port);returnbind(listerFd,(structsockaddr*)&serv_addr,sizeof(serv_addr));}intListen(){returnlisten(listerFd,512);}intAccept(){intclientFd=accept(listerFd,nullptr,nullptr);//返回ok,然后关闭TCP连接constchar*rsp="connectok";write(clientFd,rsp,sizeof(rsp));close(clientFd);return0;}intSetsockopt(intlevel,intoptionName,constvoid*optionValue,socklen_toption_len){returnssetsockopt(listerFd,level,optionName,optionValue,option_len);}intClose(){returnclose(listerFd);}都是对socket网络编程的封装。最后,我们通过Noserver.js启动服务器。所有的代码都执行完之后,我们终于在accept里面阻塞了。while(1){server.accept();}这时候我们启动客户端。constnet=require('net');functionhandle(){setTimeout(()=>{constsocket=net.connect({host:'127.0.0.1',port:8989});socket.on('connect',()=>{console.log('ok');socket.destroy();handle();});},1000);}handle();我们将看到连续输出正常,因为它甚至不断断开和重新连接。至此我们通过扩展V8完成了一个服务器的开发。后记:本文讲解如何通过扩展V8实现一个简单的TCP服务器来扩展V8,Node.js就是采用这种方式。然后把操作系统的文件、网络、进程、线程、IPC等封装起来,我们也可以实现一个Node.js。当然,这是理论上的。No.js仓库https://github.com/theanarkh/No.js