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.

Node Underground
Node Underground
Node Underground
Flexible Socket-Based IPC in Node.js: Build Cross-Process Communication with Midway 5.0

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 = 120

Conclusion

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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Node.jsIPCMidwaySocketinter-process communication
Node Underground
Written by

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.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.