Skip to content

Commit

Permalink
feat: implement ping / pong connction check on client side, fix #86
Browse files Browse the repository at this point in the history
  • Loading branch information
b-ma committed Feb 19, 2024
1 parent 8ec847b commit bb29d24
Show file tree
Hide file tree
Showing 24 changed files with 1,104 additions and 78 deletions.
60 changes: 29 additions & 31 deletions src/client/Socket.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import { isBrowser } from '@ircam/sc-utils';
import WebSocket from 'isomorphic-ws';

import {
PING_INTERVAL,
PING_LATENCY_TOLERANCE,
PING_MESSAGE,
PONG_MESSAGE,
} from '../common/constants.js';
import logger from '../common/logger.js';
import {
packBinaryMessage,
unpackBinaryMessage,
packStringMessage,
unpackStringMessage,
} from '../common/sockets-utils.js';
import logger from '../common/logger.js';

// WebSocket events:
//
Expand Down Expand Up @@ -130,7 +136,28 @@ class Socket {
ws.addEventListener('open', connectEvent => {
// parse incoming messages for pubsub
this.ws = ws;

// ping/pong behaviour
let pingTimeout = null;

const heartbeat = () => {
clearTimeout(pingTimeout);

pingTimeout = setTimeout(() => {
this.terminate();
}, PING_INTERVAL + PING_LATENCY_TOLERANCE);
}

heartbeat();

this.ws.addEventListener('message', e => {
if (e.data === PING_MESSAGE) {
heartbeat();
this.ws.send(PONG_MESSAGE);
// do not propagate ping / pong messages
return;
}

const [channel, args] = unpackStringMessage(e.data);
this._emit(false, channel, ...args);
});
Expand All @@ -144,6 +171,7 @@ class Socket {

// forward open event
this._emit(false, 'open', connectEvent);

// continue with raw socket
resolve();
});
Expand Down Expand Up @@ -173,36 +201,6 @@ class Socket {
trySocket();
});

// @todo - review/fix
// - the `ws.on` method only exists on node implementation, and the 'ping'
// message is not received on addEventListener
// - there seems to be no way to access the ping event in browsers...
//
// let pingTimeoutId = null;
// const pingInterval = config.env.websockets.pingInterval;
// // detect broken connection
// // cf. https://github.com/websockets/ws#how-to-detect-and-close-broken-connections
// const heartbeat = () => {
// try {
// console.log('ping received');
// clearTimeout(pingTimeoutId);

// // pingTimeoutId = setTimeout(() => {
// // console.log('terminate');
// // this.terminate();
// // }, pingInterval + 2000);
// } catch (err) {
// console.error(err);
// }
// };
//
// this.ws.on('ping', heartbeat);
// this.ws.addEventListener('close', () => {
// clearTimeout(pingTimeoutId);
// });

// heartbeat();

// ----------------------------------------------------------
// init binary socket
// ----------------------------------------------------------
Expand Down
5 changes: 5 additions & 0 deletions src/common/constants.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
// id of the server when owner of a state
export const SERVER_ID = -1;

export const PING_INTERVAL = 10 * 1000;
export const PING_LATENCY_TOLERANCE = 1000;
export const PING_MESSAGE = 'h:ping';
export const PONG_MESSAGE = 'h:pong';

// batched transport channel
export const BATCHED_TRANSPORT_CHANNEL = 'b:t';

Expand Down
97 changes: 50 additions & 47 deletions src/server/Socket.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { getTime } from '@ircam/sc-utils';
import {
PING_INTERVAL,
PING_MESSAGE,
PONG_MESSAGE,
} from '../common/constants.js';
import {
packBinaryMessage,
unpackBinaryMessage,
Expand Down Expand Up @@ -32,17 +37,7 @@ import {
* @hideconstructor
*/
class Socket {
constructor(ws, binaryWs, rooms, sockets, options = {}) {
/**
* Configuration object
*
* @type {object}
*/
this.config = {
pingInterval: 5 * 1000,
...options,
};

constructor(ws, binaryWs, rooms, sockets) {
/**
* `ws` socket instance configured with `binaryType=blob` (string)
*
Expand Down Expand Up @@ -82,15 +77,54 @@ class Socket {
/** @private */
this._binaryListeners = new Map();

// heartbeat system (run only on string socket), adapted from:
// https://github.com/websockets/ws#how-to-detect-and-close-broken-connections
/** @private */
this._isAlive = true;
let msg = {
type: 'add-measurement',
value: {
ping: 0,
pong: 0,
},
};

// ----------------------------------------------------------
// init string socket
// String socket
// implements ping/pong behavior
// ----------------------------------------------------------
this.ws.addEventListener('message', e => {
if (e.data === PONG_MESSAGE) {
this._isAlive = true;

msg.value.pong = getTime();
this.sockets._latencyStatsWorker.postMessage(msg);
// do not propagate ping / pong messages
return;
}

const [channel, args] = unpackStringMessage(e.data);
this._emit(false, channel, ...args);
});

// broadcast all `ws` "native" events
const heartbeat = () => {
if (this._isAlive === false) {
// emit a 'close' event to go trough all the disconnection pipeline
this._emit(false, 'close');
return;
}

this._isAlive = false;
msg.value.ping = getTime();

this.ws.send(PING_MESSAGE);

setTimeout(heartbeat, PING_INTERVAL);
};

setTimeout(heartbeat, PING_INTERVAL);

// broadcast all "native" events
[
'close',
'error',
Expand All @@ -107,14 +141,14 @@ class Socket {
});

// ----------------------------------------------------------
// init binary socket
// Binary socket
// ----------------------------------------------------------
this.binaryWs.addEventListener('message', e => {
const [channel, data] = unpackBinaryMessage(e.data);
this._emit(true, channel, data);
});

// broadcast all `ws` "native" events
// broadcast all "native" events
[
'close',
'error',
Expand All @@ -129,38 +163,6 @@ class Socket {
this._emit(true, eventName, e.data);
});
});

// heartbeat system (run only on string socket), adapted from:
// https://github.com/websockets/ws#how-to-detect-and-close-broken-connections
this._isAlive = true;
let msg = {
type: 'add-measurement',
value: {
ping: 0,
pong: 0,
},
};

// heartbeat system, only on "regular" socket
this.ws.on('pong', () => {
this._isAlive = true;

msg.value.pong = getTime();
this.sockets._latencyStatsWorker.postMessage(msg);
});

this._intervalId = setInterval(() => {
if (this._isAlive === false) {
// emit a 'close' event to go trough all the disconnection pipeline
this._emit(false, 'close');
return;
}

this._isAlive = false;
msg.value.ping = getTime();

this.ws.ping();
}, this.config.pingInterval);
}

/**
Expand All @@ -172,6 +174,7 @@ class Socket {
terminate() {
// clear ping/pong check
clearInterval(this._intervalId);

// clean rooms
for (let [_key, room] of this.rooms) {
room.delete(this);
Expand Down
12 changes: 12 additions & 0 deletions tests/integration/ping-pong/.editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
root = true

[*]
indent_style = space
indent_size = 2
end_of_line = LF
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[*.md]
trim_trailing_whitespace = false
3 changes: 3 additions & 0 deletions tests/integration/ping-pong/.eslintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "@ircam",
}
16 changes: 16 additions & 0 deletions tests/integration/ping-pong/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# transpiled files and dependencies
/node_modules
# application build files
.build
.data

# ignore all environment config files
/config/env-*

# junk files
package-lock.json
.DS_Store
Thumbs.db

# TLS certificates
/**/*.pem
1 change: 1 addition & 0 deletions tests/integration/ping-pong/.npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package-lock=false
5 changes: 5 additions & 0 deletions tests/integration/ping-pong/.soundworks
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "ping-pong",
"eslint": true,
"language": "js"
}
28 changes: 28 additions & 0 deletions tests/integration/ping-pong/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
Copyright (c) 2014-present IRCAM – Centre Pompidou (France, Paris)

All rights reserved.

Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:

* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.

* Redistributions in binary form must reproduce the above copyright notice, this
list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.

* Neither the name of the IRCAM nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Loading

0 comments on commit bb29d24

Please sign in to comment.