vscode-通信机制设计解读(Electron)

3,474 阅读6分钟

Electron是多进程的,因此,在写应用的时候少不了的就是进程间的通信,尤其是主进程与渲染进程之间的通信。但是如果不好好设计通信机制,那么,在应用里的通信就会混乱不堪,无法管理。这里,我们就是解读的vscode的通信机制,值得借鉴。

通信机制会有如下内容需要设计:

  • 协议设计-Protocol
  • 通信频道-Channel
  • 连接-Connection
  • 服务端-IPCServer
  • 客户端-IPCClient

基本原理

主进程和渲染进程的通信基础还是ElectronwebContents.sendipcRender.sendipcMain.on,通信如下图:

Electron默认通信

协议设计-Protocol

协议,即约定,约定内容其实就是:在哪个channel: string发消息。如下:

约定:

  • 用'ipc:message'通信
  • 用'ipc:disconnect'解除通信

协议结构如下:

入参:

  • sender # 发送者
  • onMessage # 消息处理函数

属性:

  • send(buffer: VSBuffer): void; // 发消息
  • onMessage: Event; // 收消息
  • dispose: void; // 停止收发消息
  export interface Sender {
    send(channel: string, msg: Buffer | null): void;
  }
  export class Protocol implements IMessagePassingProtocol {
    constructor(
      private sender: Sender,
      readonly onMessage: Event<VSBuffer>
    ) { }
    send(message: VSBuffer): void {
      try {
        this.sender.send('ipc:message', (<Buffer>message.buffer));
      } catch (e) {
        // systems are going down
      }
    }
    dispose(): void {
      this.sender.send('ipc:disconnect', null);
    }
  }

通信频道设计-Channel

频道Channel通常会分为:

  • 服务端Channel(ChannelServer): 频道服务端(是频道的服务端,本质是个服务端)

    • 注册Channel(IServerChannel: 表示服务端频道,是服务端的频道,本质是频道)
    • 监听客户端消息
    • 处理客户端消息并返回请求结果
  • 客户端Channel(ChannelClient): 频道客户端

    • 获得Channel(IChannel: 结构和IServerChannel一直,表示的是客户端的频道)
    • 发送频道请求
    • 接收请求结果,并处理

频道通信会约定通信的数据格式,具体约定如下:

  // 请求类型约定
  export const enum RequestType {
    Promise = 100,
    PromiseCancel = 101,
    EventListen = 102,
    EventDispose = 103
  }

  type IRawPromiseRequest = { type: RequestType.Promise; id: number; channelName: string; name: string; arg: any; };
  type IRawPromiseCancelRequest = { type: RequestType.PromiseCancel, id: number };
  type IRawEventListenRequest = { type: RequestType.EventListen; id: number; channelName: string; name: string; arg: any; };
  type IRawEventDisposeRequest = { type: RequestType.EventDispose, id: number };
  type IRawRequest = IRawPromiseRequest | IRawPromiseCancelRequest | IRawEventListenRequest | IRawEventDisposeRequest;

  // 返回类型约定
  export const enum ResponseType {
    Initialize = 200,
    PromiseSuccess = 201,
    PromiseError = 202,
    PromiseErrorObj = 203,
    EventFire = 204
  }

  type IRawInitializeResponse = { type: ResponseType.Initialize };
  type IRawPromiseSuccessResponse = { type: ResponseType.PromiseSuccess; id: number; data: any };
  type IRawPromiseErrorResponse = { type: ResponseType.PromiseError; id: number; data: { message: string, name: string, stack: string[] | undefined } };
  type IRawPromiseErrorObjResponse = { type: ResponseType.PromiseErrorObj; id: number; data: any };
  type IRawEventFireResponse = { type: ResponseType.EventFire; id: number; data: any };
  type IRawResponse = IRawInitializeResponse | IRawPromiseSuccessResponse | IRawPromiseErrorResponse | IRawPromiseErrorObjResponse | IRawEventFireResponse;

根据以上规则,我们有如下几个对象:

  • IServerChannel 服务端频道
  • IChannel 客户端频道
  • IChannelServer 频道的服务端
  • IChannelClient 频道客户端

?> 无论是客户端频道(IChannel)还是服务端频道(IChannelServer),作为一个频道而言,类似于电台频道,它有两个功能,一个是点播,即频道的: call,一个是收听电台频道节目,即: listen。

IServerChannel 服务端频道

  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>;
  }

IChannel 客户端频道

  export interface IChannel {
    call<T>(command: string, arg?: any, cancellationToken?: CancellationToken): Promise<T>;
    listen<T>(event: string, arg?: any): Event<T>;
  }

IChannelServer 频道的服务端

  export interface IChannelServer<TContext = string> {
    registerChannel(channelName: string, channel: IServerChannel<TContext>): void;
  }
  export class ChannelServer<TContext = string> implements IChannelServer<TContext>, IDisposable {
    private channels = new Map<string, IServerChannel<TContext>>();
    private protocolListener: IDisposable | null;
    ...
    /**
    *
    * @param protocol 频道通信协议
    * @param ctx 连接此server的窗口
    * @param timeoutDelay 设置超时响应时间
    */
    constructor(private protocol: IMessagePassingProtocol, private ctx: TContext, private timeoutDelay: number = 1000) {
        this.protocolListener = this.protocol.onMessage(msg => this.onRawMessage(msg));// 监听客户端消息、处理与返回结果:onRawMessage
        this.sendResponse({ type: ResponseType.Initialize }); // 初始化连接消息
    }
    // 注册频道
    registerChannel(channelName: string, channel: IServerChannel<TContext>): void {
      this.channels.set(channelName, channel);
    }
    ...
  }

IChannelClient 频道客户端

  export class ChannelClient implements IChannelClient, IDisposable {
    private handlers = new Map<number, IHandler>();// 存储要处理的handler,当请求返回时,根据请求id,选择相应的handler进行返回
    private protocolListener: IDisposable | null;
    ...
    /**
  	 * @param protocol 通信协议
  	 */
    constructor(private protocol: IMessagePassingProtocol) {
      this.protocolListener = this.protocol.onMessage(msg => this.onBuffer(msg)); // 监听服务端返回,并处理onBuffer
    }
    // 获得通信频道
    getChannel<T extends IChannel>(channelName: string): T {
      const that = this;
      return {
        call(command: string, arg?: any, cancellationToken?: CancellationToken) {
          // 发送请求,去调用服务端频道的相应功能
          return that.requestPromise(channelName, command, arg, cancellationToken);
        },
        listen(event: string, arg: any) {
          // 监听服务端频道发布的内容。
          return that.requestEvent(channelName, event, arg);
        }
      } as T;
    }
  }

服务端-IPCServer、客户端-IPCClient、连接-Connection

一个连接信息通常有这两种情况:

  • 简单的: 客户端连接了服务端。
  • 复杂的: 客户端连接了服务端的某个服务(或者频道)

这里面有两个概念,客户端、服务端,具体是如下意思:

  • 客户端: 发起连接的一端
  • 服务端: 被连接的一端

此处探讨的是第二种情况:客户端连接了服务端的某个服务(或者频道), 具体结构图如下,咱细细品:

通信设计

细品----服务端(IPCServer)

服务端,即提供服务,因此服务端具有的第一样东西,就是:服务。 服务端提供服务,需要保留与客户端的沟通渠道,便于将服务提供给客户端,因此,服务端具有的第二件东西是:通信通道。 因此,服务端具有:

  • connections(通信通道)
  • channels(服务)

细品----建立连接的过程(参考上图)

  • 客户端发起ipc:hello连接消息。
  • 服务端收到ipc:hello连接消息,存储Client连接信息至Clients,并创建一个connectionClient一一对应。
  • 每一个connection中,会存在一个ChannelServer,用于管理此连接已订阅的serverChannel,以及处理客户端对serverChannel的需求。
  • 每个连接建立的时候,会将通用服务注册给connectionChannelServer,表名此连接可默认使用的服务。

细品----频道订阅、服务请求与处理

  • 假设,我们有一个支付宝全家桶服务商(IPCServer)公众号,提供了电缴费查询服务、水缴费服务、气缴费服务、社保查询服务。
  • 我们关注了服务商(客户端发起ipc:hello连接消息)。
  • 服务商的粉丝列表里,就有一个我(服务端收到ipc:hello连接消息,存储Client连接信息至Clients,并创建一个connectionClient一一对应)。
  • 我们在我们的服务列表里,发现了:电缴费查询服务、水缴费服务、气缴费服务服务。(每个连接建立的时候,会将通用服务注册给connectionChannelServer,表名此连接可默认使用的服务。
  • 我打开了电缴费查询服务,发起了电费查询请求,并给我返回了查询结果(请求电缴费查询服务查询电费,服务端接收到我的请求,找到了电缴费查询服务,并查询了我的电费信息,将结果返回给了我)

根据上述过程,我们发现了:

  • 服务的订阅(频道订阅),分为主动和被动,被动即服务商默认提供,主动即我们需要发起申请,比如关注'xxx'服务。
  • 根据我们前面定义的通信内容发现,这是单向的,即客户端发出请求,服务端给予响应,而不能服务端主动给我们推送消息。
    • 因为server里,只有sendResponse, client里只有sendRequest
    • 规定了response和request的通信格式,则反向的通信内容就不会被识别。
    • 因此无法反向通信。

因此,我们接下来会改造设计

  • 频道仍然是被动关注,主动关注先不做。
  • 支持双向通信。

非常简单,如图,在服务端connection里,保存channelClient,在客户端保存channelServer,服务端channelClient连接客户端channelServer即可。

通信设计2