DEFINE(Armagetron, Armagetron)
Introduction | Layer One | Layer Two | Layer Three |
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.
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.
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.
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.
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.
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.)
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.
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: Tue Oct 14 09:46:43 CEST 2003
Introduction | Layer One | Layer Two | Layer Three |