[VSCode 源码阅读笔记] VSCode 的进程间通讯 ( 一 )

从 mainIpcServer 开始

Posted by Nicodechal on 2020-03-16

本文从 mainIpcServer 相关源码出发了解 VSCode 中的进程间通讯的大致过程

mainIpcServer 的产生

mainIpcServerCodeMain 中的 startup() 中产生:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class CodeMain {
private async startup(args: ParsedArgs): Promise<void> {
// ...
await instantiationService.invokeFunction(async accessor => {
const environmentService = accessor.get(IEnvironmentService);
const logService = accessor.get(ILogService);
const lifecycleMainService = accessor.get(ILifecycleMainService);
// ...
const mainIpcServer = await this.doStartup(logService, environmentService, lifecycleMainService, instantiationService, true);
// ...
});
// ...
}
}

这里调用 doStartup 得到 mainIpcServer,它是一个 Server 对象,如果此时没有启动的 Server,该函数会调用 serve 函数启动一个新的 Server,该 Server 监听传入的 mainIPCHandle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// src/vs/base/parts/ipc/node/ipc.net.ts
let server: Server;
try {
server = await serve(environmentService.mainIPCHandle);
once(lifecycleMainService.onWillShutdown)(() => server.dispose());
} catch (error) {
// ...
}
// src/vs/base/parts/ipc/node/ipc.net.ts
export function serve(hook: any): Promise<Server> {
return new Promise<Server>((c, e) => {
const server = createServer();

server.on('error', e);
server.listen(hook, () => {
server.removeListener('error', e);
c(new Server(server));
});
});
}

产生的 mainIpcServer 作为参数传递给 CodeApplication,并在其 openFirstWindow 函数中注册 Channel

1
2
3
4
5
6
7
8
// src/vs/code/electron-main/main.ts
instantiationService.createInstance(CodeApplication, mainIpcServer, instanceEnvironment).startup();

// src/vs/code/electron-main/app.ts
// Register more Main IPC services
const launchMainService = accessor.get(ILaunchMainService);
const launchChannel = createChannelReceiver(launchMainService, { disableMarshalling: true });
this.mainIpcServer.registerChannel('launch', launchChannel);

这里首先使用 createChannelReceiver 函数将一个服务包装成一个 Channel,然后调用 mainIpcServerregisterChannel 将得到的 Channel 注册到 mainIpcServer

createChannelReceiver 返回一个 Channel,每个 Channel 都有两个方法,calllisten,其功能如下:

  1. call 函数可以调用传入服务包含的所有函数。
  2. listen 可以得到服务的所有事件。
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
43
44
45
46
export function createChannelReceiver(service: unknown, options?: IChannelReceiverOptions): IServerChannel {
const handler = service as { [key: string]: unknown };
const disableMarshalling = options && options.disableMarshalling;

// Buffer any event that should be supported by
// iterating over all property keys and finding them
// 将所有的事件加入到一个 { 事件名称 -> 事件 } 的 Map 中
const mapEventNameToEvent = new Map<string, Event<unknown>>();
for (const key in handler) {
if (propertyIsEvent(key)) {
mapEventNameToEvent.set(key, Event.buffer(handler[key] as Event<unknown>, true));
}
}

// 返回一个 Channel
return new class implements IServerChannel {
// 输入 Event 名称,返回名称对应的 Event
listen<T>(_: unknown, event: string): Event<T> {
// 从 Map 中取出 Event
const eventImpl = mapEventNameToEvent.get(event);
if (eventImpl) {
return eventImpl as Event<T>;
}

throw new Error(`Event not found: ${event}`);
}

// 输入函数名,调用对应函数
call(_: unknown, command: string, args?: any[]): Promise<any> {
const target = handler[command];
if (typeof target === 'function') {

// Revive unless marshalling disabled
if (!disableMarshalling && Array.isArray(args)) {
for (let i = 0; i < args.length; i++) {
args[i] = revive(args[i]);
}
}
// 调用函数
return target.apply(handler, args);
}

throw new Error(`Method not found: ${command}`);
}
};
}

这里暂时不展开 launchMainService 具体做了什么,先分析下 Channel 注册过程。

Channel 的注册

先看一下 mainIpcServer 中的 registerChannel 做了什么:

1
2
3
4
5
6
7
registerChannel(channelName: string, channel: IServerChannel<TContext>): void {
this.channels.set(channelName, channel);

this._connections.forEach(connection => {
connection.channelServer.registerChannel(channelName, channel);
});
}

这里首先把 channel 加入到 channels 列表中,然后对于所有和该服务器相关的连接 ( connection ),也注册该 channel

connection 在建立新的连接时产生,此时也会将 channels 中的所有 channel 注册到 connectionchannelServer 中。因此每个 connection 都包含 channels 中的所有 channel

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
class IPCServer {
constructor(onDidClientConnect: Event<ClientConnectionEvent>) {
// 当客户端已经连接
onDidClientConnect(({ protocol, onDidClientDisconnect }) => {
const onFirstMessage = Event.once(protocol.onMessage);
// 当收到第一条消息
onFirstMessage(msg => {
const reader = new BufferReader(msg);
const ctx = deserialize(reader) as TContext;
// channelServer 包含 IPCServer 中的所有 channel
const channelServer = new ChannelServer(protocol, ctx);
// channelClient 可以得到一个 channel,用于请求调用函数和处理事件
const channelClient = new ChannelClient(protocol);
// 这里把已有的 channel 都注册到 server 中
this.channels.forEach((channel, name) => channelServer.registerChannel(name, channel));

const connection: Connection<TContext> = { channelServer, channelClient, ctx };
this._connections.add(connection);
this._onDidAddConnection.fire(connection);

// 连接关闭回收资源
onDidClientDisconnect(() => {
channelServer.dispose();
channelClient.dispose();
this._connections.delete(connection);
this._onDidRemoveConnection.fire(connection);
});
});
});
}
}

connection 的建立

一个 connection 主要包含三个部分,一个包含一组 channelchannelServer 和一个可以向这些 channel 发起请求的 channelClient,以及一个继承自 Client 类的 ctx

1
2
3
4
5
6
7
8
// src/vs/base/parts/ipc/common/ipc.ts
interface Connection<TContext> extends Client<TContext> {
readonly channelServer: ChannelServer<TContext>;
readonly channelClient: ChannelClient;
}
export interface Client<TContext> {
readonly ctx: TContext;
}

channelServerchannelClient 通过一个 protocol 进行通信,protocol 实现了 IMessagePassingProtocol 接口,该接口包含接收消息的事件 onMessage 和发送消息的函数 send

1
2
3
4
export interface IMessagePassingProtocol {
send(buffer: VSBuffer): void;
onMessage: Event<VSBuffer>;
}

protocol 提供了 socket 通信能力,可以用于交换如下格式的数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
/-------------------------------|------\
| HEADER | |
|-------------------------------| DATA |
| TYPE | ID | ACK | DATA_LENGTH | |
\-------------------------------|------/

The header is 9 bytes and consists of:
- TYPE is 1 byte (ProtocolMessageType) - the message type
- ID is 4 bytes (u32be) - the message id (can be 0 to indicate to be ignored)
- ACK is 4 bytes (u32be) - the acknowledged message id (can be 0 to indicate to be ignored)
- DATA_LENGTH is 4 bytes (u32be) - the length in bytes of DATA

Only Regular messages are counted, other messages are not counted, nor acknowledged.

因此,channelClientchannelServer 都通过 onMessage 接收对方传来的消息,使用 send 方法发送消息。

传递的消息

channelClient 传递的消息有四种类型,具体含义如下:

1
2
3
4
5
6
export const enum RequestType {
Promise = 100, // 调用方法
PromiseCancel = 101, // 取消调用方法
EventListen = 102, // 监听事件
EventDispose = 103 // 取消监听事件
}

channelClient 可以得到一个 channel,它的 calllisten 方法分别用于发起调用方法请求和监听事件请求,相关请求会被包装成消息发送通过 protocolsend 方法发出,此时 channelServeronMessage 会处理相关的请求,提取消息中的信息,执行请求的方法和监听相关的事件,完成后还会使用 send 返回相应的响应。

channelServer 会响应五种结果,含义如下:

1
2
3
4
5
6
7
export const enum ResponseType {
Initialize = 200, // 初始化
PromiseSuccess = 201, // 方法调用成功
PromiseError = 202, // 方法调用错误
PromiseErrorObj = 203, // 方法调用错误对象
EventFire = 204 // 事件触发
}

总的来说,可以用下图概括:

1
2
3
4
5
6
7
              call / listen         call / listen
result response
<------------ <------------
channelClient Message channelServer
------------> ------------>
call / listen do
request call / listen

此时对于使用 channelClient 的对象来说可以认为 channelServer 不存在,因为自己调用 channelClientcall 就是执行了 call 的工作,只不过这部分工作是由 channelServer 完成的,自己只是发起请求。

小结

Server 对象维护了一个 Channel 的列表,客户端连接 Server 时可以会创建一个连接 ConnectionConnection 中的 channelClient 可以发起请求调用方法或监听事件,请求打包成一条消息发送给 channelServerchannelServer 负责和 ServerChannel 交互得到响应的结果,再通过消息将结果返回给 channelClient,至此实现了客户端本地调用服务器上的方法和监听服务器上的事件。