编写自己的js运行时第二篇

开发 前端
第一版基于V8实现了一个朴素版的服务器,第二版支持了多进程架构,并且支持了SO_REUSEPORT。本文介绍一下第二版的一些实现,设计上还是比较随意的,目前主要关注功能。

[[410500]]

前言:第一版基于V8实现了一个朴素版的服务器,第二版支持了多进程架构,并且支持了SO_REUSEPORT。本文介绍一下第二版的一些实现,设计上还是比较随意的,目前主要关注功能。

首先我们看看第二版怎么使用。

1 通过fork共享端口

const TCPServer = TCP(); 
 
const tcpServer = new TCPServer('127.0.0.1', 8989); 
 
tcpServer.socket(); 
tcpServer.setReusePort(1); 
tcpServer.bind(); 
tcpServer.listen(); 
 
for (let i = 0; i < 3; i++) { 
 
    // 等于0说明是子进程,进入处理连接的逻辑,否则是主进程,循环创建多个进程 
    if (Child_Process.fork() === 0) { 
        while(1) { 
            tcpServer.accept(); 
        }  
    } 
 

 
// 主进程创建完子进程后自己进入阻塞状态 
 
Child_Process.wait(); 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.

通过fork共享端口版本的原理是主进程首先创建一个socket并且绑定一个端口。然后通过fork的方式让多个子进程共享监听的端口。最后主进程进入阻塞模式。核心实现是fork,我们看看代码。

static Local<Object> ChildProcess(Isolate * isolate) { 
  Local<ObjectTemplate> target = ObjectTemplate::New(isolate); 
  Local<String> forkName = String::NewFromUtf8(isolate, "fork", NewStringType::kNormal, strlen("fork")).ToLocalChecked(); 
  Local<String> waitName = String::NewFromUtf8(isolate, "wait", NewStringType::kNormal, strlen("wait")).ToLocalChecked(); 
 
  target->Set(forkName, FunctionTemplate::New(isolate, Child_Process::Fork)); 
  target->Set(waitName, FunctionTemplate::New(isolate, Child_Process::Wait)); 
  Local<Object> obj; 
  bool ignore = target->NewInstance(isolate->GetCurrentContext()).ToLocal(&obj); 
  return obj; 
 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.

第二版加入了进程模块,上面的代码定义了进程模块的功能。然后注入到全局变量,No.js目前的设计中,每个模块是一个全局变量,和我们使用Object、Array一样,不像Node.js的C++模块是链成一条链表。

// 模块名称 
Local<Value> child_process_name = String::NewFromUtf8(isolate, "Child_Process",  strlen("Child_Process")).ToLocalChecked();// 注册全局变量 
global->Set(context, child_process_name, ChildProcess(isolate)); 
  • 1.
  • 2.
  • 3.

这样就完成了模块的注入,在JS层就可以使用了。下面我们看看具体的实现。

class Child_Process { 
    public
 
        static void Fork(const FunctionCallbackInfo<Value>& info) { 
            info.GetReturnValue().Set(Number::New(info.GetIsolate(), fork())); 
        } 
 
        static void Wait(const FunctionCallbackInfo<Value>& info) { 
            int status; 
            wait(&status); 
        } 
 
}; 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.

实现很简单,只是对fork函数的封装,重点在于对fork函数的理解, 执行fork函数后会创建一个子进程,子进程的fork返回0,主进程返回子进程id,通过这个特性,我们可以写一个if判断处理下一步的逻辑。

2 通过fork+execve+reuserport共享端口

第二种模式是比较复杂且比较高性能的模式,之前的文章介绍过不同服务器架构的实现和优缺点,第一种fork共享端口的模式中,会有惊群和负载不均衡的问题,有兴趣可以参考之前的文章,就不多介绍。接下来看第二种模式的使用(下面代码是execve-server.js)。

const TCPServer = TCP(); 
 
const tcpServer = new TCPServer('127.0.0.1', 8989); 
 
tcpServer.socket(); 
tcpServer.setReusePort(1); 
tcpServer.bind(); 
tcpServer.listen(); 
 
const isMaster = Child_Process.getEnv("isMaster") === ""
 
if (isMaster) { 
 
    for (let i = 0; i < 3; i++) { 
        Child_Process.execve("./No""execve-server.js");   
    } 
    Child_Process.wait(); 
 
else { 
 
    while(1) { 
        tcpServer.accept(); 
    } 
 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.

我们知道多个进程是不能绑定同一个端口的,第一种模式中通过fork绕过了这个限制,第二版面对并解决了这个问题。上面代码的逻辑看起来也很简单,主进程创建多个子进程,并且在每个子进程里执行同一个文件execve-server.js。然后在execve-server.js中通过环境变量isMaster区分主子进程进行不同的处理,当然也可以执行新的文件。这里是为了提到isMaster这个环境变量。上面代码中,重点是setReusePort和execve,下面我们具体看一下实现。

static Local<Object> ChildProcess(Isolate * isolate) { 
  Local<ObjectTemplate> target = ObjectTemplate::New(isolate); 
  Local<String> execveName = String::NewFromUtf8(isolate, "execve", NewStringType::kNormal, strlen("execve")).ToLocalChecked(); 
  Local<String> getEnvName = String::NewFromUtf8(isolate, "getEnv", NewStringType::kNormal, strlen("getEnv")).ToLocalChecked(); 
 
  target->Set(execveName, FunctionTemplate::New(isolate, Child_Process::Execve)); 
  target->Set(getEnvName, FunctionTemplate::New(isolate, Child_Process::GetEnv)); 
  Local<Object> obj; 
  bool ignore = target->NewInstance(isolate->GetCurrentContext()).ToLocal(&obj); 
  return obj; 
 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.

同样,先定义入口使得JS可以调用。另外给TCP模块定义了一个新接口setReusePort。

SetProtoMethod(isolate, TCPServer, "setReusePort", TCPServer::TCPServerSetUserPort); 
  • 1.

接下来看底层的实现,首先看TCPServerSetUserPort的实现。

static void TCPServerSetUserPort(const FunctionCallbackInfo<Value>& info) { 
    int on = info[0].As<Uint32>()->Value(); 
    GetTCPServer(info.Holder())->Setsockopt(SOL_SOCKET, SO_REUSEPORT, &on, sizeof(on)); 
 

 
 
 
int Setsockopt(int levelint optionName, const void *optionValue, socklen_t option_len) { 
 
    return setsockopt(listerFd, level, optionName, optionValue, option_len); 
 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.

简单地对setsockopt的封装,没有太多需要讲的。下面看环境变量和execve的逻辑。

class Child_Process { 
    public
 
        static void GetEnv(const FunctionCallbackInfo<Value>& info) { 
            String::Utf8Value key(info.GetIsolate(), info[0]); 
            char * value = getenv(*key); 
            // Logger::log(value); 
            Local<String> str = String::NewFromUtf8(info.GetIsolate(), value, NewStringType::kNormal, strlen(value)).ToLocalChecked(); 
            info.GetReturnValue().Set(str); 
        } 
 
        static void Execve(const FunctionCallbackInfo<Value>& info) { 
            int length = info.Length(); 
            char** args = new char*[length + 1]; 
            int i = 0; 
            for (i = 0; i < length; i++) { 
                String::Utf8Value arg(info.GetIsolate(), info[i]); 
                args[i] = strdup(*arg); 
            } 
            args[i] = NULL
            char *env[] = { "isMaster=0"NULL }; 
            // int fd[2]; 
            // socketpair(AF_UNIX, SOCK_STREAM, 0, fd); 
 
            int pid = fork(); 
            if (pid == 0) { 
                // close(fd[0]); 
                execve(args[0], args, env); 
                // execve会加载可执行文件,从新的入口开始执行,执行到这说明execve出错了 
                write(1, strerror(errno), sizeof(strerror(errno))); 
                exit(-1); 
            } 
            // close(fd[1]); 
            if (args) { 
                for (int i = 0; i < length && args[i]; i++) { 
                    free(args[i]); 
                } 
                delete [] args; 
            } 
        } 
 
}; 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.

目前只实现了获取环境变量的逻辑,主要是对getenv的封装。execve的代码看起来很多,主要是参数的处理,我们只需要关注下面的代码。

int pid = fork(); 
 // 子进程重新加载新的可执行文件 
 if (pid == 0) { 
     // close(fd[0]); 
     execve(args[0], args, env); 
     // execve会加载可执行文件,从新的入口开始执行,执行到这说明execve出错了 
     write(1, strerror(errno), sizeof(strerror(errno))); 
     exit(-1); 
 } 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

首先通过fork创建一个子进程,然后通过execve加载要执行的代码(这里是./No execve-server.js)。重点是execve函数会重新加载可执行文件,然后从新的地址(可执行文件中指定)开始执行,所以我们看到execve后是不需要return的,因为下面的代码不会执行了,除非execve执行出错了,这里我们打印错误信息然后退出进程。第二种模式的好处就是我们可以随意在多个js文件中绑定同一个端口而不会报错,这得益于SO_REUSEPORT的特性。SO_REUSEPORT让每个进程对应一个连接队列,解决了惊群问题,并且内核负责连接分发的复杂均衡,不仅提高了性能,同时使得应用程序变得简单。

3 和Node.js相比

Node.js的进程是通过fork+execve实现的,Cluster模块基于进程模块实现了多进程架构,主要有两种模式:轮询和共享,轮询就是主进程接收连接分发给子进程处理,子进程不接收连接只负责处理业务逻辑。这种模式的好处是没有惊群现象,但是主进程的能力会成为服务器的瓶颈,共享模式和本文的第一种一样,多个子进程共享一个端口,但是实现不一样,本文是主进程创建socket通过fork子进程共享,Node.js是主进程创建socket通过文件描述符的方式传递给子进程,不过殊途同归,主要是让多个子进程共享监听socket。本文的第二种模式,目前Node.js还不支持,因为SO_REUSEPORT是比较新的特性,但是对性能提升非常大。

后记:以上就是第二版新增的功能,我们已经具备了一个可以处理请求的多进程架构服务器,但是目前还是单进程里串行处理请求的,我们还需要很多东西,文件、IPC、事件驱动模块、HTTP解析器等等,后续会考虑把最近写的Node.js io_uring Addon合进来。最近把头文件和V8静态库都打包了,有兴趣的同学可以自行编译运行https://github.com/theanarkh/No.js。

 

责任编辑:姜华 来源: 编程杂技
相关推荐

2024-03-21 09:15:58

JS运行的JavaScrip

2021-08-27 00:21:19

JSJust源码

2014-03-28 13:30:36

2022-10-08 00:00:00

V8channel对象

2011-06-21 10:28:49

Oracle

2015-07-20 15:44:46

Swift框架MJExtension反射

2022-08-02 10:26:09

网络层网络网络协议

2017-04-10 14:46:29

AndroidGradleBuild.gradl

2011-03-14 16:05:17

2023-09-12 17:38:41

2022-10-08 00:06:00

JS运行V8

2020-12-07 13:31:43

GoMutex开发者

2019-07-12 09:30:12

DashboardDockerDNS

2021-09-11 15:38:23

容器运行镜像开放

2021-10-14 09:53:38

鸿蒙HarmonyOS应用

2021-09-07 11:19:42

操作系统华为鸿蒙

2022-09-07 08:11:30

LinuxLKRG结构体

2023-08-21 09:37:57

MySQL工具MariaDB

2023-08-27 21:07:02

2024-01-29 08:07:42

FlinkYARN架构
点赞
收藏

51CTO技术栈公众号