mirror of
https://github.com/logsol/chuck.js.git
synced 2026-05-11 10:37:34 +00:00
Route gameCommand traffic through WebRTC unreliable DataChannel
Socket.IO (TCP) holds back later packets while it retransmits a lost one, which stalls worldUpdate delivery on lossy long-distance links — exactly the pattern game state suffers worst from. WebRTC DataChannels in unreliable mode (ordered:false, maxRetransmits:0) drop late packets instead of queueing them, which is what we want for high-frequency state sync. Adds a per-user WebRTCTransport on top of the existing Socket.IO connection. Socket.IO stays in charge of bootstrap, signaling (SDP/ICE exchange), and control messages — only gameCommand payloads get routed onto the unreliable channel once it's open. If WebRTC fails to negotiate, gameCommand transparently falls back to Socket.IO, so the game keeps working unchanged. A new StatsLogger writes per-session JSONL events (session_start, webrtc_ready with negotiation time, per-second stats with transport, RTT samples, recv/send rates, seq gaps) so we can compare real-world runs (e.g. Germany server <-> Korea client) instead of guessing. URL flag ?webrtc=0 forces fallback for A/B testing. scripts/webrtc-browser-test.js spins up a headless Chromium against a freshly-started server and asserts the unreliable channel opens and gameCommand traffic actually rides it.
This commit is contained in:
parent
47faae81e5
commit
a0481ed867
9 changed files with 1412 additions and 138 deletions
|
|
@ -1,10 +1,12 @@
|
|||
define([
|
||||
"Game/Core/User",
|
||||
"Lib/Utilities/Protocol/Helper",
|
||||
"Lib/Utilities/NotificationCenter"
|
||||
"Lib/Utilities/NotificationCenter",
|
||||
"Server/WebRTCTransport",
|
||||
"Server/StatsLogger"
|
||||
],
|
||||
|
||||
function (Parent, ProtocolHelper, nc) {
|
||||
function (Parent, ProtocolHelper, nc, WebRTCTransport, StatsLogger) {
|
||||
|
||||
"use strict";
|
||||
|
||||
|
|
@ -16,39 +18,75 @@ function (Parent, ProtocolHelper, nc) {
|
|||
this.channelPipe = null;
|
||||
this.options = null;
|
||||
|
||||
var self = this;
|
||||
|
||||
socketLink.on('message', this.onMessage.bind(this));
|
||||
socketLink.on('disconnect', this.onDisconnect.bind(this));
|
||||
|
||||
nc.on(nc.ns.server.events.controlCommand.user + this.id, this.socketLink.send, this.socketLink);
|
||||
// Outbound messages from the rest of the server land here. We decide
|
||||
// whether to send over Socket.IO (control) or the WebRTC unreliable
|
||||
// channel (gameCommand, when ready).
|
||||
nc.on(nc.ns.server.events.controlCommand.user + this.id, this.sendOutbound, this);
|
||||
|
||||
// Spin up the WebRTC peer immediately. Negotiation happens via the
|
||||
// socket signaling messages once the client posts its offer.
|
||||
this.webrtc = new WebRTCTransport(this.id, {
|
||||
onLocalDescription: function (desc) {
|
||||
self.socketLink.send(ProtocolHelper.encodeCommand("webrtcAnswer", desc));
|
||||
},
|
||||
onLocalCandidate: function (cand) {
|
||||
self.socketLink.send(ProtocolHelper.encodeCommand("webrtcIce", cand));
|
||||
},
|
||||
onReady: function (negotiationMs) {
|
||||
console.log("[webrtc] " + self.id + " unreliable channel open (" + negotiationMs + "ms)");
|
||||
StatsLogger.log({
|
||||
type: "webrtc_ready",
|
||||
session: self.id,
|
||||
dtMs: negotiationMs
|
||||
});
|
||||
},
|
||||
onMessage: function (raw) {
|
||||
// Inbound gameCommand from client over the unreliable channel.
|
||||
// Same shape as if it came over Socket.IO: a JSON-encoded
|
||||
// gameCommand envelope. Feed it to applyCommand the same way.
|
||||
try {
|
||||
var parsed = JSON.parse(raw);
|
||||
if (parsed.gameCommand !== undefined) {
|
||||
self.onGameCommand(parsed.gameCommand);
|
||||
} else {
|
||||
// Future: other unreliable-channel message types.
|
||||
ProtocolHelper.applyCommand(raw, self);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("[webrtc] bad inbound message:", e.message);
|
||||
}
|
||||
},
|
||||
onClosed: function () {
|
||||
console.log("[webrtc] " + self.id + " unreliable channel closed");
|
||||
}
|
||||
});
|
||||
|
||||
StatsLogger.log({
|
||||
type: "session_start",
|
||||
session: this.id,
|
||||
hasWebrtc: true
|
||||
});
|
||||
}
|
||||
|
||||
User.prototype = Object.create(Parent.prototype);
|
||||
|
||||
/*
|
||||
User.prototype.setChannelPipe = function(channelPipe) {
|
||||
if(channelPipe) {
|
||||
if (channelPipe.isWithinUserLimit()) {
|
||||
this.channelPipe = channelPipe;
|
||||
this.channelPipe.addUser(this);
|
||||
} else {
|
||||
var message = ProtocolHelper.encodeCommand("joinError", {message:"Channel is full"});
|
||||
this.socketLink.send(message);
|
||||
}
|
||||
|
||||
} else {
|
||||
var message = ProtocolHelper.encodeCommand("joinError", {message:"Channel not found"});
|
||||
this.socketLink.send(message);
|
||||
}
|
||||
};
|
||||
*/
|
||||
|
||||
// Socket callbacks
|
||||
// ---------- Inbound from Socket.IO ----------
|
||||
|
||||
User.prototype.onMessage = function (message) {
|
||||
ProtocolHelper.applyCommand(message, this);
|
||||
}
|
||||
};
|
||||
|
||||
User.prototype.onDisconnect = function () {
|
||||
if (this.webrtc) {
|
||||
this.webrtc.destroy();
|
||||
this.webrtc = null;
|
||||
}
|
||||
StatsLogger.log({ type: "session_end", session: this.id });
|
||||
|
||||
if(!this.channelPipe) {
|
||||
console.warn("Disconnecting user without a channel. (Maybe channel was full)");
|
||||
|
|
@ -56,11 +94,32 @@ function (Parent, ProtocolHelper, nc) {
|
|||
}
|
||||
|
||||
this.channelPipe.removeUser(this);
|
||||
};
|
||||
|
||||
|
||||
// ---------- Outbound routing ----------
|
||||
|
||||
// Decides whether a server-generated message should ride Socket.IO or the
|
||||
// unreliable WebRTC channel. gameCommand traffic uses WebRTC when ready;
|
||||
// everything else stays on Socket.IO.
|
||||
User.prototype.sendOutbound = function (message) {
|
||||
if (this.webrtc && this.webrtc.isReady() && isGameCommandMessage(message)) {
|
||||
if (this.webrtc.send(message)) return;
|
||||
// Send failed (channel state changed mid-send) — fall through.
|
||||
}
|
||||
this.socketLink.send(message);
|
||||
};
|
||||
|
||||
function isGameCommandMessage(message) {
|
||||
// The protocol wraps every message as {"<command>": <payload>}; we only
|
||||
// peek at the first key without fully parsing the inner payload.
|
||||
if (typeof message !== "string") return false;
|
||||
// Cheap heuristic — robust because the envelope key is always first.
|
||||
return message.indexOf('{"gameCommand"') === 0;
|
||||
}
|
||||
|
||||
|
||||
// User command callbacks
|
||||
// Remember: control commands are coordinator relevant commands
|
||||
// ---------- Command callbacks (received from client) ----------
|
||||
|
||||
User.prototype.onJoin = function(options) {
|
||||
|
||||
|
|
@ -83,14 +142,12 @@ function (Parent, ProtocolHelper, nc) {
|
|||
var userOptions = {
|
||||
id: this.id,
|
||||
nickname: options.nickname
|
||||
}
|
||||
};
|
||||
this.options = userOptions;
|
||||
this.channelPipe.addUser(this);
|
||||
};
|
||||
|
||||
/* FIXME: watch out and check in wich direction game and control commands flow */
|
||||
User.prototype.onGameCommand = function(options) {
|
||||
// repacking for transport via pipe
|
||||
var message = ProtocolHelper.encodeCommand("gameCommand", options);
|
||||
this.channelPipe.sendToUser(this.id, message);
|
||||
};
|
||||
|
|
@ -100,6 +157,31 @@ function (Parent, ProtocolHelper, nc) {
|
|||
nc.trigger(nc.ns.server.events.controlCommand.user + this.id, message);
|
||||
};
|
||||
|
||||
return User;
|
||||
// WebRTC signaling messages from the client (arriving over Socket.IO).
|
||||
|
||||
});
|
||||
User.prototype.onWebrtcOffer = function (options) {
|
||||
if (!this.webrtc || !options) return;
|
||||
this.webrtc.handleRemoteDescription(options.sdp, options.type || "offer");
|
||||
};
|
||||
|
||||
User.prototype.onWebrtcIce = function (options) {
|
||||
if (!this.webrtc || !options) return;
|
||||
this.webrtc.handleRemoteCandidate(options.candidate, options.mid);
|
||||
};
|
||||
|
||||
// Periodic stats report from client.
|
||||
User.prototype.onStats = function (options) {
|
||||
if (!options) return;
|
||||
StatsLogger.log({
|
||||
type: "stats",
|
||||
session: this.id,
|
||||
transport: options.transport || "socketio",
|
||||
rttSamples: options.rttSamples || [],
|
||||
recvRate: options.recvRate || 0,
|
||||
sendRate: options.sendRate || 0,
|
||||
gaps: options.gaps || 0
|
||||
});
|
||||
};
|
||||
|
||||
return User;
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue