Network Game Programming - Issue 05 - Watch your Language by Dan Royer (28 July 1999) | Return to The Archives

Introduction

Now that we've got a socket open and we're all set, we're ready to transmit data. While I will do my best to explain the essentials, I would like to point out that everyting from here on is my solution. You may come up with something completely different. If you do, I'd like to hear about it.
(note the start of new paragraph as I take a deep mental breath)
Sending and receiving data is easily the hardest part of communicating on the internet. One of the advantages of modern networking is that you are guaranteed to receive all the data that was sent in the order it was sent (FIFO: first in, first out) if you use TCP/IP. That's all well and good but there's one little problem: the machine sending the data knows what it's sending but the machine that's receiving the data needs some kind of hint or it will be as usefull as a random string of numbers.
Most of the time the solution is fairly straightforward. For example, an FTP program would send strings such as PWD, CWD, USER, PASS and so on. A typical chat application might send [user id][message length+1][message+terminating null] to and from the server once the users are logged in to a chat room, some special user ids being reserved for non-chat messages such as leave chat room, kick user, etc...
The first thing I did when designing my communication language was step back and try to find something in common with every single message. I found only one thing. Every message is unique in one way or another. Even if the same TYPE of data is sent its USE will be different for each type of message. This also means that some message may be of the same size, only their type will be unique.
What it really breaks down into is this: we need to send messages in some kind of pattern that the server and client have agreed upon. The messages should probably begin with an id code so that the receiving computer knows what the message is and how big it is. So I created

    // The default cComm Message structure.
    struct cCMData {
      int      d_idCode;
    };

    // Other structures begin the same as cCMDefault
    // and can be safely typecast as cCMDefault.

When the machine receiving the data first start reading it in, it reads in a single int and based on the unique id code it knows what kind of data block it is receiving. This enables it to allocate ram to store the block in with a minimum of fuss and no wasted space. Then it fills out the rest of the allocated space with the data left to be read (if any) and we're all done.
There are three major problems left with this system. Firstly, it can happen that lag gets in the way and only half a message is sent or received (the rest arriving with the next FD_READ). Second, we need a place to store the data blocks that have been received but not processed and we need a place to store the messages waiting to be sent. The solution is a new message queue class.

    // The cComm Message Queue class
    class cCMQueue {
    protected:
      void       *d_pData;
      int        d_size, d_transmitted;
      cCMQueue   *d_pNext, *d_pPrev;

    public:
      cCMQueue();
      virtual ~cCMQueue();

      // Some Linked List access methods
      // d_size access methods
      // d_pData access methods

      // adds amnt to d_transmitted and returns the sum.
      int  AddTransmitted( int amnt );
    };

I keep a "to send" linked list and a "being received" linked list for each client (or each socket, depending on how you want to look at it). With each transmission d_transmitted goes up. When d_transmitted equals d_size, the entire message has been transmitted. If cCMQueue is in the list of messages to send and the entire block has been sent then the queue can be destroyed. If the cCMQueue is in the list of received messages and it's message has been downloaded and processed then it too can be destroyed.
The nice thing about cCMQueue is that it can be used without modification in both client and server, so all the cCMQueue management stuff as well as all the read/send stuff can go in cComm. It has one complication, however (remember how I said there were three problems?): the cComm class only knows about one message type, cCMData. If our client and server want to send anything more than a single int they'll need a way for cComm to recognize the type and be able to figure out what size it is...What if we override a cComm base class method? Joy!

    int cComm::DetermineSizeFromType( int type ) {
      return sizeof( int );
    }

    int cClient::DetermineSizeFromType( int type ) {
      switch( type ) {
        case RECOGNIZED_TYPE_ID_1:  return sizeof( /* recognizedStructure1 */ );
        case RECOGNIZED_TYPE_ID_2:  return sizeof( /* recognizedStructure2 */ );
        // ...
        case RECOGNIZED_TYPE_ID_N:  return sizeof( /* recognizedStructureN */ );
        default:  // includes cComm default type
          return sizeof( int );
      }
    }

    int cServer::DetermineSizeFromType( int type ) {
      // this method should match EXACTLY the cClient::DetermineSizeFromType()
    }

So how does all this work together? Well, if we're reading in data it would be a little like this:

> - If we are not in the middle of receiving a data block,

> - Read a single int.
> - Check to see if that int is a recognized data block type flag.
> - if it isn't, return.
> - if it is, allocate enough ram to hold the data block.
> - also allocate a new instance of cCMQueue.
> - set queue.d_size to the size of the data block.
> - set queue.d_transmitted to sizeof( int ).
> - set queue.d_pBuffer = the new data block.
> - attach queue to the "incoming" linked list.

- read into ( queue.d_pBuffer + queue.d_transmitted )
- add the ammount read in to d_transmitted.
- if d_size equals d_transmitted then we have finished receiving this data block. And sending data is a very similar process. Lately I've been trying to come up with a method that would avoid allocating/destroying data blocks over and over but I'm not saying any more until I get it working.

  So now we've covered everything you'll need to know in order to listen, connect, transmit and disconnect. But what about games?! How does it all fit in with games? WASN'T THAT THE WHOLE POINT!?! @#$%(&!! Calm down, you're gonna bust a vein if you keep that up. Next week we'll cover everything you'd need to know about WHAT to send in order to run a game. Until then take something to lower that blood pressure and get plenty of rest, OK?

Article Series:

> - Network Game Programming - Issue 01 - Things that make you go "hmm..."
> - Network Game Programming - Issue 02 - cComm one, cComm all
> - Network Game Programming - Issue 03 - cClient. cClient run
> - Network Game Programming - Issue 04 - cServer? I barely know her!
> - Network Game Programming - Issue 05 - Watch your Language
> - Network Game Programming - Issue 06 - Talk about things you like to do...
> - Network Game Programming - Issue 07 - I bent my Wookie...

Copyright 1999-2008 (C) FLIPCODE.COM and/or the original content author(s). All rights reserved.
Please read our Terms, Conditions, and Privacy information.