1.1.2 • Published 8 years ago

nonhub v1.1.2

Weekly downloads
3
License
MIT
Repository
github
Last release
8 years ago

Nonhub

Nonhub is a free opensource multiplayer framework for Lua clients. Nonhub made-up of two parts: one is Node.js server and other is Lua client. Server works on any computer with Node.js and supports Heroku platform while client runs on any Lua implementation with LuaSocket library. There are examples for pure Lua and all popular Lua engines: Gideros, Corona, MOAI, LÖVE, Defold.

Features:

  • Easy to use
  • High performance
  • Fully customizable via modes
  • Optional UDP for fast paced games
  • Developer-friendly
  • On-the-fly server patching
  • Heroku PaaS support

Table of Contents

Local installation

You must have installed copy of Node.js in order to run Nonhub server and to install NPM modules.

Nonhub can be installed as a module via NPM npm install nonhub or downloaded from GitHub and used directly.

Difference

The module will be installed to ./node_modules/nonhub in the current project folder. You need to create a launch file with .js extension (e.g. app.js) and write the line of code to it: require('nonhub'). The launch file can be used to start the server (e.g. nodejs app.js).

If you downloaded Nonhub from GitHub you can also use it as a module: just put nonhub folder to node_modules in your project folder. Or you can use nonhub folder as a project folder and nonhub.js as a launch file for the server: nodejs nonhub.js.

Module installation results in cleaner project structure and seems more natural for Node.js.

Notes

  • All configuration files will be created and updated in the current project folder.
  • Modes will be loaded from both ./modes and ./node_modules/nonhub/modes folders (adjustable in settings.json/modePaths).
  • Node modules required for some modes must be installed manually.

Additional tools

To simplify Lua development I recommend ZeroBrane Studio.

To write and debug Nonhub modes you can use Visual Studio Code.

First run (ping-pong time!)

1) Start the server locally from Nonhub folder via CLI nodejs nonhub.js or run it from your JavaScript IDE. Server will show debug messages and generate settings.json, cfg.lua and modes.json files in the Nonhub folder. 2) Copy a ping-pong example for your Lua implementation from Nonhub examples folder to your Lua projects folder. 3) Copy nonhub.lua and cfg.lua files from Nonhub folder to the root of your project. 4) Run main.lua via your Lua IDE or via CLI lua main.lua if you are using pure Lua. You will see configuration info and connection info at start and every 5 seconds [ping] and [pong] messages will popup. On the server you will see debug info when client connects and disconnects.

Congratulations, your server is working! Isn't that awesome? :sunglasses:

Deployment to Heroku

You can run Nonhub server on the Heroku platform for free but beware about main limitation of the free plan: all apps must sleep (i.e. they will be disabled) 6 hours in a 24 hour period. Also Heroku conceptually has no persistent file storage and you need to use an addon like Heroku Postgres for this. And no UDP thus you need to set settings.json/UDP key to false. So let's start.

1) Register Heroku account. 2) Intall Heroku Toolbelt. 3) Create file named Procfile in your server project folder and add this line to it:

web: node your_launch_file.js

where your_launch_file must be replaced with the name of your launch file, for example nonhub.js if you are working from Nonhub directory. 4) Use CLI from your server project folder:

git init
echo "node_modules" > .gitignore
git add .
git commit -m "your_commit_description_here"
heroku apps:create your_app_name
git push heroku master

5) In Lua project set address to your Heroku project address (it's shown after heroku apps:create command) and port to 80. For example:

client = nonhub.new {
    address = "nonhubserver.herokuapp.com",
    port = 80
}

Run your Lua client and if debug is enabled you will see the server handshake, something like this:

HTTP/1.1 101 Switching Protocols
Connection: Upgrade
Upgrade: WebSocket
Via: 1.1 vegur

Your Heroku server is working and that's great!

Later, if you need to update your server you can use the following CLI commands from the server project folder:

git add .
git commit -m "your_commit_description_here"
git push heroku master

First custom mode

Functionality of the Nonhub can be extended through the modes. You will need basic knowledge of JavaScript and Node.js libraries. The most important Node.js library for Nonhub is Buffer used for message processing. It allows to keep Nonhub performance at high level. Also don't forget about tons of ready NPM modules. You can easily use them in your modes, just require them in server section and add their names to nodemodules section of your mode. Well, let's start from the scratch. 1) Create empty file in Nonhub modes folder and name it testmode.js. The file must have .js extension while it's basename can be arbitrary. 2) Run the server. You will see warnings about mode creation:

mode 'testmode' created from mode template
modes.json: new modes added and enabled:
• testmode

Modes created from the template already work although as it is they can be used only for simple tests. That's because their handler key is empty function and level key is at 0 so server just sends any received message back to the client without any processing. Also such modes have same names as their files and version 1.0.0 by default. 3) Restart the server to load it with the mode. 4) Copy updated cfg.lua to Lua project folder. (You can set the path to your Lua project and change the name of cfg.lua file in the clientCfgPath key of server section in cfg.json file. If you do it will be automatically updated each time you start the server and no copying will be needed.) 5) In Lua project add client.send.testmode = "test" line to the game loop, and run it. You will see how client sends and receives it's own "test" message at the speed of your game loop (usually 60 fps). This means your first mode is working but it's useless. 6) To make it more handy set testmode.js/unpacker key to 'function(data, client) return tonumber(data) end' function. Now your mode can send and receive any valid number and we can use it to measure server response time! 7) Replace client.send.testmode = "test" with client.send.testmode = socket.gettime() and add client.on.testmode = function(msg) print(socket.gettime() - msg) end callback before the game loop. Disable debug info for a while: add debug = false to the options table of nonhub.new. Run your Lua project and check latency of your server. Great! You easily created simple but useful mode. Of course most modes are more complex although they all have the same base. Check Modes section for more info.

Nonhub folder structure

  • examples:Lua app examples
    • engine1: — each engine has it's own examples
      • example1copy nonhub.lua and cfg.lua to an example folder before the run
      • ...
    • ...
  • modes:this plugins can be enabled in modes.json and adjusted in settings.json
    • mode1.js
    • mode2.js
    • ...
  • cfg.luarequired for Lua client, generated and updated at server start
  • core.jsmain part of Node.js server, can be tuned via settings.json
  • modes.jsonlist of modes to enable/disable, generated and updated at server start
  • nonhub.jsserver loader: checks modes, updates configs, starts the server
  • nonhub.luaclient library, depends on cfg.lua
  • settings.jsonrequired for Node.js server, generated and updated at server start

Lua project

Intro

Require Nonhub module:

nonhub = require "nonhub"

Create new client and overwrite options of cfg.lua if needed:

client = nonhub.new(options)

Add callbacks to the client:

  • onConnect = function(clientHS, serverHS) ... end
  • onDisconnect = function(src, err) ... end
  • onmode = function(msg, id) ... end
  • onErrormode = function(err, num) ... end

Use networking methods of the client:

  • connect()
  • disconnect()
  • sendmode = msg
  • receive()

nonhub.new(options)

After you required nonhub.lua module you need to create a client (or multiple clients for local testing). This function accepts only one argument called options which must be a table. Nonhub loads client configuration from cfg.lua first and then updates it from this options table. Therefore you can use the options to redefine any key from cfg.lua. For example:

client = nonhub.new {
	debug = false,
    interval = 3
}

Callbacks

Nonhub client also works as event emitter so you need callbacks to react to that events.

onConnect(clientHS, serverHS)

Called right after successful connection to the server. Then client will start receiving and will be able to send messages. Handshakes (clientHS and serverHS) can be used mainly for debugging. For example:

client.onConnect = switchToMultiplayerScreen -- absctract game function

onDisconnect(src, err)

Called after normal disconnect via client disconnect method or on a failure.

Parameter src represents the source (place) of an error. Can be nil in case of normal disconnect or one of the following strings:

  • create socket
  • connect
  • send handshake
  • receive handshake
  • send message
  • receive message

Parameter err describes the error. Can be nil in case of normal disconnect, luasocket error (e.g. "closed", "timeout", "host not found") if connection aborted or special "version mismatch" error. Last one indicates that client app must be updated.

Parameters src and err can be used in if-branching to cover all possible failures. For example:

client.onDisconnect = function(src, err)
	if err == "version mismatch" then print "Update your app"
    elseif src == "connect" then print "Cannot connect to server"
    elseif err then print "Server is busy" end
    switchToMainScreenScene() -- absctract game function
end

onmode(msg, id)

If client's request was processed without errors the message msg will be sent to that client (in this case id is nil) or broadcasted to the group (id is integer number). This is the most important callback to control your multiplayer app. It's like keyboard, mouse or touchscreen input for singleplayer. For example:

client.on.login = switchToPublicRoom 
client.on.position = function(msg, id)
    if not players[id] then players[id] = Player.new() end
    players[id]:setPosition(msg.x, msg.y)
end

onErrormode(err, num)

If client's request cannot be processed normally server sends back error message. You can find out the kind of error either by it's description (err) or by it's number. If-branching can be used to handle each error differently. For example:

client.onError.login = function(err, num)
    showWarning(err)
    if err == "wrong password" then addRestorePasswordButton() end
    if err == "multiple login" then switchToCreateAccountScene() end
end

Networking

Functions to do main network operations.

connect()

Tries to connect the client to the server and calls onConnect on success or onDisconnect on error. After successful connection client will be able to send and receive messages. When client disconnected send and receive functions do nothing. For example:

client.connect()

disconnect()

Disconnects the client from the server. When client disconnected send and receive functions do nothing. For example:

client.disconnect()

sendmode = msg

Each time you assign a msg value to a mode key newindex metamethod of the send table is called. Your message will be encoded with packer function of this mode__ and sent to the server. If packed message is bigger than maxMessageLength the message will not be sent at all. In such a case you need to adjust that key in settings.json. If server is overloaded by requests not all messages could be sent on the first try. Then the client caches the request and tries to resend it before retries counter becomes bigger than maxRetries value or the client will disconnect itself from the server. For example:

client.send.position = {players[client.id].x, players[client.id].y}

receive()

This function must be put in the game loop to give luasocket's receive method some time and Nonhub does this automatically for some Lua engines. (You can change this behaviour via autoReceive key.) When normal message received it will decoded by the appropriate unpacker and passed to on callback for the message mode. When error message received it will be passed to onError callback for the message mode. For example:

timer:addEventListener(Event.TIMER, client.receive)

Options

Each Nonhub client created with options from cfg.lua file by default. Nonhub server generates and updates this file automatically from available modes and server settings at each server start. You can set settings.json/clientCfgPath to your project path for direct updates. It's rare case when you want create or update this file manually because every key from this file can be redefined in Nonhub new method's options. Without modes cfg.lua looks like this:

return {

address = "localhost",
autoReceive = true,
clientHandshake = "GET /%s HTTP/1.1\r\nHost: %s\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n\r\n",
debug = true,
interval = 5,
luamodules = {},
maxMessageLength = 65535,
maxModeIndex = 5,
maxRetries = 5,
maxUDPPackets = 60,
modeErrors = {},
modeIndexes = {},
modeLevels = {},
modeNames = {},
modePackers = {},
modeUnpackers = {},
on = {},
onConnect = function(clientHS, serverHS) end,
onDisconnect = function(src, err) end,
onError = {},
port = 80,
requiredModes = {},
timeout = 5,
version = "1.0.0",

}

address

Address of the Nonhub server. Can be an IP address or a host name.

autoReceive

Some Lua engines support game loop events like 'onEnterFrame'. If this key set to true Nonhub tries to add Nonhub receive method as a callback to such event. If not you must add it manually to the game loop.

clientHandshake

This string must contain two "%s" placeholders: one for server address and other for client version. While connection client sends this string to the server.

debug

Very detailed info about Nonhub client work. Can be enabled/disabled with true/false values.

interval

Keep alive interval in seconds. Some platforms can automatically disconnect a client if it's not active (i.e. no message received from the client) for specific period of time. To prevent this Nonhub client after inactivity interval sends a ping message to the server and waits pong message from it.

luamodules

For simplicity each Lua module listed here will be automatically required globally to be available for some modes. If Nonhub client doesn't detect these modules at start you will get an error message.

maxMessageLength

This key must always have same value as settings.json/maxMessageLength key. It defines buffer size for each client on the server i.e. acceptable length of a message after processing with packer function on the client side. Server will silently disconnect clients attempting to send a message exceeding this length. That's why each client always compares resulting message length with this key value to report about the error via onDisconnect("send message", "oversized message") call.

maxModeIndex

When received message mode index is higher than this value received message will be treated as error message.

maxRetries

When settings.json/maxConnections is too high and server overloaded by requests a client cannot always send a message. In this case client caches last unsent message and automatically tries to send it again increasing retries counter after each failure. When retries exceeds maxRetries the client disconnects with timeout error.

maxUDPPackets

This key defines the number of UDP packets your client will be able to receive per each client.receive() call. Since each client sends same number of packets per second you can set this key to the maximum size of the 'game room' i.e. maximum number of players in the broadcasting group. This must be an integer number higher than 0 (otherwise UDP receiving will be disabled).

modeErrors

Table with error descriptions for each mode. They will be passed to onError callbacks as first argument.

modeIndexes

Table to convert mode names to mode indexes.

modeLevels

Table with level for each mode.

modeNames

Table to convert mode indexes to mode names.

modePackers

Table of packer functions for each mode.

modeUnpackers

Table of unpacker functions for each mode.

on

This is the table of mode callbacks with (msg, id) for normal messages.

onConnect

This is callback with (clientHS, serverHS) for connect event.

onDisconnect

This is callback with (src, err) for disconnect event.

onError

This is the table of mode callbacks with (err, num) for error messages.

port

External port of the Nonhub server. Must be an integer number in the range 1..64K.

requiredModes

This can be used for an extra check on the client side (very useful for examples). If cfg.lua doesn't contain some modes from this list Nonhub client will give you error message with unfound modes listed.

timeout

This key defines a limit on the amount of time for establishing a connection to the server, in seconds. Do not set this value too low or client will not be able to connect to some servers and onDisconnect event will happen. Also do not set this value too high because connect is a blocking operation.

version

When client connects to the server it sends a handshake with the version value. Then server compares received version with the version from settings.json and if it doesn't match disconnects the client. In this case client's onDisconnect callback will be called with ("connect", "version mismatch") arguments. This serves as a simple protection from unwelcome connections to the server and can be used to warn clients about server update so clients must be updated too in order to use the server.

Node.js project

Server loads all modes and configurations automatically. It has two configuration files in the form of modes.json and settings.json, server loader nonhub.js and server core core.js. If you are using Nonhub as a Node.js module you need to create a file with .js extension and add this line to it:

require("nonhub")

How nonhub.js works

1) It tries to load core.js, settings.json and modes.json. 2) It loads all available modes from paths in settings.json/modePaths. Modes must have no syntax errors or loader will crash while require them. 3) It updates modes.json. All newly added modes are added to it and enabled while unfound ones will be removed from this file. All disabled modes will not be used for server work. 4) It checks all modes for error of different kinds. If any error occurs it will show error description and throw an error message. 5) It creates server object (shared state between all clients) and updates settings.json and cfg.lua. If some function keys cannot be processed it will throw an error message. If cfg.lua file cannot be written it will show a warning. If the number of enabled modes is over 255 it will throw an error message. 6) It creates connection listener and port listener to process clients' messages.

settings.json

Configuration file for the server. Modes can add their own settings here. Without modes the file looks like this:

{
    "clientCfgPath": "./cfg.lua",
    "debug": true,
    "internalPort": 80,
    "maxConnections": 10000,
    "maxMessageLength": 65535,
    "modePaths": [
        "./node_modules/nonhub/modes/",
        "./modes/"
    ],
    "serverHandshake": "HTTP/1.1 101 Switching Protocols\r\nConnection: Upgrade\r\n\r\n",
    "UDP": null,
    "version": "1.0.0"
}

clientCfgPath

Each Nonhub client loads it's settings from cfg.lua configuration file by default. You can set clientCfgPath to that file right inside your project to overwrite it automatically when you change your server configuration and modes. For example:

"clientCfgPath": "D:/Projects/LuaMultiplayer/cfg.lua"

debug

Shows some debug info when server starts or a client connects/disconnects.

internalPort

When your server is not on the Heroku platform this port will be used.

maxConnections

If server fully loaded by client requests some clients will not be able to send their messages straightway i.e. they will look laggy for other clients. To prevent such behaviour you can use this key. When number of clients is equal to this key new clients will be disconnected from the server.

maxMessageLength

This parameter defines max size of each message received by the server. When client sends a message bigger than this limit it will be silently disconnected. For each client the server creates a buffer of this size so this can be used to lower server memory consumption. The value must be an integer in range 0..65535.

modePaths

When server starts it tries to load modes from this paths in the array order. You can use as many paths as you want but beware: if some modes have same name only last one will be used.

serverHandshake

This can be used when trying a PaaS other than Heroku. Some platforms work only with certain Websocket libs and thus cannot be used for Nonhub e.g. OpenShift Online.

UDP

Can have one of the following values:

  • null — server will use UDP for modes with level 3 and TCP for modes with level 2
  • true — server will use UDP for all modes with level 2 and 3
  • false — server will use TCP for all modes and it will not bind UDP socket

version

This key used to control server and client updates. When you update your Nonhub server you need to adjust this key so client will see that it needs an update too (client has it's own cfg.lua/version key). Can be any string allowed for HTML requests. For example:

"version": "multiplayer-v.1.2.3-beta"

modes.json

Modes are plugins for Nonhub. Each message encoded with first byte as a mode index so total modes number cannot exceed 255 (0b11111111 value used for an error) or you will get an error message. At start Nonhub server checks modes folder and updates modes.json file where modes can be enabled or disabled. The file looks like this:

{
    "mode1": true,
    "mode2": false,
    "...": "...",
    "modeN": true
}

Each mode can be enabled or disabled here by setting it's key to true or false. All newly added modes are enabled by default.

Warning: even disabled modes are required by Nonhub loader at start so any mode in modes folder must not contain syntax errors.

Modes

Each mode has the following structure and values must have the same type as here:

module.exports = {

name: "",
author: "",
version: "",
description: ``,
nodemodules: [],
nonhubmodes: [],
luamodules: [],
packer: `''`,
unpacker: `''`,
client: {},
server: {},
luacfg: {},
handler: function(client, server) {},
finalizer: function(client, server) {},
errors: {},
level: 0,

}

name

A mode can be placed in a file with arbitrary filename without any influence on mode behavior. Only the name from this key matters:

  • You use this name in mode callbacks and send function. If you are mode developer you can try to use names which are Lua identifiers because of short dot-syntax. For example name 'to all friends' can be used only as client.send"to all friends" = "Hello" while name 'to_all_friends' can be alternatively used as client.send.to_all_friends = "Hello".
  • Server uses this name when checks modes dependencies from nonhubmodes.
  • File modes.json contains this mode names as keys to enable/disable them.

author

Doesn't affect mode behaviour. Just some information about mode creator which can be useful for a search.

version

Doesn't affect mode behaviour. Useful for mode developers.

description

Doesn't affect mode behaviour. Important key for users of the mode.

nodemodules

A mode can depend on some node modules. Their names can be listed in this array as strings.

nonhubmodes

A mode can depend on other modes. Their names can be listed in this array as strings.

luamodules

packer and unpacker functions can depend on some Lua modules. Their names can be listed in this array as strings.

packer

Lua function on client side with two parameters (message, client) which converts specific Lua value (i.e. table) to Lua string before sending over TCP. This function is the first one in the 'packer -> handler -> unpacker' processing chain. The key accepts Lua function like 'function(msg, client) if type(msg) == "table" then return json.encode(msg) else return tostring(msg) end'. Value of this key must be inside additional quotes in order to be recognized as a Lua code and to be saved in cfg.lua correctly.

unpacker

Lua function on client side with two parameters (data, client) which converts Lua string to specific Lua value (e.g. table) after receiving over TCP from the server. This function is the last one in the 'packer -> handler -> unpacker' processing chain. The key accepts Lua function like 'function(data, client) local ok, msg = pcall(json.decode, data); if ok then return msg else return data end'. Value of this key must be inside additional quotes in order to be recognized as a Lua code and to be saved in cfg.lua correctly.

client

Non-function keys from this object will be added to client template server.client. Function keys will be added to builder server.clientBuilder. After successful hanshake client object will be created from the template, initialized by the builder and attached to server.clients with unique address and port.

server

All non-function keys from this object will be added to settings.json and can be redefined there. All function keys from this section will be processed before luacfg ones. Then all this keys will be added to global server object.

luacfg

All keys from this object will be processed and added to cfg.lua file required by client library. This object always processed after server object. Function keys processed after non-function ones.

handler

JavaScript function on server side with two parameters (client, server) which handles received data processing and can define group of recipients for processed data. This function is the middle one in the 'packer -> handler -> unpacker' processing chain.

To process received data without performance degradation handler works with Node.js buffer. Normally this function returns nothing because it modifies buffer (client.buf), buffer length (client.bufLen), server object and/or client object and server sends content of the modified buffer sliced at client.bufLen to the client(s). For example:

handler: function(client, server) {
    client.username = client.buf.toString("utf8", 3, client.bufLen)
    if (client.id) return undefined
    client.id = server.idsReserved.pop() || ++server.idsCounter
    server.ids[client.id] = client
}

Nonhub uses first 3 bytes (0, 1, 2 for Node.js buffer) to store mode index and message length therefore nonempty message must be processed from 4th byte (3 for Node.js buffer).

If something goes wrong it must return error number (these can be later described in errors) This must be an integer in range 0..254. Number 255 means "no id"-error which handled by server automatically. When handler returns error server sends back to the client error message containing the mode and error number. For example:

handler: function(client, server) {
    var usernameLength = client.buf[3]
    if (usernameLength > 16) return 0
}

When level of the mode is at 2 (broadcast) handler must also define client.group If not defined group of receivers will be empty and server will send nothing. For example:

handler: function(client, server) {
    client.group = server.clients
}

finalizer

JavaScript function on server side with two parameters (client, server) which triggers on client disconnect. Can be used to clean up server object or to write some info about client into database. For example:

finalizer: function(client, server) {
    if (client.id) {
        server.idsReserved.push(client.id)
        delete server.ids[client.id]
    }
}

If finalizer is not needed then it must be set to empty function without parameters i.e. function() {} to save some resources.

errors

If handler returns error codes then here they can be described in the form of strings so users of the mode will know what exactly happen. Also useful for mode developers themselves. For example:

errors: {
    0: "database error"
    1: "request too long"
    2: "user not found"
}

level

Restricts access to the mode. Currently 4 levels supported:

  • 0 (unauthorized singlecast) — any client can use this mode even ones without assigned id (i.e. id == 0) and result will be sent back to the client.
  • 1 (authorized singlecast) — only clients with assigned id (i.e. id > 0) can use this mode and result will be sent back to the client.
  • 2 (reliable broadcast) — only clients with assigned id (i.e. id > 0) can use this mode and result will be sent as a TCP packet to all clients from the client.group defined in handler.
  • 3 (unreliable broadcast) — only clients with assigned id (i.e. id > 0) can use this mode and result will be sent as a UDP packet to all clients from the client.group defined in handler.

Acknowledgments

I would like to offer my special thanks to my best friend, Kostya Krivulya aka Bones, for supporting me all this time and for his valuable advices.

I would also like to thank all Gideros maintainers for Gideros Studio and Gideros Engine what I used for writing and testing Nonhub client library.

MIT License

Copyright (c) 2016 Nikolay Yevstakhov

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

1.1.2

8 years ago

1.1.1

8 years ago

1.1.0

8 years ago

1.0.0

8 years ago