服务粉丝

我们一直在努力
当前位置:首页 > 财经 >

在 Node.js 中如何通过子进程与其他语言(Go)进行 IPC 通信

日期: 来源:大淘宝前端技术收集编辑:高扬(洗剑)

Node.js 如何与子进程进行通信

在 Node.js 官方文档中有这样一段描述:在子进程中,可以通过 NODE_CHANNEL_FD这个环境变量来获取到一个文件描述符来与父进程进行通信,那这个 NODE_CHANNEL_FD是从哪里来的?又该如何使用呢?首先,我们从 child_process.spawn 这个创建子进程的方法开始说起,下面是一段在 Node.js 中启动一个子进程,执行 go run main.go这样命令的代码:

const { spawn } = require('child_process');
const { join } = require('path');
const childProcess = spawn('go', ['run', 'main.go'], {
    stdio: [0, 1, 2, 'ipc']
});

可以看到,我们在 stdio数组中包含了 ipc这样一个字符串,在 Node.js 中是这样处理这个参数的:

// https://github.com/nodejs/node/blob/7b1e15353062feaa3f29f4fe53e11a1bc644e63c/lib/internal/child_process.js#L1025-L1043
 stdio = ArrayPrototypeReduce(stdio, (acc, stdio, i) => {
    if (stdio === 'ignore') {
      // 忽略里面的 N 行代码
    } else if (stdio === 'ipc') {
      ipc = new Pipe(PipeConstants.IPC);
      ipcFd = i;

      ArrayPrototypePush(acc, {
        type: 'pipe',
        handle: ipc,
        ipc: true
      });
    } else if (stdio === 'inherit') {
      // 忽略里面的 N 行代码
    }
    return acc;
  }, []);

可以看出,这里会迭代 stdio,如果其中包含 ipc那就往 acc上面添加属性 type: pipeipc:true等,同时赋值 ipcFd = i,根据我们之前调用 spawn的参数,ipc这个字符串所在的索引位置 i 为 3,那么 ipcFd 的值就是 3,在 child_process.spawn的实现中可以看到会把 ipcFd 赋值到 NODE_CHANNEL_FD上(lib/internal/child_process.js#L380[1])。在文件描述符表中,0/1/2 分别代表标准输入(stdin)、标准输出(stdout)和标准错误输出(stderr),3 代表的是第一个文件描述符,接下来继续看这个文件描述符是怎么来的,为什么 NODE_CHANNEL_FD 会是第一个文件描述符?在 child_process.spawn中调用了 _handler.spawn 方法(lib/internal/child_process.js#L395[2]),这个 _handler来源于实例化 process_wrap.cc导出的 Process,同时 spawn 执行时的参数中的 stdio属性,来源于上面迭代 stdio之后的返回值 (lib/internal/child_process.js#L366[3] )。在 ProcessWrap::Spawn 的实现中(src/process_wrap.cc#LL233C5-L233C22[4]),会调用 ParseStdioOptions 来处理 stdio参数,将 type:pipe处理成对应的 flag,然后调用 uv_spawn(src/process_wrap.cc#L264[5])。

uv_spwan中有一个很关键的步骤,在其中调用uv__process_init_stdio,在其中根据之前处理的 flag,调用了 uv_socketpair,在这个方法内部调用了 socketpair来创建一对相互连接的 socket 用于之后再父子进程之间进行通信,同时将这个 socket 的文件描述符存储起来,以用于在后面传递给子进程。然后再在 uv_spwan中通过 uv__spawn_and_init_child(src/unix/process.c#L991[6])来调用 uv__spawn_and_init_child_fork方法,在其中 fork 子进程。

// https://github.com/nodejs/node/blob/f313b39d8f282f16d36fe99372e919c37864721c/deps/uv/src/unix/process.c#L789
static int uv__spawn_and_init_child_fork(const uv_process_options_t* options,
                                         int stdio_count,
                                         int (*pipes)[2],
                                         int error_fd,
                                         pid_t* pid) {
  // 忽略 N 行代码

  *pid = fork();

  if (*pid == 0) {
    /* Fork succeeded, in the child process */
    uv__process_child_init(options, stdio_count, pipes, error_fd);
    abort();
  }
  if (pthread_sigmask(SIG_SETMASK, &sigoldset, NULL) != 0)
    abort();
  if (*pid == -1)
    /* Failed to fork */
    return UV__ERR(errno);
  /* Fork succeeded, in the parent process */
  return 0;
}

众所周知,在执行 fork函数创建一个子进程时,会同时有两个进程运行,在父进程中,fock函数会返回子进程的进程 id,在子进程中,会返回 0,所以判断如果返回 0,那就执行子进程中的一些初始化逻辑。在子进程中调用 uv__process_child_init中,通过 dup2让子进程中 3(也就是父进程中创建的环境变量 NODE_CHANNEL_FD)这个文件描述符执行的文件重定向到父进程通过 socketpair打开的文件描述符指向的文件:

// stdio_count 的值为 4,对应了 spawn 的 stdio 参数 [0, 1, 2, 'ipc']
for (fd = 0; fd < stdio_count; fd++) {
    close_fd = -1;
    // 当 fd 为3的时候,对应了 socketpair 创建的用来通信的文件描述符,假设是 24
    // 也就是说 fd = 3、use_fd = 24
    use_fd = pipes[fd][1];

    if (use_fd < 0) {
    }

    if (fd == use_fd) {

    }
    else {
      // dep2(24, 3);
      fd = dup2(use_fd, fd);
    }

    // 忽略 N 行代码
  }

最后通过调用 execvp来加载要执行的子进程程序(deps/uv/src/unix/process.c#L382[7]) 对于父进程在 fork 之前打开的文件,比如 socket 等,由于在 uv__cloexec中通过 fcntl函数设置了 FD_CLOEXEC,那么在 execcp 的时候都会自动进行关闭,而通过 socketpair创建的这个文件,会被保留,这也就给后续再 Golang 里面与 Node.js 进行通信创造了条件。

Golang 进程如何与 Node.js 父进程进行通信

由于 NODE_CHANNEL_FD这个环境变量指向了与父进程进行通信的 socket 文件,那么在 Go 里面,我们就可以通过对 socket 进行数据的写入和读取,来实现与父进程进行通信:

nodeChannelFD := os.Getenv(NODE_CHANNEL_FD)
nodeChannelFDInt, _ := strconv.Atoi(nodeChannelFD)
fd := os.NewFile(uintptr(int(nodeChannelFDInt)), "lbipc"+nodeChannelFD)

通过 Linux 文档,可以发现有 recvmsg (https://linux.die.net/man/2/recvmsg[8]) 和 sendmsg (https://linux.die.net/man/2/sendmsg[9])这两个函数,分别来实现对一个 socket 进行数据读取和发送操作,同时在 Go 的官方提供的 syscall包中,提供了对应的 RecvmsgSendmsg 这两个方法,所以通信就很简单了。发送数据:

// 发送数据
type Message struct {
 Id      string `json:"id"`
 MsgType string `json:"type"`
 Data    string `json:"data"`
}

fdHandler := int(fd.Fd())
responseMsg := Message{
    Id:      "id:1",
    Data:    "hello world",
    MsgType: "test",
}
jsonData, _ := json.Marshal(responseMsg)
syscall.Sendmsg(fdHandler, append(jsonData, '\n'), nil, nil, 0)

接受数据:

// 接受数据

fdHandler := int(fd.Fd())
syscall.Recvmsg(fdHandler, dataBuf, attachedDataBuf, 0)

相关代码实现可以查看 https://github.com/midwayjs/lb[10]

参考资料

[1]

lib/internal/child_process.js#L380: https://github.com/nodejs/node/blob/main/lib/internal/child_process.js#L380

[2]

lib/internal/child_process.js#L395: https://github.com/nodejs/node/blob/81ab00d913a878a47510f8ea3ccf8dadb2971a7d/lib/internal/child_process.js#L395

[3]

lib/internal/child_process.js#L366: https://github.com/nodejs/node/blob/81ab00d913a878a47510f8ea3ccf8dadb2971a7d/lib/internal/child_process.js#L366

[4]

src/process_wrap.cc#LL233C5-L233C22: https://github.com/nodejs/node/blob/f313b39d8f282f16d36fe99372e919c37864721c/src/process_wrap.cc#LL233C5-L233C22

[5]

src/process_wrap.cc#L264: https://github.com/nodejs/node/blob/f313b39d8f282f16d36fe99372e919c37864721c/src/process_wrap.cc#L264

[6]

src/unix/process.c#L991: https://github.com/nodejs/node/blob/f313b39d8f282f16d36fe99372e919c37864721c/deps/uv/src/unix/process.c#L991

[7]

deps/uv/src/unix/process.c#L382: https://github.com/nodejs/node/blob/f313b39d8f282f16d36fe99372e919c37864721c/deps/uv/src/unix/process.c#L382

[8]

https://linux.die.net/man/2/recvmsg: https://linux.die.net/man/2/recvmsg

[9]

https://linux.die.net/man/2/sendmsg: https://linux.die.net/man/2/sendmsg

[10]

https://github.com/midwayjs/lb: https://github.com/midwayjs/lb


相关阅读

  • 图形编辑器:旋转选中的元素

  • 大家好,我是前端西瓜哥。最近更文比较少,是因为本人在做个人开源项目,用 Canvas 做一个设计工具,做个乞丐版 figma。期间遇到了不少问题,在这里记录一下。今天开始会恢复高频更新
  • 图形编辑器:标尺功能的实现

  • 大家好,我是前端西瓜哥。今天我们来实现图形编辑器的标尺功能。项目地址:https://github.com/F-star/suika线上体验:https://blog.fstars.wang/app/suika/标尺指的是画布上边和
  • 图形编辑器:图形和辅助线绘制的坐标问题

  • 大家好,我是前端西瓜哥。今天看看绘制图形和辅助线时,坐标转换的一些注意点。项目地址,欢迎 star:https://github.com/F-star/suika线上体验:https://blog.fstars.wang/app/suika
  • 回顾 2022 / 展望 2023

  • 其实不想写的,因为今年挺糟心的,下不动笔。但快过年了,思前想后,最后还是写一下吧。回顾 2022先过一下 2022 立下的 flag。1、每个月至少三篇文章,再尝试看能不能投稿。这个对我
  • 图形编辑器:工具管理和切换

  • 大家好,我是前端西瓜哥。今天我们看看对于一款图形编辑器,应该怎么去实现工具,比如绘制矩形、选中工具,以及如何去管理它们的。项目地址,欢迎 star:https://github.com/F-star/sui
  • WebSocket 入门:简易聊天室

  • 大家好,我是前端西瓜哥,今天我们用 WebSocket 来实现一个简单的聊天室。WebSocket 是一个应用层协议,有点类似 HTTP。但和 HTTP 不一样的是,它支持真正的全双工,即不仅客户端可以
  • 在 VSCode 中像写 TypeScript 一样写 JavaScript

  • 大家好,我是前端西瓜哥。我们在 VSCode 编辑器中编写 js 代码,是会提供类型提示的。VSCode 会推断一个变量是什么类型,并在你输入内容的时候,提供对应的 API 属性或方法补全。如

热门文章

  • “复活”半年后 京东拍拍二手杀入公益事业

  • 京东拍拍二手“复活”半年后,杀入公益事业,试图让企业捐的赠品、家庭闲置品变成实实在在的“爱心”。 把“闲置品”变爱心 6月12日,“益心一益·守护梦想每一步”2018年四

最新文章

  • 元宵橘子洲有烟花吗?附元宵长沙烟花汇总

  • 宝子们注意啦关于元宵节橘子洲烟花官方回复来了1月29日长沙橘子洲烟花燃放活动执行委员会办公室通过长沙发布公告了2023年元宵节橘子洲不举办烟花燃放活动2023年元宵节橘子
  • 官方答疑:长沙离异单亲家庭享受二孩购房政策

  • 今年1月上旬长沙下发了《依法生育两个及以上子女的本地户籍家庭购买商品住宅实施细则》但是还有很多朋友对该细则还存在不少疑问❓❓❓1月29日上午,长沙市房屋交易管理中心针