Whole document tree
    

Whole document tree

Armagetron: network code documentation
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 shorts):

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 shorts; 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: Mit Nov 15 21:39:41 CET 2000

Introduction Layer One Layer Two Layer Three