[Node] 进程间消息分片处理

1. LSP: VSCode + TypeScript

VSCode 是通过 LSP 向 tsserver 发送名为 'completionInfo' 的消息,来实现自动补全的。
请求消息示例如下,

{"seq":35,"type":"request","command":"completionInfo","arguments":{"file":"/Users/thzt/.../index.ts","line":1,"offset":8,"includeExternalModuleExports":true,"includeInsertTextCompletions":true,"triggerCharacter":".","includeAutomaticOptionalChainCompletions":true}}

返回消息示例如下(太长已截断),

{ "seq": 0, "type": "response", "command": "completionInfo", "request_seq": 35, "success": true, "performanceData": { "updateGraphDurationMs": 8 }, "body": { "isGlobalCompletion": false, "isMemberCompletion": true, "isNewIdentifierLocation": false, "entries": [ { "name": "__dirname", "kind": "var", "kindModifiers": "declare", "sortText": "0" }, ...

VSCode 1.45.1 和 TypeScript 3.7.3 的联合调试,可以看这里:
https://www.jianshu.com/p/c6032eb3e8ce

2. 触发补全

VSCode 最后发送消息的的源码位置位于,
VSCode 1.45.1: extensions/typescript-language-features/src/tsServer/spawner.ts#L233

通过向子进程的 stdin 写入内容来发送消息,

this._process.stdin!.write(JSON.stringify(serverRequest) + '\r\n', 'utf8');

tsserver 接受消息的源码位置位于,
TypeScript 3.7.3: src/tsserver/server.ts#L575

使用了 readline 模块,监听当前进程的 stdin,

rl.on("line", (input: string) => {
  // ...
});

3. 返回数据

但是补全信息的数据量有时候是会很大的,甚至超出了父子进程一次可传输的数据量。
tsserver 返回补全信息的位置位于,
TypeScript 3.7.3: src/server/session.ts#L769

可以看到,tsserver 返回了大量的补全数据,

this.host.write(msgText);

VSCode 是这样接收的,源码位置位于,
VSCode 1.45.1: extensions/typescript-language-features/src/utils/wireProtocol.ts#L90

可以看到,数据是以 Buffer 进行传输的,且一次传输长度只有 8192

readable.on('data', data => this.onLengthData(data));

4. 数据处理

VSCode 采用了分批接收数据,再合并一起处理的方式,获取了完整的从子进程返回的内容。
源码位于,VSCode 1.45.1: extensions/typescript-language-features/src/utils/wireProtocol.ts#L99 onLengthData 函数中,

private onLengthData(data: Buffer | string): void {
  if (this.isDisposed) {
    return;
  }

  try {
    this.buffer.append(data);  // 将每次收到的 buffer 暂存起来
    while (true) {
      if (this.nextMessageLength === -1) {
        this.nextMessageLength = this.buffer.tryReadContentLength();  // 读取 Content-Length 后的数字,表示消息长度
        if (this.nextMessageLength === -1) {
          return;
        }
      }
      const msg = this.buffer.tryReadContent(this.nextMessageLength);  // 读取给定长度的消息
      if (msg === null) {
        return;
      }
      this.nextMessageLength = -1;
      const json = JSON.parse(msg);
      this._onData.fire(json);
    }
  } catch (e) {
    this._onError.fire(e);
  }
}

子进程返回的消息,进行了如下编码格式,
位于 TypeScript 3.7.3: src/server/session.ts#L137 formatMessage 函数中,

`Content-Length: ${1 + len}\r\n\r\n${json}${newLine}`

这些消息太长了,会以最长为 8192 的单元,分多次发送出去。

VSCode 对这样的消息进行了如下处理,

  • 每次收到消息,都将单元消息存起来
  • 每次接受到消息,都尝试解码现存的所有单元中的内容,解码方式如下,
    • 先读取消息头里 Content-Length 后面的数字,表示本批次所有消息的长度
    • 然后再读取消息体

关键逻辑都在 VSCode 1.45.1: extensions/typescript-language-features/src/utils/wireProtocol.ts 文件中。

5. 示例

事实上,涉及父子进程通过 stdio 进行通信时,都会有上述消息被拆分发送的问题,
所以我这里写了一个示例项目,来展示这样的问题应该如何处理。

github: ipc-chunk


参考

VSCode 1.45.1
TypeScript 3.7.3

你可能感兴趣的:([Node] 进程间消息分片处理)