Code Monkey home page Code Monkey logo

nbnet's Introduction

nbnet

logo

nbnet CodeFactor Docs site

nbnet is a single header C (C99) library designed to implement client-server architecture, more precisely for online video games. The library is based on this great series of articles by Glenn Fiedler.

nbnet can target different protocols such as UDP or WebRTC through "drivers" (see below for more information).

The API is meant to be as straightforward as possible and relies on event polling, making it easy to integrate into a game loop.

Disclaimer: nbnet is in the early stages of its development and is, first and foremost, a learning project of mine as I explore online game development. If you are looking for a battle-tested, production-ready library, this is probably not the one.

You can see nbnet in action in this video.

If you want to discuss the library or need help, join the nbnet's discord server.

Features

  • Connection management
  • Sending/Receiving both reliable ordered and unreliable ordered messages
  • Sending/Receiving messages larger than the MTU (using nbnet's message fragmentation)
  • Bit-level serialization (for bandwidth optimization): integers (signed and unsigned), floats, booleans, and byte arrays
  • Network conditions simulation: ping, jitter, packet loss, packet duplication, and out-of-order packets)
  • Network statistics: ping, bandwidth (upload and download) and packet loss
  • Web (WebRTC) support (both natively and through WASM using emscripten)
  • Encrypted and authenticated packets

Thanks

nbnet encryption and packet authentication use the following open-source libraries:

the native WebRTC driver relies on:

Made with nbnet

Boomshakalaka

A fun action game that runs into a web browser, by Duncan Stead (@duncanstead86).

See on YouTube.

nbBR

A WIP battle royal game playable in a web browser.

See on YouTube

Llamageddon

An online multiplayer RTS made for the Ludum Dare 49.

https://ldjam.com/events/ludum-dare/49/llamageddon

nb_tanks

A little online tank game prototype.

See on GitHub

Bindings

A list of user-contributed bindings (they are not officially supported, so they may be outdated):

Drivers

nbnet does not directly implement any low-level "transport" code and relies on drivers.

A driver is a set of function definitions that live outside the nbnet header and provide a transport layer implementation for nbnet used to send and receive packets.

nbnet comes with three ready-to-use drivers:

  • UDP: works with a single UDP socket, designed for desktop games
  • WebRTC (WASM): works with a single unreliable/unordered data channel, implemented in JS using emscripten API
  • WebRTC (Native): works the same way as the WASM WebRTC driver but can be natively compiled

How to use

In exactly one of your source files do:

#define NBNET_IMPL

#include "nbnet.h"

Provide a driver implementation. For instance, for the UDP driver, just add:

#include "net_drivers/udp.h"

after including the nbnet header in the same source file where you defined NBNET_IMPL.

nbnet does not provide any logging capabilities so you have to provide your own:

#define NBN_LogInfo(...) SomeLoggingFunction(__VA_ARGS__)
#define NBN_LogError(...) SomeLoggingFunction(__VA_ARGS__)
#define NBN_LogDebug(...) SomeLoggingFunction(__VA_ARGS__)
#define NBN_LogTrace(...) SomeLoggingFunction(__VA_ARGS__)
#define NBN_LogWarning(...) SomeLoggingFunction(__VA_ARGS__)

For memory management, nbnet uses malloc, realloc and free. You can redefine them if needed:

#define NBN_Allocator malloc
#define NBN_Reallocator realloc
#define NBN_Deallocator free

All set, from here, I suggest you hop into the examples. If you are interested in WebRTC, go here.

Byte arrays

nbnet comes with a primitive bit-level serialization system; but, if you want to use your own serialization solution, nbnet lets you send and receive raw byte arrays.

See the echo_bytes example.

nbnet's People

Contributors

alanparadis avatar ashn-dot-dev avatar dependabot[bot] avatar dunkuk avatar mdeneen avatar nathhb avatar raisinrand avatar sshsphere avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

nbnet's Issues

Need help building raylib example

I don't understand cmake well enough to follow a single step apparently:

This driver requires the code to be compiled with emscripten.

emcmake cmake -DRAYLIB_LIBRARY_PATH=<path to raylib lib file> -DRAYLIB_INCLUDE_PATH=<path to raylib headers folder> .

So I do:
emcmake cmake -DRAYLIB_LIBRARY_PATH="../../../raylib-web/src/libraylib.a"
-DRAYLIB_INCLUDE_PATH="../../../raylib-web/src/" .

It can find the headers but gives me this even though it exists:
cannot find -l../../../raylib-web/src/libraylib.a: No such file or directory

Isn't it supposed to be -lraylib anyway? Regardless after hours of trying nothing works...

PS: I'm on windows with mingw makefiles

Add a clean disconnection function to the client API

There is currently no way to cleanly disconnect from the server on the client side: you can just stop the client and the server will detect the disconnection after a few seconds as packets will no longer be received.

The idea is to add a function to the client API that attempts to clean disconnection by sending a specific (library reserved) message (reliably) from the client to the server. Upon reception, the server should probably just close the connection.

On the client side it might be needed to add a "disconnecting" to probably handle the amount of time between sending the disconnection message and actually being disconnected.

Rapid WebRTC connection/disconnection (page refreshes) can cause type errors

This is the result when testing rapid page reloads on the Raylib example project:
RuntimeError: abort(TypeError: Cannot read property 'error' of undefined) at Error
at jsStackTrace (E:\Git\C++\nbnetlatest\examples\raylib\server.js:1:41013)
at stackTrace (E:\Git\C++\nbnetlatest\examples\raylib\server.js:1:41189)
at process.abort (E:\Git\C++\nbnetlatest\examples\raylib\server.js:1:25525)
at process.emit (events.js:314:20)
at processPromiseRejections (internal/process/promises.js:209:33)
at processTicksAndRejections (internal/process/task_queues.js:98:32)
at process.abort (E:\Git\C++\nbnetlatest\examples\raylib\server.js:1:25556)
at process.emit (events.js:314:20)
at processPromiseRejections (internal/process/promises.js:209:33)
at processTicksAndRejections (internal/process/task_queues.js:98:32)

My guess is that an NBN_Connection is being referenced after being cleared to an undefined value.

Firefox browsers crash the game server

When using NBNet's WebRTC driver compiled through emscripten and running inside nodejs, a connecting Firefox client will cause the game server to crash.
Unfortunately the server error logs don't provide any clues, nor does the firefox javascript console.

If I had to guess, it seems most likely to be a protocol issue in the WebRTC handshake.

Soak test: Segfault when calling deinit functions

On all tested platforms except for OSX, there is a segfault when calling NBN_GameClient_Deinit or NBN_GameServer_Deinit. For now, it was only reproduced with the soak test but I believe it's a library issue.

Fix 1 Complexity, 7 Maintainability, 1 Performance issues in multiple files

CodeFactor found multiple issues:

Complex Code

'reject' is defined but never used.

'_' is defined but never used.

'ev' is defined but never used.

Useless cat. Consider 'cmd < file | ..' or 'cmd file | ..' instead.

'err' is defined but never used.

'options' is assigned a value but never used.

Segfault when restarting nbnet

Calling NBN_GameServer_Start or NBN_GameClient_Start after calling NBN_GameServer_Stop and NBN_GameClient_Stop, respectively, causes a segfault when allocating memory for pooling.

To reproduce the bug, put the calls in a RAII class/struct like so:

struct GameServer
{
    GameServer() { NBN_GameServer_Start(PROTOCOL_NAME, PORT, false); }
    ~GameServer() { NBN_GameServer_Stop(); }
};

Then in the main loop (using raylib):

// For simplicity's sake, I use GameServer here
// but should be an interface to switch between GameServer or GameClient
std::unique_ptr<GameServer> networkState; 

// Other code

while (!window.ShouldClose())
{
    // Other code

    // Toggle the network state using F1
    if ( IsKeyPressed(KEY_F1) )
    {
        if (networkState)
            networkState.reset();
        else
            networkState.reset(new GameServer());
    }
    // Rest of the code
}

Pressing F1 thrice should give a segfault.

NBN_Connection layout is cache unfriendly

__NBN_Connection is big enough (a little above 1 MB per connection) that it doesn't fit into the L2 cache of many systems.

A quick look into a frequently called function like Connection_ReadNextMessageFromStream shows that channels and endpoint are accessed often and immediately after each other, however, their positions in the struct are very far apart. On my Linux 64b system they look like this:

(gdb) p offsetof(NBN_Connection, channels)
$1 = (NBN_Channel *(*)[32]) 0x107318
(gdb) p offsetof(NBN_Connection, endpoint)
$2 = (struct __NBN_Endpoint **) 0x30

More than a MB apart. I believe this is introducing cache misses and impacting performance. The fix is very straightforward: move frequently used members of the struct closer together.

Use of reserved identifier names invokes undefined behaviour

According to https://en.cppreference.com/w/c/language/identifier

The following identifiers are reserved and may not be declared in a program (doing so invokes undefined behavior): 
[...]
2. All external identifiers that begin with an underscore.
3. All identifiers that begin with an underscore followed by a capital letter or by another underscore (these reserved identifiers allow the library to use numerous behind-the-scenes non-external macros and functions).

Identifiers like those in extern NBN_MemoryManager __mem_manager (violates 2., 3.) or struct __NBN_Connection { ... }; (violates 3.) could cause the compiler to see adjacent code as UB and optimize it in a destructive way.

Dead link to Documentation

Hey nathhB,

I wanted to read the documentation since I need some extra information additionally to the examples.
The link in the github https://nbnet.docsforge.com/ is not working anymore unfortunately.
Would you be able to provide a new link?
Thanks!

Best,

Richard

Echo & Echo bytes example do not work on windows

Those two examples do not work on windows because of the Sleep function conflicting with winapi.

Also, the examples need to be compiled with -lwsock2 so this should be stated in the README.

NBN_GameServer_GetDisconnectedClient() don't return the client on MinGW

I compiled and run the echo programs with MinGW and got this assertion:

Assertion failed!

Program: C:\<path>\nbnet\examples\echo\build\echo_server.exe
File: C:\<path>\nbnet\examples\echo\server.c, Line 135

Expression: NBN_GameServer_GetDisconnectedClient() == client

I think the error is inside NBN_GameServer_GetDisconnectedClient because for my other program the client returned by NBN_GameServer_GetDisconnectedClient is always not inside my clients.

Thank you for your time and for this amazing project,

Flayme

NBN_GameClient_Start does not support domain name

Hello, the second parameter of NBN_GameClient_Start can not be domain name:
int NBN_GameClient_Start(const char *protocol_name, const char *ip_address, uint16_t port, bool encryption, uint8_t *connection_data)

So if I try to use it like:
NBN_GameClient_Start(EXAMPLE_PROTOCOL_NAME, "localhost", SERVER_PORT, false, NULL)

It fails: ERROR: Failed to resolve IP address from localhost

It means only dotted IP address is supported. Could you make it support domain name? Thank you!

Rework game server clients data structure

At the moment I use a static array on the game server to store client connections. I believe it's not great because:

  • it limits the number of clients that can be connected at the same time
  • in the UDP driver, I do a linear search to find the connection based on the IP address every time a packet is received
  • in the WebRTC driver I use the NBN_GameServer_FindClientById method which also does a linear search by connection ID

Linear search if far from optimal and it's done every time a packet is received by a driver. I believe a better solution would be to use a hashmap (or another suitable data structure) instead of an array (in nbnet core) and let the drivers compute the hashes themselves as they might have different ways of storing/retrieving connections from this hashmap.

Here is are the relevant sources:

NBN_GameServer_FindClientById

FindClientConnectionByAddress

`assert` triggers when sending messages too quickly

Summary

Sending too many messages too quickly appears to trigger an assert that should probably be a condition check + error return. The assert message from nbnet.h @ commit 4df322c running on MacOS is:

Assertion failed: (outgoing_message->ref_count == 0), function Endpoint_CreateOutgoingMessage, file nbnet.h, line 4839.
Abort trap: 6

Looking at nbnet.h, the offending assert can be found here within Endpoint_CreateOutgoingMessage:

    assert(outgoing_message->ref_count == 0); // problem with outgoing message pool

In this case, I think that the message pool has run out of space, and has not had the ability to recycle the message (although I could be wrong about that). In this case, I think Endpoint_CreateOutgoingMessage should return an error condition (return NULL?) instead of aborting, since running out of messages is a valid error case. This scenario was discovered during a refactor of a work-in-progress game where my server was sending messages at a much faster rate than the client was able to process them (the server had no sleep or equivalent function in its main-loop).

Example Code

shared.h

#ifndef SHARED_H
#define SHARED_H

#define NBN_LogInfo(...) /* nothing */
#define NBN_LogError(...) /* nothing */
#define NBN_LogDebug(...) /* nothing */
#define NBN_LogTrace(...) /* nothing */
#define NBN_LogWarning(...) /* nothing */

#include "nbnet.h"
#include "net_drivers/udp.h"

#define NAME "test"
#define ADDR "127.0.0.1"
#define PORT 31415
#define BUSY_CODE 42

#define MESSAGE_TYPE 0
#define MESSAGE_LENGTH 255
struct message {
    unsigned int length;
    char data[MESSAGE_LENGTH];
};

struct message* message_create(void);
void message_destroy(struct message* self);
int message_serialize(struct message* self, NBN_Stream* stream);

#endif /* SHARED_H */

shared.c

#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

#include "shared.h"

struct message*
message_create(void)
{
    return malloc(sizeof(struct message));
}

void
message_destroy(struct message* self)
{
    free(self);
}

int
message_serialize(struct message* self, NBN_Stream *stream)
{
    NBN_SerializeUInt(stream, self->length, 0, MESSAGE_LENGTH);
    NBN_SerializeBytes(stream, self->data, self->length);
    return 0;
}

client.c

#include <stdio.h>
#include <stdbool.h>
#include <string.h>
#include <time.h>
#include <unistd.h>

#define NBNET_IMPL
#include "shared.h"

int
main(int argc, char** argv)
{
    NBN_UDP_Register();
    if (NBN_GameClient_StartEx(NAME, ADDR, PORT, false, NULL, 0) < 0) {
        fprintf(stderr, "error: failed to start client\n");
        exit(EXIT_FAILURE);
    }

    NBN_GameClient_RegisterMessage(
        MESSAGE_TYPE,
        (NBN_MessageBuilder)message_create,
        (NBN_MessageDestructor)message_destroy,
        (NBN_MessageSerializer)message_serialize);

    bool connected = false;
    bool running = true;
    while (running) {
        int ev = -1;
        while ((ev = NBN_GameClient_Poll()) != NBN_NO_EVENT) {
            if (ev < 0) {
                fprintf(stderr, "error: failed to poll event\n");
                running = false;
                break;
            }

            if (ev == NBN_CONNECTED) {
                fprintf(stderr, "info: connected\n");
                connected = true;
            }

            if (ev == NBN_DISCONNECTED) {
                fprintf(stderr, "info: disconnected\n");
                connected = false;
                running = false;
                if (NBN_GameClient_GetServerCloseCode() == BUSY_CODE) {
                    fprintf(stderr, "error: another client is already connected\n");
                }
                break;
            }

            if (ev == NBN_MESSAGE_RECEIVED) {
                fprintf(stderr, "info: message received\n");
                NBN_MessageInfo info = NBN_GameClient_GetMessageInfo();
                assert(info.type == MESSAGE_TYPE);
                struct message *received = (struct message *)info.data;
                fprintf(stderr, "incoming message: %.*s (%u bytes)\n", (int)received->length, received->data, received->length);
                message_destroy(received);
            }
        }

        if (NBN_GameClient_SendPackets() < 0) {
            fprintf(stderr, "error: failed to send packets\n");
            running = false;
            break;
        }
    }

    NBN_GameClient_Stop();
    return 0;
}

server.c

#include <ctype.h>
#include <stdio.h>
#include <stdbool.h>
#include <string.h>
#include <time.h>
#include <unistd.h>

#define NBNET_IMPL
#include "shared.h"

int
main(void)
{
    NBN_UDP_Register();
    if (NBN_GameServer_StartEx(NAME, PORT, false) < 0) {
        fprintf(stderr, "error: failed to start the server\n");
        exit(EXIT_FAILURE);
    }
    NBN_GameServer_RegisterMessage(
        MESSAGE_TYPE,
        (NBN_MessageBuilder)message_create,
        (NBN_MessageDestructor)message_destroy,
        (NBN_MessageSerializer)message_serialize);

    NBN_ConnectionHandle client = 0;
    while (true) {
        int ev = -1;
        while ((ev = NBN_GameServer_Poll()) != NBN_NO_EVENT) {
            if (ev < 0) {
                fprintf(stderr, "error: failed to poll event\n");
                break;
            }

            if (ev == NBN_NEW_CONNECTION) {
                fprintf(stderr, "info: new connection\n");
                if (client != 0) {
                    NBN_GameServer_RejectIncomingConnectionWithCode(BUSY_CODE);
                }
                else {
                    client = NBN_GameServer_GetIncomingConnection();
                    NBN_GameServer_AcceptIncomingConnection();
                }
            }

            if (ev == NBN_CLIENT_DISCONNECTED) {
                fprintf(stderr, "info: disconnected\n");
                assert(NBN_GameServer_GetDisconnectedClient() == client);
                client = 0;
            }
        }

        // This message is sent every server-side game tick!!!
        if (client != 0) {
            char const* const str = "HELLO";
            size_t const len = strlen(str);
            struct message* outgoing = message_create();
            outgoing->length = len;
            memcpy(outgoing->data, str, len);
            NBN_GameServer_SendReliableMessageTo(client, MESSAGE_TYPE, outgoing);
        }

        if (NBN_GameServer_SendPackets() < 0) {
            fprintf(stderr, "error: failed to send packets\n");
            break;
        }
    }

    NBN_GameServer_Stop();
    return 0;
}

Ditch facil.io library and use only libdatachannel

nbnet's native WebRTC driver uses two external libraries: facil.io for web sockets for WebRTC signaling and libdatachannel for WebRTC data channels. libdatachannel provides web socket support, so it should be possible to use only libdatachannel instead of relying on facil.io. This would reduce the dependencies and simplify things.

`nbnet` logo

This issue is just a recommendation. I think nbnet deserves some catchy logo to get more identity and become more popular.

I can propose something if you want.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.