posted by Jeremy Friesner on Mon 12th Aug 2002 20:52 UTC

"The FRCPlayerSession Class"

Now that the server is set up, let's look at the FRCPlayerSession class: Remember that one FRCPlayerSession object is created for each TCP connection that the server receives, and its responsibility is to handle I/O to and from the client on the other end of that TCP connection.

Here is the declaration of the FRCPlayerSession class, taken from frc/server/FRCPlayerSession.h:


class FRCPlayerSession : public StorageReflectSession
{
public:
   FRCPlayerSession();
   virtual ~FRCPlayerSession();

   /** Overridden in order to send out the MOTD, if one exists. */
   virtual status_t AttachedToServer();

   /** Overridden to inform the game-state session about our impending departure */
   virtual void AboutToDetachFromServer();

   virtual void MessageReceivedFromGateway(MessageRef msg);
   virtual void MessageReceivedFromSession(AbstractReflectSession & from, MessageRef msg, void * us
erData);

protected:
   virtual const char * GetTypeName() const {return "FRCPlayer Session";}
};

Besides the (not very interesting) constructor and destructor, there are several virtual "hook" methods that we've overridden here in order to customize the session's behaviour. Let's review them, in order:


virtual void AttachedToServer();

This method is called when the new FRCPlayerSession is first added to the FRCServer. When this method is called, everything has been set up and it is safe to call most of the methods in the StorageReflectSession API. The FRCPlayerSession class overrides this method for two reasons: so that it can look for a motd.txt file to read in and send as the server's welcome text message to the new user, and so it can tell the FRCGameStateSession that a new client has appeared. Here is the body of the method:


status_t
FRCPlayerSession :: AttachedToServer()
{
   if (StorageReflectSession::AttachedToServer() != B_NO_ERROR) return B_ERROR;

   // Try to read the motd.txt and send it the client
   FILE * fpIn = fopen("motd.txt", "r");
   if (fpIn)
   {
      int bufSize = 2048;
      char * buf = newnothrow char[bufSize];
      if (buf)
      {
         MessageRef motdRef = GetMessageFromPool(FRC_COMMAND_CHAT_TEXT);
         if (motdRef())
         {
            motdRef()->AddString(PR_NAME_SESSION, "0");  // for FRC, session 0 means from motd
            while(fgets(buf, bufSize, fpIn)) motdRef()->AddString("text", buf);
            AddOutgoingMessage(motdRef);
         }
         delete [] buf;
      }
      fclose(fpIn);
   }

   // Notify the game-state session that we are here (other sessions will ignore this)
   BroadcastToAllSessions(GetMessageFromPool(FRC_INTERNAL_NOTIFY_NEW_CLIENT));

   return B_NO_ERROR;
}        

There are several important things things to note here:

First, it is critical that AttachedToServer() call the superclass's AttachedToServer() implementation before doing anything else; otherwise the session's state will not be set up correctly and Bad Things Will Happen.

Second, this method demonstrates how easy it is to send a Message from the server to the client's PC. All we need to do is allocate a new Message object (calling GetMessageFromPool() is the most efficient way to do this), add our data to it, and then call AddOutgoingMessage() with it. AddOutgoingMessage() will add the Message to the outgoing Message queue, and it will be sent as soon as possible. Because the Message is queued rather than sent synchronously, AddOutgoingMessage() is guaranteed not to block, and thus you can add large amounts of outgoing data at once without worrying about hurting the server's responsiveness. (Of course, you might use up lots of RAM that way!)

The last thing that AttachedToServer() does is send a Message to the FRCGameStateSession, letting the FRCGameStateSession know that a new client has connected to the server. Actually, that isn't quite accurate -- the Message is broadcast to all the sessions in the server's session list, not just the FRCGameStateSession -- but all the FRCPlayerSessions are programmed to ignore it. This isn't as inefficient as it sounds, because all sessions are in the server's process space. Because of that, "sending a Message to a session" is no more expensive than a virtual method call (okay, actually it IS a virtual method call ;^)).

We have AttachedToServer() return B_NO_ERROR to indicate that everything is okay, and the session is ready to join the list of connected sessions. If we had returned B_ERROR instead, that would indicate that the session had fatal problems initializing, and the server would immediately close the TCP connection and delete the FRCPlayerSession object.


virtual void AboutToDetachFromServer();

This method is the counterpart to AttachedToServer(). It is called just before the session object is to be removed from the server -- typically this happens right after the TCP connection to the client has been closed. In this method, you can do any cleanup that you need to do -- the FRCPlayerSession class overrides this method to send another Message to the FRCGameStateSession, this one notifying the FRCGameStateSession that the session is leaving.


void
FRCPlayerSession :: AboutToDetachFromServer()
{
   // Make sure that the game controller removes our pieces from the board
   BroadcastToAllSessions(GetMessageFromPool(FRC_COMMAND_EXIT_GAME));
   StorageReflectSession::AboutToDetachFromServer();
}   

Again, it is critical that the AboutToDetachFromServer() call is passed on to the superclass -- only this time, it must be passed on as the last thing the method does, rather than the first.


virtual void MessageReceivedFromGateway(MessageRef msg);

This method is a very important one -- it is called whenever our client sends us a Message. Our FRCPlayerSession class overrides this method to do a little integrity checking -- it makes sure that any Messages sent by the client contain the correct PR_NAME_SESSION string (so that the client can't pretend to be another client), and that the Messages aren't labelled with any command codes that only the server is allowed to generate (so that the client can't pretend to be the server). Once it is is satisfied that the Message the client sent to us is legitimate, it passes the MessageReceivedFromGateway() call on up to the superclass, where it is handled in the usual MUSCLE fashion (which is to say, any Messages that are MUSCLE commands are parsed and executed, and any other Messages are broadcast to all the other sessions for them to look at)


void
FRCPlayerSession :: MessageReceivedFromGateway(MessageRef msgRef)
{
   // Don't allow clients to spoof any command codes that should only be generated internally.
   Message * msg = msgRef();
   if (msg)
   {
      // Make sure all messages received from the client contain only the client's
      // own session ID string, to thwart any attempts at spoofing the other clients...
      msg->ReplaceString(true, PR_NAME_SESSION, GetSessionIDString());

      // Also make sure that the Message's command code isn't a command code that only the server is allowed to generate
      if ((muscleInRange(msg->what, FRC_BEGIN_INTERNAL_COMMANDS, FRC_END_INTERNAL_COMMANDS) == false)&&
          (muscleInRange(msg->what, FRC_BEGIN_REPLIES, FRC_END_REPLIES) == false))
      {
         // If it passed all the tests, hand the Message over for default MUSCLE-style processing.
         StorageReflectSession::MessageReceivedFromGateway(msgRef);
      }
   }
}    


virtual void MessageReceivedFromSession(AbstractReflectSession & from, MessageRef msgRef, void * userData);

This method is the "local equivalent" of MessageReceivedFromGateway() -- whenever a session object passes a Message to a neighbor session (either by calling BroadcastMessageToAllSessions(), or through one of several alternate methods), the target session's MessageReceivedFromSession() is called. Inside MessageReceivedFromSession(), the target session can decide what to do with the Message -- it can examine the Message and take some action based on the Message's contents, or pass the Message on to its client by calling AddOutgoingMessage() with it, or simply ignore it. A reference to the sending session is passed in, so the target session can "reply" to the Message, if it likes, simply by calling from.MessageReceivedFromSession() with another Message object. In our FRCPlayerSession class, however, the implementation is very simple: we check the Message to make sure it's not one of the "internal" Messages that are never supposed to leave the server, and if it isn't, we pass it on up to the superclass for the usual default MUSCLE processing (which is usually to send the Message on out to the client via AddOutgoingMessage()).


void
FRCPlayerSession :: MessageReceivedFromSession(AbstractReflectSession & from, MessageRef msgRef, void * userData)
{
   const Message * msg = msgRef();
   if (msg)
   {
      if (muscleInRange(msg->what,FRC_BEGIN_INTERNAL_COMMANDS,FRC_END_INTERNAL_COMMANDS))
      {
         // No internal commands to process yet.... but don't send them back to the client anyway.
      }
      else StorageReflectSession::MessageReceivedFromSession(from, msgRef, userData);
   }
}

And that's all there is to the FRCPlayerSession class -- as you can see, it's a very thin veneer over the standard MUSCLE StorageReflectSession class. If it wasn't for a bit of cheater-detection and the Message Of the Day, we wouldn't need to have a custom subclass at all! All the game-specific FoxRabbitCarrot logic is contained in the FRCGameStateSession object, which will we take a look at next.

Table of contents
  1. "Foxes, Rabbits, and Carrots"
  2. "The Game and How to Play it"
  3. "A Brief Review of the MUSCLE Networking Layer"
  4. "muscled, The basic MUSCLE server"
  5. "Customizing the MUSCLE server"
  6. "How to Set up the Custom Server Logic"
  7. "The FRCPlayerSession Class"
  8. "The FRCGameStateSession class"
  9. "Overview of the FRC server"
  10. "The FoxRabbitCarrot Client Program"
  11. "How the Client Handles Database Update Messages"
  12. "MUSCLE gaming Performance Issues"
e p (0)    8 Comment(s)

Technology White Papers

See More