Armagetron: network code documentation
Author: Manuel Moos
Date: 2020-11-05T23:48:08+01:00

DEFINE(Armagetron Advanced, Armagetron)

Introduction | Layer One | Layer Two | Layer Three

# Layer two: network messages

This layer is responsible for the basic communication in a client/server framework (Files: `network.h` and `network.C`). It allows the clients to connect to/disconnect from a server and provides a way to send messages in both directions; each server/client gets a unique user ID (zero for the server, 1...n for the clients). The average round trip time of network packets (the ping) is estimated and stored in `REAL avg_ping[user ID]` (unit: seconds). A bandwidth limitation and message queue based on priorities is provided. For every message the server/client receives, an acknowledgement is returned. If no acknowledgement for a sent message is received within a reasonable time (about ping*1.1), a message is considered lost and is sent again.

## Usage

A sample program for layer two is `l2_demo.cpp` from the source directory. Compile it with `make l2_demo`; the syntax is `l2_demo` to start it in server mode, just listening to clients, or `l2_demo servername` to start it in client mode connecting to the server given by `servername`; it will send some simple messages from the client to the server, who will display the results.

### Bandwidth control

Before starting up the network, set `max_in_rate` and `max_out_rate` to the input/output bandwidth (in kB/s) of the used network interface. It's best to let the user decide that. The network subsystem will take care that the actual rates stay below these given numbers, avoiding massive problem with lost packets.

### Communication setup

Armagetron's network subsystem can be in three different states: standalone (no networking), server, or client (connected to a server). The current state can be read via `get_netstate()` (and is an enum type `netstate`, which can be one of `NET_STANDALONE, NET_SERVER` and `NET_CLIENT`); `set_netstate(netstate state)` is used to set the current state. The client state is most conveniently set by `void connect(const string &server)`, which will set the state and establish a connection to the given server; check with `get_netstate()` whether the login was sucessfull. Logging out is simply done with `set_netstate(NET_STANDALONE)`. When switching between server and client state, one should visit the standalone state in between.

### Network loop

The network subsystem does not use threads; so, to receive network messages, you have to call the function `receive()` every once in a while to get the messages the other computers send. Do it at least once in the game loop. `receive()` is responsible for sending away queued messages, too.

### Messages

Before writing a piece of code that sends a message to another computer, you have to decide what the receiver should do with it. Layer two already takes the responsibility to sort the incoming messages by type, so there's no need to write one big `receive` function that analyses and processes the messages. Instead, for every type of message (player moves, player dies, player shoots,...) you want to have, you write an own small receive function, accepting a reference to an object of the class `netmessage`, for example, if you want to handle a "player dies"-message consisting of only one `short` containing the ID of the player dying, you write

    void kill_handler(netmessage &m){
      short player_id;
      m >> player_id; // read the ID from the message

      // do some security checks; is the computer we got the message
      // from really allowed to kill the player? The user ID of the
      // message's sender can be read by m.net_id(). If he's cheating,
      // kick him by throwing a killhim-exception.

      .......

      // kill player player_id.

      .......
    }

Then, you need to define a class of messages responsible for killing players; to do that, you create an object of the class `netdescriptor` with your message handler's address and a nice name as arguments, i.e. by writing

    static netdescriptor kill_descriptor(&kill_handler,"Kill");

directly after `kill_handler`. To send a message, you have to create an object of class `netmessage` with `new` and your descriptor as argument:

    netmessage *m=new netmessage(kill_descriptor);

Then, you write the data into the message, in our example only

    short this_player_has_to_die=3;
    (*m) << this_player_has_to_die;

And send the message to the computer with user ID `peer` with the message's member function `send(int peer, REAL priority=0, bool ack=true)` or to all connected computers (the server if the network subsystem is in client state, all clients if it's the server state) via the member function `broadcast(bool ack=true)`, in our case most conveniently

    m->broadcast();

Normally, the message is guaranteed to arrive sooner or later (and is sent again if it gets lost). Not so important messages (like position updates in a racing game) may be market by giving `ack=false` as an argument. As usual, giving a lower `priority` than 0 will make the message more urgent, giving a higher `priority` will make it sent later (`priority` is given in seconds; that is, a message with priority one, already waiting for one second, is considered as important as a message with priority zero that just entered the queue). DO NOT use `delete` to get rid of the message; it will delete itself as soon as it no longer needed. Of course, feel free to encapsulate all the steps above in a derived class of `netmessage`.

And that's about all there is. With the operators `<<` and `>>`, you can write/read shorts, ints, REALs and strings. All data is sent in a portable manner; you do not have to worry about endianness or different float formats. If you want to send/receive messages with variable length, you can use the member function `netmessage::end()`. It tells you whether the message you are just reading out with `>>` has been fully read. (Any more reads when `end()` returns `true` result in a network error, causing the connection to be brought down.)

### Some special functions

#### `void client_con(const string &message,int client=-1);`

Lets client No. `client` display `message` on his console. If `client` is left to `-1`, all clients display the message.

#### `void client_center_message(const string &message,int client=-1);`

The same as above, only the message is displayed in large letters at the center of the screen.

#### `void sync(REAL timeout,bool sync_netobjects=false);`

Synchronises the clients with the server; waits until all queued network packets are sent or `timeout` seconds pass. Umm, already a part or layer three: if `sync_netobjects` is set to `true`, the network objects are synchronised, too.

## Protocol details

A network message is sent in the following structure (all data types are `unsigned short`s):

Descriptor ID | The type of message (login, logout, acknowledgement, user input...). Determines what message handler is called at the receiver.
Message ID | A locally unique identification number. Used for the acknowledgement and for discarding double messages.
Data length | The length of the following data in `short`s; used mainly to catch errors.
Data | The real message; everything that was written by `<<`

Since every network protocol has a considerable amount of overhead per call of `ANET_Write` (56 bytes in the case of UDP, I think...), multiple messages are transmitted with each low level write operation. The data send with each `ANET_Write` has the following form:

Message 1
. . .
Message n
Sender's user ID (as always, as a `unsigned short`)

The user ID is sent to simplify the server distinguishing the clients; with the possibility of clients hiding behind masquerading firewalls and thus appearing to send from changing ports, simply checking the sender's addresses may not be enough.

This Page was created by Manuel Moos(
).

Last modification: Thu Nov 5 23:48:08 MST 2020

Introduction | Layer One | Layer Two | Layer Three