“I feel what is needed is an UDP version of WebSockets. That’s all I wish we had.” - Matheus Valadares, creator of agar.io
WebSocket is built on TCP. However, UDP is much preferred over TCP for networking in realtime multiplayer games. Refer to the awesome visualizations in Gaffer On Games: Deterministic Lockstep to see why.
udp-ws is a UDP version of WebSocket built on WebRTC, which allows peer-to-peer UDP communication in the browser. However, udp-ws is designed for client-server communication (e.g. for server-authoritative browser games), not for peer-to-peer communication. udp-ws accomplishes this by treating your server as a peer.
udp-ws is inspired by and includes code snippets from geckos.io by yandeu and node-webrtc-examples.
Client (resembles WebSocket):
const ws = new UDPWebSocket('ws://localhost:3000');
ws.onmessage = ev => {
console.log(ev.data);
};
setInterval(() => {
if (ws.readyState === 'open') {
ws.send('client says hi');
}
}, 1000);
Server (resembles ws):
const wss = new UDPWebSocketServer(3000);
wss.on('connection', ws => {
ws.on('message', data => {
console.log(data);
if (ws.readyState === 'open') {
ws.send('server says hi');
}
});
});
In examples/barebones/server/
, run npm i
followed by npm run launch
, and see the client-server example running in localhost. Observe the output in your console log (e.g. using Ctrl
+ Shift
+ I
in Windows Chrome).
Structure of packet: object conforming to JSON.stringify
and JSON.parse
:
{ event: 'string representing event', data: { arbitrary object representing data } }
Client:
(async () => {
// WebSocketType.TCP for WebSocket, WebSocketType.UDP for UDPWebSocket
const webSocketHandler = new WebSocketHandler('ws://localhost:3000', WebSocketType.UDP);
webSocketHandler.bind('ServerResponseEvent', data => {
console.log(data);
});
try {
await webSocketHandler.connect();
// Code that must be executed only after the WebSocket is open
setInterval(() => {
webSocketHandler.send({
event: 'ClientMessageEvent',
data: {
message: 'client says hi'
}
});
}, 1000);
} catch (err) {
throw err;
}
})();
Server:
// WebSocketType.TCP for WebSocket, WebSocketType.UDP for UDPWebSocket
const webSocketServerHandler = new WebSocketServerHandler(3000, WebSocketType.UDP);
webSocketServerHandler.bind('ClientMessageEvent', (iws, data) => {
console.log(data);
webSocketServerHandler.send(iws, {
event: 'ServerResponseEvent',
data: {
reply: 'server says hi'
}
});
});
In examples/handler/server/
, run npm i
followed by npm run launch
.
3. examples/binaryHandler/
- Handling WebSocket life cycle, binary messaging, and event callbacks using
Structure of packet: ArrayBuffer
with leading Uint8
byte representing the event (thus 2^8 = 256 maximum handled events unless Uint8
is replaced with something greater, e.g. Uint16
) followed by an arbitrary number of bytes representing the data
ArrayBuffer(NUM_BYTES_UINT8 + NUM_BYTES_UINT32 * X + NUM_BYTES_FLOAT64 * Y + NUM_BYTES_CHAR * Z + ...)
Client:
import { NUM_BYTES_UINT8, NUM_BYTES_FLOAT64, NUM_BYTES_CHAR, writeStringToBuffer, bufferToString } from './binaryTools';
// Uint8 representing event (NumberEvent = 0, StringEvent = 1),
// which is the first byte of each packet sent and received
enum WebSocketEvent {
NumberEvent = 0,
StringEvent
}
(async () => {
// WebSocketType.TCP for WebSocket, WebSocketType.UDP for UDPWebSocket
const webSocketHandler = new BinaryWebSocketHandler('ws://localhost:3000', WebSocketType.UDP);
webSocketHandler.bind(WebSocketEvent.NumberEvent, buffer => {
const view = new DataView(buffer);
// read Float64 after first byte representing event
console.log(view.getFloat64(NUM_BYTES_UINT8));
});
webSocketHandler.bind(WebSocketEvent.StringEvent, buffer => {
// read string after first byte representing event
console.log(bufferToString(buffer.slice(NUM_BYTES_UINT8)));
});
try {
await webSocketHandler.connect();
// Code that must be executed only after the WebSocket is open
setInterval(() => {
const buffer = new ArrayBuffer(NUM_BYTES_UINT8 + NUM_BYTES_FLOAT64);
const view = new DataView(buffer);
// set first byte representing event
view.setUint8(0, WebSocketEvent.NumberEvent);
// set Float64 after first byte
view.setFloat64(NUM_BYTES_UINT8, 12345.6789);
webSocketHandler.send(buffer);
}, 1000);
setInterval(() => {
// 14 characters needed for 'client says hi'
const buffer = new ArrayBuffer(NUM_BYTES_UINT8 + NUM_BYTES_CHAR * 14);
const view = new DataView(buffer);
// set first byte representing event
view.setUint8(0, WebSocketEvent.StringEvent);
// set string after first byte
writeStringToBuffer('client says hi', buffer, NUM_BYTES_UINT8);
webSocketHandler.send(buffer);
}, 1000);
} catch (err) {
throw err;
}
})();
Server:
import { NUM_BYTES_UINT8, NUM_BYTES_FLOAT64, NUM_BYTES_CHAR, writeStringToBuffer, bufferToString } from './binaryTools';
enum WebSocketEvent {
NumberEvent = 0,
StringEvent
}
// WebSocketType.TCP for WebSocket, WebSocketType.UDP for UDPWebSocket
const webSocketServerHandler = new BinaryWebSocketServerHandler(3000, WebSocketType.UDP);
webSocketServerHandler.bind(WebSocketEvent.NumberEvent, (iws, buffer) => {
const inView = new DataView(buffer);
console.log(inView.getFloat64(NUM_BYTES_UINT8));
const outBuffer = new ArrayBuffer(NUM_BYTES_UINT8 + NUM_BYTES_FLOAT64);
const outView = new DataView(outBuffer);
outView.setUint8(0, WebSocketEvent.NumberEvent);
outView.setFloat64(NUM_BYTES_UINT8, 9876.54321);
webSocketServerHandler.send(iws, outBuffer);
});
webSocketServerHandler.bind(WebSocketEvent.StringEvent, (iws, buffer) => {
console.log(bufferToString(buffer.slice(NUM_BYTES_UINT8)));
const outBuffer = new ArrayBuffer(NUM_BYTES_UINT8 + NUM_BYTES_CHAR * 14);
const outView = new DataView(outBuffer);
outView.setUint8(0, WebSocketEvent.StringEvent);
writeStringToBuffer('server says hi', outBuffer, NUM_BYTES_UINT8);
webSocketServerHandler.send(iws, outBuffer);
});
In examples/binaryHandler/server/
, run npm i
followed by npm run launch
.
- Install
node
andnpm
- Install TypeScript
npm i -g typescript
- Install webpack
npm i -g webpack
- Install webpack-cli
npm i -g webpack-cli
Run npm i
in examples/<name of example>/server/
to install all npm packages for all required folders (via postinstall).
You can then run npm run launch
in examples/<name of example>/server/
, as mentioned above.
Before deploying to production, provision your own dedicated ICE/STUN servers and add them to iceServers.ts
. By default, STUN servers by Google are included for development:
export const iceServers: RTCIceServer[] = [
{ urls: 'stun:stun1.l.google.com:19302' },
{ urls: 'stun:stun2.l.google.com:19302' },
{ urls: 'stun:stun3.l.google.com:19302' },
{ urls: 'stun:stun4.l.google.com:19302' }
];
src/
client/
ts/
contains client-sideUDPWebSocket.ts
andWebSocketHandler.ts
server/
ts/
contains server-sideUDPWebSocketServer.ts
andWebSocketServerHandler.ts
If you want Javascript sources, in src/
, run npm run build
to compile TypeScript to Javascript in src/client/js/
and src/server/js/
. Feel free to use webpack or rollup for browser compatibility (on the client side) and/or minification.
examples/
barebones/
client/
ts/
contains a client-side example usingUDPWebSocket.ts
server/
ts/
contains a server-side example usingUDPWebSocketServer.ts
handler/
client/
ts/
contains a client-side example usingWebSocketHandler.ts
server/
ts/
contains a server-side example usingWebSocketServerHandler.ts
binaryHandler/
client/
ts/
contains a client-side example usingBinaryWebSocketHandler.ts
server/
ts/
contains a server-side example usingBinaryWebSocketServerHandler.ts
gbz