Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 92 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,104 @@
Netcode
=======

A simple 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.

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

## Installation

`npm add netcode`

## Usage
## Get started

### Define a list of events

The server and the client __must__ share the same events.
An event 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()],
];
```

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:

- `client.on('id', id => { /* Do something */ });`
- `client.on('say', sentence => { /* Do something */ });`

Now let's create a server and an client that use this 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 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 `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

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('ws://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!

### Server side (node)
_See an [full example of client setup](demo-client.js)._

See an [example of server setup](demo-server.js).
## Complete documentation

### Client side (browser)
To go further, see in-depth documentation and how-to's.

See an [example of client setup](demo-client.js).
- [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).
15 changes: 8 additions & 7 deletions demo-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)],
Expand All @@ -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();

Expand All @@ -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);
Expand All @@ -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.');
});
});
11 changes: 5 additions & 6 deletions demo-server.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)],
Expand All @@ -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);
Expand All @@ -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!');
Expand All @@ -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);
});

Expand Down
79 changes: 79 additions & 0 deletions doc/API.md
Original file line number Diff line number Diff line change
@@ -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_<br />- client _Client_ | An error occurred. |
| `close` | client _{Client_} | Client connection is closed. |
| * | - eventData _{Number\|String\|Boolean\|Object}_<br />- 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._
54 changes: 54 additions & 0 deletions doc/codecs.md
Original file line number Diff line number Diff line change
@@ -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()]`<br />`send('pause')` | 0 |
| `BooleanCodec` | `true\|false` | `['active', new BooleanCodec()]`<br />`send('active', true)` | 1 |
| `StringCodec` | String up to 255 characters | `['player:name', new StringCodec()]`<br />`send('player:name', 'DarkShadow73')` | 1 + (String length * 2) |
| `LongStringCodec` | String up to 65536 characters | `['url', new LongStringCodec()]`<br />`send('url', 'https://my.long.url/hash/xxx...')` | 2 + (String length * 2) |
| `Int8Codec` | Integer from 0 to 255 | `['id', new Int8Codec()]`<br />`send('id', 42)` | 1 |
| `Int16Codec` | Integer from 0 to 65536 | `['score', new Int16Codec()]`<br />`send('score', 9999)` | 2 |
| `Int32Codec` | Integer from 0 to 4294967295 | `['position', new Int32Codec()]`<br />`send('position', 4294967295)` | 4 |
| `LongIntCodec(byteLength)` | Integer encoded as string | `['timestamp', new LongIntCodec(13)`]<br />`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.
Loading