【VSCode Web Server】从插件下载看IPC通信机制

798 阅读7分钟

上一篇在介绍VSCode插件市场的文章中,我们刻意的忽略了一个问题:用户在客户端点击安装插件,服务端会进行下载、解压等一系列操作的,这个过程中客户端与服务端的是如何通信的?本篇我们将会以插件下载的过程为例子,介绍一下VSCode的IPC通信机制。注意,本篇更侧重于具体场景的执行过程,如果你希望全面地、理论地了解IPC通信机制,建议参考 zhuanlan.zhihu.com/p/360106947

客户端

执行链路

要了解通信机制,最直接的方式是找到客户端发起请求的源码。考虑到VSCode的代码量与复杂度,这不是一件容易的事情。这里说句题外话,在阅读大型ts项目的源码时,一定要善用VSCode的"Go to"功能。

image.png

以下列出了点击"install"之后的调用栈,不打算阅读源码可以忽略,直接看最后一步即可。

  • extensionsActions.ts

    • AbstractInstallAction#install
  • extensionsWorkbenchService.ts

    • ExtensionsWorkbenchService#install
    • ExtensionsWorkbenchService#installFromGallery
  • extensionManagementService.ts

    • ExtensionManagementService#installFromGallery
    • ExtensionManagementService#getExtensionManagementServerToInstall
  • extensionManagementServerService.ts

    • ExtensionManagementServerService#constructor
  • extensionManagementIpc.ts

    • ExtensionManagementChannelClient#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,似乎它们是这样一一对应的。其实细心的同学肯定已经发现了,在IPCClientIPCServer中不仅实例化了ChannelClient,也实例化了ChannelServer,这说明我们下意识以为的“一一对应”是似乎是不正确的,那问题会出现在哪里呢?

// 无论在IPCClient`还是IPCServer,都实例化了二者
new ChannelClient();
new ChannelServer();

一般地,我们认为客户端=信息发送端,服务端=信息接收端。比如本文的插件下载的场景中,客户端=浏览器环境,调用Channel.Call发起安装插件请求;服务端=Node服务环境,负责接收信息并调用ServerChannel.Call执行业务逻辑。上文为了叙述更容易理解,也刻意忽略了一些源码,把IPC通信理解为单向数据流。

然而事实上,IPC通信是双向的,无论客户端还是服务端,既需要发送信息Channel.Call的能力,也需要接收消息执行业务逻辑ServerChannel.Call的能力。比如,Node服务执行完安装插件的逻辑之后,需要告知浏览器安装成功与否,这是服务端就成了消息发送端,客户端就成了消息接收端。所以,无论客户端还是服务端,都需要同时实例化ChannelServerChannelClient才行。

另外还值得一提的是,服务端的IPCServer会比客户端IPClient复杂了许多。因为服务端只有一个,客户端却可能有无数个,当客户端的发起Channel.Call时,并不需要多考虑,可是服务端发起Channel.Call,就必须知道是要发送消息至哪一个客户端。所以在IPCServer的源码里,会把一个ChannelServer实例、一个ChannelClient实例和当前请求的上下文打包成一个Connection,以此解决多客户端的问题。有兴趣的同学可以进一步阅读IPCServer的源码。