michaelfranzl / socket.io-bridge

Isomorphic library for real-time, bidirectional, event-based communication between two Socket.IO clients

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

socket.io-bridge

Real-time, bidirectional, event-based communication between two Socket.IO clients.

There are no dependencies on message queue or pub/sub servers. The Node.js server creates client-client 'bridges' (private namespaces used by two clients) using just Socket.IO.

Test

Features

  • Client library works with Node.js and browsers
  • Works through firewalls and with NAT (because clients connect to the server)
  • Addressing using self-assigned string IDs (locally unique, communicated via some other channel)
  • Real-time peer connection and disconnection notification
  • Private namespaces ('bridges') have random names
  • The server is transparent to event names; clients can emit any signal (using .emit and .on) without the need to change the server
  • Promise and/or callback-based peer connection notification
  • Connection multiplexing over just one TCP connection (this is a Socket.IO feature)

Example

See a working demo at packages/client/demo.

Without socket.io-bridge, with only plain Socket.IO, to route an event from one client to another client, one has to do:

Client 1:

socket.emit('event');

Server:

client1.on('event', () => client2.emit('event'));

Client 2:

socket.on('event', () => console.log('event'));

Disadvantages in this approach:

  • Three separate implementations are tighly coupled (server, client1, client2)
  • client1 does not know if client2 is currently connected to the server
  • client1 does not know when client2 disconnects
  • client1 does not know who client2 is

These issues can be solved using the present library, and are illustrated below.

On the backend (assuming a server at http://localhost:3000), adding the client-client briging behavior to an already existing Socket.IO server just requires one additional line:

import http from 'http';
import SocketIoServer from 'socket.io';
import makeBridgeServer from 'socket.io-bridge-server';

const server = http.createServer().listen(3000);
const socketIoServer = SocketIoServer(server);
const namespace = socketIoServer.of('/bridge');

makeBridgeServer({ namespace }); // <-- that's it

On client "Alice" (running in a browser or in Node.js):

import SocketIoClient from 'socket.io-client';

// setup
const socket = SocketIoClient('http://localhost:3000/bridge');
const bridgeClient = new SocketIoBridgeClient({ socket });

// usage
bridgeClient
  .make({ id: 'Alice', peerId: 'Bob' }) // We are "Alice", and we want to talk to "Bob".
  .then((peer) => { // Bob is now connected. `peer` is a regular Socket.IO client socket.
    // Application-specific implementation.
    peer.emit('hello');
    peer.on('concatenate two words', (txt1, txt2) => callback(`${txt1} ${txt2}!`));
  });

On client "Bob" (running in a browser or in Node.js):

import SocketIoClient from 'socket.io-client';

// setup
const socket = SocketIoClient('http://localhost:3000/bridge');
const bridgeClient = new SocketIoBridgeClient({ socket });

// usage
bridgeClient.make({
  // We are "Bob". We don't want to talk to anybody specific, but others may want to talk to us.
  id: 'Bob',
  onconnection: (peer) => {
    console.log('someone connected to us');
    socket.on('hello', () => { console.log('someone said hello') });

    socket.emit('concatenate two words', 'Hello', 'world', (answer) => {
      console.log(answer); // This prints "Hello world!" if the peer implements this event.
    });
  },
});

Advantages of this approach:

  • Only the client implementations are coupled, the server is decoupled.
  • There are no event listeners on all the used sockets except those set by the clients themselves.
  • Socket.IO callbacks work (i.e. functions as payload).
  • If one client disconnects, the other socket will be disconnected too.
  • Only one client needs to provide the argument peerId, the other peer does not. The peer who is not providing peerId expects that other peers connect to it (it is loosely equivalent to a 'server').
  • The order in which the clients connect to the server does not matter.
  • Two mechanisms are provided for when a peer connects, which can be used interchangeably: a Promise, and a callback function onconnection. Note that a Promise can resolve only once. For multiple connections to one peer, you can only use the callback on this peer.
  • For one connection to the server, an arbitrary number of client-client connections can be established (by calling make repeatedly).
  • Because Socket.IO connections are multiplexed over just one TCP connection, even multiple connections between two endpoints established in such a way are multiplexed too.

Connection establishment protocol

This custom protocol is implemented by the packages socket.io-bridge-server and socket.io-bridge-client. The user does not need to know about it, it's working behind the scenes and ends when the onconnection callback is called, or when the connection promise resolves.

In the example below, client "c1" wants to connect to client "c2".

One namespace on the server (under the path /bridge) is used to negotiate the creation of new private sub-namespaces (in the example below called nname)

CLIENT c1                     SERVER                      CLIENT c2
                            nsp:/bridge
---------connection----------->  |
-----------login-------------->  |
 <--------logged_in------------- |
-----request_bridge(c2)------->  |
                                 |
                                 |  <--------connection-------------
                                 |  <----------login----------------
                                 | ----------logged_in------------>
<--- connect_to_bridge(nname)--- | -----connect_to_bridge(nname)-->

Both clients will receive the connect_to_bridge event at the same time (disregarding network delays) and will connect to the new namespace, the name of which is known only to the clients:


CLIENT c1                     SERVER                      CLIENT c2
                         nsp:/bridge/nname
---------connection----------->  |
-----------start-------------->  |
                                 |
                                 |  <--------connection------------
                                 |  <----------start---------------
 <--------peer_connected-------- | ---------peer_connected------->

At this point, the server removes all event listeners from the sockets, and the clients perform an echo test to test connectivity without the active participation of the server:

CLIENT c1                     SERVER                      CLIENT c2
                         nsp:/bridge/nname
                                 |
--------------echo------------>  | --------------echo------------>
 <--------------echo------------ |  <-------------echo------------
                                 |
 <-------------echo------------- |  <--------------echo-----------
--------------echo------------>  | --------------echo------------->

After this succeeds, the clients remove the echo event listener from the sockets, which now are a 'transparent pipe' to the other peer, and pass them to the user code using a resolving promise and the onconnection callback.

Development and testing

The test file is also a documentation on various use cases.

Run the tests:

npm test

To use the debugger during testing, run:

npm test -- --inspect

About

Isomorphic library for real-time, bidirectional, event-based communication between two Socket.IO clients

License:MIT License


Languages

Language:JavaScript 99.4%Language:Shell 0.6%