Start the
git clone [email protected]:spcBackToLife/jupiter-electron-ipc-demo.git
cd jupiter-electron-ipc-demo
yarn install
yarn dev
Copy the code
Introduction to the
This project is a complete realization of the ipc communication mechanism in vscode, we can see the above startup mode, start and experience.
Service Usage Examples
[Create a windowSercice]
export class WindowService {
doSomething(): string {
console.log('do something and return done')
return 'done'; }}Copy the code
[Create a channel for a service]
import { IServerChannel } from ".. /core/common/ipc";
import { WindowService } from "./windowService";
import { Event } from '.. /base/event';
export class WindowChannel implements IServerChannel {
constructor(
public readonly windowService: WindowService,
) {}
listen(_: unknown, event: string): Event<any> {
// Currently not supported
throw new Error(`Not support listen event currently: ${event}`);
}
call(_: unknown, command: string, arg? :any) :Promise<any> {
switch (command) {
case 'doSomething':
return Promise.resolve(this.windowService.doSomething());
default:
return Promise.reject('No service to call! '); }}}Copy the code
[render process – SRC /render/index.html]
<html>
<div>jupiter electron</div>
<script>
const { Client } = require('.. /.. /core/electron-render/IPCClient');
const mainProcessConnection = new Client(`window_1`);
const channel = mainProcessConnection.getChannel('windowManager');
channel.call('doSomething').then((result) = > console.log('result:', result));
</script>
</html>
Copy the code
[main process-src /main.ts]
import { app, BrowserWindow } from 'electron';
import path from 'path';
import { Server as ElectronIPCServer } from '.. /core/electron-main/ipc.electron-main';
import { WindowChannel } from './windowServiceIpc';
import { WindowService } from './windowService';
app.on('ready'.() = > {
const electronIpcServer = new ElectronIPCServer();
electronIpcServer.registerChannel('windowManager'.new WindowChannel(new WindowService()))
const win = new BrowserWindow({
width: 1000.height: 800.webPreferences: {
nodeIntegration: true}});console.log('render index html:', path.join(__dirname, 'render'.'index.html'));
win.loadFile(path.join(__dirname, 'render'.'index.html'));
})
Copy the code
Start and run a wave:
"scripts": {..."dev": "tsc && electron ./src/main.js". },Copy the code
Activation:
yarn dev
Copy the code
At this point, we have implemented the ipc mechanism of vscode, you can go here to experience:
jupiter-electron-ipc-demo
Model is introduced
You can also use Vscode’s communication mechanism in your Electron: implement Vscode communication mechanism from zero to one
Electron is multi-process, so interprocess communication is essential when writing applications, especially between the main process and the renderer process. But if you don’t design the communication mechanism well, then the communication in the application can be chaotic and unmanageable, as those of you who developed Electron may well know.
Let’s take an example of what communication looks like in traditional Electron and Jupiter Electron:
Example: A window sends a message to the main process to do something and returns a result: done.
In Electron, we might need to do this:
【 Main process 】
/ / do something
const doSomething = (. params) = > {
console.log('do some sync things');
return Promise.resolve('complete');
}
app.on('ready'.() = > {
const win = new BrowserWindow({});
ipcMain.on('dosomething'.async (e, message) => {
const result = await doSomething(message.params);
// Return the result (the renderer needs to pass the request ID to ensure unique channel communication)
win.webContents.send(`dosomething_${message.requestId}`, result); })})Copy the code
[Rendering process]
const doSomething = () = > {
return new Promise((resolve, reject) = > {
const requestId = new Date().getTime();
// Listen for only one return
ipcRenderer.once(`dosomething_${requestId}`.(result) = > {
console.log('result:', result);
resolve(result);
})
// Send a message -dosomething
ipcRenderer.send('dosomething', {
requestId,
params: {... }})})}... doSomething();Copy the code
The obvious problem is that if the main process fails, it has to send the failure message back and do some processing.
In fact, there are many problems in the above traditional Electron communication writing method, which will not be added here.
What do we do in Jupiter Electron?
【 Main process 】
const doSomething = (params) = > {
console.log('do something');
return Promise.resolve('complete');
}
Copy the code
[Rendering process]
import bridge from '@jupiter-electron/runtime';
export const doAthing = () = > {
return bridge.call('doSomething', params)
.catch((err) = > console.log('main exec error:', err));
}
doAthing();
Copy the code
- Don’t worry about uniqueness, internal mechanics take care of it.
- Don’t worry about how failed exceptions are handled back to the front end, internal mechanisms return errors to the renderer process.
- Communication compression optimization? Don’t worry, it’s taken care of.
It can be found that in Jupiter Electron, communication is a very simple thing, the main process and the renderer process communicate; Window to window communication.
So how does this work and what’s the design behind it?
In fact, the communication mechanism is based on Vsocde source mechanism abstract out, below, on the design mechanism behind the interpretation, with everyone to achieve a set of IPC communication mechanism.
What is the goal of designing a communication mechanism? Process communication what?
Send, ipcRender. Send and ipcmain. on in Electron. Our goal of designing the communication mechanism is:
- Simplify our communication flow
- Ensure the stability of communication
- Improve the quality of communication
- Improves communication efficiency and reduces resource usage
To design the communication mechanism, we have to say, what is the purpose of our communication in Electron? What are the characteristics?
Communication mechanism design
Because of the Electron multi-process feature, sometimes to do one thing, we have to need multi-process cooperation, therefore, we need communication.
In general, we have these characteristic scenarios:
- The renderer expects the main process to do something and returns an execution result.
- The Renderer notifies the main process of a message without returning a result.
- The main process expects the renderer process to do something and returns the result of its execution.
- Renderer expects renderer to do something and returns an execution result.
- The Renderer listens for messages from the main process.
In general, based on the above characteristics, we can sum up as: servitization, which is another feature of the difference between Electron development and Web that I put forward.
Servitization means providing services
- When your renderer expects the main process to do something and returns an execution result, the main process needs to do something that can be abstracted into a corresponding service that the main process is responsible for providing.
- When your main process expects the renderer process to do something and return the result of its execution, what the renderer process needs to do can be abstracted into the corresponding service that the renderer is responsible for providing.
Therefore, we can design the following form:
The server can be either a “renderer” or a “main” process, depending on who is serving whom.
As you can see, the server provides n services for clients to access. But there is such a problem, here, the services provided by the service end, is all the client can access, there will be a problem, like: alipay provides the basis for all user services, such as: electricity, taxes, social security query service, but some services may only certain people can access, such as: optimizing 100% money fund services.
Therefore, our design cannot meet this requirement, so we need to make an adjustment:
We added the channel service concept, where each client accesses the service based on the channel, for example:
- Client 1 accesses channel service 1, and client 2 accesses channel service 2
- Channel services 1 and 2 both have common services and their own privileged services.
- When the customer service accesses the service, it generates a channel for each customer to provide him with the service he has
- When creating the corresponding channel service for customer service, the general service of the server will be registered in the channel, and the special service will be registered according to the characteristics of users.
Through the above mode, the above problems are solved, but it also brings some problems:
Does a user create a new channel service every time he accesses the service?
According to the above logic, this would be the case, so to solve this problem, we need the following design:
On the server, we add a new concept, called connect (Connection), when the client is initialized, initiate communication links, the channel will be to create a new service, and stored in the server, the client the next time a service access request, go directly to obtain channel services, to perform the corresponding services.
Such a scheme looks perfect, but there is a problem. The above scheme can be applied to the following scenarios:
- The renderer expects the main process to do something and returns an execution result.
If it is such a design, how to meet:
- The main process expects the renderer process to do something and returns the result of its execution.
It’s as if the “server” and “client” have swapped identities. How do you keep both of these things together?
We can do the following design:
We add “channel client” on the server connection and “channel service” on the client, so that the client can access the service of the channel service on the server, and also call the service of the channel service on the client, so as to achieve the above problem.
So far, we have basically completed the design and analysis of vscode’s entire communication mechanism, and the next step is the concrete implementation. Of course, in the implementation process, we also need to consider:
- Buffer processing of communication messages
- Exception handling is performed during communication
- The uniqueness of communication is guaranteed
Implementation of communication mechanism
In the above statement, we have mentioned services and channels. Here we unify the concept:
- A channel provides a service, and a service is a channel
The subsequent unified use of “channel” to refer to a “service”
Let’s go through some of the concepts that we’ve come up with in this process.
- Server -> IPCServer
- The outermost server in the figure, which manages all connections
- Client -> IPCClient
- The outermost client in the figure is used to establish connections, send and receive messages and process messages in a unified manner
- Connection – > Connection
- ChannelServer -> ChannelServer
- Provide one end of the service
- ChannelClient -> ChannelClient
- The channel client is the end that accesses a channel (service) on the server
- ServerChannel -> ServerChannel
- Channel The channel (service) registered by the server is the “server channel”
Of course, before the implementation of vscode communication mechanism, in fact, there are many required courses, but these will be explained in the future, now you can understand their role, and use it, does not affect the use and understanding of IPC mechanism.
Before we start vscode communication design, we need to provide you with some basic utility classes:
- "cancelablePromise/" cancelablePromise - "disposable/" listen to resource release base class - "buffer, buffer-utils" buffer processing of messages - "events" is vscode Their implementation of the event class, is also a decoration of the event class, very nice!! - "iterator" for linkedList data structure iteration - double linked data structure implementation of "linkedList" JS - "utils" helper classCopy the code
The following will be implemented in the following order:
- Design and implementation of message communication protocol
- ServerChannel -> ServerChannel
- ChannelServer -> ChannelServer
- ChannelClient -> ChannelClient
- Connection – > Connection
- Client -> IPCClient
- Server -> IPCServer
Design and implementation of message communication protocol
In the figure above, we have described that a “client” and “server” will establish a “connection” and have a “channel server”. In “client”, how to access “channel server”? Here we need to define the access protocol:
We stipulate that:
- When the client is initialized, it sends an ipc: Hello message to establish a connection with the server.
// Something like this, not implementation code, just a hint
class IPCClient {
constructor() {
ipcRenderer.send('ipc:hello'); }}Copy the code
- Messages are received and sent on the IPC: Message channel on both the client and the server.
xxx.webContents.send('ipc:message', message); . ipcRenderer.send('ipc:message', message);Copy the code
- When the “client” is uninstalled (e.g. the window is closed), send the disconnect message: IPC :disconnect to destroy all message listening.
Therefore, we design the following protocol:
[core/common/ipc.electron.ts]
import { Event } from '.. /.. /base/event'; / / tools
import { VSBuffer } from '.. /.. /base/buffer'; / / tools
export interface IMessagePassingProtocol {
onMessage: Event<VSBuffer>;
send(buffer: VSBuffer): void;
}
export interface Sender {
send(channel: string.msg: Buffer | null) :void;
}
export class Protocol implements IMessagePassingProtocol {
constructor(
private readonly 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); }}Copy the code
Instructions for use:
.const protocol = newProtocol(webContents, onMessage); .const protocol = new Protocol(ipcRenderer, onMessage);
Copy the code
Define the server channel: IServerChannel
A Server Channel is a channel (service) registered in channel Services on the Server.
[core/common/ipc.ts]
export interface IServerChannel<TContext = string> {
call<T>(
ctx: TContext,
command: string, arg? :any, cancellationToken? : CancellationToken, ):Promise<T>; // Initiate a service request
listen<T>(ctx: TContext, event: string, arg? :any): Event<T>;// Listen for messages
}
Copy the code
The specific implementation will define the service only when it is actually used. Therefore, the definition and use of “server channel” will be introduced in the use case after the IPC mechanism is completed.
Define the server side of the channel
Before the “client” accesses the service, it establishes a “connection” with the “server”. In the “connection”, a “channel server” manages the “service channels” that the service provides to the “client”.
First, we define the “service channel” interface:
[core/common/ipc.ts]
export interface IChannelServer<TContext = string> {
registerChannel(channelName: string.channel: IServerChannel<TContext>): void;
}
Copy the code
- There is basically a way to register channels
Next, we implement a “channel service”
export class ChannelServer<TContext = string>
implements IChannelServer<TContext>, IDisposable {
// Save the channel information that the client can access
private readonly channels = new Map<string, IServerChannel<TContext>>();
// Message communication protocol monitor
private protocolListener: IDisposable | null;
// Save the active request, cancel the execution after receiving the cancellation message, release the resource
private readonly activeRequests = new Map<number, IDisposable>();
// Before the channel server is registered, many requests may arrive, at which point they will stay in the queue
// If timeoutDelay is out of date, it will be removed
// If the channel is registered, it will be taken out of the queue and executed
private readonly pendingRequests = new Map<string, PendingRequest[]>();
constructor(
private readonly protocol: IMessagePassingProtocol, // Message protocol
private readonly ctx: TContext, / / service name
private readonly timeoutDelay: number = 1000.// Communication timeout
) {
// Receive ChannelClient messages
this.protocolListener = this.protocol.onMessage(msg= >
this.onRawMessage(msg),
);
// When the channel server is instantiated, we need to return a message to the channel server that the instantiation is complete:
this.sendResponse({ type: ResponseType.Initialize });
}
public dispose(): void{... }public registerChannel(
channelName: string.channel: IServerChannel<TContext>): void{... }private onRawMessage(message: VSBuffer): void{... }private disposeActiveRequest(request: IRawRequest): void{... }private flushPendingRequests(channelName: string) :void{... }private sendResponse(response: IRawResponse): void{... }private send(header: any.body: any = undefined) :void{... }private sendBuffer(message: VSBuffer): void{... }private onPromise(request: IRawPromiseRequest): void{... }private collectPendingRequest(request: IRawPromiseRequest): void{... }Copy the code
Let’s explain one important set: activeRequests
private readonly activeRequests = new Map<number, IDisposable>();
Copy the code
This Map stores active “service requests”, which are still in progress and are disposed uniformly if the client destroys (such as window closing)
public dispose(): void {
if (this.protocolListener) {
this.protocolListener.dispose();
this.protocolListener = null;
}
this.activeRequests.forEach(d= > d.dispose());
this.activeRequests.clear();
}
Copy the code
In addition to activeRequests, a protocol release protocol is set up when a connection is released: protocolListener
Next, we implement the “register channel” method
registerChannel(
channelName: string.channel: IServerChannel<TContext>): void {
// Save the channel
this.channels.set(channelName, channel);
// If there are many requests before the channel is registered, the request is executed at this point.
// https://github.com/microsoft/vscode/issues/72531
setTimeout(() = > this.flushPendingRequests(channelName), 0);
}
private flushPendingRequests(channelName: string) :void {
const requests = this.pendingRequests.get(channelName);
if (requests) {
for (const request of requests) {
clearTimeout(request.timeoutTimer);
switch (request.request.type) {
case RequestType.Promise:
this.onPromise(request.request);
break;
default:
break; }}this.pendingRequests.delete(channelName); }}Copy the code
Next, we implement onRawMessage:
OnRawMessage Is used to process Buffer messages.
[core/common/ipc.ts]
private onRawMessage(message: VSBuffer): void {
// Read the Buffer message
const reader = new BufferReader(message);
// Unscramble headers:
/ / /
// type indicates the message type
// message id
// channelName, the channelName
// name Specifies the name of the service method
// ]
// deserialize is a tool method, read Buffer
const header = deserialize(reader);
// Unscramble the body of the message, which is the argument to execute the service method
const body = deserialize(reader);
const type = header[0] as RequestType;
// Returns the execution result
switch (type) {
case RequestType.Promise:
//
return this.onPromise({
type.id: header[1].channelName: header[2].name: header[3].arg: body,
});
case RequestType.PromiseCancel:
return this.disposeActiveRequest({ type.id: header[1]});default:
break; }}Copy the code
Where onPromise executes the service method and returns the result to the “client” to start accessing the specific service:
[core/common/ipc.ts]
private onPromise(request: IRawPromiseRequest): void {
const channel = this.channels.get(request.channelName);
// If the channel does not exist, put it into PendingRequest and wait for the channel to be executed after registration or cleared after expiration.
if(! channel) {this.collectPendingRequest(request);
return;
}
// Cancel request token -> Mechanism see cancelable Promise section
const cancellationTokenSource = new CancellationTokenSource();
let promise: Promise<any>;
try {
// Call the channel call to perform the specific service method
promise = channel.call(
this.ctx,
request.name,
request.arg,
cancellationTokenSource.token,
);
} catch (err) {
promise = Promise.reject(err);
}
const { id } = request;
promise.then(
data= > {
// The execution result is displayed
this.sendResponse(<IRawResponse>{
id,
data,
type: ResponseType.PromiseSuccess,
});
// Clear the request from the active request
this.activeRequests.delete(request.id);
},
err= > {
// If there is an exception, handle the exception of the message and return the response result.
if (err instanceof Error) {
this.sendResponse(<IRawResponse>{
id,
data: {
message: err.message,
name: err.name,
stack: err.stack
? err.stack.split
? err.stack.split('\n')
: err.stack
: undefined,},type: ResponseType.PromiseError,
});
} else {
this.sendResponse(<IRawResponse>{
id,
data: err,
type: ResponseType.PromiseErrorObj,
});
}
this.activeRequests.delete(request.id); });// Stores requests to active requests and provides tokens that can be released.
const disposable = toDisposable(() = > cancellationTokenSource.cancel());
this.activeRequests.set(request.id, disposable);
}
Copy the code
SendResponse returns a specific type of message based on the execution result; Send serializes the message to buffer. SendBuffer sends the message to the client.
[core/common/ipc.ts]
export enum ResponseType {
Initialize = 200.// Initialize the message return
PromiseSuccess = 201./ / promise success
PromiseError = 202./ / promise to fail
PromiseErrorObj = 203,
EventFire = 204,}type IRawInitializeResponse = { type: ResponseType.Initialize };
type IRawPromiseSuccessResponse = {
type: ResponseType.PromiseSuccess; / / type
id: number; / / request id
data: any; / / data
};
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 IRawResponse =
| IRawInitializeResponse
| IRawPromiseSuccessResponse
| IRawPromiseErrorResponse
| IRawPromiseErrorObjResponse;
private sendResponse(response: IRawResponse): void {
switch (response.type) {
case ResponseType.Initialize:
return this.send([response.type]);
case ResponseType.PromiseSuccess:
case ResponseType.PromiseError:
case ResponseType.EventFire:
case ResponseType.PromiseErrorObj:
return this.send([response.type, response.id], response.data);
default:
break; }}private send(header: any.body: any = undefined) :void {
const writer = new BufferWriter();
serialize(writer, header);
serialize(writer, body);
this.sendBuffer(writer.buffer);
}
private sendBuffer(message: VSBuffer): void {
try {
this.protocol.send(message);
} catch (err) {
// noop}}Copy the code
If the request is cancelled, we do the following:
[core/common/ipc.ts]
private disposeActiveRequest(request: IRawRequest): void {
const disposable = this.activeRequests.get(request.id);
if (disposable) {
disposable.dispose();
this.activeRequests.delete(request.id); }}Copy the code
The client that defines the channel
The channel client is used to send a request to the service and receive the result of the request:
First, we can define an interface to handle the returned result:
type IHandler = (response: IRawResponse) = > void;
Copy the code
export interface IChannelClient {
getChannel<T extends IChannel>(channelName: string): T;
}
Copy the code
export class ChannelClient implements IChannelClient.IDisposable {
private protocolListener: IDisposable | null;
private state: State = State.Uninitialized; // Channel status
private lastRequestId = 0; // Communication request unique ID management
// Active request, which is used to close when canceling; If the channel is closed (Dispose), Unified sends a cancel message to all channels to ensure the reliability of communication.
private readonly activeRequests = new Set<IDisposable>();
private readonly handlers = new Map<number, IHandler>(); // Process the result after communication
private readonly _onDidInitialize = new Emitter<void> ();// An event is raised when a channel is initialized
readonly onDidInitialize = this._onDidInitialize.event;
constructor(private readonly protocol: IMessagePassingProtocol) {
this.protocolListener =
this.protocol.onMessage(msg= > this.onBuffer(msg)); }}Copy the code
enum State {
Uninitialized, // Not initialized
Idle, / / ready
}
private state: State = State.Uninitialized;
Copy the code
Channel client has two states, one is “uninitialized”, or “ready”. Uninitialized means the channel server is not ready. When it is ready, the _onDidInitialize event is triggered to update the channel state.
constructor(
private readonly protocol: IMessagePassingProtocol) {
this.protocolListener =
this.protocol.onMessage(msg= > this.onBuffer(msg));
}
Copy the code
When the Channel client is initialized, it listens for messages from the Channel server. When the Channel server is ready, it listens for ready messages sent by the channel server.
OnBuffer is read Buffer message; OnResponse performs message processing based on the interpreted message and returns to the place where it was called.
private onBuffer(message: VSBuffer): void {
const reader = new BufferReader(message);
const header = deserialize(reader);
const body = deserialize(reader);
const type: ResponseType = header[0];
switch (type) {
case ResponseType.Initialize:
return this.onResponse({ type: header[0]});case ResponseType.PromiseSuccess:
case ResponseType.PromiseError:
case ResponseType.EventFire:
case ResponseType.PromiseErrorObj:
return this.onResponse({ type: header[0].id: header[1].data: body }); }}private onResponse(response: IRawResponse): void {
// Channel server ready message processing
if (response.type === ResponseType.Initialize) {
this.state = State.Idle;
this._onDidInitialize.fire();
return;
}
// Channel server for message processing and return
const handler = this.handlers.get(response.id);
if(handler) { handler(response); }}Copy the code
Before making a request, the Channel server constructs a channel structure to send a message.
Why construct a channel to send messages instead of sending them directly? Leave a doubt.
getChannel<T extends IChannel>(channelName: string): T {
const that = this;
return {
call(
command: string.// Service method namearg? :any./ / parameterscancellationToken? : CancellationToken) { / / cancel
return that.requestPromise(
channelName,
command,
arg,
cancellationToken,
);
},
listen(event: string, arg: any) {
// TODO
// return that.requestEvent(channelName, event, arg);}},as T;
}
Copy the code
RequestPromise is to initiate a service invocation request:
private requestPromise(
channelName: string.name: string, arg? :any,
cancellationToken = CancellationToken.None,
): Promise<any> {
const id = this.lastRequestId++;
const type = RequestType.Promise;
const request: IRawRequest = { id, type, channelName, name, arg };
// If the request is cancelled, it will not be executed.
if (cancellationToken.isCancellationRequested) {
return Promise.reject(canceled());
}
let disposable: IDisposable;
const result = new Promise((c, e) = > {
// If the request is cancelled, it will not be executed.
if (cancellationToken.isCancellationRequested) {
return e(canceled());
}
// The channel will be queued until the channel is registered
// When the Channel server is ready, a ready message is sent back, which triggers the state to change to Idle
/ / which will trigger uninitializedPromise. Then
// So that messages can be sent
let uninitializedPromise: CancelablePromise<
void
> | null = createCancelablePromise(_= > this.whenInitialized());
uninitializedPromise.then(() = > {
uninitializedPromise = null;
const handler: IHandler = response= > {
console.log(
'main process response:'.JSON.stringify(response, null.2));// Depending on the type of result returned, Initialize is not handled here, which is handled higher
switch (response.type) {
case ResponseType.PromiseSuccess:
this.handlers.delete(id);
c(response.data);
break;
case ResponseType.PromiseError:
this.handlers.delete(id);
const error = new Error(response.data.message);
(<any>error).stack = response.data.stack;
error.name = response.data.name;
e(error);
break;
case ResponseType.PromiseErrorObj:
this.handlers.delete(id);
e(response.data);
break;
default:
break; }};// Save the processing of this request
this.handlers.set(id, handler);
// Start sending requests
this.sendRequest(request);
});
const cancel = () = > {
// If not initialized, cancel
if (uninitializedPromise) {
uninitializedPromise.cancel();
uninitializedPromise = null;
} else {
// If already initialized and on request, an interrupt message is sent
this.sendRequest({ id, type: RequestType.PromiseCancel });
}
e(canceled());
};
const cancellationTokenListener = cancellationToken.onCancellationRequested(
cancel,
);
disposable = combinedDisposable(
toDisposable(cancel),
cancellationTokenListener,
);
// Save the request to the active request
this.activeRequests.add(disposable);
});
// Remove the request from the active request after execution
return result.finally(() = > this.activeRequests.delete(disposable));
}
Copy the code
The method of sending a message is the same as the method of receiving a message, and is not redundant:
private sendRequest(request: IRawRequest): void {
switch (request.type) {
case RequestType.Promise:
return this.send(
[request.type, request.id, request.channelName, request.name],
request.arg,
);
case RequestType.PromiseCancel:
return this.send([request.type, request.id]);
default:
break; }}private send(header: any.body: any = undefined) :void {
const writer = new BufferWriter();
serialize(writer, header);
serialize(writer, body);
this.sendBuffer(writer.buffer);
}
private sendBuffer(message: VSBuffer): void {
try {
this.protocol.send(message);
} catch (err) {
// noop}}Copy the code
Define a Connection: Connection
According to the above design drawing, as follows:
export interface Client<TContext> {
readonly ctx: TContext;
}
export interface Connection<TContext> extends Client<TContext> {
readonly channelServer: ChannelServer<TContext>; // Channel server
readonly channelClient: ChannelClient; // Channel client
}
Copy the code
Define the server: IPCServer
class IPCServer<TContext = string>
implements
IChannelServer<TContext>,
IDisposable {
// Channel accessible to the server side
private readonly channels = new Map<string, IServerChannel<TContext>>();
// Connection between client and server
private readonly _connections = new Set<Connection<TContext>>();
private readonly _onDidChangeConnections = new Emitter<
Connection<TContext>
>();
// An event listener is triggered when a connection changes
readonly onDidChangeConnections: Event<Connection<TContext>> = this
._onDidChangeConnections.event;
// All connections
get connections() :Array<Connection<TContext> > {const result: Array<Connection<TContext>> = [];
this._connections.forEach(ctx= > result.push(ctx));
return result;
}
// Release all listeners
dispose(): void {
this.channels.clear();
this._connections.clear();
this._onDidChangeConnections.dispose(); }}Copy the code
Earlier we mentioned “message communication protocol”, namely:
- Send and receive messages on the ‘IPC: Message’ channel
- Send the ‘IPC: Hello’ channel message to start the link
- When disconnecting, send a message to the ‘IPC: Disconnect’ channel
You may have a question, why our communication message also need to establish a connection, this means a long connection?
In fact, it is not long connection, or one-time communication, in fact, the process is like this:
- At first the renderer sends the ‘IPC :hello’ message, with the intention that subsequent communication may occur at’ IPC :message’. Please prepare the main process.
- The main process receives the ‘IPC: Hello’ message, discovers that it is the renderer that needs the ‘IPC :message’ channel for the first time, and starts listening for it.
- When the render process unloads, an ‘IPC :disconnect’ message is emitted.
- The main process received the ‘IPC: Disconnect’ message from the renderer process. Disable listening on ‘IPC :message’.
One thing to note here is that ‘IPC :hello’ is actually a listener set up when the main process IPCServer is instantiated to see what communication each renderer needs.
Thus, the complete flow of this communication mechanism actually looks like this:
TODO
The renderer communicates with the main process via ‘IPC :message’, so the message sent to window A will also be received by window B. Of course not! , please listen to the following explanation. The above figure is a simple demonstration, but of course there is a retry mechanism for connections.
Next, let’s start implementing the above process.
First, define a client connection event interface:
export interface ClientConnectionEvent {
protocol: IMessagePassingProtocol; // Message communication protocol
onDidClientDisconnect: Event<void>; // Disconnect event
}
Copy the code
Next, we implement a method that listens for client connections, getOnDidClientConnect
export class IPCServer<TContext = string>
implements
IChannelServer<TContext>,
IDisposable {
private static getOnDidClientConnect(): Event<ClientConnectionEvent> {
const onHello = Event.fromNodeEventEmitter<Electron.WebContents>(
ipcMain,
'ipc:hello'.({ sender }) = >sender, ); . }constructor(onDidClientConnect: Event<ClientConnectionEvent>) {
onDidClientConnect(({ protocol, onDidClientDisconnect }) = >{... }}}Copy the code
From the flowchart, we know that when IPCServer is instantiated, we register the ‘IPC: Hello’ message listener, which we can receive as a signal to establish a connection.
If I send multiple onHello messages, I’m connected multiple times, right? Of course not. Listen to the following analysis.
First, a brief explanation of this sentence, which will be explained in more detail in the upcoming vscode event mechanism:
const onHello = Event.fromNodeEventEmitter<Electron.WebContents>(
ipcMain,
'ipc:hello'.({ sender }) = > sender,
);
Copy the code
Ipc :hello = ipc:hello = ipc:hello = ipc:hello
const handler = (e) = > {
console.log('sender:', e.sender.id);
};
ipcMain.on('ipc:hello', handler)
// Remove the listener
ipcMain.removeListener('ipc:hello', handler);
Copy the code
And here it is:
const onHello = Event.fromNodeEventEmitter<Electron.WebContents>(
ipcMain,
'ipc:hello'.({ sender }) = > sender,
);
const listener = onHello((sender) = > {
console.log('sender');
});
// Remove the listener
listener.dispose();
Copy the code
Written as has many benefits, and said the Event. How fromNodeEventEmitter implementation, will continue to update later on.
So let’s go ahead and implement getOnDidClientConnect()
export class IPCServer<TContext = string>
implements
IChannelServer<TContext>,
IDisposable {
private static getOnDidClientConnect(): Event<ClientConnectionEvent> {
const onHello = Event.fromNodeEventEmitter<Electron.WebContents>(
ipcMain,
'ipc:hello'.({ sender }) = > sender,
);
return Event.map(onHello, webContents= > {
const{ id } = webContents; .const onMessage = createScopedOnMessageEvent(id, 'ipc:message') as Event<
VSBuffer
>;
const onDidClientDisconnect = Event.any(
Event.signal(createScopedOnMessageEvent(id, 'ipc:disconnect')),
onDidClientReconnect.event,
);
const protocol = new Protocol(webContents, onMessage);
return{ protocol, onDidClientDisconnect }; }); }... }Copy the code
As we explained earlier, after we define onHello, we can listen for events like this:
const listener = onHello((sender) = > {
console.log('sender');
});
Copy the code
As you can see, the message argument for all ipc: Hello messages is filtered to sender instead of e. And:
getOnDidClientConnect(): Event<ClientConnectionEvent> {
return Event.map(onHello, webContents= >{...return{ protocol, onDidClientDisconnect }; })}Copy the code
Filter onHello to {protocol, onDidClientDisconnect}. This is equivalent to using decorator mode to decorate the parameters again on the onHello event
Maybe it’s a little confusing, but let me compare it horizontally, after two decorations
// For the first time, the event argument e becomes the sender argument
const onHello = Event.fromNodeEventEmitter<Electron.WebContents>(
ipcMain,
'ipc:hello'.({ sender }) = > sender,
);
// The second decorator event parameter sender(webContents) becomes {protocol, onDidClientDisconnect}
getOnDidClientConnect(): Event<ClientConnectionEvent> {
return Event.map(onHello, webContents= >{...return{ protocol, onDidClientDisconnect }; })}Copy the code
From the original:
const handler = (e) = > {
console.log('sender:', e.sender.id);
};
ipcMain.on('ipc:hello', handler)
// Remove the listener
ipcMain.removeListener('ipc:hello', handler);
Copy the code
It becomes:
// Still ipc: Hello event, only {protocol, onDidClientDisconnect} instead of the message parameter e
const onDidClientConnnect = getOnDidClientConnect();
const listener = onDidClientConnnect(({protocol, onDidClientDisconnect}) = >{... });// Remove the listener
listener.dispose();
Copy the code
Ok, so let’s look at {protocol, onDidClientDisconnect}.
Protocol, the communication protocol we defined above, contains:
- Sender is the interface that sends the object, as long as there is a method: send.
- The agreement states:
- Send the ‘IPC: Hello’ channel message to start the link
- Send and receive messages on the ‘IPC: Message’ channel
- When disconnecting, send a message to the ‘IPC: Disconnect’ channel
Therefore:
const onMessage = createScopedOnMessageEvent(id, 'ipc:message') as Event<
VSBuffer
>;
const protocol = new Protocol(webContents, onMessage);
Copy the code
- WebContents is sender: the object that sends the message.
- OnMessage is the method that listens for messages on the IPC: Message channel
The onMessage here can simply be understood as listening via encapsulated messages, from the original:
ipcMain.on('ipc:message'.(e, message) = > {
console.log('message:', message);
})
Copy the code
Becomes:
onMessage((message) = > {
console.log('message:', message);
})
Copy the code
- One notable feature is that the event parameter is changed from (e, message) to (message)
- Messages are compressed using buffer, which is part of the communication optimization.
- In addition to this, of course, createScopedOnMessageEvent another ability, is the filter, the front have mentioned, all the rendering process, all through
Ipc: Message channel communication, how to avoid the message sent to render process A, render process B also received, is filtered here, after filtering, render process A will only receive messages to A.
OnDidClientDisconnect again, ‘IPC: Disconnect’ message listener, I won’t explain.
How does IPCServer register ipc: Hello listener
class IPCServer<TContext = string>
implements
IChannelServer<TContext>,
IDisposable {
constructor(onDidClientConnect: Event<ClientConnectionEvent>) {
onDidClientConnect(({ protocol, onDidClientDisconnect }) = > {
const onFirstMessage = Event.once(protocol.onMessage);
// Receive the message for the first time
onFirstMessage(msg= > {
const reader = new BufferReader(msg);
const ctx = deserialize(reader) as TContext; // Further explanation
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._onDidChangeConnections.fire(connection);
onDidClientDisconnect(() = > {
channelServer.dispose();
channelClient.dispose();
this._connections.delete(connection); }); })}}}Copy the code
When we first receive ipc:message, we create a new connection: Connection; When receiving: IPC :disconnect, delete the connection and remove the listener.
The key method here is to register channels, as shown in the design above. For each new channel, change channels are added to the existing connection:
registerChannel(
channelName: string.channel: IServerChannel<TContext>,
): void {
this.channels.set(channelName, channel);
// At the same time in all connections, you need to register channels
this._connections.forEach(connection= > {
connection.channelServer.registerChannel(channelName, channel);
});
}
Copy the code
With the server ready, we need to implement the client details:
export class IPCClient<TContext = string>
implements IChannelClient, IChannelServer<TContext>, IDisposable {
private readonly channelClient: ChannelClient;
private readonly channelServer: ChannelServer<TContext>;
constructor(protocol: IMessagePassingProtocol, ctx: TContext) {
const writer = new BufferWriter();
serialize(writer, ctx);
// Send the service registration message CTX, the service name.
protocol.send(writer.buffer);
this.channelClient = new ChannelClient(protocol);
this.channelServer = new ChannelServer(protocol, ctx);
}
getChannel<T extends IChannel>(channelName: string): T {
return this.channelClient.getChannel(channelName);
}
registerChannel(
channelName: string.channel: IServerChannel<TContext>,
): void {
// Register channels
this.channelServer.registerChannel(channelName, channel);
}
dispose(): void {
this.channelClient.dispose();
this.channelServer.dispose(); }}Copy the code
export class Client extends IPCClient implements IDisposable {
private readonly protocol: Protocol;
private static createProtocol(): Protocol {
const onMessage = Event.fromNodeEventEmitter<VSBuffer>(
ipcRenderer,
'ipc:message'.(_, message: Buffer) = > VSBuffer.wrap(message),
);
ipcRenderer.send('ipc:hello');
return new Protocol(ipcRenderer, onMessage);
}
constructor(id: string) {
const protocol = Client.createProtocol();
super(protocol, id);
this.protocol = protocol;
}
dispose(): void {
this.protocol.dispose(); }}Copy the code