上一篇在介绍VSCode插件市场的文章中,我们刻意的忽略了一个问题:用户在客户端点击安装插件,服务端会进行下载、解压等一系列操作的,这个过程中客户端与服务端的是如何通信的?本篇我们将会以插件下载的过程为例子,介绍一下VSCode的IPC通信机制。注意,本篇更侧重于具体场景的执行过程,如果你希望全面地、理论地了解IPC通信机制,建议参考 zhuanlan.zhihu.com/p/360106947
客户端
执行链路
要了解通信机制,最直接的方式是找到客户端发起请求的源码。考虑到VSCode的代码量与复杂度,这不是一件容易的事情。这里说句题外话,在阅读大型ts项目的源码时,一定要善用VSCode的"Go to"功能。
以下列出了点击"install"之后的调用栈,不打算阅读源码可以忽略,直接看最后一步即可。
-
extensionsActions.tsAbstractInstallAction#install
-
extensionsWorkbenchService.tsExtensionsWorkbenchService#installExtensionsWorkbenchService#installFromGallery
-
extensionManagementService.tsExtensionManagementService#installFromGalleryExtensionManagementService#getExtensionManagementServerToInstall
-
extensionManagementServerService.tsExtensionManagementServerService#constructor
-
extensionManagementIpc.tsExtensionManagementChannelClient#installFromGallery
一路追踪下来,终于到了实现客户端IPC的核心源码:
// extensionManagementIpc.ts
export class ExtensionManagementChannelClient extends Disposable implements IExtensionManagementService {
// ...
installFromGallery(extension: IGalleryExtension, installOptions?: InstallOptions): Promise<ILocalExtension> {
return Promise.resolve(this.channel.call<ILocalExtension>('installFromGallery', [extension, installOptions])).then(local => transformIncomingExtension(local, null));
}
// ...
}
这里可以看到this.channel.call('installFromGallery'),实际就是通知服务端执行installFromGallery命令,那它是如何实现的呢?我们首先来这个channel是什么。
Channel
/**
* An `IChannel` is an abstraction over a collection of commands.
* You can `call` several commands on a channel, each taking at
* most one single argument. A `call` always returns a promise
* with at most one single return value.
*/
export interface IChannel {
call<T>(command: string, arg?: any, cancellationToken?: CancellationToken): Promise<T>;
listen<T>(event: string, arg?: any): Event<T>;
}
Channel是一个非常简单接口,仅提供了两个语义化非常好理解的方法:
call执行命令listen监听事件执行回调
ChannelClient
ChannelClient可以看作是Channel在客户端的具体实现,它暴露getChannel方法返回一个Channel实例,而Channel的具体实现则是调用的ChannelClient内的其它私有方法:
// src/vs/base/parts/ipc/common/ipc.ts
export class ChannelClient implements IChannelClient, IDisposable {
getChannel<T extends IChannel>(channelName: string): T {
return {
call(command: string, arg?: any, cancellationToken?: CancellationToken) {
if (that.isDisposed) {
return Promise.reject(errors.canceled());
}
// 调用call实际就是执行requestPromise
return that.requestPromise(channelName, command, arg, cancellationToken);
},
listen(event: string, arg: any) {
if (that.isDisposed) {
return Promise.reject(errors.canceled());
}
return that.requestEvent(channelName, event, arg);
}
} as T;
}
private requestPromise(channelName: string, name: string, arg?: any, cancellationToken = CancellationToken.None): Promise<any> {
// 最终调用 this.sendBuffer
}
private sendBuffer(message: VSBuffer): number {
try {
// 这个protocol是什么呢?
this.protocol.send(message);
return message.byteLength;
} catch (err) {
// ...
}
}
}
上述源码可以看出channel.call实际上是调用ChannelClient的私有方法requestPromise。继续一路读下来,发现也并非直接发起请求,而是调用一个叫protocol.send方法,而这个protocol是基于websocket封装的一个服务,这一块可以单开一篇暂时不展开,具体实现可以看:
// src/vs/base/parts/ipc/common/ipc.net.ts
export class PersistentProtocol implements IMessagePassingProtocol {
// ...
}
IPCClient
IPCClient比较简单,主要作用有三个
- 实例化
ChannelClient - 提供
getChannel方法暴露Channel实例给业务调用 - 销毁
ChannelClient
// src/vs/base/parts/ipc/common/ipc.ts
export class IPCClient<TContext = string> implements IChannelClient, IChannelServer<TContext>, IDisposable {
private channelClient: ChannelClient;
private channelServer: ChannelServer<TContext>;
constructor(protocol: IMessagePassingProtocol, ctx: TContext, ipcLogger: IIPCLogger | null = null) {
const writer = new BufferWriter();
serialize(writer, ctx);
protocol.send(writer.buffer);
// 实例化channelClient
this.channelClient = new ChannelClient(protocol, ipcLogger);
this.channelServer = new ChannelServer(protocol, ctx, ipcLogger);
}
getChannel<T extends IChannel>(channelName: string): T {
return this.channelClient.getChannel(channelName) as T;
}
// ...
dispose(): void {
this.channelClient.dispose();
this.channelServer.dispose();
}
}
如果想知道IPCClient是什么时候实例化的,可以看ExtensionManagementServerService的构造函数源码,理解为Service实例化时也会把IPCClient实例化好就OK了。
服务端
上文我们说到客户端执行channel.call最终通过ChannelClient调用protocol.send向服务端发送消息,那服务端是是如何接收消息和执行后续逻辑的呢?
ChannelServer
ChannelServer和上文说到的ChannelClient有些类似,实现了向远程发送消息的能力。除此之外,它还会在初始化时监听远程发来的消息,同理也是通过protocol实现的:
// src/vs/base/parts/ipc/common/ipc.ts
export class ChannelServer<TContext = string> implements IChannelServer<TContext>, IDisposable {
// ...
constructor(private protocol: IMessagePassingProtocol, private ctx: TContext, private logger: IIPCLogger | null = null, private timeoutDelay: number = 1000) {
// 这里会监听客户端发送而来得消息,调用onRawMessage方法
this.protocolListener = this.protocol.onMessage(msg => this.onRawMessage(msg));
this.sendResponse({ type: ResponseType.Initialize });
}
private onRawMessage(message: VSBuffer): void {
// ...
return this.onPromise({ type, id: header[1], channelName: header[2], name: header[3], arg: body });
// ...
}
private onPromise(request: IRawPromiseRequest): void {
// ...
try {
// 调用channel.call
promise = channel.call(this.ctx, request.name, request.arg, cancellationTokenSource.token);
} catch (err) {
promise = Promise.reject(err);
}
// ...
promise.then(data => {
// 执行成功后会返回一个消息给客户端
this.sendResponse(<IRawResponse>{ id, data, type: ResponseType.PromiseSuccess });
}, err => {
// ...
});
}
}
从上述源码可以看出,服务端接收到消息后,最终依然是调用channel.call,那这服务端的channel又是什么呢?
ServerChannel
上文中提到,ChannelClient里的channe.call对应的是Channel,而ChannelServer里的channe.call则是ServerChannel。它们结构基本一致:
export interface IServerChannel<TContext = string> {
call<T>(ctx: TContext, command: string, arg?: any, cancellationToken?: CancellationToken): Promise<T>;
listen<T>(ctx: TContext, event: string, arg?: any): Event<T>;
}
不同的是,Channel是调用远程的服务的,而ServerChannel是调用具体的业务方法,每一个ServerChannel关联一个Service,对于我们下载插件场景则是ExtensionManagementChannel:
// extensionManagementIpc.ts
export class ExtensionManagementChannel implements IServerChannel {
// 传入具体的业务service
constructor(private service: IExtensionManagementService, private getUriTransformer: (requestContext: any) => IURITransformer | null) {
// ...
}
listen(context: any, event: string): Event<any> {
// ...
}
call(context: any, command: string, args?: any): Promise<any> {
const uriTransformer: IURITransformer | null = this.getUriTransformer(context);
// 定义了各种命令供调用,对应的一个service的方法
switch (command) {
case 'zip': return this.service.zip(transformIncomingExtension(args[0], uriTransformer)).then(uri => transformOutgoingURI(uri, uriTransformer));
case 'unzip': return this.service.unzip(transformIncomingURI(args[0], uriTransformer));
case 'install': return this.service.install(transformIncomingURI(args[0], uriTransformer), args[1]);
case 'getManifest': return this.service.getManifest(transformIncomingURI(args[0], uriTransformer));
case 'getTargetPlatform': return this.service.getTargetPlatform();
case 'canInstall': return this.service.canInstall(args[0]);
case 'installFromGallery': return this.service.installFromGallery(args[0], args[1]);
case 'uninstall': return this.service.uninstall(transformIncomingExtension(args[0], uriTransformer), args[1]);
case 'reinstallFromGallery': return this.service.reinstallFromGallery(transformIncomingExtension(args[0], uriTransformer));
case 'getInstalled': return this.service.getInstalled(args[0]).then(extensions => extensions.map(e => transformOutgoingExtension(e, uriTransformer)));
case 'updateMetadata': return this.service.updateMetadata(transformIncomingExtension(args[0], uriTransformer), args[1]).then(e => transformOutgoingExtension(e, uriTransformer));
case 'updateExtensionScope': return this.service.updateExtensionScope(transformIncomingExtension(args[0], uriTransformer), args[1]).then(e => transformOutgoingExtension(e, uriTransformer));
case 'getExtensionsControlManifest': return this.service.getExtensionsControlManifest();
}
throw new Error('Invalid call');
}
}
IPCServer
和IPCClient类似,IPCServer作用也可以总结为三点:
- 实例化
ChannelServer - 提供
getChannel方法暴露Channel实例给业务调用 - 销毁
ChannelServer
比较特殊的是,ChannelServer是在IPCServer第一只接收到消息时实例化的。
// src/vs/base/parts/ipc/common/ipc.ts
export class IPCServer<TContext = string> implements IChannelServer<TContext>, IRoutingChannelClient<TContext>, IConnectionHub<TContext>, IDisposable {
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
const channelServer = new ChannelServer(protocol, ctx);
const channelClient = new ChannelClient(protocol);
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);
});
});
});
}
getChannel<T extends IChannel>(channelName: string, router: IClientRouter<TContext>): T;
getChannel<T extends IChannel>(channelName: string, clientFilter: (client: Client<TContext>) => boolean): T;
getChannel<T extends IChannel>(channelName: string, routerOrClientFilter: IClientRouter<TContext> | ((client: Client<TContext>) => boolean)): T {
const that = this;
return {
call(command: string, arg?: any, cancellationToken?: CancellationToken): Promise<T> {
// ...
},
listen(event: string, arg: any): Event<T> {
// ...
}
} as T;
}
// ...
dispose(): void {
// ...
}
}
对称性连接的思想
上文中我们介绍了客户端与ChannelClient,服务端与ChannelServer,似乎它们是这样一一对应的。其实细心的同学肯定已经发现了,在IPCClient和IPCServer中不仅实例化了ChannelClient,也实例化了ChannelServer,这说明我们下意识以为的“一一对应”是似乎是不正确的,那问题会出现在哪里呢?
// 无论在IPCClient`还是IPCServer,都实例化了二者
new ChannelClient();
new ChannelServer();
一般地,我们认为客户端=信息发送端,服务端=信息接收端。比如本文的插件下载的场景中,客户端=浏览器环境,调用Channel.Call发起安装插件请求;服务端=Node服务环境,负责接收信息并调用ServerChannel.Call执行业务逻辑。上文为了叙述更容易理解,也刻意忽略了一些源码,把IPC通信理解为单向数据流。
然而事实上,IPC通信是双向的,无论客户端还是服务端,既需要发送信息Channel.Call的能力,也需要接收消息执行业务逻辑ServerChannel.Call的能力。比如,Node服务执行完安装插件的逻辑之后,需要告知浏览器安装成功与否,这是服务端就成了消息发送端,客户端就成了消息接收端。所以,无论客户端还是服务端,都需要同时实例化ChannelServer和ChannelClient才行。
另外还值得一提的是,服务端的IPCServer会比客户端IPClient复杂了许多。因为服务端只有一个,客户端却可能有无数个,当客户端的发起Channel.Call时,并不需要多考虑,可是服务端发起Channel.Call,就必须知道是要发送消息至哪一个客户端。所以在IPCServer的源码里,会把一个ChannelServer实例、一个ChannelClient实例和当前请求的上下文打包成一个Connection,以此解决多客户端的问题。有兴趣的同学可以进一步阅读IPCServer的源码。