Flexible Socket-Based IPC in Node.js: Build Cross-Process Communication with Midway 5.0
This article demonstrates how to replace Node.js's limited parent‑child IPC with a simple, newline‑delimited JSON protocol over TCP or file sockets using Midway 5.0, providing full‑duplex client‑server communication for arbitrary processes.
Background
Node.js built‑in IPC is simple but limited to parent‑child processes. The official example shows a parent process ( parent.js) and a child process ( sub.js) communicating via message events.
const cp = require('child_process');
const n = cp.fork(`${__dirname}/sub.js`);
n.on('message', (m) => {
console.log('PARENT got message:', m);
});
n.send({ hello: 'world' });The child process implementation:
process.on('message', (m) => {
console.log('CHILD got message:', m);
});
process.send({ foo: 'bar' });If the two processes are not in a parent‑child relationship, how can they communicate? This article explains how to use the more flexible socket implementation in Midway 5.0 for arbitrary process communication.
Protocol Design
Because communication needs a protocol, the article adopts a simple “over” style: each message ends with a newline character \n to indicate the end of a packet. Messages are encoded/decoded with JSON.stringify / JSON.parse, similar to the parent‑child IPC.
The same simple protocol can be used for TCP or file sockets, avoiding the complexity of RPC protocols such as HSF.
Implementation
The communication channel is a full‑duplex socket. One side is called server, the other client. The following sections describe the parser, client, and server implementations.
Protocol Parser (parse.js)
'use strict';
const StringDecoder = require('string_decoder').StringDecoder;
const EventEmitter = require('events');
class Parser extends EventEmitter {
constructor() {
super();
this.decoder = new StringDecoder('utf8');
this.jsonBuffer = '';
}
encode(message) {
return JSON.stringify(message) + '
';
}
feed(buf) {
let jsonBuffer = this.jsonBuffer;
jsonBuffer += this.decoder.write(buf);
let i, start = 0;
while ((i = jsonBuffer.indexOf('
', start)) >= 0) {
const json = jsonBuffer.slice(start, i);
const message = JSON.parse(json);
this.emit('message', message);
start = i + 1;
}
this.jsonBuffer = jsonBuffer.slice(start);
}
}
module.exports = Parser;Socket Communication
TCP sockets are typical for remote processes; for local IPC a file socket (Unix domain socket) is more efficient and does not consume a network port.
Client
The client must be able to connect to the server, listen for data and decode messages, and provide a send interface.
On Windows, the local domain is implemented using a named pipe. The path must refer to an entry in \\?\pipe\ or \\.\pipe.
'use strict';
const path = require('path');
const net = require('net');
const Parser = require('./parser');
const EventEmitter = require('events');
const os = require('os');
const tmpDir = os.tmpDir();
let sockPath = path.join(tmpDir, 'midway.sock');
if (process.platform === 'win32') {
sockPath = sockPath.replace(/^\//, '');
sockPath = sockPath.replace(/\//g, '-');
sockPath = '\\.\pipe\' + sockPath;
}
class Client extends EventEmitter {
constructor(options) {
super();
options = options || {};
if (options.socket) {
this.socket = options.socket;
} else {
this.socket = net.connect(sockPath);
}
this.bind();
}
bind() {
const parser = new Parser();
const socket = this.socket;
socket.on('data', (buf) => {
parser.feed(buf);
});
parser.on('message', (message) => {
this.emit('message', message);
});
this.parser = parser;
}
send(message) {
this.socket.write(this.parser.encode(message));
}
}
module.exports = Client;Server
The server must create a net server, listen on a file, accept client connections, and decode incoming data according to the protocol.
'use strict';
const path = require('path');
const fs = require('fs');
const net = require('net');
const Client = require('./client');
const EventEmitter = require('events');
const os = require('os');
const tmpDir = os.tmpDir();
let sockPath = path.join(tmpDir, 'midway.sock');
if (process.platform === 'win32') {
sockPath = sockPath.replace(/^\//, '');
sockPath = sockPath.replace(/\//g, '-');
sockPath = '\\.\pipe\' + sockPath;
}
class Server extends EventEmitter {
constructor() {
super();
this.server = net.createServer((socket) => this.handleConnection(socket));
}
listen(callback) {
if (fs.existsSync(sockPath)) {
fs.unlinkSync(sockPath);
}
this.server.listen(sockPath, callback);
}
handleConnection(socket) {
const client = new Client({ socket });
client.on('message', (message) => {
this.handleRequest(message, client);
});
this.emit('connect', client);
}
handleRequest(message, client) {
this.emit('message', message, client);
}
}
module.exports = Server;Demo
A simple RPC‑style demo is built on top of the socket communication. The client registers available methods, sends requests, and handles responses.
'use strict';
const Client = require('../lib/Client');
let rid = 0;
const service = {};
const requestQueue = new Map();
function start(ready) {
const client = new Client();
function send() {
rid++;
let args = [].slice.call(arguments);
const method = args.slice(0, 1)[0];
const callback = args.slice(-1)[0];
const req = { rid, method, args: args.slice(1, -1) };
requestQueue.set(rid, Object.assign({ callback }, req));
client.send(req);
}
client.on('message', function (message) {
if (message.action === 'register') {
message.methods.forEach((method) => {
service[method] = send.bind(null, method);
});
ready(service);
} else {
const req = requestQueue.get(message.rid);
const callback = req.callback;
if (message.success) {
callback(null, message.data);
} else {
callback(new Error(message.error));
}
requestQueue.delete(message.rid);
}
});
}
start((service) => {
service.add(1,2,3,4,5, function (err, result) {
console.log(`1+2+3+4+5 = ${result}`);
});
service.time(1,2,3,4,5, function (err, result) {
console.log(`1*2*3*4*5 = ${result}`);
});
}); 'use strict';
const Server = require('../lib/server');
const server = new Server();
server.listen();
const service = {
add() {
const args = [].slice.call(arguments);
return args.reduce((a, b) => a + b);
},
time() {
const args = [].slice.call(arguments);
return new Promise((resolve) => {
setTimeout(() => {
const ret = args.reduce((a, b) => a * b);
resolve(ret);
}, 1000);
});
}
};
server.on('connect', (client) => {
client.send({ action: 'register', methods: Object.keys(service) });
});
server.on('message', function (message, client) {
let ret = { success: false, rid: message.rid };
const method = message.method;
if (service[method]) {
try {
const result = service[method].apply(service, message.args);
ret.success = true;
if (result.then) {
return result.then((data) => {
ret.data = data;
client.send(ret);
}).catch((err) => {
ret.success = false;
ret.error = err.message;
client.send(err);
});
}
ret.data = result;
} catch (err) {
ret.error = err.message;
}
}
client.send(ret);
});Running the server then the client prints:
1+2+3+4+5 = 15
1*2*3*4*5 = 120Conclusion
The presented socket‑based IPC is a lightweight alternative to Node.js parent‑child messaging and can be extended to more complex RPC scenarios, handling errors, network issues, and protocol parsing as needed.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Node Underground
No language is immortal—Node.js isn’t either—but thoughtful reflection is priceless. This underground community for Node.js enthusiasts was started by Taobao’s Front‑End Team (FED) to share our original insights and viewpoints from working with Node.js. Follow us. BTW, we’re hiring.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
