mudb
Version:
Real-time database for multiplayer games
376 lines • 14.6 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const ws = require("ws");
const url = require("url");
const socket_1 = require("../socket");
const system_1 = require("../../scheduler/system");
const logger_1 = require("../../logger");
const error_1 = require("../../util/error");
const stream_1 = require("../../stream");
const error = error_1.makeError('socket/web/server');
function noop() { }
function coallesceFragments(frags) {
let size = 0;
for (let i = 0; i < frags.length; ++i) {
size += frags[i].length;
}
const result = new Uint8Array(size);
let offset = 0;
for (let i = 0; i < frags.length; ++i) {
result.set(frags[i], offset);
offset += frags[i].length;
}
return result;
}
class MuWebSocketConnection {
constructor(sessionId, reliableSocket, serverClose, logger, bufferLimit) {
this.bufferLimit = bufferLimit;
this.started = false;
this.closed = false;
this.unreliableSockets = [];
this.lastReliablePing = 0;
this.lastUnreliablePing = [];
this.pendingMessages = [];
this.onMessage = noop;
this.onClose = noop;
this.sessionId = sessionId;
this.reliableSocket = reliableSocket;
this.serverClose = serverClose;
this._logger = logger;
this.reliableSocket.onmessage = ({ data }) => {
if (this.closed) {
return;
}
if (this.started) {
if (typeof data === 'string') {
this.onMessage(data, false);
}
else if (data.length === 1) {
this.onMessage(data[0], false);
}
else if (data.length > 1) {
let size = 0;
for (let i = 0; i < data.length; ++i) {
size += data[i].length;
}
const buffer = stream_1.allocBuffer(size);
const result = buffer.uint8;
let offset = 0;
for (let i = 0; i < data.length; ++i) {
result.set(data[i], offset);
offset += data[i].length;
}
this.onMessage(result.subarray(0, offset), false);
stream_1.freeBuffer(buffer);
}
}
else {
if (typeof data === 'string') {
this.pendingMessages.push(data);
}
else {
this.pendingMessages.push(coallesceFragments(data));
}
}
};
this.reliableSocket.onclose = () => {
if (!this.closed) {
this._logger.log(`unexpectedly closed websocket connection for ${this.sessionId}`);
}
else {
this._logger.log(`closing websocket connection for ${this.sessionId}`);
}
this.closed = true;
for (let i = 0; i < this.unreliableSockets.length; ++i) {
this.unreliableSockets[i].close();
}
this.onClose();
this.serverClose();
};
this.reliableSocket.onerror = (e) => {
this._logger.error(`error on reliable socket ${this.sessionId}. reason ${e} ${e.stack ? e.stack : ''}`);
};
}
addUnreliableSocket(socket) {
if (this.closed) {
return;
}
this.unreliableSockets.push(socket);
this.lastUnreliablePing.push(0);
socket.onmessage = ({ data }) => {
if (this.closed) {
return;
}
if (this.started) {
if (typeof data === 'string') {
this.onMessage(data, true);
}
else if (data.length === 1) {
this.onMessage(data[0], true);
}
else if (data.length > 1) {
let size = 0;
for (let i = 0; i < data.length; ++i) {
size += data[i].length;
}
const buffer = stream_1.allocBuffer(size);
const result = buffer.uint8;
let offset = 0;
for (let i = 0; i < data.length; ++i) {
result.set(data[i], offset);
offset += data[i].length;
}
this.onMessage(result.subarray(0, offset), true);
stream_1.freeBuffer(buffer);
}
}
};
socket.onclose = () => {
const idx = this.unreliableSockets.indexOf(socket);
this.unreliableSockets.splice(idx, 1);
this.lastUnreliablePing.splice(idx, 1);
if (!this.closed) {
this._logger.error(`unreliable socket closed unexpectedly: ${this.sessionId}`);
}
};
socket.onerror = (e) => {
this._logger.error(`unreliable socket ${this.sessionId} error: ${e} ${e.stack ? e.stack : ''}`);
};
}
send(data, unreliable) {
if (this.closed) {
return;
}
if (unreliable) {
const sockets = this.unreliableSockets;
if (sockets.length > 0) {
let socket = sockets[0];
let bufferedAmount = socket.bufferedAmount || 0;
let idx = 0;
for (let i = 1; i < sockets.length; ++i) {
const s = sockets[i];
const b = s.bufferedAmount || 0;
if (b < bufferedAmount) {
socket = s;
bufferedAmount = b;
idx = i;
}
}
if (bufferedAmount < this.bufferLimit) {
socket.send(typeof data === 'string' ? data : new Uint8Array(data));
sockets.splice(idx, 1);
sockets.push(socket);
}
}
}
else {
this.reliableSocket.send(typeof data === 'string' ? data : new Uint8Array(data));
}
}
close() {
this.reliableSocket.close();
}
doPing(now, pingCutoff) {
if (this.closed) {
return;
}
if (this.lastReliablePing < pingCutoff) {
this.lastReliablePing = now;
this.reliableSocket.ping();
}
for (let i = 0; i < this.unreliableSockets.length; ++i) {
if (this.lastUnreliablePing[i] < pingCutoff) {
this.lastUnreliablePing[i] = now;
this.unreliableSockets[i].ping();
}
}
}
}
exports.MuWebSocketConnection = MuWebSocketConnection;
class MuWebSocketClient {
constructor(connection, scheduler, logger) {
this._state = socket_1.MuSocketState.INIT;
this.sessionId = connection.sessionId;
this._connection = connection;
this._logger = logger;
this.scheduler = scheduler;
}
state() { return this._state; }
open(spec) {
if (this._state !== socket_1.MuSocketState.INIT) {
throw error(`socket had been opened`);
}
this.scheduler.setTimeout(() => {
if (this._state !== socket_1.MuSocketState.INIT) {
return;
}
this._state = socket_1.MuSocketState.OPEN;
spec.ready();
for (let i = 0; i < this._connection.pendingMessages.length; ++i) {
if (this._connection.closed) {
break;
}
try {
spec.message(this._connection.pendingMessages[i], false);
}
catch (e) {
this._logger.exception(e);
}
}
this._connection.pendingMessages.length = 0;
if (this._connection.closed) {
this._state = socket_1.MuSocketState.CLOSED;
spec.close();
}
else {
this._connection.started = true;
this._connection.onMessage = spec.message;
this._connection.onClose = () => {
this._state = socket_1.MuSocketState.CLOSED;
spec.close();
};
}
}, 0);
}
send(data, unreliable) {
this._connection.send(data, !!unreliable);
}
close() {
this._logger.log(`close called on websocket ${this.sessionId}`);
if (this._state !== socket_1.MuSocketState.CLOSED) {
this._state = socket_1.MuSocketState.CLOSED;
this._connection.close();
}
}
reliableBufferedAmount() {
return this._connection.reliableSocket.bufferedAmount;
}
unreliableBufferedAmount() {
let amount = Infinity;
for (let i = 0; i < this._connection.unreliableSockets.length; ++i) {
amount = Math.min(amount, this._connection.unreliableSockets[i].bufferedAmount);
}
return amount;
}
}
exports.MuWebSocketClient = MuWebSocketClient;
class MuWebSocketServer {
constructor(spec) {
this._state = socket_1.MuSocketServerState.INIT;
this._connections = [];
this.clients = [];
this._onClose = noop;
this._pingInterval = 10000;
this._logger = spec.logger || logger_1.MuDefaultLogger;
this.bufferLimit = spec.bufferLimit || 1024;
this._options = {
server: spec.server,
clientTracking: false,
};
spec.backlog && (this._options['backlog'] = spec.backlog);
spec.maxPayload && (this._options['maxPayload'] = spec.maxPayload);
spec.handleProtocols && (this._options['handleProtocols'] = spec.handleProtocols);
spec.path && (this._options['path'] = spec.path);
spec.perMessageDeflate && (this._options['perMessageDeflate'] = spec.perMessageDeflate);
this.scheduler = spec.scheduler || system_1.MuSystemScheduler;
if ('pingInterval' in spec) {
this._pingInterval = spec.pingInterval || 0;
}
}
state() { return this._state; }
_findConnection(sessionId) {
for (let i = 0; i < this._connections.length; ++i) {
if (this._connections[i].sessionId === sessionId) {
return this._connections[i];
}
}
return null;
}
start(spec) {
if (this._state !== socket_1.MuSocketServerState.INIT) {
throw error(`server had been started`);
}
if (this._pingInterval) {
this._pingIntervalId = this.scheduler.setInterval(() => {
const now = Date.now();
const pingCutoff = now - this._pingInterval;
for (let i = 0; i < this._connections.length; ++i) {
this._connections[i].doPing(now, pingCutoff);
}
}, this._pingInterval * 0.5);
}
this.scheduler.setTimeout(() => {
this._wsServer = new ws.Server(this._options)
.on('connection', (socket, req) => {
if (this._state === socket_1.MuSocketServerState.SHUTDOWN) {
this._logger.error('connection attempt from closed socket server');
socket.terminate();
return;
}
this._logger.log(`muwebsocket connection received: extensions ${socket.extensions} protocol ${socket.protocol}`);
const query = url.parse(req.url, true).query;
const sessionId = query['sid'];
if (typeof sessionId !== 'string') {
this._logger.error(`no session id`);
return;
}
socket.binaryType = 'fragments';
socket.onerror = (e) => {
this._logger.error(`socket error in opening state: ${e}`);
};
socket.onopen = () => this._logger.log('socket opened');
let connection = this._findConnection(sessionId);
if (connection) {
socket.send(JSON.stringify({
reliable: false,
}));
connection.addUnreliableSocket(socket);
}
else {
socket.send(JSON.stringify({
reliable: true,
}));
connection = new MuWebSocketConnection(sessionId, socket, () => {
if (connection) {
this._connections.splice(this._connections.indexOf(connection), 1);
for (let i = this.clients.length - 1; i >= 0; --i) {
if (this.clients[i].sessionId === connection.sessionId) {
this.clients.splice(i, 1);
}
}
}
}, this._logger, this.bufferLimit);
this._connections.push(connection);
const client = new MuWebSocketClient(connection, this.scheduler, this._logger);
this.clients.push(client);
spec.connection(client);
}
})
.on('error', (e) => {
this._logger.error(`internal websocket error ${e}. ${e.stack ? e.stack : ''}`);
})
.on('listening', () => this._logger.log(`muwebsocket server listening: ${JSON.stringify(this._wsServer.address())}`))
.on('close', () => {
if (this._pingIntervalId) {
this.scheduler.clearInterval(this._pingIntervalId);
}
this._logger.log('muwebsocket server closing');
})
.on('headers', (headers) => this._logger.log(`muwebsocket: headers ${headers}`));
this._onClose = spec.close;
this._state = socket_1.MuSocketServerState.RUNNING;
spec.ready();
}, 0);
}
close() {
if (this._state === socket_1.MuSocketServerState.SHUTDOWN) {
return;
}
this._state = socket_1.MuSocketServerState.SHUTDOWN;
if (this._wsServer) {
this._wsServer.close(this._onClose);
}
}
}
exports.MuWebSocketServer = MuWebSocketServer;
//# sourceMappingURL=server.js.map