Version 3 of geckos.io is based on node-datachannel@0.5.x and only supports ESM and Node.js >=16. There are no other breaking changes.
Version 2 has huge performance improvements. I switched from wrtc to node-datachannel, which is much lighter and faster compared to wrtc.
Geckos.io is now shipped as ECMAScript modules and will only support Node.js ^14.15 and >=16
.
npm i @geckos.io/client @geckos.io/server
Want to know more? Join the discussions!
-
People who have never build a multiplayer game, should probably use a library like socket.io instead, since there are way more examples/tutorial available.
Socket.io and geckos.io use a similar API. The switch from socket.io to geckos.io should be easy. -
People who have no experiences setting up their own servers with UDP port forwarding, should probably look for a simple solution like websocket, although it is slower.
- What is it made for?
- Getting Started
- Changelog
- Documentation
- And some more things at the end of this file.
It's designed specifically for your HTML5 real-time multiplayer games by lowering the average latency and preventing huge latency spikes. It allows you to communicate with your node.js server via UDP, which is much faster than TCP (used by WebSocket). Take a look at the comparison video between UDP and TCP. https://youtu.be/ZEEBsq3eQmg
First things first, install it via npm:
npm install @geckos.io/client @geckos.io/server
And now, read the Documentation.
Btw, make sure you also check out enable3d.io.
When true (default), the first available port in the port range will be used for all connections, instead of assigning a new port for each connection.
Thanks to @arthuro555 and @paullouisageneau.
// server.js
const io = geckos({
multiplex: true // default
})
You can now pass a more complex url to the client when you set port to null
. This is useful if, for example, you use the geckos.io server behind a proxy.
// client.js
// default is
const channel = geckos({
url: `${location.protocol}//${location.hostname}`,
port: 9208
})
// connect to http://1.2.3.4:3000
const channel = geckos({
url: 'http://1.2.3.4',
port: 3000
})
// connect to https://geckos.example.com:9208
const channel = geckos({
url: 'https://geckos.example.com',
port: 9208 // not required, since 9208 is the default
})
// connect to https://api.example.com:3000/geckos
const channel = geckos({
url: 'https://api.example.com:3000/geckos',
port: null
})
// connect to https://api.example.com/geckos
const channel = geckos({
url: 'https://api.example.com/geckos',
port: null
})
Allows you to set a custom port range for the WebRTC connection.
// server.js
const io = geckos({
portRange: {
min: 10000,
max: 20000
}
})
You now have access to the connections manager.
// get any channel by its ID via the connectionsManager
const connection = io.connectionsManager.getConnection(channel.id)
if (connection) {
// here, you could emit a message ...
connection.channel.emit('chat message', 'You have been kicked for cheating!')
// ... or close the channel
connection.channel.close()
}
Finally you can send rawMessages from the io scope.
server
// emit a raw message to all channels
io.raw.emit(rawMessage)
// emit a raw message to a specific room
io.raw.room('roomId').emit(rawMessage)
The client is now able to send a authorization header with the connection request. If the authorization fails, the server will respond with 401 (unauthorized).
Whatever you add to the option authorization (must be a string) will be sent as a Authorization request header. You could, for example, send Basic base64-encoded credentials, Bearer tokens or a simple string, as in the example below.
Read more about HTTP authentication here: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization.
const username = 'Yannick'
const password = '12E45'
const auth = `${username} ${password}` // 'Yannick 12E45'
const channel = geckos({ authorization: auth })
channel.onConnect(error => {
if (error) {
console.error('Status: ', error.status)
console.error('StatusText: ', error.statusText)
}
console.log(channel.userData) // { username: 'Yannick', level: 13, points: 8987 }
})
const io: GeckosServer = geckos({
/**
* A async function to authenticate and authorize a user.
* @param auth The authentication token
* @param request The incoming http request
* @param response The outgoing http response
*/
authorization: async (auth: string | undefined, request: http.IncomingMessage, response: http.OutgoingMessage) => {
const token = auth.split(' ') // ['Yannick', '12E45']
const username = token[0] // 'Yannick'
const password = token[1] // '12E45'
// Use "request.connection.remoteAddress" to get the users ip.
// ("request.headers['x-forwarded-for']" if your server is behind a proxy)
// add a custom response header if you want
response.setHeader(
'www-authenticate',
'Bearer realm="example", error="invalid_token", error_description="The access token expired"'
)
// reach out to a database if needed (this code is completely fictitious)
const user = await database.getByName(username)
// whatever you return here, will later be accessible via channel.userData to authenticate the user
if (user.username === username && user.password === password)
return { username: user.username, level: user.level, points: user.points }
// if you return true, you will authorize the connection, without adding any data to channel.userData
return true
// if you return false, the server will respond with 401 (unauthorized)
return false
// if you return a number between 100 and 599, the server will respond with the respective HTTP status code
return 400 // will return 400 (Bad Request)
return 404 // will return 404 (Not Found)
return 500 // will return 500 (Internal Server Error)
// and so on ...
},
cors: { allowAuthorization: true } // required if the client and server are on separate domains
})
io.onConnection((channel: ServerChannel) => {
console.log(channel.userData) // { username: 'Yannick', level: 13, points: 8987 }
})
By default the RTCDataChannel queues data if it can't be send directly. This is very bad for multiplayer games, since we do not want to render old state. In version 1.5.0, the option autoManageBuffering was added. It is set to true by default. If autoManageBuffering is on, Geckos.io will prefer to drop messages instead of adding them to the send queue. (Messages with the option { reliable: true }, will still be added to the queue)
If you send 30Kbytes @60fps and the client only has a 10Mbit connection, he can never receive all messages. So it is necessary to drop some of them, which will be done automatically with autoManageBuffering.
Another good solution to this problem would be to decrease the send rate for that specific client. Use the new channel.onDrop(drop => {})
method to track dropped messages. If, for example, you notice that 20% of the messages for a specific client are dropped, decrease the send rate.
import geckos from '@geckos.io/client'
// or add a minified version to your index.html file
// https://github.com/geckosio/geckos.io/tree/master/bundles
const channel = geckos({ port: 3000 }) // default port is 9208
channel.onConnect(error => {
if (error) {
console.error(error.message)
return
}
channel.on('chat message', data => {
console.log(`You got the message ${data}`)
})
channel.emit('chat message', 'a short message sent to the server')
})
import geckos from '@geckos.io/server'
const io = geckos()
io.listen(3000) // default port is 9208
io.onConnection(channel => {
channel.onDisconnect(() => {
console.log(`${channel.id} got disconnected`)
})
channel.on('chat message', data => {
console.log(`got ${data} from "chat message"`)
// emit the "chat message" data to all channels in the same room
io.room(channel.roomId).emit('chat message', data)
})
})
- Geckos does not run on
http://localhost:PORT/
? Tryhttp://127.0.0.1:PORT/
instead. - If server listener is listening but never establishes a connection, that might be due to your machine not exposing OPENSSL environment variables (see #260). To add them try the following:
Exposing OPENSSL environment variables
- Find the path to OpenSSL:
brew --prefix openssl
- Set the environment variables. You can set the environment variables in your shell by adding the following lines to your shell profile file (usually ~/.bash_profile, ~/.bashrc, or ~/.zshrc for Zsh):
export OPENSSL_ROOT_DIR=/usr/local/opt/openssl@1.1
export OPENSSL_CRYPTO_LIBRARY=/usr/local/opt/openssl@1.1/lib
export OPENSSL_INCLUDE_DIR=/usr/local/opt/openssl@1.1/include
Replace /usr/local/opt/openssl@1.1
with the path you got from the second step. Then, save the file and restart your terminal for the changes to take effect.
- Check that the environment variables are set (maybe you'll need to restart your terminal):
echo $OPENSSL_ROOT_DIR
echo $OPENSSL_CRYPTO_LIBRARY
echo $OPENSSL_INCLUDE_DIR
- Done β
Here a list of available methods.
// import geckos.io client
import geckos from '@geckos.io/client'
/**
* start geckos client with these options
* @param options.iceServers Default: []
* @param options.iceTransportPolicy Default: 'all'
* @param options.label Default: 'geckos.io'
* @param options.port default is 9208
* @param options.url default is `${location.protocol}//${location.hostname}`
*/
const channel = geckos(options)
// the channel's id and maxMessageSize (in bytes)
const { id, maxMessageSize } = channel
// once the channel is connected to the server
channel.onConnect(error => {
if (error) console.error(error.message)
// listens for a disconnection
channel.onDisconnect(() => {})
// listens for a custom event from the server
channel.on('chat message', data => {})
// emits a message to the server
channel.emit('chat message', 'Hi!')
// closes the WebRTC connection
channel.close()
})
// import geckos.io server
import geckos from '@geckos.io/server'
/**
* start geckos server with these options
* @param options.authorization The async authorization callback
* @param options.autoManageBuffering By default, geckos.io manages RTCDataChannel buffering for you. Default 'true'
* @param options.cors Set the CORS options.
* @param options.cors.allowAuthorization Required if the client and server are on separate domains. Default: false
* @param options.cors.origin String OR (req: http.IncomingMessage) => string. Default '*'
* @param options.iceServers An array of RTCIceServers. See https://developer.mozilla.org/en-US/docs/Web/API/RTCIceServer.
* @param options.iceTransportPolicy RTCIceTransportPolicy enum defines string constants which can be used to limit the transport policies of the ICE candidates to be considered during the connection process.
* @param options.bindAddress If defined, bind only to the given local address. Default: undefined
* @param options.label A human-readable name for the channel. This string may not be longer than 65,535 bytes. Default: 'geckos.io'.
* @param options.maxPacketLifeTime The maximum number of milliseconds that attempts to transfer a message may take in unreliable mode. While this value is a 16-bit unsigned number, each user agent may clamp it to whatever maximum it deems appropriate. Default: undefined.
* @param options.maxRetransmits The maximum number of times the user agent should attempt to retransmit a message which fails the first time in unreliable mode. While this value is a16-bit unsigned number, each user agent may clamp it to whatever maximum it deems appropriate. Default: 0.
* @param options.ordered Indicates whether or not messages sent on the RTCDataChannel are required to arrive at their destination in the same order in which they were sent (true), or if they're allowed to arrive out-of-order (false). Default: false.
* @param options.portRange Custom port range for the WebRTC connection (available in >= v1.7.0)
* @param options.portRange.min Default: 1025
* @param options.portRange.max Default: 65535
* @param options.multiplex When true (default), the first available port in the port range will be used for all connections, instead of assigning a new port for each connection.
*/
io = geckos(options)
/**
* make the server listen on a port
* @param {number} port default port is 9208
*/
io.listen()
// whenever a new channel is connected
io.onConnection(channel => {
// the channel's id and maxMessageSize (in bytes)
const { id, maxMessageSize } = channel
// whenever the channel got disconnected
// the reason will be 'disconnected', 'failed' or 'closed'
channel.onDisconnect(reason => {})
// listen for a custom event
channel.on('chat message', data => {})
// channel joins a room
channel.join('someRoomId')
// channel leaves a room
channel.leave()
// channel closes the webRTC connection
channel.close()
// get notified when a message got dropped
channel.onDrop(drop => {})
// will trigger a specific event on all channels in a
// specific room and add the senderId as a second parameter
channel.forward(channel.roomId).emit('chat message', 'Hello!')
// listen for a forwarded message
channel.on('chat message', (data, senderId) => {
// we know that the message was forwarded if senderId is defined
if (senderId) {
// ...
} else {
// ...
}
})
// emits a message to the channel
channel.emit('chat message', 'Hello to myself!')
// emits a message to all channels, in the same room
channel.room.emit('chat message', 'Hello everyone!')
// emits a message to all channels, in the same room, except sender
channel.broadcast.emit('chat message', 'Hello friends!')
// emits a message to all channels
io.emit('chat message', 'Hello everyone!')
// emits a message to all channels in a specific room
// (if you do not pass a roomId, the message will be sent to everyone who is not in a room yet)
io.room(roomId).emit('chat message', 'Hello everyone!')
})
Note: The following event names are reserved:
sendOverDataChannel
receiveFromDataChannel
disconnected
disconnect
connection
connect
error
dataChannelIsOpen
sendToRoom
sendToAll
forwardMessage
broadcastMessage
rawMessage
dropped
You can send and receive USVString
, ArrayBuffer
and ArrayBufferView
using rawMessages.
client
// emit a raw message to the server
channel.raw.emit(rawMessage)
server
// emit a raw message to all channels
io.raw.emit(rawMessage)
// emit a raw message to a specific room
io.raw.room('roomId').emit(rawMessage)
// emit a raw message to the channel
channel.raw.emit(rawMessage)
// emit a raw message to all users in the same room
channel.raw.room.emit(rawMessage)
// broadcast a raw message
channel.raw.broadcast.emit(rawMessage)
// listen for a raw message
channel.onRaw(rawMessage => {})
All emit function can send reliable message if needed. This is NOT meant to be used as the default. Just use it to send important messages back and forth.
It works by simply transferring multiple messages after each other. The receiver will simply reject a message if it has already been processed.
channel.emit(
'end of game',
{
points: 147,
time: 650,
achievements: ['crucial_hit', 'golden_trophy']
},
{
// Set the reliable option
// Default: false
reliable: true,
// The interval between each message in ms (optional)
// Default: 150
interval: 150,
// How many times the message should be sent (optional)
// Default: 10
runs: 10
}
)
import geckos from '@geckos.io/server'
const io = geckos()
io.onConnection( channel => { ... })
io.listen(3000) // default port is 9208
import geckos from '@geckos.io/server'
import http from 'http'
const server = http.createServer()
const io = geckos()
io.addServer(server)
io.onConnection( channel => { ... })
// make sure the client uses the same port
// @geckos.io/client uses the port 9208 by default
server.listen(3000)
import geckos from '@geckos.io/server'
import http from 'http'
import express from 'express'
const app = express()
const server = http.createServer(app)
const io = geckos()
io.addServer(server)
io.onConnection( channel => { ... })
// make sure the client uses the same port
// @geckos.io/client uses the port 9208 by default
server.listen(3000)
You have to make sure you deploy it to a server which forwards all traffic on ports 9208/tcp (or another port you define) and 1025-65535/udp to your application.
Port 9208/tcp (or another port you define) is used for the peer signaling. The peer connection itself will be on a random port between 1025-65535/udp.
Geckos.io provides a default list of ICE servers for testing. In production, you should probably use your own STUN and TURN servers.
import geckos, { iceServers } from '@geckos.io/server'
// use an empty array if you are developing locally
// use the default iceServers if you are testing it on your server
const io = geckos({ iceServers: null, TESTING_LOCALLY ? [] : iceServers })
Watch a useful video about ICE Servers on YouTube.
Geckos.io is written in TypeScript. If you import geckos.io with the import
statement, the types will be imported as well.
// client.js
import geckos, { Data } from '@geckos.io/client'
const channel = geckos({ url: 'YOUR_SERVER_URL' })
channel.onConnect(() => {
channel.on('chat message', (data: Data) => {
// ...
})
})
// server.js
import geckos, { Data } from '@geckos.io/server'
const io = geckos()
io.onConnection(channel => {
channel.on('chat message', (data: Data) => {
// ...
})
})
See the docker example.
- Simple Chat App Example
- Geckos v2 cjs example
- Geckos on Docker example
- Multiplayer Game with phaser.io
TODO: Note some differences here.
socket.io | geckos.io | peerjs | |
---|---|---|---|
Real-Time Multiplayer Game (with authoritative server) |
β | ||
Real-Time Multiplayer Game (without authoritative server) |
β | ||
Turn based Multiplayer Game (with authoritative server) |
β | ||
Turn based Multiplayer Game (without authoritative server) |
β | β | |
Chat App | β | β | |
Any other App with Real-Time communication | β | β | β |
For now WebRTC in the best way to send fast (unordered and unreliable) messages between browser and server. But once a better technology will be widely available (for example quic), we will implement it as well.
- DatTank.io - is a free multiplayer browser online tank game.
- DinoBattle.one - is a free multiplayer browser online dino battle royale 2D.
Take a look at these other packages you might be interested in.
Easily build 3D Game for Web, Mobile and PC (https://enable3d.io).
Run Phaser 3 Games on Node.js (@geckos.io/phaser-on-nodejs)
Snapshot Interpolation for Multiplayer Games (@geckos.io/snapshot-interpolation)
To help developing geckos.io, install this repository via npm install
. Test it with npm test
. Then start the development server with npm run dev
.
The BSD 3-Clause License (BSD-3-Clause) 2021 - Yannick Deubel. Please have a look at the LICENSE for more details.