Last update: 04-10-11
This document describes the socket implementations that XBlast uses as of version 2.9.23 and their protocols. There are quite substantial changes to previous versions, due to extending (chat, version check, level check, host state requests) and modifying (async check, host state signal) the protocol.
See Message Formats for a description of the data blocks used in the protocol. See XBComm structures for a more detailed description how the sockets are managed.
The description is now fairly complete, with 2.9.23 enhanced parts in yellow and todo's in red. Some more links would also be good and some parts could be arranged in a better way, but at least it should be way easier than looking at the code itself ;) Suggestions are welcome at xblast channel at irc.freenode.net.
TODO: extend server/client player id's and host id's to one full byte instead of four bits ? Major protocol change, but would enable FogofWar-scenarios. Similarly, extend current action byte to two or even four bytes ?
Client sockets are those sockets which are used to connect the XBlast user to some service.
Using the "Register Player" button in the Player Options, establishes a XBCommToCentral socket to central. If the connection is successful, Tele(SendData,PlayerConfig,any,config) messages terminated by a Tele(SendData,PlayerConfig,any,empty) are sent to central. The config data blocks are the player config lines of the selected player in the options.
A special role plays the atomPID entry: if it exists and is not zero, central will consider the message an update request. If it doesn't exist or is zero, it will be considered a new registration.
On receiving a Tele(DataAvailable,PID,any,pid) message, the result of the registration is determined as follows from the received PID:
PID meaning --- ------- -3 error in central (invalid PID returned or non-unique name) -2 update failed (submitted pid was unknown) -1 update failed (wrong password) >0 registration successful, returned PID assigned
After this reply has been received, a disconnect signal is sent to central.
TODO: implement unregister option. Current unregistering just removes PID/pass locally, central data is untouched.
Using the "Search LAN" button in the Join Menu creates XBCommQuery sockets on every broadcast interface. Then Dgram(Browse(Query,serial,empty)) signals are broadcasted to port 16168. The serial is incremented for every send, but it is currently ignored otherwise.
Each returned Dgram(Browse(Reply,serial,gamecfg)) messages are added to the list of network games which currently are open.
The sockets are closed after either leaving the menu or successfully connecting to one of those network games.
Using the "Search Central" works exactly like the "Search Lan" button, the only difference is the destination address for outgoing messages, which is central in this case, as opposed to the broadcast address of the interface in the LAN case.
A client attempts to join a server either on using the "Connect" button in the Join menu, or choosing an entry in either the "Search LAN", "Search Central" or "Ip History" menu. The client starts to establish a TCP connection first:
To join a server, the client host attempts to establish a XBCommToServer connection to the server listen socket. If it is successful, the client sets up an empty game table and waits for server data.
Tele(SendData,GameConfig,id,config) messages, the game config line is added to the game config for the received client id. On receiving Tele(SendData,GameConfig,id,empty) , the client has received all game config for the client id that server has. Starting at 2.9.23, the client checks complete game configs for a proper version string and closes the connection, if the version check fails.
On receiving Tele(SendData,PlayerConfig,hpid,config) messages, the player config line is added to the player config for client and player determined by hpid: the higher four bits denote the host id, the lower four bits denote the player id. On receiving Tele(SendData,PlayerConfig,hpid,empty) , the client has received all game config for client and player defined by hpid that server has.
On receiving a Tele(Activate,DgramPort,id,port) messages, the UDP connection is set up to the received port at the server host. On success, a Tele(Activate,DgramPort,id,port) is returned with port either the local port number of the UDP socket (non-NAT) or 0 if the client is NAT'ed. The received id is the server assigned id for the local client.
On receiving a Tele(RequestData,GameConfig,id,empty) message, Tele(DataAvailable,GameConfig,id,config) messages terminated by a Tele(DataAvailable,GameConfig,empty) message are returned, containing the local player config. The id received is the server assigned id for the client. Starting at 2.9.23, the game config contains the version data for the local client.
On receiving a Tele(RequestData,PlayerConfig,id,empty) message, the player config for the local player id is returned as Tele(DataAvailable,PlayerConfig,id,config) messages terminated by a Tele(DataAvailable,PlayerConfig,id,empty) message.
On receiving a Tele(Activate,HostChange,id,hoststate) message the host state for the host id is changed to the state defined by hoststate. This is new in 2.9.23 and replaces the HostIsIn/HostIsOut Tele-ID's that have been used before.
TODO: Use the upper 4 bits of host state (or a second byte?) to denote a client id that requested the host state for id. Only if that client id is 0 (=server), the host state is actually changed.
Since 2.9.23 the client has the option of sending a Tele(Spontaneous,HostChange,id,hoststate) to request a state change for the given host. If the request is approved, the server will send back the Tele(Activate,HostChange,id, hoststate) message, at which point the host state becomes active.
TODO: determine a simple/elegant way to show both current host state and pending requests by all clients. Ultimate goal is that if all clients plus server have host state "ready", the game should start automatically.
On receiving a Tele(Activate,TeamChange,hpid,teamstate) message the team state for the host/player determined by hpid (see above)is changed to the state defined by teamstate.
TODO: implement a Tele(Spontaneous,TeamChange,hpid,teamstate) message for team change requests. Determine a simple/elegant way to show both current teams state and pending team state requests by all clients. Perhaps it suffices to only allow team state requests for the local host. Clean up the implementation of team change (ugly construct with TeamChange event and TeamChangeData event currently).
On receiving a Tele(Spontaneous,HostDisconnected,id,empty) , the host id is removed from the local game table. If id=0, then the server itself has disconnected and the game table can be cleared completely, An explicit disconnect request is expected to follow.
On receiving a Tele(Activate,RequestDisconnect,0,empty) , the host closes its end of the connection: it has been kicked or the server disconnected.
TODO: The reason for the disconnect can be transmitted in the IOB field, so that the disconnected client can display the reason.
On receiving a Tele(Activate,StartGame,id,empty) message, the game is started. The host proceeds to the first sync point (XBNW_SyncEndOfInit).
On receiving a Tele(Activate,Sync,event,empty) message, the client is informed that all clients have triggered the given event on server and that server has proceeded to the following sync point. Normally, the client should be currently waiting at the sync point determined by event, when this message is received.
On receiving a Tele(Activate,RandomSeed,id,seed) , the random seed is initialized according to the server data.
On receiving a Tele(SendData,LevelConfig,section,config) the config line is added to the current level config, in the received section. On receiving a Tele(SendData,LevelConfig,section,empty) the level config is considered complete and checked for playability (via level version). A Tele(Activate,LevelConfig,0/1,empty) message is returned, with 0 if level is rejected, 1 if level is approved. (new in 2.9.23)
TODO: implement a level check that actually checks the entries and returns a list of entries that are unknown to server, who then can decide to simply modify the level accordingly.
On receiving a Tele(Activate,LevelConfig,how,empty) message, the currently negotiated level is either reset (how=0, server choses another level) or activated (how=1, server plays level). New feature since 2.9.23, replacing the somewhat ugly async checking that was done during 2.9.18-2.9.22.
On receiving a Tele(Activate,Async,XBNW_SyncLevelResult,empty) message, the last level result has async'ed and should be converted to a draw. New feature since 2.9.23, replacing the somewhat ugly async checking that was done during 2.9.18-2.9.22.
On reception of a Tele(Spontaneous, Chat, how, chat) , the chat line is displayed locally.
TODO: move the display part to the GUI event loop and separate it from the actual network layer.
After receiving the server UDP port, a local XBCommDgramServer socket is set up and pointed to the server port.
During the join phase, the client regularly sends Dgram(empty) message pings to the server, which will finish the server setup of its UDP port, in case client is NAT'ed. With each such ping, the socket is checked for the last reception of data - after a timeout of currently 10 secs, the link is considered lost and the client closes both UDP and TCP sockets.
On receiving a Dgram(times) message, the client ping times are stored and announced to the GUI to display.
During a level, client starts with gametime=0 after receiving the SyncLevelIntro from server, which means that server is currently at least in his first timer loop. The client then enters a loop to wait for a server event - during this loop previously queued data is sent on writeable sockets and local keys are noted. A server event occurs for example if a Dgram(frames), is received. It usually contains data for a single gametime - if the gametime does not exceed the current gametime, this indicates an error and probably should make the client disconnect (ignored currently). If the received gametime equals current+1, it is the expected data, otherwise data loss has probably occurred (current reaction: raise server event for next gametime ?!). The client proceeds to send his local keys to server which will normally be received back from server with other clients keys during the next wait loop and then actually applied in the following gametime. Any data received during the wait loop will then be displayed locally and gametime will be increased. Another server event occurs on receiving a Dgram(frames) message with one finish byte (0xFF), which will finish the lvel on the client and prompt the client to acknowledge receiving the finish by returning it to server. A third possible server event is disconnection, which will quit the game.
Under normal circumstances, clients will receive server keys only at gametime=0 and then client actions for the expected gametime in the following wait loops, including the local keys queued to client before the current wait loop and sent during the current wait loop. The local keys will be ignored by the server if they fail to reach the server before the timer triggers for the respective gametime.
The "Update statistics" button attempts to establish a XBCommToCentral connection to central, on which a Tele(RequestData,PlayerConfig,empty) message is then sent and a temporary player config is created.
On receiving a Tele(DataAvailable,PlayerConfig,config) , the config line is added to the temporary player config.
On receiving a Tele(DataAvailable,PlayerConfig,empty) , the temporary player config is added to the player database for central (atomCentralRemote) and the temporary player is reset.
TODO: add some filtering functions to the GUI reading the data (not really network todo)
On receiving a Tele(DataNotAvailable,PlayerConfig,empty) , all players at central are received and a disconnect signal is sent to central.
Server sockets are those sockets that are used in creating and starting a network game with the XBlast application. The following shorthand notations are used in this section:
A disconnect signal from server to ID means queueing Tele(Spontaneous, HostDisconnected, 0, empty) and Tele(Activate,RequestDisconnect, 0, empty) messages to ID on the TCP connection.
Performing a close of ID means closing both the TCP and UDP sockets of the client ID and informing the still connected clients by queueing Tele(Spontaneous, HostDisconnected, ID, empty) messages to them on their respective TCP connections. It also raises a XNBW_Disconnected event for ID.
Syncing a network event E means that server waits that this specific event or a XBNW_Error or a XBNW_Disconnected has to be raised by all connected TCP sockets. After this happened, server queues a Tele(Activate,Sync,E,empty) to notify the clients.
TODO: move these batch signals to someplace more appropriate, with links.
After "Create a Network Game" a XBCommListen socket is created to wait for incoming clients and assigning to them a XBCommToClient connection.
It is removed when the game is started.
TODO: keeping the listening socket could enable on-the-fly joining of clients.
The communication from game-server to game-client is controlled by two sockets: the TCP socket is mainly used in the pre-game phase, while the UDP socket is only used in-game and for pings.
The TCP connection is set up as XBCommToClient connection after a client connects to the server listen port. On creation of the connection, the following happens:
TODO: distinguish between pre-game and in-game connection for on-the-fly. In-game connections should probably held without processing (except some kind of wait signal to the client) until the level is over, at which point the joining procedure can be resumed.
The next event on this socket normally is the reception of Tele(DataAvailable,GameConfig,ID,data) messages.
On reception of a Tele(Activate,DgramPort,ID,port) , there are two cases:
On reception of a Tele(DataAvailable,PlayerConfig,ID,data) message:
On reception of a Tele(Spontaneous, Chat, how, chat) , the chat line is displayed locally and relayed to other client by Tele(SendData, Chat, how, chat) messages, according to the received chat specifications.
TODO: move the display part to the GUI event loop and separate it from the actual network layer.
On receiving a Tele(Spontaneous, HostChange, ID, hoststate) , the clients host state is updated to the received host state.
TODO: store incoming requests and determine approval via some reasonable policy (all clients agree, all clients and server agree, a single server-assigned master client requests...). Figure out some sane way to display current state and pending requests.
On an i/o error or a parse error (COT neither of DataAvailable, DataNotAvailable, Activate, Spontaneous), an XBNW_Error is raised for GUI. A disconnect signal is sent to ID and a close is performed.
On receiving eof from ID, a close is performed to ID.
Each change of the ID host state, as determined in the Server GUI, leads to queueing of a Tele(Activate, HostChange, ID, hoststate) message to to all connected clients.
Each change of the ID team state, as determined in the Server GUI, leads to queueing of a Tele(Activate, TeamChange, ID, team) message to all connected clients with team reflecting the current team state of ID.
Kicking ID in the GUI leads to a disconnect signal to ID.
Closing the server in the GUI leads to disconnect signals to all connected clients, in particular ID.
Starting the server in the GUI leads to Tele(Activate,StartGame,C,empty) message for all clients id=C that are fully joined, in particular for ID.
Receiving a Tele(Spontaneous,Sync,E,empty) message generally raises the network event E on the server, which enables the syncing procedure during the game: XBNW_EndOfInit, XBNW_LevelIntro, XBNW_LevelEnd, XBNW_Scoreboard are sync'ed that way.
Between the XBNW_EndOfInit and XBNW_LevelIntro sync points, server queues Tele(Activate,RandomSeed,ID,seed) messages to all clients, in particular ID. It then starts the level negotiation routine:
TODO: allow Tele(Data(Not)Available,LevelConfig,section,config) messages instead of the Tele(SendData,LevelConfig,section,config) signal, the received config being those level lines the client needs changed/defaulted to be able to play. After all modification configs are in, decide if those entries should be defaulted or a new level should be chosen.
After the level ended, but before the XBNW_LevelEnd sync point, the server waits for the XBNW_SyncLevelResult event from all clients, which is raised by receiving Tele(Activate,WinnerTeam,T,empty) messages. After all winner teams are in, the results are matched and if two results differ, the game is drawed and Tele(Activate,Async,XBNW_SyncLevelResult,empty) is queued to inform the clients. Otherwise Tele(Activate,Sync,XBNW_SyncLevelResult,empty) is queued.
This socket is created after a client has connected on the corresponding TCP connection. It is pointed ("connected") to the client after the client port number has been communicated via TCP in the non-NAT case or to the sender of the first datagram which matches the correct host in the NAT case (assuming NAT is enabled on server).
Incoming Dgram(empty) messages update the current ping time of the client by calculating the time difference to the last send on the socket.
Incoming Dgram(frames) messages update the client keys for the transmitted game time. Multiple action entries are not an error, but only the first entry is considered.
Server sends the collected ping times of all clients as Dgram(times) whenever at least 500ms have passed after the last send. If either the last send or receipt was more than 10secs ago, a disconnect signal is queued to the client.
During a level, server starts with gametime=0 and initializes a timer that continuously triggers after fixed time spans. While the server waits for the timer to trigger the first time, it should normally send out queued actions from the previous gametime (or a SyncLevelIntro at gametime=0). It also notes local keys and incoming actions from clients matching the gametime (there are no incoming actions at gametime=0, since clients still wait for the sync then). When the timer triggers, a Dgram(frames), is queued to the clients, which contain the local keys and just received actions, stamped with gametime+1. This message is then usually actually sent during the next timerloop. After displaying these actions locally and in case the level goes on, the gametime is increased and the next timerloop entered. Otherwise a Dgram(frames) message with one finish byte (0xFF) is queued to the clients to inform clients of level end.
Under normal circumstances, servers will send out a sync at gametime=0 and a single action for the current gametime during the timer loop. If for some reason the dgram socket does not become writeable during the timer loop, new actions will be appended to the already queued data and sent together when the socket becomes writeable. Clients won't have the chance to send actions for the appended gametime, since they still wait for the data of the previous gametime(s). Actions from clients from previous gametimes are ignored, as those gametimes are already displayed locally - this can happen if the client has bad ping and the action comes in after the corresponding timer has triggered on the server. Actions from future gametime should be unable to occur and indicate a client error - a client sends a specific gametime action only after server has sent actions with the same gametime and that hasn't happened yet (those clients should probably disconnected, currently data is ignored).
The socket is always removed together with its corresponding TCP socket, so at the latest at the end of the network game.
On receiving a Dgram(Browse(Query,serial,empty)) message, a Dgram(Browse(Reply,serial,gamecfg)) message is returned to the sender. The serial is client-assigned and just returned back.
After "Create a network game" a XBCommNewGame socket is established on the configure port, assuming visibility in Central is enabled. This socket is created on the first interface on which the creation succeeds. A Dgram(Browse(NewGame,S,newgame)) is subsequently queued to central.
TODO: interface choice has to be optimized somehow.
On receiving a Dgram(Browse(NewGameOk,S,gameid)) message, the received game id at central is stored in the newgame block for subsequent updates and closing the entry.
During the pre-game phase the Dgram(Browse(NewGame,S,newgame)) message is resent every 255 cycles of the GUI event loop, the game name updated to reflect the number of joined players.
During game phase the Dgram(Browse(NewGame,S,newgame)) message is resent at the start of the level and every 1024 frames, the game name updated to reflect the current score.
The socket is removed at the end of the game.
At the start of the game, before the XBNW_SyncEndOfInit sync point, a tcp socket is connected to central.
After each level, more exactly between the XBNW_SynxLevelEnd and XBNW_SyncScoreboard sync points, the level result is queued to central as Tele(SendData,GameStat,0,result) message, with positive number of players.
After the game ended with a winner that reached maxWins, the game result is queued to central as Tele(SendData,GameStat,0,result) , with negated number of players, followed by a disconnect signal to central.
TODO: the result data block may suffer from a little/big-endian problem and should be should be done in little endian.
The socket is closed on receiving an eof. On an i/o error the event XBNW_Error is raised before closing the socket.
Central sockets are those sockets that are used after starting a central with the XBlast application.
On start of the central, a XBCommListen socket is created to accept incoming users, which creates a user connection.
The socket is closed when central is stopped.
A user connection is created as a XBCommFromCentral connection when an incoming user is accepted by the XBCommListen socket.
On receiving a Tele(SendData,PlayerConfig,any,config) message, the config line is added to a temporary player database.
On receiving a Tele(SendData,PlayerConfig,any,empty) message, the temporary player database is checked for the atomPID entry. If it is positive, it is considered a player update and if PID is valid and password matches, the local player database is updated. If the received PID is not positive, it is considered a new registration: the player name is checked for uniqueness and if it is, a new player PID will be assigned and the temporary player added to the local database. In either event, a Tele(DataAvailable,PID,any,pid) message is sent back, with pid denoting the pid entry modified in the local database or a negative error code (see "Registering Players" above).
On receiving a Tele(RequestData,PlayerConfig,any,empty) message, the local database is sent back (passwords excluded). This is done by sending Tele(DataAvailable,PlayerConfig,any,config) messages terminated by a Tele(DataAvailable,PlayerConfig,,empty) message for each player database and finishing with a Tele(DataNotAvailable,PlayerConfig,,empty) after the last player.
On receiving a Tele(SendData,GameStat,any,result) message, the player databases will be updated according to the result data.
TODO: use little-endian format in result data block.
On start of the central, a XBCommNewGameOk socket will be created on the same port as the Listen socket.
On receiving a Dgram(Browse(Query,serial)) message, the currently registered games will be sent back by sending Dgram(Browse(Reply,serial,gamecfg)) messages for each registered game. The serial is assigned by the sender and not modified by central.
On receiving a Dgram(Browse(NewGame,serial,newgame)) message, central first checks if an already assigned id holds the host:port combination of the server. If so, that entry will be updated (this step was added for 2.9.23 to avoid duplicate entries for UDP-blocking servers). Otherwise the id is extracted from the newgame data block and validated: if the id is out of range, the message is considered a register request and a free id is assigned under which the game data will be stored. Otherwise, the message is an update request for an already existing entry. In any event, a Dgram(Browse(NewGameOk,serial,gameid) will be sent back, where the serial is the same as the received one.
The player name checking is new in 2.9.23. Older clients receive a central error if the registration fails due to an already used name.
On receiving a Dgram(Browse(NewGameOk,serial,gameid)) message, the game registered under the received gameid will be removed from central.
Each registered game will currently timeout after 30 secs without the game data being updated by an incomng Dgram(Browse(NewGame,serial,newgame)) message.
A client-controlled server should in theory allow the clients to force a game start or set game parameter without having to rely on a user at the server machine. Client-control will also enable creating a standalone gameserver that simply functions as central relay station for the client's data.
The basic philosophy of the current implementation is that clients make state requests which are sent to the server and relayed to the other clients. The server alone decides on granting the request and if the request is granted, server sends state commands activation signals to the clients, which have to be obeyed.
In the wait menu, each host has several states displays: the current state (as sent by the server), the local request (as determined by menu handling) and the external requests (as sent by the server) - both for host and team states. To view the current display of these states, compile with -DSTATES; there might be better ways for displaying these data.
The first way to change the states is protocol-induced: the server sets initial values on connect and kicks out unaccepted hosts on start game. The second way is to manually make requests - on menu level, the next reasonable request following the current is determined and displayed. On servers, the request is passed to the request receiver on server level; on clients, the request is sent to server. So any local requests automatically end up at the receiver at the server. Here the request relayed to any other clients and then processed by the server. Depending on the processing, appropriate commands are then sent out which change the states.
The implemented signals are:
To allow for standalone game servers, the server should be separated into an actual Server unit and a Client unit (for potential local players). The Server unit just processes incoming requests and issues server commands or relays data. The Client unit is treated as just any other client - to simplify things, one might want to have the Client Unit actually connect to the Server Unit via a local socket connection instead of direct, internal communication.
Finally, server policies are needed that define how the server processes requests. Possible such policies are:
Implementation of these features should make it easy to add a "View" state which allows connected clients to not take part in a game but rather watch the game - a server with no local players would be automatically in that mode (provided it has GUI).
Team Mode handling could be done by generally having the team display - playing in teams is only activated if all clients are assigned teams (more than one different team).
Joining on the fly could be enabled by not closing the Server Listen socket at the start of the game. Clients that connect during a level, would be have to be sent a "Wait" signal instead of the normal game config. This message probably has to be relatively short, to not influence the game.
When the level ends, before the new level is chosen, the clients are moved back to the Join phase where the setup of the wating clients is completed. The newly joined client(s) are first assigned the state "Out" or maybe "View" and have to be voted/chosen "In" by the other clients and assigned teams, before they can play.
It might make sense to have server options for auto-dealing with on-the-fly joining (auto-in, auto-view, deny) and also allow clients to vote on those options (involving a new OptionChange signal).