Electron: IPC vs. WebSocket

Pour les applications Node.js locales, lequel des ces deux modes d'échange de données choisir?

Nous allons comparer leurs fonctionnalités et leurs performances respectives...

Précisons que si WebSocket est disponible pour tous les systèmes basés sur Node, même un simple script de serveur tel que celui qui est décrit dans la section JavaScript de ce site, IPC quand à lui ne fonctionne qu'avec Electron.

Coté serveur, les commandes WebSocket sont basées sur le module ws. D'autres choix seraient possibles, avec des syntaxes différentes. Coté interface on utilise l'objet WebSocket standard.

  WebSocket IPC
 
Coté SERVEUR
Importation const WebSocketServer = require("ws").Server; const {ipcMain}=require('electron').ipcMain
Création d'un objet de communication w = new WebSocketServer(port) -
Attente d'ouverture de communication w.on('connection', (w) =>{}) -
Envoi de données à l'interface w.send(data) event.sender.send('canal', data)
Envoi synchrone de données - event.returnValue = data
Réception de données venant de l'interface w.on('message', (m) =>{}) ipcMain.on('canal', (e, o) => {})
Clôture du canal de communication w.on('close', () =>{}) -
     
 
Coté INTERFACE
Importation - const ipcRenderer = require('electron').ipcRenderer
Création de l'objet const w = new WebSocket(port); -
Envoi de données au serveur w.send(data) ipcRenderer.send('canal', data)
Envoi synchrone de données - ipcRenderer.sendSync('canal', data)
Réception de données du serveur w.onmessage = function(event){}) ipcRenderer.on('canal'', (event)=>{})
Clôture du canal de communication w.close() -

On peut voir les différences entre les deux protocoles:

Sur le plan des capacités offertes, IPC l'emporte avec la possibilité d'échanger des données de façon synchrone. N'utilisant pas les ports du système il évite aussi tout risque de collision quand le port est déjà utilisé par une autre application.

Démonstration IPC

Nous allons construire une application basique avec une fenêtre et un backend qui communique avec l'interface en mode synchrone et asynchrone.

L'interface envoie le message "hello server" au backend qui répond par "hello interface".
Dans le mode asynchrone, un listener dans l'interface attend la réception du message, sur le canal "message".
Dans le mode synchrone, la réponse du serveur est la valeur de retour de la fonction qui envoie le message au serveur.

Le code coté serveur

const path = require("path")
const { app, BrowserWindow, ipcMain } = require('electron')
const print = console.log

let win
function createWindow() {
    win = new BrowserWindow({
        width: 960, height: 600, 
        title:"IPC vs. WebSocket",
        webPreferences : { nodeIntegration:true }
    })
 
    win.setMenu(null)
    const fpath = path.join(__dirname, 'ipc.html')
    win.loadURL(fpath)
    win.on('closed', () => { win = null })
}

// IPC

ipcMain.on('message', (event, data) => {
  print("Received: " + data) 
  event.sender.send('message', 'Hello interface!')
})

ipcMain.on('message-sync', (event, data) => {
  print("Received: " + data) 
  event.returnValue = 'Hello interface (synchronous)!'
})

// App

app.on('ready', createWindow)
app.on('window-all-closed', () => { 
    app.quit()
    process.exit(1)
})
app.on('quit', function () { 
    print("Done.")
})

Dans la démo, la commande event.sender.send répond sur le même canal "message" qui est utilisé en réception, mais il est aussi possible d'envoyer des données à des canaux multiples différents (contrairement au mode synchrone).

Le code coté navigateur

<!DOCTYPE html>
<html>
<body>
<h1>IPC Demo</h1>
<fieldset id="storage"></fieldset>
<fieldset id="storageSync"></fieldset>
<script>
const {ipcRenderer} = require('electron')
ipcRenderer.on('message', (event, data) => {
document.getElementById("storage").innerHTML = data
})
ipcRenderer.send('message', 'Hello server!')
var answer = ipcRenderer.sendSync('message-sync', 'Hello server sync!')
document.getElementById("storageSync").innerHTML = answer
</script>
</body>
</html>

Pour exécuter le programme, tapez "electron ipc.js " dans le répertoire des scripts.

Démonstration WebSocket

Comme précédemment, l'interface envoie le message "Hello server!" au backend qui en retour envoie "Hello interface!" au navigateur.

Le code coté serveur

Le backend importe le module ws, qui est inclut dans l'archive.

const path = require("path")
const { app, BrowserWindow  } = require('electron')
const WebSocket = require("ws")

const wss = new WebSocket.Server( { port: 1040 } )

let win
function main() {
    win = new BrowserWindow({
        width: 960, height: 600, 
        title:"WebSocket Demo"
    })
    win.setMenu(null)

    const fpath = path.join(__dirname, 'websocket.html')
    win.loadURL(fpath)
    win.on('closed', () => { win = null })
    
    wss.on('connection', function (w) {  
        w.on( 'message' , function (data)  {
             console.log(data)
        })  
        w.on('close', function() { 
             console.log("Closed") 
        })    
        w.send("Hello interface!")
    })    
}

app.on('ready', main)
app.on('window-all-closed', () => { 
    app.quit()
    process.exit(1)
})
app.on('quit', function () { 
    console.log("Done.")
})

Le code coté navigateur

L'interface utilise l'objet standard WebSocket du navigateur.

<!DOCTYPE html>
<html>
<body>
<h1>WebSocket Demo</h1>
<fieldset id="storage"></fieldset>
<script>
const socket = new WebSocket("ws://localhost:1040");
socket.onmessage = function(event) {
var data = event.data
document.getElementById("storage").innerHTML = data
}
socket.onopen = function() {
socket.send('Hello server!')
}
</script>
</body>
</html>

Le code est un peu plus simple parce que l'on ne teste qu'un mode asynchrone et on n'a pas besoin cette fois d'inclure le module Electron.

Tapez "electron websocket.js" pour lancer le script.

Vitesses comparées

Mon intention initiale était de continuer la comparaison avec de nouveaux scripts échangeant une série de données pour comparer la vitesse des deux protocoles. Mais quand on a exécuté les deux scripts précédents, on voit que c'est inutile. Alors que les données s'affichent instantanément avec IPC, il y a un délai notable avec WebSocket.
Et c'est normal, IPC est interne à Electron tandis que WebSocket passe par le système réseau de l'ordinateur, avec toutes ses contraintes et contrôles nécessaires.

Par conséquent, dès lors que l'on a choisi d'utiliser Electron, IPC devrait aussi devenir le mode de communication à préférer à moins que l'on ait besoin d'un système bidirectionnel, avec des notifications du serveur par exemple.

Télécharger les scripts