moved latency calculation server-side

made host per channel exclusive
added auto-reconnection
This commit is contained in:
Steffen 2020-09-21 11:19:46 +02:00
parent 9e03d16160
commit 0b5d9c5865
4 changed files with 74 additions and 14 deletions

View File

@ -17,6 +17,7 @@ const wss = new WebSocket.Server({
function noop() {} function noop() {}
function heartbeat() { function heartbeat() {
this.latency = Math.round((new Date().getTime() - this.pingStart) / 2);
this.isAlive = true; this.isAlive = true;
} }
@ -25,11 +26,32 @@ wss.on("connection", function connection(ws, req) {
.split("/") .split("/")
.pop() .pop()
.toLocaleLowerCase(); .toLocaleLowerCase();
if (ws.channel.match(/-host$/i)) {
ws.isHost = true;
ws.channel = ws.channel.substr(0, ws.channel.length - 5);
// check for another host on this channel
if (
Array.from(wss.clients).some(
client =>
client !== ws &&
client.readyState === WebSocket.OPEN &&
client.channel === ws.channel &&
client.isHost
)
) {
console.log(ws.channel, "duplicate host");
ws.close(1000, `The channel "${ws.channel}" already has a host`);
return;
}
}
ws.isAlive = true; ws.isAlive = true;
ws.pingStart = new Date().getTime();
ws.ping(noop);
ws.on("pong", heartbeat); ws.on("pong", heartbeat);
ws.on("message", function incoming(data) { ws.on("message", function incoming(data) {
if (!data.match(/^\["ping/i)) { const isPing = data.match(/^\["ping/i);
console.log(ws.channel, wss.clients.size, data); if (!isPing) {
console.log(new Date(), wss.clients.size, ws.channel, data);
} }
wss.clients.forEach(function each(client) { wss.clients.forEach(function each(client) {
if ( if (
@ -37,8 +59,13 @@ wss.on("connection", function connection(ws, req) {
client.readyState === WebSocket.OPEN && client.readyState === WebSocket.OPEN &&
client.channel === ws.channel client.channel === ws.channel
) { ) {
// inject latency between both clients if ping message
if (isPing && client.latency && ws.latency) {
client.send(data.replace(/latency/, client.latency + ws.latency));
} else {
client.send(data); client.send(data);
} }
}
}); });
}); });
}); });
@ -47,6 +74,7 @@ const interval = setInterval(function ping() {
wss.clients.forEach(function each(ws) { wss.clients.forEach(function each(ws) {
if (ws.isAlive === false) return ws.terminate(); if (ws.isAlive === false) return ws.terminate();
ws.isAlive = false; ws.isAlive = false;
ws.pingStart = new Date().getTime();
ws.ping(noop); ws.ping(noop);
}); });
}, 30000); }, 30000);

View File

@ -3,7 +3,10 @@
<Screenshot ref="screenshot"></Screenshot> <Screenshot ref="screenshot"></Screenshot>
<span <span
class="session" class="session"
:class="{ spectator: session.isSpectator }" :class="{
spectator: session.isSpectator,
reconnecting: session.isReconnecting
}"
v-if="session.sessionId" v-if="session.sessionId"
@click="leaveSession" @click="leaveSession"
:title=" :title="
@ -342,6 +345,16 @@ export default {
&.spectator { &.spectator {
color: $townsfolk; color: $townsfolk;
} }
&.reconnecting {
animation: blink 1s infinite;
}
}
}
@keyframes blink {
50% {
opacity: 0.5;
color: gray;
} }
} }

View File

@ -20,6 +20,7 @@ const handleVote = (state, [index, vote]) => {
const state = () => ({ const state = () => ({
sessionId: "", sessionId: "",
isSpectator: false, isSpectator: false,
isReconnecting: false,
playerCount: 0, playerCount: 0,
ping: 0, ping: 0,
playerId: "", playerId: "",
@ -38,6 +39,7 @@ const mutations = {
setSessionId: set("sessionId"), setSessionId: set("sessionId"),
setPlayerId: set("playerId"), setPlayerId: set("playerId"),
setSpectator: set("isSpectator"), setSpectator: set("isSpectator"),
setReconnecting: set("isReconnecting"),
setPlayerCount: set("playerCount"), setPlayerCount: set("playerCount"),
setPing: set("ping"), setPing: set("ping"),
setVotingSpeed: set("votingSpeed"), setVotingSpeed: set("votingSpeed"),

View File

@ -2,7 +2,7 @@ import rolesJSON from "../roles.json";
class LiveSession { class LiveSession {
constructor(store) { constructor(store) {
// this._wss = "ws://localhost:8081/"; //this._wss = "ws://localhost:8081/";
this._wss = "wss://baumgart.biz:8080/"; this._wss = "wss://baumgart.biz:8080/";
this._socket = null; this._socket = null;
this._isSpectator = true; this._isSpectator = true;
@ -10,6 +10,7 @@ class LiveSession {
this._store = store; this._store = store;
this._pingInterval = 30 * 1000; // 30 seconds between pings this._pingInterval = 30 * 1000; // 30 seconds between pings
this._pingTimer = null; this._pingTimer = null;
this._reconnectTimer = null;
this._players = {}; // map of players connected to a session this._players = {}; // map of players connected to a session
this._pings = {}; // map of player IDs to ping this._pings = {}; // map of player IDs to ping
// reconnect to previous session // reconnect to previous session
@ -25,14 +26,26 @@ class LiveSession {
*/ */
_open(channel) { _open(channel) {
this.disconnect(); this.disconnect();
this._socket = new WebSocket(this._wss + channel); this._socket = new WebSocket(
this._wss + channel + (this._isSpectator ? "" : "-host")
);
this._socket.addEventListener("message", this._handleMessage.bind(this)); this._socket.addEventListener("message", this._handleMessage.bind(this));
this._socket.onopen = this._onOpen.bind(this); this._socket.onopen = this._onOpen.bind(this);
this._socket.onclose = () => { this._socket.onclose = err => {
this._socket = null; this._socket = null;
this._store.commit("session/setSessionId", "");
clearInterval(this._pingTimer); clearInterval(this._pingTimer);
this._pingTimer = null; this._pingTimer = null;
if (err.code !== 1000) {
// connection interrupted, reconnect after 3 seconds
this._store.commit("session/setReconnecting", true);
this._reconnectTimer = setTimeout(
() => this.connect(channel),
3 * 1000
);
} else {
this._store.commit("session/setSessionId", "");
if (err.reason) alert(err.reason);
}
}; };
} }
@ -69,7 +82,7 @@ class LiveSession {
this._send("ping", [ this._send("ping", [
this._isSpectator, this._isSpectator,
this._store.state.session.playerId, this._store.state.session.playerId,
new Date().getTime() "latency"
]); ]);
this._handlePing(); this._handlePing();
clearTimeout(this._pingTimer); clearTimeout(this._pingTimer);
@ -168,9 +181,11 @@ class LiveSession {
this._pings = {}; this._pings = {};
this._store.commit("session/setPlayerCount", 0); this._store.commit("session/setPlayerCount", 0);
this._store.commit("session/setPing", 0); this._store.commit("session/setPing", 0);
this._store.commit("session/setReconnecting", false);
clearTimeout(this._reconnectTimer);
if (this._socket) { if (this._socket) {
this._send("bye", this._store.state.session.playerId); this._send("bye", this._store.state.session.playerId);
this._socket.close(); this._socket.close(1000);
this._socket = null; this._socket = null;
} }
} }
@ -378,7 +393,7 @@ class LiveSession {
* @param timestamp * @param timestamp
* @private * @private
*/ */
_handlePing([isSpectator, playerId, timestamp] = []) { _handlePing([isSpectator, playerId, latency] = []) {
const now = new Date().getTime(); const now = new Date().getTime();
// remove players that haven't sent a ping in twice the timespan // remove players that haven't sent a ping in twice the timespan
for (let player in this._players) { for (let player in this._players) {
@ -404,10 +419,12 @@ class LiveSession {
alert("Another storyteller joined the session!"); alert("Another storyteller joined the session!");
} else if (this._isSpectator && !isSpectator) { } else if (this._isSpectator && !isSpectator) {
// ping to ST // ping to ST
this._store.commit("session/setPing", now - timestamp); if (parseInt(latency, 10)) {
} else { this._store.commit("session/setPing", parseInt(latency, 10));
}
} else if(parseInt(latency, 10)) {
// ping to Players // ping to Players
this._pings[playerId] = now - timestamp; this._pings[playerId] = parseInt(latency, 10);
const pings = Object.values(this._pings); const pings = Object.values(this._pings);
this._store.commit( this._store.commit(
"session/setPing", "session/setPing",