From 26ddba3862412f4ec1d32f0a49c2b0fc7dd4dc9c Mon Sep 17 00:00:00 2001 From: Thomas Jarrand Date: Sat, 10 Nov 2018 18:09:25 +0100 Subject: [PATCH 1/2] Improving documentation --- README.md | 220 ++++++++++++++++++++++++++- demo-client.js | 15 +- demo-server.js | 11 +- doc/webpack.config.js | 37 +++++ index.html | 2 +- package.json | 3 + src/client/BinaryEncoder.js | 3 + src/client/Client.js | 7 +- src/client/index.js | 2 + src/encoder/BinaryEncoder.js | 11 ++ src/encoder/JsonEncoder.js | 11 ++ src/encoder/codec/LongIntCodec.js | 2 +- src/encoder/codec/LongStringCodec.js | 34 +++++ src/encoder/codec/index.js | 2 + src/server/Beacon.js | 40 +++++ src/server/Client.js | 41 +---- src/server/Server.js | 32 ++-- src/server/index.js | 2 + 18 files changed, 402 insertions(+), 73 deletions(-) create mode 100644 doc/webpack.config.js create mode 100644 src/client/BinaryEncoder.js create mode 100644 src/encoder/codec/LongStringCodec.js create mode 100644 src/server/Beacon.js diff --git a/README.md b/README.md index 5e4b037..841f4a2 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,228 @@ Netcode ======= -A simple client & server binary-encoded websocket communication system for web video games. +A simple JavaScript client & server binary-encoded websocket communication system for web video games. ## Installation `npm add netcode` +## Requirements + +- Node >= v8.0.0 + ## Usage -### Server side (node) +### Define an encoder (common to server and client) + +The server and the client __must__ share the same events. +An events is defined by its _unique_ name and the corresponding codec, responsible for encoding and decoding the data. + +```javascript +// events.js +import Int8Codec from 'netcode/src/encoder/codec/Int8Codec'; +import StringCodec from 'netcode/src/encoder/codec/StringCodec'; + +export default [ + ['id', new Int8Codec()], + ['say', new StringCodec()], +]; +``` + +These encoder configuration allow you to send the following events over websocket: + +- `send('id', 255);` +- `send('say', 'Hello world!');` + +### Setup a Server (server-side / node) + +We setup a server specifying the port and host that should be listen on and the encoder to use. Here we use a BinaryEncoder to communicate in binary over websocket, with the previously configured event list. + +```javascript +import Server from 'netcode/src/server/Server'; +import BinaryEncoder from 'netcode/src/server/BinaryEncoder'; +import events from './envents'; + +// Listen on ws://localhost:8080 +const server = new Server(8080, 'localhost', new BinaryEncoder(events)); + +server.on('client:join', client => { + client.on('say', sentence => console.log(sentence)); + client.send('id', client.id); +}); +``` + +Now we've got a server running at `ws://localhost:8080/` that listen for a `say` text event and send a `id` integer event to every client that connects. + +Server parameters: + +| Parameter | Type | Default value | Description | +| --------- | ------------------------------ | ----------------- | ---------------------------------------------------- | +| port | _Number_ | 8080 | Port to listen on. | +| host | _String_ | 0.0.0.0 | Host to listen on. | +| encoder | _BinaryEncoder \| JsonEncoder_ | new JsonEncoder() | Encoder to use to read/write event messages. | +| ping | _Number_ | 30 | Ping frequency in seconds (0 for no ping). | +| maxLength | _Number_ | 512 | Paquet max length in bit (should be a power of two). | +| protocols | _Array String[]_ | `['websocket']` | Protocols tu use | + +_See an [full example of server setup](demo-server.js)._ + +### Write a Client (browser side) + +Now we write a client for the browser that connects to `localhost:8080` and use a BinaryEncoder with the same event configuration as the server. + +```javascript +import Client from 'netcode/src/client/Client'; +import BinaryEncoder from 'netcode/src/client/BinaryEncoder'; +import events from './envents'; + +const client = new Client('localhost:8080', new BinaryEncoder(events)) + +client.on('open', () => { + client.on('id', id => console.log(`My id is ${id}.`)); + client.send('say', 'Hello world!'); +}); +``` + +Now we've got client that listen for the `id` event and sent a sentence in a `say` event. + +Connection is alive and well! + +_See an [full example of client setup](demo-client.js)._ + +## Events + +You can interface your code with the Server and both Client objects through the designated events and the `on(event, callback)` and `off(event, callback)` methods. + +_Note: If you're on node `< 10.0.0`, use `removeListener` method instead of `off`_ + +__Server events:__ + +| Name | Callback parameters | Description | +|---|---|---| +| `ready` | | Server is listening and ready to accept connections. | +| `client:join` | client _Client_ | New connected client. | +| `client:leave` | client _Client_ | Client left. | +| `error` | error _Error_ | An error occured. | + +__Client events:__ + +| Name | Callback parameters | Description | +|---|---|---| +| `open` | client _Client_ | Client connection is open and ready to transmit. | +| `error` | error _Error_, client _Client_ | An error occurred. | +| `close` | client _Client_ | Client connection is closed. | + +## Codecs + +Codecs are responsible for encoding your events data into ArrayBuffer that will be transmitted over the binary websocket connection and decoded back into usable data on the other side. + +You will probably need to write your own codecs but a few standard needs are alreay adressed by the simple codecs that follow: + +###Packaged codecs + +Packaged codecs are available in the `netcode/server` and `netcode/client` packages or as source code in `netcode/src/encoder/codec` folder (see [Note on packaging](#Note on packaging)) + +| Class | Data format | Example | Size (in byte) | +| -------------------------- | ---------------------------------- | ------------------------------------------------------------ | ----------------------- | +| `Codec` | No data (just send the event name) | `['pause', new Codec()]`
`send('pause')` | 0 | +| `BooleanCodec` | `true|false` | `['active', new BooleanCodec()]`
`send('active', true)` | 1 | +| `StringCodec` | String up to 255 characters | `['player:name', new StringCodec()]`
`send('player:name', 'DarkShadow73')` | 1 + (String length * 2) | +| `LongStringCodec` | String up to 65536 characters | `['url', new LongStringCodec()]`
`send('url', 'https://my.long.url/hash/xxx...')` | 2 + (String length * 2) | +| `Int8Codec` | Integer from 0 to 255 | `['id', new Int8Codec()]`
`send('id', 42)` | 1 | +| `Int16Codec` | Integer from 0 to 65536 | `['score', new Int16Codec()]`
`send('score', 9999)` | 2 | +| `Int32Codec` | Integer from 0 to 4294967295 | `['position', new Int32Codec()]`
`send('position', 4294967295)` | 4 | +| `LongIntCodec(byteLength)` | Integer encoded as string | `['timestamp', new LongIntCodec(13)`]
`send('timestamp', Date.now())` | byteLength | + +## Note on packaging + +Netcode provides you with 2 pre-packaged versions of the library for Node and for the Browser, so you can use it out of the box. You can also import the ES6 source code directly and manage the packaging yourself. + +Let's see an example of both these setups: + +### Using the pre-packaged libraries +On the server-side (node): + +```javascript +const { + Server, + BinaryEncoder, + Int16Codec, + BooleanCodec, + StringCodec, + // ... +} = require('netcode/server');` +``` + +And in the browser as well: + +```html + + + + + +``` + +### Using the ES6 source code + +In your Webpack configuration, include the source code of Netcode so that Babel will compile this ES6 code as well. + +```javascript +module.exports = { + //... + module: { + rules: [{ + // ... + include: '/node_modules/netcode/src/', + }] + } +}; +``` + +_See a [full Webpack/Babel config example](doc/webpack.config.js)._ + +Now you can import source file from Netcode directly in your code and it will be compiled as well : + +```javascript +// src/server.js +import Server from 'netcode/src/server/Server'; +import BinaryEncoder from 'netcode/src/server/BinaryEncoder'; // Import from src/server! +import BooleanCodec from 'netcode/src/encoder/codec/BooleanCodec'; + +new Server( + 8080, + 'localhost', + new BinaryEncoder([ + ['foo', new BooleanEncoder()] + ]) +); +``` + +```javascript +// src/client.js +import Client from 'netcode/src/client/Client'; +import BinaryEncoder from 'netcode/src/client/BinaryEncoder'; // Import from src/client! +import BooleanCodec from 'netcode/src/encoder/codec/BooleanCodec'; -See an [example of server setup](demo-server.js). +new Client( + 'ws://localhost:8080', + new BinaryEncoder([ + ['foo', new BooleanCodec()] + ]) +); +``` -### Client side (browser) +## Debugging -See an [example of client setup](demo-client.js). +The BinaryEncoder should emit errors if anything wrong happen durring encoding or decoding, but even so, working with binary data can be tricky. +You can replace your `BinaryEncoder` with a `JsonEncoder` at any time without any other change to you code to switch to a JSON communication and to help you find out what's wrong with transmitted data. \ No newline at end of file diff --git a/demo-client.js b/demo-client.js index 56bdef7..0afcf21 100644 --- a/demo-client.js +++ b/demo-client.js @@ -4,7 +4,6 @@ window.addEventListener('load', () => { // Register your events const encoder = new BinaryEncoder([ - ['open', new Codec()], ['id', new Int16Codec()], ['ping', new LongIntCodec(6)], ['pong', new LongIntCodec(6)], @@ -17,12 +16,12 @@ window.addEventListener('load', () => { let ping; // Listen for a "pong" event - client.addEventListener('pong', pong => { + client.on('pong', pong => { console.info('pong: %s ms', pong - ping); }); // Listen for an "id" event - client.addEventListener('id', id => { + client.on('id', id => { console.log('connected with id %s', id); ping = Date.now(); @@ -31,7 +30,7 @@ window.addEventListener('load', () => { }); // Listen for an "inverse" event - client.addEventListener('inverse', status => { + client.on('inverse', status => { // Answer with an "inverse" event client.send('inverse', !status); console.log('Inverse received: %s', status); @@ -41,17 +40,19 @@ window.addEventListener('load', () => { }); // Listen for a "greeting" event - client.addEventListener('greeting', message => { + client.on('greeting', message => { console.log('Servers geets you: "%s"', message); }); // Listen for oppening connection - client.addEventListener('open', () => { + client.on('open', () => { console.info('Connection open.'); + + setTimeout(() => client.close(), 20 * 1000); }); // Listen for connection close - client.addEventListener('close', () => { + client.on('close', () => { console.info('Connection closed.'); }); }); diff --git a/demo-server.js b/demo-server.js index 057f4cb..d1acdf1 100644 --- a/demo-server.js +++ b/demo-server.js @@ -2,7 +2,6 @@ const { Server, BinaryEncoder, Codec, Int16Codec, LongIntCodec, BooleanCodec, St // Register your events const encoder = new BinaryEncoder([ - ['open', new Codec()], ['id', new Int16Codec()], ['ping', new LongIntCodec(6)], ['pong', new LongIntCodec(6)], @@ -14,11 +13,11 @@ const encoder = new BinaryEncoder([ const server = new Server(process.argv[2], 'localhost', encoder); // Listen for new clients -server.addListener('client:join', client => { +server.on('client:join', client => { console.log('Client %s joined.', client.id); // Listen for "ping" event - client.addListener('ping', ping => { + client.on('ping', ping => { // Answer with a "pong" event client.send('pong', Date.now()); console.log('Client %s ping received: %s.', client.id, ping); @@ -28,12 +27,12 @@ server.addListener('client:join', client => { }); // Listen for "inverse" event - client.addListener('inverse', status => { + client.on('inverse', status => { console.log('Client %s inverse received: %s.', client.id, status); }); // Listen for "greeting" event - client.addListener('greeting', message => { + client.on('greeting', message => { console.log('Client %s geets you: "%s"', client.id, message); // Send a "greeting" event client.send('greeting', 'Hello, I\'m server!'); @@ -44,7 +43,7 @@ server.addListener('client:join', client => { }); // Listen for disconnecting clients -server.addListener('client:leave', client => { +server.on('client:leave', client => { console.log('Client %s left.', client.id); }); diff --git a/doc/webpack.config.js b/doc/webpack.config.js new file mode 100644 index 0000000..5ee6187 --- /dev/null +++ b/doc/webpack.config.js @@ -0,0 +1,37 @@ +const webpack = require('webpack'); + +const rules = [ + { + test: /\.js$/, + exclude: /node_modules/, + include: '/node_modules/netcode/src/', + use: { + loader: 'babel-loader', + options: { + presets: ['@babel/preset-env'] + } + } + } +]; + +const clientConfig = { + target: 'web', + entry: './src/client.js', + output: { + filename: 'client.js', + path: `${__dirname}/dist/`, + }, + module: { rules } +}; + +const serverConfig = { + target: 'node', + entry: './src/server.js', + output: { + filename: 'server.js', + path: `${__dirname}/dist/server/`, + }, + module: { rules } +}; + +module.exports = [ serverConfig, clientConfig ]; diff --git a/index.html b/index.html index a81deca..444e6a0 100644 --- a/index.html +++ b/index.html @@ -1,7 +1,7 @@ - + Netcode Demo diff --git a/package.json b/package.json index 7c3c25f..78516c2 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,9 @@ "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, + "engines" : { + "node" : ">=8.0.0" + }, "keywords": [ "netcode", "websocket", diff --git a/src/client/BinaryEncoder.js b/src/client/BinaryEncoder.js new file mode 100644 index 0000000..e468585 --- /dev/null +++ b/src/client/BinaryEncoder.js @@ -0,0 +1,3 @@ +import BinaryEncoder from 'netcode/src/encoder/BinaryEncoder'; + +export default BinaryEncoder; diff --git a/src/client/Client.js b/src/client/Client.js index ff1f10c..8d50a25 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -39,14 +39,13 @@ export default class Client extends EventEmitter { */ close() { this.socket.close(); - //this.onClose(); } /** * On connexion open */ onOpen() { - this.emit('open'); + this.emit('open', this); } /** @@ -68,7 +67,7 @@ export default class Client extends EventEmitter { this.socket.removeEventListener('close', this.onClose); this.socket.removeEventListener('error', this.onError); this.socket.removeEventListener('message', this.onMessage); - this.emit('close'); + this.emit('close', this); } /** @@ -77,6 +76,6 @@ export default class Client extends EventEmitter { * @param {Error} error */ onError(error) { - this.emit('error', error); + this.emit('error', error, this); } } diff --git a/src/client/index.js b/src/client/index.js index d0c0bb4..2c9e9c3 100644 --- a/src/client/index.js +++ b/src/client/index.js @@ -8,6 +8,7 @@ import Int32Codec from 'netcode/src/encoder/codec/Int32Codec'; import LongIntCodec from 'netcode/src/encoder/codec/LongIntCodec'; import BooleanCodec from 'netcode/src/encoder/codec/BooleanCodec'; import StringCodec from 'netcode/src/encoder/codec/StringCodec'; +import LongStringCodec from 'netcode/src/encoder/codec/LongStringCodec'; module.exports = { Client, @@ -20,4 +21,5 @@ module.exports = { LongIntCodec, BooleanCodec, StringCodec, + LongStringCodec, }; diff --git a/src/encoder/BinaryEncoder.js b/src/encoder/BinaryEncoder.js index 9b88b18..9de9599 100644 --- a/src/encoder/BinaryEncoder.js +++ b/src/encoder/BinaryEncoder.js @@ -8,6 +8,13 @@ export default class BinaryEncoder { */ static get binaryType() { return 'arraybuffer'; } + /** + * Reserved event names + * + * @return {String[]} + */ + static get reservedEvents() { return ['open', 'close', 'error']; } + /** * @param {Array} handlers */ @@ -20,6 +27,10 @@ export default class BinaryEncoder { handlers.forEach(([name, handler], index) => { handler.id = index; handler.name = name; + + if (this.constructor.reservedEvents.includes(name)) { + throw new Error(`"${name}" is a reserved event name.`); + } }); } diff --git a/src/encoder/JsonEncoder.js b/src/encoder/JsonEncoder.js index 3a41fc2..cdbcf21 100644 --- a/src/encoder/JsonEncoder.js +++ b/src/encoder/JsonEncoder.js @@ -6,6 +6,13 @@ export default class JsonEncoder { */ static get binaryType() { return 'blob'; } + /** + * Reserved event names + * + * @return {String[]} + */ + static get reservedEvents() { return ['open', 'close', 'error']; } + /** * Encode * @@ -15,6 +22,10 @@ export default class JsonEncoder { * @return {String} */ encode(name, data) { + if (this.constructor.reservedEvents.includes(name)) { + throw new Error(`"${name}" is a reserved event name.`); + } + return JSON.stringify({ name, data }); } diff --git a/src/encoder/codec/LongIntCodec.js b/src/encoder/codec/LongIntCodec.js index 36617aa..ea21c42 100644 --- a/src/encoder/codec/LongIntCodec.js +++ b/src/encoder/codec/LongIntCodec.js @@ -6,7 +6,7 @@ import Codec from './Codec'; */ export default class LongIntCodec extends Codec { /** - * @param {Number} byteLength} + * @param {Number} byteLength */ constructor(byteLength) { super(); diff --git a/src/encoder/codec/LongStringCodec.js b/src/encoder/codec/LongStringCodec.js new file mode 100644 index 0000000..69c570c --- /dev/null +++ b/src/encoder/codec/LongStringCodec.js @@ -0,0 +1,34 @@ +import Codec from './Codec'; + +/** + * String codec (limited to 65536 chars) + */ +export default class LongStringCodec extends Codec { + /** + * @type {Number} + */ + getByteLength(data) { + return 2 + data.length * 2; + } + + /** + * {@inheritdoc} + */ + encode(buffer, offset, data) { + const view = new DataView(buffer, offset, this.getByteLength(data)); + + view.setUint16(0, data.length); + + Array.from(data).forEach((letter, index) => view.setUint16(2 + (index * 2), letter.charCodeAt(0))); + } + + /** + * {@inheritdoc} + */ + decode(buffer, offset) { + const view = new DataView(buffer, offset); + const length = view.getUint16(0); + + return new Array(length).fill(null).map((value, index) => String.fromCharCode(view.getUint16(2 + index * 2))).join(''); + } +} diff --git a/src/encoder/codec/index.js b/src/encoder/codec/index.js index 2bdbd7e..2b06a36 100644 --- a/src/encoder/codec/index.js +++ b/src/encoder/codec/index.js @@ -5,6 +5,7 @@ import Int16Codec from 'netcode/src/encoder/codec/Int16Codec'; import Int32Codec from 'netcode/src/encoder/codec/Int32Codec'; import LongIntCodec from 'netcode/src/encoder/codec/LongIntCodec'; import StringCodec from 'netcode/src/encoder/codec/StringCodec'; +import LongStringCodec from 'netcode/src/encoder/codec/LongStringCodec'; module.exports = { Codec, @@ -14,4 +15,5 @@ module.exports = { Int32Codec, LongIntCodec, StringCodec, + LongStringCodec, }; diff --git a/src/server/Beacon.js b/src/server/Beacon.js new file mode 100644 index 0000000..d6948d9 --- /dev/null +++ b/src/server/Beacon.js @@ -0,0 +1,40 @@ +/** + * Send ping at fixed interval + */ +export default class Beacon { + /** + * @param {Socket} socket + * @param {Number} frequency Frequency in second + */ + constructor(socket, frequency = 30) { + this.socket = socket; + this.frequency = frequency * 1000; + this.interval = null; + + this.start = this.start.bind(this); + this.ping = this.ping.bind(this); + this.stop = this.stop.bind(this); + + this.socket.on('open', this.start); + this.socket.on('close', this.stop); + } + + start() { + if (!this.interval) { + this.interval = setInterval(this.ping, this.frequency); + } + } + + stop() { + if (this.interval) { + clearInterval(this.interval); + this.interval = null; + } + } + + ping() { + if (this.socket) { + this.socket.ping(); + } + } +} diff --git a/src/server/Client.js b/src/server/Client.js index a790abb..82ac80c 100644 --- a/src/server/Client.js +++ b/src/server/Client.js @@ -7,30 +7,25 @@ export default class Client extends EventEmitter { * @param {WebSocket} socket * @param {String} ip * @param {Encoder} encoder - * @param {Number} pingFrequency Ping frequency in milliseconds */ - constructor(socket, ip, encoder, pingFrequency = 0) { + constructor(socket, ip, encoder) { super(); this.id = ++INDEX; this.ip = ip; this.socket = socket; this.encoder = encoder; - this.pingInterval = null; + this.onOpen = this.onOpen.bind(this); this.onMessage = this.onMessage.bind(this); this.onError = this.onError.bind(this); this.onClose = this.onClose.bind(this); - this.ping = this.ping.bind(this); + this.socket.on('open', this.onOpen); this.socket.on('message', this.onMessage); this.socket.on('error', this.onError); this.socket.on('close', this.onClose); - if (pingFrequency > 0) { - this.socket.on('open', () => this.startPing(pingFrequency)); - } - this.socket.send = this.encoder.constructor.binaryType === 'arraybuffer' ? this.socket.binary : this.socket.text; this.socket.start(); @@ -56,30 +51,10 @@ export default class Client extends EventEmitter { } /** - * Start ping at given interval - * - * @param {Number} frequency Ping frequency in milliseconds + * On socket open */ - startPing(frequency) { - if (frequency) { - this.pingInterval = setInterval(this.ping, frequency); - } - } - - ping() { - if (this.socket) { - this.socket.ping(); - } - } - - /** - * Stop ping interval - */ - stopPing() { - if (this.pingInterval) { - clearInterval(this.pingInterval); - this.pingInterval = null; - } + onOpen() { + this.emit('open', this); } /** @@ -103,7 +78,8 @@ export default class Client extends EventEmitter { * @param {Event} event */ onError(event) { - console.error(`Client ${this.id}: `, event.message); + console.error(`[ERROR] In socket for client ${this.id}: `, event.message); + this.emit('error', new Error(event.message), this); this.close(); } @@ -111,7 +87,6 @@ export default class Client extends EventEmitter { * On close */ onClose() { - this.stopPing(); this.socket = null; this.emit('close', this); } diff --git a/src/server/Server.js b/src/server/Server.js index 542d87d..618fb02 100644 --- a/src/server/Server.js +++ b/src/server/Server.js @@ -3,21 +3,22 @@ import EventEmitter from 'events'; import WebSocket from 'websocket-driver'; import JsonEncoder from 'netcode/src/encoder/JsonEncoder'; import Client from 'netcode/src/server/Client'; +import Beacon from 'netcode/src/server/Beacon'; export default class Server extends EventEmitter { /** - * @param {Number} port Port - * @param {String} host Host - * @param {JsonEncoder|BinaryEncoder} encoder + * @param {Number} port Port to listen on + * @param {String} host Host to listen on + * @param {JsonEncoder|BinaryEncoder} encoder Encoder to use to read/write event messages * @param {Number} ping Ping frequency in seconds (0 for no ping) * @param {Number} maxLength Paquet max length in bit * @param {Array} protocols Supported protocols */ - constructor(port = 8080, host = 'localhost', encoder = new JsonEncoder(), ping = 0, maxLength = Math.pow(2, 9) - 1, protocols = ['websocket']) { + constructor(port = 8080, host = '0.0.0.0', encoder = new JsonEncoder(), ping = 30, maxLength = Math.pow(2, 9), protocols = ['websocket']) { super(); this.onUpgrade = this.onUpgrade.bind(this); - //this.onRequest = this.onRequest.bind(this); + this.onRequest = this.onRequest.bind(this); this.onError = this.onError.bind(this); this.removeClient = this.removeClient.bind(this); @@ -30,9 +31,9 @@ export default class Server extends EventEmitter { protocols, }; - this.server.on('error', this.onError); this.server.on('upgrade', this.onUpgrade); - //this.server.on('request', this.onRequest); + this.server.on('request', this.onRequest); + this.server.on('error', this.onError); this.start(port, host); } @@ -55,7 +56,7 @@ export default class Server extends EventEmitter { */ addClient(client) { this.clients.set(client.id, client); - client.addListener('close', this.removeClient); + client.on('close', this.removeClient); this.emit('client:join', client); } @@ -88,7 +89,11 @@ export default class Server extends EventEmitter { driver.io.write(body); socket.pipe(driver.io).pipe(socket); - this.addClient(new Client(driver, ip, this.encoder, this.ping * 1000)); + if (this.ping) { + new Beacon(driver, this.ping); + } + + this.addClient(new Client(driver, ip, this.encoder)); } /** @@ -97,19 +102,14 @@ export default class Server extends EventEmitter { * @param {Request} request * @param {Response} response */ - /*onRequest(request, response) { + onRequest(request, response) { switch (request.url) { - case '/': - response.writeHead(200, { 'Content-Type': 'application/json' }); - response.end(JSON.stringify(this.getStatus())); - break; - default: response.writeHead(404); response.end(); break; } - }*/ + } /** * On error diff --git a/src/server/index.js b/src/server/index.js index 01c6a5d..2854143 100644 --- a/src/server/index.js +++ b/src/server/index.js @@ -8,6 +8,7 @@ import Int32Codec from 'netcode/src/encoder/codec/Int32Codec'; import LongIntCodec from 'netcode/src/encoder/codec/LongIntCodec'; import BooleanCodec from 'netcode/src/encoder/codec/BooleanCodec'; import StringCodec from 'netcode/src/encoder/codec/StringCodec'; +import LongStringCodec from 'netcode/src/encoder/codec/LongStringCodec'; module.exports = { Server, @@ -20,4 +21,5 @@ module.exports = { BooleanCodec, LongIntCodec, StringCodec, + LongStringCodec, }; From f4918b7889fc5d206661aefc204a4e6e6bff6a47 Mon Sep 17 00:00:00 2001 From: Thomas Jarrand Date: Sun, 11 Nov 2018 17:07:31 +0100 Subject: [PATCH 2/2] Split doc --- README.md | 212 ++++++++++------------------------------------- doc/API.md | 79 ++++++++++++++++++ doc/codecs.md | 54 ++++++++++++ doc/packaging.md | 89 ++++++++++++++++++++ doc/ssl.md | 39 +++++++++ 5 files changed, 305 insertions(+), 168 deletions(-) create mode 100644 doc/API.md create mode 100644 doc/codecs.md create mode 100644 doc/packaging.md create mode 100644 doc/ssl.md diff --git a/README.md b/README.md index 841f4a2..cbd13d2 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,29 @@ Netcode ======= -A simple JavaScript client & server binary-encoded websocket communication system for web video games. +> A simple JavaScript client & server binary-encoded websocket communication system aimed towards web video games development. -## Installation - -`npm add netcode` +Features: +- 🔌 Server / Client duo, for node and the browser, that handle Websocket connection and communication. +- ⚡️ Handle the binary encoding and decoding of your data, with performances in mind. +- 📢 Listen for event dispatched over websocket with simple `on`/`off` event emitter system. +- 💬 Fallback to JSON for easy debugging. +- 🗜[COMMING SOON] set a tick-rate and group events for lesser data consumption. ## Requirements - Node >= v8.0.0 -## Usage +## Installation + +`npm add netcode` + +## Get started -### Define an encoder (common to server and client) +### Define a list of events The server and the client __must__ share the same events. -An events is defined by its _unique_ name and the corresponding codec, responsible for encoding and decoding the data. +An event is defined by its _unique_ name and the corresponding codec, responsible for encoding and decoding the data. ```javascript // events.js @@ -24,63 +31,60 @@ import Int8Codec from 'netcode/src/encoder/codec/Int8Codec'; import StringCodec from 'netcode/src/encoder/codec/StringCodec'; export default [ - ['id', new Int8Codec()], - ['say', new StringCodec()], + ['id', new Int8Codec()], + ['say', new StringCodec()], ]; ``` -These encoder configuration allow you to send the following events over websocket: +In this example, the event list define how to send the following events over websocket: + +- `client.send('id', 255);` +- `client.send('say', 'Hello world!');` + +Then you'll be able to listen to this events on the client as follow: -- `send('id', 255);` -- `send('say', 'Hello world!');` +- `client.on('id', id => { /* Do something */ });` +- `client.on('say', sentence => { /* Do something */ });` -### Setup a Server (server-side / node) +Now let's create a server and an client that use this event list. -We setup a server specifying the port and host that should be listen on and the encoder to use. Here we use a BinaryEncoder to communicate in binary over websocket, with the previously configured event list. +### Setup a Server + +We setup a server specifying the port and host on which the server will listen and the type of encoder to use. +Here we use a BinaryEncoder to communicate in binary over websocket, with the previously configured event list. ```javascript import Server from 'netcode/src/server/Server'; import BinaryEncoder from 'netcode/src/server/BinaryEncoder'; import events from './envents'; -// Listen on ws://localhost:8080 +// Listen on localhost:8080 const server = new Server(8080, 'localhost', new BinaryEncoder(events)); server.on('client:join', client => { - client.on('say', sentence => console.log(sentence)); - client.send('id', client.id); + client.on('say', sentence => console.log(sentence)); + client.send('id', client.id); }); ``` -Now we've got a server running at `ws://localhost:8080/` that listen for a `say` text event and send a `id` integer event to every client that connects. - -Server parameters: - -| Parameter | Type | Default value | Description | -| --------- | ------------------------------ | ----------------- | ---------------------------------------------------- | -| port | _Number_ | 8080 | Port to listen on. | -| host | _String_ | 0.0.0.0 | Host to listen on. | -| encoder | _BinaryEncoder \| JsonEncoder_ | new JsonEncoder() | Encoder to use to read/write event messages. | -| ping | _Number_ | 30 | Ping frequency in seconds (0 for no ping). | -| maxLength | _Number_ | 512 | Paquet max length in bit (should be a power of two). | -| protocols | _Array String[]_ | `['websocket']` | Protocols tu use | +Now we've got a server running at `localhost:8080` that listen for a `say` text event and send a `id` integer event to every client that connects. _See an [full example of server setup](demo-server.js)._ -### Write a Client (browser side) +### Write a Client -Now we write a client for the browser that connects to `localhost:8080` and use a BinaryEncoder with the same event configuration as the server. +Now we write a client, for the browser, that connects to our running server on `ws://localhost:8080` and use a BinaryEncoder with the same event list as the server. ```javascript import Client from 'netcode/src/client/Client'; import BinaryEncoder from 'netcode/src/client/BinaryEncoder'; import events from './envents'; -const client = new Client('localhost:8080', new BinaryEncoder(events)) +const client = new Client('ws://localhost:8080', new BinaryEncoder(events)) client.on('open', () => { - client.on('id', id => console.log(`My id is ${id}.`)); - client.send('say', 'Hello world!'); + client.on('id', id => console.log(`My id is ${id}.`)); + client.send('say', 'Hello world!'); }); ``` @@ -90,139 +94,11 @@ Connection is alive and well! _See an [full example of client setup](demo-client.js)._ -## Events - -You can interface your code with the Server and both Client objects through the designated events and the `on(event, callback)` and `off(event, callback)` methods. - -_Note: If you're on node `< 10.0.0`, use `removeListener` method instead of `off`_ - -__Server events:__ - -| Name | Callback parameters | Description | -|---|---|---| -| `ready` | | Server is listening and ready to accept connections. | -| `client:join` | client _Client_ | New connected client. | -| `client:leave` | client _Client_ | Client left. | -| `error` | error _Error_ | An error occured. | - -__Client events:__ - -| Name | Callback parameters | Description | -|---|---|---| -| `open` | client _Client_ | Client connection is open and ready to transmit. | -| `error` | error _Error_, client _Client_ | An error occurred. | -| `close` | client _Client_ | Client connection is closed. | - -## Codecs - -Codecs are responsible for encoding your events data into ArrayBuffer that will be transmitted over the binary websocket connection and decoded back into usable data on the other side. - -You will probably need to write your own codecs but a few standard needs are alreay adressed by the simple codecs that follow: - -###Packaged codecs - -Packaged codecs are available in the `netcode/server` and `netcode/client` packages or as source code in `netcode/src/encoder/codec` folder (see [Note on packaging](#Note on packaging)) - -| Class | Data format | Example | Size (in byte) | -| -------------------------- | ---------------------------------- | ------------------------------------------------------------ | ----------------------- | -| `Codec` | No data (just send the event name) | `['pause', new Codec()]`
`send('pause')` | 0 | -| `BooleanCodec` | `true|false` | `['active', new BooleanCodec()]`
`send('active', true)` | 1 | -| `StringCodec` | String up to 255 characters | `['player:name', new StringCodec()]`
`send('player:name', 'DarkShadow73')` | 1 + (String length * 2) | -| `LongStringCodec` | String up to 65536 characters | `['url', new LongStringCodec()]`
`send('url', 'https://my.long.url/hash/xxx...')` | 2 + (String length * 2) | -| `Int8Codec` | Integer from 0 to 255 | `['id', new Int8Codec()]`
`send('id', 42)` | 1 | -| `Int16Codec` | Integer from 0 to 65536 | `['score', new Int16Codec()]`
`send('score', 9999)` | 2 | -| `Int32Codec` | Integer from 0 to 4294967295 | `['position', new Int32Codec()]`
`send('position', 4294967295)` | 4 | -| `LongIntCodec(byteLength)` | Integer encoded as string | `['timestamp', new LongIntCodec(13)`]
`send('timestamp', Date.now())` | byteLength | - -## Note on packaging - -Netcode provides you with 2 pre-packaged versions of the library for Node and for the Browser, so you can use it out of the box. You can also import the ES6 source code directly and manage the packaging yourself. - -Let's see an example of both these setups: - -### Using the pre-packaged libraries -On the server-side (node): - -```javascript -const { - Server, - BinaryEncoder, - Int16Codec, - BooleanCodec, - StringCodec, - // ... -} = require('netcode/server');` -``` - -And in the browser as well: - -```html - - - - - -``` - -### Using the ES6 source code - -In your Webpack configuration, include the source code of Netcode so that Babel will compile this ES6 code as well. - -```javascript -module.exports = { - //... - module: { - rules: [{ - // ... - include: '/node_modules/netcode/src/', - }] - } -}; -``` - -_See a [full Webpack/Babel config example](doc/webpack.config.js)._ - -Now you can import source file from Netcode directly in your code and it will be compiled as well : - -```javascript -// src/server.js -import Server from 'netcode/src/server/Server'; -import BinaryEncoder from 'netcode/src/server/BinaryEncoder'; // Import from src/server! -import BooleanCodec from 'netcode/src/encoder/codec/BooleanCodec'; - -new Server( - 8080, - 'localhost', - new BinaryEncoder([ - ['foo', new BooleanEncoder()] - ]) -); -``` - -```javascript -// src/client.js -import Client from 'netcode/src/client/Client'; -import BinaryEncoder from 'netcode/src/client/BinaryEncoder'; // Import from src/client! -import BooleanCodec from 'netcode/src/encoder/codec/BooleanCodec'; - -new Client( - 'ws://localhost:8080', - new BinaryEncoder([ - ['foo', new BooleanCodec()] - ]) -); -``` +## Complete documentation -## Debugging +To go further, see in-depth documentation and how-to's. -The BinaryEncoder should emit errors if anything wrong happen durring encoding or decoding, but even so, working with binary data can be tricky. -You can replace your `BinaryEncoder` with a `JsonEncoder` at any time without any other change to you code to switch to a JSON communication and to help you find out what's wrong with transmitted data. \ No newline at end of file +- [Full API reference](doc/API.md). +- [Default codecs and how-to write your own](doc/codecs.md). +- [About packaging and setting up your own webpack configuration](doc/packaging.md). +- [Use netcode over a custom domain name and/or secured SSL connection](doc/ssl.md). diff --git a/doc/API.md b/doc/API.md new file mode 100644 index 0000000..ec01b30 --- /dev/null +++ b/doc/API.md @@ -0,0 +1,79 @@ +## Server + +### Constructor + +The `Server` class takes the following arguments: + +| Parameter | Type | Default value | Description | +| --------- | ------------------------------ | ----------------- | ---------------------------------------------------- | +| port | _Number_ | 8080 | Port to listen on. | +| host | _String_ | 0.0.0.0 | Host to listen on. | +| encoder | _BinaryEncoder \| JsonEncoder_ | new JsonEncoder() | Encoder to use to read/write event messages. | +| ping | _Number_ | 30 | Ping frequency in seconds (0 for no ping). | +| maxLength | _Number_ | 512 | Paquet max length in bit (should be a power of two). | +| protocols | _Array String[]_ | `['websocket']` | Protocols tu use | + +### Methods + +#### on(name, callback) + +Listen for event. + +- `name {String}` The name of the event to listen to. +- `callback {Function}` The callback to execute when the event occure. + + +#### off(name, callback) + +Remove listener for this event / callback. + +_Note: If you're on node `< 10.0.0`, use `removeListener` method instead of `off`_ + +### Events + +| Name | Callback parameters | Description | +|------|---------------------|---| +| `ready` | | Server is listening and ready to accept connections. | +| `client:join` | client _Client_ | New connected client. | +| `client:leave` | client _Client_ | Client left. | +| `error` | error _Error_ | An error occured. | + +## Client + +### Methods + +#### send(name, data) + +Send data to the other end of the Websocket. + +- `name {String}` The name of the event (must be one from the list passed to the BinaryEncoder). +- `data {Number|String|Boolean|Object}` Any data handled by the corresponding Codec. + +#### close() + +Close the connexion. + +#### on(name, callback) + +Listen for event. + +- `name {String}` The name of the event to listen to. +- `callback {Function}` The callback to execute when the event occure. + + +#### off(name, callback) + +Remove listener for this event / callback. + +_Note: If you're on node `< 10.0.0`, use `removeListener` method instead of `off`_ + +### Events + +| Name | Callback parameters | Description | +|---|---|---| +| `open` | client _Client_ | Client connection is open and ready to transmit. | +| `error` | - error _Error_
- client _Client_ | An error occurred. | +| `close` | client _{Client_} | Client connection is closed. | +| * | - eventData _{Number\|String\|Boolean\|Object}_
- client _Client_ | Every event sent through the websocket pipe will emit an event on the other end of the socket. | + +_Note: `open`, `error` and `close` are reserved event names and the Encoder will throw an exeption if you define a custom event with either of these names._ \ No newline at end of file diff --git a/doc/codecs.md b/doc/codecs.md new file mode 100644 index 0000000..985e88a --- /dev/null +++ b/doc/codecs.md @@ -0,0 +1,54 @@ +# Codecs + +Codecs are responsible for encoding your events data into ArrayBuffer that will be transmitted over the binary websocket connection and decoded back into usable data on the other side. + +You will probably need to write your own codecs but a few standard needs are alreay adressed by the simple codecs that follow: + +## Packaged codecs + +Packaged codecs are available in the `netcode/server` and `netcode/client` packages or as source code in `netcode/src/encoder/codec` folder (see [Note on packaging](#Note on packaging)) + +| Class | Data format | Example | Size (in byte) | +| -------------------------- | ---------------------------------- | ------------------------------------------------------------ | ----------------------- | +| `Codec` | No data (just send the event name) | `['pause', new Codec()]`
`send('pause')` | 0 | +| `BooleanCodec` | `true\|false` | `['active', new BooleanCodec()]`
`send('active', true)` | 1 | +| `StringCodec` | String up to 255 characters | `['player:name', new StringCodec()]`
`send('player:name', 'DarkShadow73')` | 1 + (String length * 2) | +| `LongStringCodec` | String up to 65536 characters | `['url', new LongStringCodec()]`
`send('url', 'https://my.long.url/hash/xxx...')` | 2 + (String length * 2) | +| `Int8Codec` | Integer from 0 to 255 | `['id', new Int8Codec()]`
`send('id', 42)` | 1 | +| `Int16Codec` | Integer from 0 to 65536 | `['score', new Int16Codec()]`
`send('score', 9999)` | 2 | +| `Int32Codec` | Integer from 0 to 4294967295 | `['position', new Int32Codec()]`
`send('position', 4294967295)` | 4 | +| `LongIntCodec(byteLength)` | Integer encoded as string | `['timestamp', new LongIntCodec(13)`]
`send('timestamp', Date.now())` | byteLength | + +## Custom codecs + +Some of your events will certainly have more complex data structure that just one of the above single scalar example. Good news is you can write your own codecs! + +Your custom codec must extends the `Codec` class of Netcode and implement the 3 following methods: + +- `getByteLength(data)`: return the number of byte in the event for the given data (your event byte length may vary over the data, like for abitrary strings for example). +- `encoder(buffer, offset, data)` encode the given data into the provided buffer, starting at the given offset (in byte). +- `decode(buffer, offset)`: read and return the data contained in the provided buffer, starting at the given offset. + +Let's say we want to send an event with the given format: + +`send('position', { id: player.id, x: 12345, y: 2354 });` + +We'll need to rite a codec that can transmit these values in an ArrayBuffer. + +For this example, I'm gonna chose to encode: + +- The player ID as an unsigned integer on 1 byte (UInt8) +- The x and y position on 2 byte each (Uint16) + +So the byte length of my event is fixed: 1 + 2 + 2 = 5 bytes. Let implement the `getByteLengthMethod` + +``` +getByteLength() { + return Uint8Array.BYTES_PER_ELEMENT + Uint16Array.BYTES_PER_ELEMENT * 2; +} +``` + +## Debugging + +The BinaryEncoder should emit errors if anything wrong happen durring encoding or decoding, but even so, working with binary data can be tricky. +You can replace your `BinaryEncoder` with a `JsonEncoder` at any time without any other change to you code to switch to a JSON communication and to help you find out what's wrong with transmitted data. diff --git a/doc/packaging.md b/doc/packaging.md new file mode 100644 index 0000000..804239e --- /dev/null +++ b/doc/packaging.md @@ -0,0 +1,89 @@ +# Note on packaging + +Netcode provides you with 2 pre-packaged versions of the library for _Node_ and for the _browser_, so you can use it out of the box. +You can also _import the ES6 source code_ directly and manage the packaging yourself with a tool like Webpack. + +Let's see an example of both these setups: + +## Using the pre-packaged libraries + +On the server-side (node): + +```javascript +const { + Server, + BinaryEncoder, + Int16Codec, + BooleanCodec, + StringCodec, + // ... +} = require('netcode/server');` +``` + +And in the browser as well: + +```html + + + + + +``` + +## Using the ES6 source code + +Configure Webpack to include the source code of _netcode_ so that Babel will compile this ES6 code as well. + +```javascript +module.exports = { + //... + module: { + rules: [{ + // ... + include: '/node_modules/netcode/src/', + }] + } +}; +``` + +_See a [full Webpack/Babel config example](doc/webpack.config.js)._ + +Now you can import source file from _netcode_ directly in your sources and it will be compiled as well: + +```javascript +// src/server.js +import Server from 'netcode/src/server/Server'; +import BinaryEncoder from 'netcode/src/server/BinaryEncoder'; // Import from src/server! +import BooleanCodec from 'netcode/src/encoder/codec/BooleanCodec'; + +new Server( + 8080, + 'localhost', + new BinaryEncoder([ + ['foo', new BooleanEncoder()] + ]) +); +``` + +```javascript +// src/client.js +import Client from 'netcode/src/client/Client'; +import BinaryEncoder from 'netcode/src/client/BinaryEncoder'; // Import from src/client! +import BooleanCodec from 'netcode/src/encoder/codec/BooleanCodec'; + +new Client( + 'ws://localhost:8080', + new BinaryEncoder([ + ['foo', new BooleanCodec()] + ]) +); +``` diff --git a/doc/ssl.md b/doc/ssl.md new file mode 100644 index 0000000..a90e3d6 --- /dev/null +++ b/doc/ssl.md @@ -0,0 +1,39 @@ +# SSL and custom hostname + +In production, you'll want your server to be run behind a real domain name and possibly on a secured SSL connection. + +So instead of connecting the `ws://localhost:8000`, you'll want something like `wss://my-game.io/live/`. + +Here's how to configure an Nginx proxy server to do just that: + +- Server setup : `new Server(8042, 'localhost', new BinaryEncoder(events));` +- Client setup : `new Client('wss://my-game.io/live/', new BinaryEncoder(events))` +- Nginx configuration : + +```nginx +server { + listen 443 ssl http2; + server_name my-game.io; + root /home/tom32i/my-game/client; + # ... the rest of the server and SSL configuration + + location /live/ { + # Proxy for the running node server + proxy_pass http://localhost:8042/;# The actual node server adress + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_connect_timeout 60s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + } + + location / { + # Serve the static HTML/JS/CSS assets + index index.html; + } +} +```