Electron
是多进程的,因此,在写应用的时候少不了的就是进程间的通信,尤其是主进程与渲染进程之间的通信。但是如果不好好设计通信机制,那么,在应用里的通信就会混乱不堪,无法管理。这里,我们就是解读的vscode
的通信机制,值得借鉴。
通信机制会有如下内容需要设计:
- 协议设计-Protocol
- 通信频道-Channel
- 连接-Connection
- 服务端-IPCServer
- 客户端-IPCClient
基本原理
主进程和渲染进程的通信基础还是Electron
的webContents.send
、ipcRender.send
、ipcMain.on
,通信如下图:
协议设计-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
,并创建一个connection
与Client
一一对应。 - 每一个
connection
中,会存在一个ChannelServer
,用于管理此连接已订阅的serverChannel
,以及处理客户端对serverChannel
的需求。 - 每个连接建立的时候,会将通用服务注册给
connection
的ChannelServer
,表名此连接可默认使用的服务。
细品----频道订阅、服务请求与处理
- 假设,我们有一个支付宝全家桶服务商(IPCServer)公众号,提供了电缴费查询服务、水缴费服务、气缴费服务、社保查询服务。
- 我们关注了服务商(客户端发起
ipc:hello
连接消息)。 - 服务商的粉丝列表里,就有一个我(服务端收到
ipc:hello
连接消息,存储Client
连接信息至Clients
,并创建一个connection
与Client
一一对应)。 - 我们在我们的服务列表里,发现了:电缴费查询服务、水缴费服务、气缴费服务服务。(每个连接建立的时候,会将通用服务注册给
connection
的ChannelServer
,表名此连接可默认使用的服务。) - 我打开了电缴费查询服务,发起了电费查询请求,并给我返回了查询结果(请求
电缴费查询服务
查询电费
,服务端接收到我的请求,找到了电缴费查询服务
,并查询了我的电费信息,将结果返回给了我)
根据上述过程,我们发现了:
- 服务的订阅(频道订阅),分为主动和被动,被动即服务商默认提供,主动即我们需要发起申请,比如关注'xxx'服务。
- 根据我们前面定义的通信内容发现,这是单向的,即客户端发出请求,服务端给予响应,而不能服务端主动给我们推送消息。
- 因为server里,只有
sendResponse
, client里只有sendRequest
。 - 规定了response和request的通信格式,则反向的通信内容就不会被识别。
- 因此无法反向通信。
- 因为server里,只有
因此,我们接下来会改造设计
- 频道仍然是被动关注,主动关注先不做。
- 支持双向通信。
非常简单,如图,在服务端connection
里,保存channelClient
,在客户端保存channelServer
,服务端channelClient
连接客户端channelServer
即可。