Connections, Managers, and Policies, Oh My

[ LiB ]

Connections, Managers, and Policies, Oh My!

So now you've got a decent networking framework set up. You can connect to servers, listen for clients , send and receive data, and so on. But all that doesn't help you organize your game in a sensible manner. In an effort to make organizing a network module for this game easier, I've developed a large library of modular classes for you to use.

I want to launch into a discussion about the reasoning for this library before I get down to the nitty-gritty details. Over the years , I've been studying all the popular open -source MUDs, and I'm disappointed. Most of them began their lives 10 to 20 years ago, long before C++ was standardized.

So I understand why most MUDs that have been reworked, improved on, and expanded are extremely ugly today. Try adding a feature to a MUD, and you'll end up running around 100 files looking for bugs , or even worse , one huge multimegabyte source file. Count me out, thank you.

Another reason some MUDs are ugly is because people use them to start learning C and C++. It's a noble causebut misguided. Because many older MUDs teach concepts that are out-of-date and no longer used, they teach people the wrong lessons.

So, I want to teach you how to program the proper way. As much as everyone hates templates, they are an integral part of C++, and when used correctly, they can add much flexibility to your programs. I hope you saw from Chapter 4, "The Basic Library," how useful templates can be for adding simple decorator features to a class.

In my extended Socket Library, I want to introduce you to the idea of a policy class. You're going to have sockets that send and receive data using a specific protocol. (I covered the concept of protocols in Chapter 2.) Now, a socket has no idea about the protocol; its job is to be dumb and just send and receive raw streams of bytes. That's a good way to think of it, because you can take the socket class and easily move it to some other type of application with no problems.

So now you have a socket class that doesn't care about what protocol it uses, but you want to expand it. You want to create some sort of flexible architecture in which you can say, "I want to create a socket that uses this protocol!" This is where the idea of a policy comes in. A socket will just receive raw data from the network and then send it off to its policy class, which will actually interpret that data. This can be done using templates. For this behavior, I've created a new kind of DataSocket , known as a Connection :

 template< class protocol > class Connection : public DataSocket 

That's just the class declaration, for now. I'll get to the guts of the class later on. So, as you can see, it inherits from DataSocket , and it will be able to do everything a regular data socket does. It also has a template parameter that determines which protocol policy class to use. I'm jumping the gun a little bit here, but just for the purposes of explanation, you can assume that you've got classes called Telnet , FTP , and HTTP .

NOTE

I don't actually create FTP and HTTP classes anywhere in this book; they are imaginary classes used only for the sake of demonstrating the connection concept.

Those classes represent various Application layer protocols. So, if you've got those classes, this connection class allows you to do stuff like this:

 Connection<Telnet> tcon;       // Telnet connection Connection<FTP> fcon;          // FTP connection Connection<HTTP> hcon;         // HTTP connection 

That is the idea behind a policy class; those three protocol classes are policies that govern how data is interpreted.

Protocol Policies

The protocol policy classes you will be using in conjunction with the SocketLib are pretty simple. They're only required to have one function, which is the function that the Connection class calls whenever it receives raw data:

 void Translate( Connection<protocol>& p_conn, char* p_buffer, int p_size ); 

The function takes as a reference the connection that is sending it data (for reasons illustrated in Figure 5.3), a pointer to the raw data buffer, and the size of the data in the buffer.

Figure 5.3. A connection receiving data.

graphic/05fig03.gif


A protocol class must have one more itema typedef (or an inner class, but I prefer a typedef)that defines the abstract class (meaning that it defines only an interface) that will handle "complete" messages that the protocol object receives. This class is called protocol::handler (in the case of the Telnet protocol, you would refer to its abstract handler base class as Telnet::handler ).

Imagine this scenario: You've got a protocol that sends discrete commands, but due to the unreliability of TCP network transmissions, each packet received by the connection may contain only a portion of the command, or even multiple commands. So, this raw data is sent to the protocol object, and when the protocol object detects that it has received one full command, it shoots that command off to the connections' current protocol handler. Figure 5.3 demonstrates this process.

I designed the system like this for a reason: The protocol classand only the protocol classshould know the format for sending complete commands to the handler. The connection class doesn't know how to send data to its current handler, because it doesn't know what format the handler expects for data. The connection class simply hands raw data to its protocol, which then translates it and sends it to the handler.

Every connection handler is required to have a constructor that takes Connection< protocol >& as its parameter (where protocol is whichever protocol policy you are currently using), so that the handler can keep track of the connection it is linked to. You'll see more of this when I get into the specifics in Chapter 6, "Telnet Protocol and Simple Chat Server."

Protocol handler classes must also have the following functions, which are called whenever certain events occur to a connection:

 void Enter();          // connection enters state void Leave();          // connection leaves state void Hungup();         // connection hangs up void Flooded();        // connection floods 

Additionally, you should always have at least one protocol handler class that has this function:

 static void NoRoom( Connection<protocol>& p_connection ); 

This function is called whenever a connection manager receives a new connection, but there isn't enough room for it, and the connection must be told so. I will show you much more about the handler classes in Chapter 6, when I teach you about Telnet. For now, this is all you need to know about the protocol and protocol handlers.

Connections

As I mentioned before, I wanted to create a more specialized socket class, one that would communicate with a protocol class, and have other features as well. For this purpose, I've created the Connection class, located in the file /Libraries/SocketLib/Connection.h. Figure 5.4 shows the UML diagram of the Connection class.

Figure 5.4. UML diagram of the Connection class.

graphic/05fig04.gif


You can see that the class adds a bunch of new features on top of the regular data socket class, which I will go over in the next few sections.

NOTE

The dotted box at the top of the diagram represents a template parameter.

New Variables

There are several new variables.

Protocol Object

First and foremost is the m_protocol member variable. You could theoretically make the protocol class static, which would mean that the class would exist without data, but that method has a problem. For example, it is entirely possible that you'll receive partial commands when receiving data from the connection; therefore, something needs to buffer that data. I've decided to let the protocol class handle the command buffering.

For the protocol class to handle the command buffering, the protocol class actually needs to be instantiated . Therefore, the Connection class will always keep an instance of its own protocol object. This method works really well if you ever plan to introduce multithreading into your engine.

NOTE

Handler classes have one special characteristic you must be aware ofhandler classes are lightweight classes, meaning that every connection has its own instances of its handler classes. Because the handler classes are also Polymorphic (which means they inherit from a common base class), each handler can behave differently. Using polymorphic inherit ance, however, requires the use of pointers, which is why the stack holds pointers to handlers, instead of actual handler objects. This introduces yet another problem: Using indi vidualized lightweight classes means that every connection needs a handler created by new . (Since handler classes are polymorphic, you need to use pointers.) This effectively makes the Connection class own all of its handlers. New handlers are passed into it, but from that point on, the Connection manages the handler, and deletes it when it is no longer needed. This is essential to avoid memory leaks.

The Stack of Handlers

The next variable is the m_handlerstack , which holds protocol::handler pointers. In a traditional network application, it is common to see connections maintaining a current state . For example, a connection could be "logging in" or "playing the game." These specific states are represented by individual protocol::handler objects. You'll have a handler class for connections that are logging in, a handler class for connections that are in the game, and a handler class for any other state you can think of. I show you a great deal about handlers in Chapter 6.

So why not just have a single handler pointer to represent the current handler? Well, there may be times when you're going to want recursive states. Say the user is in state 1 and then switches to state 2. When the user in state 2 is finished, you might want the connection to go back to the previous state. This is why I'm using a stack. When state 1 is on the top of the stack, you're in state 1, but when you switch to state 2, the new handler is pushed onto the top of the stack. Later on, when you exit the state, it is popped off the stack, and state 1 is back at the top. Basically, this system gives you flexibility. Figure 5.5 shows this process.

Figure 5.5. With the handler stack process, you can add new states on top of earlier states and go back to the earlier states later on, without the new states knowing which state to return to.

graphic/05fig05.gif


A Sending Buffer

The next variable is m_sendbuffer , which is an std::string . This buffer is used to store data that you want to send, until it can actually be sent. Why a string? Well, strings are good at storing plain bytes. You can easily make your job easier by adding large chunks of bytes to the end of strings, and the strings can also be turned into plain char* arrays by calling their data() function. Whenever you buffer data into a connection, it is put into this string, and then sent out later (when a connection manager deems it appropriate).

Rates and Times

The m_datarate variable keeps track of how many bytes-per-second the socket is receiving during the current "time chunk ." (I'll explain this in a bit.) Likewise, m_lastdatarate stores the amount of data that was received during the previous time chunk. I calculate these values in chunks of time because computers are discrete machines; there's really no way to get an exact instantaneous value of data-per-time; instead, the best that can be done is to count the number of bytes received over a certain period. That's what these variables store.

Other related variables are m_lastReceiveTime and m_lastSendTime . As you can probably guess, these related variables store the last time data was received and sent. The m_checksendtime Boolean is used to check client deadlock. This is an issue I will discuss a little later on.

NOTE

These time variables are stored as second values, not millisecond values.

The m_creationtime variable holds the system time (in seconds) at which the connection was created, so you can tell how long the connection has been open.

Other Data

BUFFERSIZE is a const integer that determines how large a Connection s receive buffer is, and then m_buffer is the actual buffer. I've hard-coded BUFFERSIZE at 1024 bytes, and I can't see much use in making it configurable, but you can change it if you want.

The last variable is a Boolean named m_closed that determines if the connection has been "closed." You'll see this in action later on when dealing with connection managers. The idea is that within the execution of a game, you may decide to close a connection, but you won't be able to inform the connection manager; instead, you will end up setting this Boolean to true , and the connection manager will go through every connection at a later point in time, to check if something wants the connection closed. At this point, the connection manager forcibly closes the connection and deletes it.

This is done because there are lots of problems with being able to close a socket immediately in many different parts of the code. For example, you may be in the middle of a loop going through all of the connections, and you receive data that makes the game want to close the connection (maybe a "quit" command). If you allow the game to shut down the connection immediately, the connection manager is going to run into problems later when it comes back to the iteration.

New Functions

There are a bunch of new functions in the classfunctions that will help you buffer sends, get statistics, and receive data.

The Constructors

There are two constructors. The plain constructor simply constructs the connection and clears all the data members in it; the other constructor takes a reference to a DataSocket .

DataSocket s and ListeningSocket s don't do anything in their destructors, so you're free to pass them around between functions. It would be a real nightmare if you had sockets automatically close when they were destroyed . Why? Examine the following code:

 void Function( DataSocket p_sock ) {     // blah } // later on: DataSocket s; // connect the socket here somewhere Function( s ); 

If sockets auto-close on destruction, what's wrong with that code? When you passed the socket into the function by-value into p_sock , you copied the socket, but when the function ended, it automatically destructed that socket object, which will tell the operating system to close whatever socket it is pointing to. So the next time you try using s , it won't be open. Believe meyou'll spend hours tracking down these kinds of bugs.

NOTE

If you're feeling really ambitious, you may want to consider reference- counted sockets. These are sockets that keep track of how many times they are referenced in your pro gram, and whenever they drop down to a count of 0, they automatically close themselves .

So, that's why I made sockets able to freely copy themselves. This becomes important when you look at the constructor of the connection class: It can take a data socket as a parameter and copy all the DataSocket information into itself. Then whoever created the original data socket can safely discard it and use the Connection . You'll see me do this later on, when I show you the ConnectionManager class. Here's the code for the two constructors:

 template< class protocol > Connection<protocol>::Connection() {     Initialize(); } template< class protocol > Connection<protocol>::Connection( DataSocket& p_socket )     : DataSocket( p_socket ) {     Initialize(); } 

The first constructor just calls the Initialize() function; the second takes a DataSocket as a parameter and then uses the standard base-class constructor notation to construct the DataSocket portion of the Connection . (See Appendix C on the CD for more information on base-class constructors.)

Both functions call Initialize :

 template< class protocol > void Connection<protocol>::Initialize() {     m_datarate = 0;     m_lastdatarate = 0;     m_lastReceiveTime = 0;     m_lastSendTime = 0;     m_checksendtime = false;     m_creationtime = BasicLib::GetTimeMS();     m_closed = false; } 

As you can see, the code just resets all members to their default values.

Receiving Data

The connection class has its own overloaded Receive function. The class has its own receive buffer and also keeps track of the incoming datarate, so this function doesn't take parameters or return anything.

Instead, the Receive function attempts to receive data into its buffer; then it shoots that buffer off to the protocol policy. Here's the function (split up into logical blocks):

 template<class protocol> void Connection<protocol>::Receive() {     int bytes = DataSocket::Receive( m_buffer, BUFFERSIZE ); 

The function first tries to receive as much data as it can. The DataSocket::Receive function either blocks, or throws an exception if it's in nonblocking mode and there is no data to receive. (This latter method assumes you've used a select -based method to poll the socket.)

 BasicLib::sint64 t = BasicLib::GetTimeS();     if( (m_lastReceiveTime / TIMECHUNK) != (t / TIMECHUNK) ) {         m_lastdatarate = m_datarate / TIMECHUNK;         m_datarate = 0;         m_lastReceiveTime = t;     } 

The previous code fragment gets the current system time (in seconds), and then makes a few calculations on it. Let me explain this segment. Data is sent in discrete chunks and is rarely continuous. For example, one second you might get data in a huge burst, and then the next second, you might get nothing. For MUDs, this could be a problem. For example, the particular client a person is using may store a large command and then send it all at once. The server will see this burst of activity, think that it's being flooded during that one second, and disconnect the user. Meanwhile, the user was just typing one command and was probably going to take a few seconds to type the next command.

Disconnecting your users like that is bound to annoy them to no end, so you need a better system. Instead of keeping track of bytes per second, the class keeps track of bytes per 16 seconds. Why 16? I like to use 16 seconds because it's a nice power-of-two that is close to 1/4 of a minute. Since it's a power-of-two, the compiler will almost certainly optimize the division so that it doesn't take much time. If you feel that 16 is inappropriate, you can change the value of the TIMECHUNK variable at the top of the file to whatever you wish.

Whenever the function has detected that a new 16-second block has begun, it records the datarate for the previous 16 seconds and resets the datarate to 0.

Here's the next part of the function:

 m_datarate += bytes;     m_protocol.Translate( *this, m_buffer, bytes ); } 

The number of bytes received is added to the datarate, and the buffer is sent to the protocol to be translated.

Sending Functions

On many systems, it is expensive in terms of processing power to repeatedly call socket functions such as send and recv . For a typical MUD game loop, you'll end up sending multiple lines of text to a single connection during one loop, but you don't actually want to make a call to the send function every time you send a line, right? So, the smart method is to buffer all the data you want to send, and then, at the end of the game loop, make just one call to send , trying to send the entire buffer. In practice, this usually works well.

 template<class protocol> void Connection<protocol>::BufferData( const char* p_buffer, int p_size ) {     m_sendbuffer.append( p_buffer, p_size ); } 

The function takes a char* buffer and its size; then it appends the buffer to the end of the m_sendbuffer string, using the append function.

Now, when you want to send it all, you do this:

 template<class protocol> void Connection<protocol>::SendBuffer() {     if( m_sendbuffer.size() > 0 ) {         int sent = Send( m_sendbuffer.data(), (int)m_sendbuffer.size() );         m_sendbuffer.erase( 0, sent ); 

All right, the first part of the function is no- nonsense . The function tries to send what's in the buffer, counts how many bytes are sent (nonblocking sockets return 0 when nothing is sent), and then erases all those bytes from the front of the buffer.

 if( sent > 0 ) {             m_lastSendTime = BasicLib::GetTimeS();             m_checksendtime = false;         } 

The previous code fragment checks to see if any data was sent. If so, the time is recorded, and the m_checksendtime Boolean is cleared to false . When this Boolean is false , it is assumed that there are no problems sending data; you'll see how this works in the next segment:

 else {             if( !m_checksendtime ) {                 m_checksendtime = true;                 m_lastSendTime = BasicLib::GetTimeS();             }         }   // end no-data-sent check     }   // end buffersize check } 

The final code segment occurs when no data was sent. If that happens, you know there is a problem sending data. (Either the client is under deadlock and it's not accepting data, or you're flooding it with too much data.) If the m_checksendtime flag has not been previously set, this is the first time you've noticed a sending problem, so you mark down the current time in m_lastSendTime . Although technically you didn't send anything, it is important to mark down the time you noticed sending problems. If the flag is already set, that means you've noticed there have been sending problems before, so don't update the time.

NOTE

This isn't an entirely accurate method of discovering when a client stops accepting data. For example, if the client hasn't been sent anything in a long time, you'll notice that the client has stopped receiving only when you try sending data to it. Because of this, it is often useful to send a ping to clients occasionally, so that if they do end up disconnecting without notifying you, you'll notice within a minute or so.

Closing Functions

Connections are designed to be used in conjunction with a connection manager, which, as the name implies, will manage connections. You'll learn about connections later on, but for now, you should know that whenever you manually close a connection, the connection manager must know about the connection closing, so it knows not to manage that connec-tion any longer. There are a number of possible solutions to this problem:

  1. You could require the programmer to manually tell the connection manager.

  2. You could make every connection be aware of its manager, and make the connection tell the manager it has been closed.

  3. You could simply store a Boolean, and have the connection manager check that later on.

Method 1 is a bad idea. Whenever you make code that's going to be difficult to use, it will anger people who use it (including yourself!), and generally make the project much more difficult to work on. Method 2 is also somewhat bad; it introduces cyclic dependencies (see Appendix C on the CD for more information about these), and generally uses more memory and management. (That is, you have to tell every connection about its manager in the first place.)

I have chosen method 3, as you may have already noticed when I showed you the data stored within the class. There are three functions concerned with closing:

 inline void Close()        { m_closed = true; } inline bool Closed()       { return m_closed; } inline void CloseSocket()  {     DataSocket::Close();     if( Handler() )  Handler()->Leave();     while( Handler() ) {         delete Handler();         m_handlerstack.pop();     } } 

The Close function simply sets the m_closed Boolean to true , so that when the connection manager checks the Boolean later using Closed , it can then really close the socket, using the CloseSocket function.

Whenever the program physically shuts down a connection using CloseSocket , it calls the underlying DataSocket::Close function to close the socket. After the socket is closed, the function checks to see if the connection is active inside a state; if so, then the handler's Leave function is called, to notify the current state that this connection has left it.

Once that has been done, the function goes through every state in the stack and deletes them (without re-entering them).

Handler Functions

There are five functions in the class concerning protocol handlers, which make working with them somewhat easier than manipulating the handler stack directly. Here are the functions:

 void AddHandler( typename protocol::handler* p_handler ); void RemoveHandler(); typename protocol::handler* Handler(); void SwitchHandler( typename protocol::handler* p_handler ); void ClearHandlers(); 

The code for those functions is really quite simple, so I won't bother to go over it here. For example, adding a handler involves pushing a new handler on top of the handler stack, and then calling its Enter function. The RemoveHandler does the opposite ; it calls its Leave function and pops it from the stack.

Switching handlers involves these actions: leaving and popping the current handler, and then pushing and entering the new handler (completely bypassing any handler that may have been below the first one). Clearing handlers involves leaving the top handler and deleting all of them.

The first function pushes a new handler onto the top of the handler stack. This is important: Whenever you pass a handler at the top, you must pass in one that has just been created using new . For example, if you have a handler named logon , you'd pass it in to a connection like this:

 conn->AddHandler( new logon( *conn ) ); 

It is also important never to delete that handler; the RemoveHandler function takes care of that for you. So whenever you pass in a new handler to the connection, it's pushed on top of the handler stack, and then the handler's NewConnection function is called, telling it that the connection has entered that state.

The RemoveHandler function deletes the handler at the top of the stack, since it's no longer needed, and then the pointer to that handler (which is no longer needed) is popped off the stack.

Finally, the Handler function is a simple way of retrieving a pointer to the handler on top of the stack.

Other Functions

All the rest of the functions are pretty simple; most are just accessor functions that return a value. One is more interesting than the rest: GetLastSendTime() .

 template< class protocol > BasicLib::sint64 Connection<protocol>::GetLastSendTime() const {     if( m_checksendtime ) {         return BasicLib::GetTimeS() - m_lastSendTime;     }     return 0; } 

When the m_checksendtime flag isn't set, the function always returns 0. This function is meant to return the amount of time that has passed since a connection has noticed sending problems, so it returns 0 if there aren't problems. Otherwise, if there are problems, the function returns the difference between the current time and the time when the connec- tion noticed sending problems.

Table 5.1 lists the other functions.

Table 5.1. Accessor Functions for Connection's

Function

Purpose

GetLastReceiveTime

Is the system time (in seconds) at which data was last received on the socket

GetCreationTime

Is the system time at which the connection was created

GetDataRate

Is the number of bytes per second received by the connection over the previous time chunk

GetCurrentDataRate

Is the number of bytes per second received by the connection during the current time chunk

GetBufferedBytes

Returns the number of bytes of data currently buffered on the connection

Protocol

Returns a reference to the connection's protocol object


All these functions are just simple accessors with nothing substantial in the code, so there is no need to show you the source for them.

Listening Manager

For the Socket Library, I've developed two manager classesclasses that will manage sockets and connections for you. The first of these is the ListeningManager class. As the name suggests, this manager takes care of listening sockets.

Altogether, the ListeningManager is really a simple manager. It allows you to add ports, listen on the listening sockets, and set a ConnectionManager . Whenever a ListeningManager detects a new connection, it sends the connection off to its current c onnection manager . Figure 5.6 shows the class diagram for the ListeningManager class.

Figure 5.6. ListeningManager class diagram.

graphic/05fig06.gif


The first thing you should take note of is the fact that this class has two template parameters: protocol and defaulthandler . Obviously, the protocol is the protocol policy class that

NOTE

Some ISPs block outgoing connec tions on certain ports, so you may find that users might not be able to connect to your server because of their ISP. In this situation, it's very useful to be able to listen on more than one port.

connections in this manager will use (like Telnet, which I cover in Chapter 6). The second template parameter is the default handler of a connection. Whenever a new connection is created, it obviously must be given a default handler, right? So that's what this template parameter represents.

You can see that the ListeningManager maintains a vector of listening sockets; this means that the ListeningManager class maintains multiple listening sockets. This is a neat feature if you want your programs to listen on more than one port for the same purpose, which can be helpful in certain situations.

Constructor and Destructor

The constructor for this class is simple; it just clears the connection manager pointer to 0, meaning that it hasn't been given a connection manager yet:

 template<typename protocol, typename defaulthandler> ListeningManager<protocol, defaulthandler>::ListeningManager() {     m_manager = 0; } 

And the destructor:

 template<typename protocol, typename defaulthandler> ListeningManager<protocol, defaulthandler>::~ListeningManager() {     for( size_t i = 0; i < m_sockets.size(); i++ ) {         m_sockets[i].Close();     } } 

This goes through every listening socket and closes them, so that whenever a listening manager is destructed, all of the sockets are automatically closed.

Adding New Sockets

Here's the function to add new listening sockets:

 template<typename protocol, typename defaulthandler> void ListeningManager<protocol, defaulthandler>::AddPort( port p_port ) {     if( m_sockets.size() == MAX ) {         Exception e( ESocketLimitReached );         throw( e );     }     ListeningSocket lsock;     lsock.Listen( p_port );     lsock.SetBlocking( false );     m_sockets.push_back( lsock );     m_set.AddSocket( lsock ); } 

You may have noticed from the class diagram in Figure 5.5 that the class contains a SocketSet . It uses the set to poll all the sockets it contains, to see if there are any new connections. Therefore, the number of listening sockets you're allowed to have is limited to the number of sockets a SocketSet can contain. If there isn't enough room, the function throws

NOTE

Nonblocking Listening Sockets

Unless you dedicate an entire thread to each socket, it is important that you make listening sockets nonblocking. There is a certain exploit when listening sockets are used in conjunction with the select() system call. Imagine a client that sends a connection request and then quickly shuts down. The select() call will detect that there's a connection, but by the time you call accept() , there's no more connection available, and your thread starts blocking. If this thread is responsible for more than just listening for new connections, your program is going to stop until another person actually connects to it. This is bad. Therefore, you should usually try to make listening sockets nonblocking.

an exception of type ESocketLimitReached .

Then, the function creates a listening socket, tells it to start listening, sets it to nonblocking mode, adds the socket to the socket vector, and finally adds the socket to the socket set as well.

Listening for New Connections

For now, the model of the ListeningManager is pretty simple. Once you've got all the ports added that you want to listen to, you must manually call the Listen() function whenever you want to listen for new connections.

Here's the function:

 template<typename protocol, typename defaulthandler > void ListeningManager<protocol, defaulthandler>::Listen() {     DataSocket datasock;     if( m_set.Poll() > 0 ) {    // check if there are active sockets         for( size_t s = 0; s < m_sockets.size(); s++ ) {             if( m_set.HasActivity( m_sockets[s] ) ) {                 try {           // accept socket and tell the connection manager                     datasock = m_sockets[s].Accept();                     m_manager->NewConnection( datasock );                 }                 catch( Exception& e ) {                     if( e.ErrorCode() != EOperationWouldBlock ) {                         throw; // rethrow on any exception but a blocking one                     }                 }             }   // end activity check         }   // end socket loop     }   // end check for number of active sockets } 

The function first polls the socket set, to see if any of the listening sockets have action. If they do, a for-loop goes through every listening socket and checks to see if it has activity.

The try-block tries to accept a data socket from any listening sockets that have activity, and then sends the socket to the m_manager , which is the ConnectionManager class I mentioned before. The catch-block after that catches any socket exceptions, and checks to see if they are EOperationWouldBlock errors. The existence of a would-block error means that someone may be trying to exploit your server, but you detected it. So, that error is just ignored. Any other kind of error, which will almost definitely be a fatal network system error, is rethrown.

NOTE

Using the threading knowledge you learned from Chapter 3 , " Introduction to Multithreading, " you should know enough about threading now to be able to create a threaded system, in which you have three threads. The first thread would simply listen for any connections, and then pass them off to your connection manager. Your connection manager would be running in a thread of its own as well, and would ship data off to the final threadthe game thread. I have not imple mented this kind of system in this book due to space con straints, but I may in the future. Check my website for updates: http://ronpenton.net/MUDBook .

Connection Manager

The ConnectionManager class is the most complex class within the SocketLib. This class manages connections of a certain type. Like the ListeningManager class, ConnectionManager is also templated with a protocol and a default handler. Figure 5.7 shows the class diagram.

Figure 5.7. Class diagram for the ConnectionManager class.

graphic/05fig07.gif


Variables

The first variable is a std::list that stores connections. The most common complaint about templates I hear is that they are ugly. The people who say that are absolutely right. Templates are ugly. Unfortunately, they are also very handy.

The good news, however, is that typedefs can be good friends and remove a lot of the ugliness when you are dealing with templates. For example, every time you want to refer to this kind of list, you'd need this code:

 std::list< Connection<protocol> > 

But with a typedef, you can do something like this instead:

 typedef std::list< Connection<protocol> > clist; typedef std::list< Connection<protocol> >::iterator clistitr; clist m_connections; 

Believe meit makes your life so much easier. Imagine that later you want to create an iterator for that kind of list:

 // without typedef: std::list< Connection<protocol> >::iterator itr; // with typedef: clistitr itr; 

Typedefs make your code cleaner and easier to read, and save you from lots of typing.

The class has three integers that represent three different limits of the connection manager. m_maxdatarate is the maximum datarate (in bytes per second) that the manager allows on a socket. As soon as that socket goes over that datarate, it is kicked for flooding. If the socket cannot send data, the m_sendtimeout variable determines how much time (in seconds) before a connection is kicked. This prevents deadlocked clients.

The third variable, m_maxbuffered , protects against attackers . Imagine that you set the send timeout value to 60, so that if data cannot be sent for 60 seconds, the connection is kicked out. The attacker might know this, and so he sets his connection to accept one byte of data per minute from your server, thus keeping it from detecting that he is having sending problems. Now this attacker is in your game, and data is continually sent to him. Data is buffered up, and only one byte of data is removed from the buffer each minute. Since the string class in the connection will theoretically keep expanding to fit all the data it's buffering, you'll eventually run out of memory. Therefore, it is logical to impose a limit on the amount of data that can be buffered. So, once the buffer reaches the given size, you can assume that there are major problems with the connection, and the connection manager will close it. As with the listening manager, this class has its own SocketSet , named m_set .

Functions

The connection manager has quite a few functions, and not many of them are accessors, so I will be showing you most of the code for the class.

Constructor and Destructor

The constructor for the connection manager has three optional parameters. More than likely, you're not going to use the default values of the parameters, as they are only guidelines. The three parameters are:

  • Maximum data reception rate, which defaults to 1024 bytes per second

  • Send timeout period, which defaults to 60 seconds

  • Maximum buffer size per connection, which defaults to 8192 bytes

The maximum data reception rate determines how many bytes per second you can receive before the connection handler kicks a connection for flooding. The send time period determines how long the connection manager waits for a connection to respond after it first notices a sending problem. The maximum buffer size per connection determines how much data can be buffered to send before it assumes there's a sending problem and terminates the connection.

 template<typename protocol, typename defaulthandler> ConnectionManager<protocol, defaulthandler>:: ConnectionManager( int p_maxdatarate, int p_sentimeout, int p_maxbuffered ) {     m_maxdatarate = p_maxdatarate;     m_sendtimeout = p_sentimeout;     m_maxbuffered = p_maxbuffered; } 

As for the destructor, it simply closes every connection:

 template<typename protocol, typename defaulthandler> ConnectionManager<protocol, defaulthandler>::~ConnectionManager() {     clistitr itr;     for( itr = m_connections.begin(); itr != m_connections.end(); ++itr )         itr->CloseSocket(); } 

Because the connections do not throw exceptions when they are closed, you don't need to catch any if something goes wrong.

Adding New Connections

The NewConnection() function adds new connections to the manager. The function takes a reference to a DataSocket as its parameter and then turns that into a Connection . Here's the code listing:

 template<typename protocol, typename defaulthandler> void ConnectionManager<protocol, defaulthandler>:: NewConnection( DataSocket& p_socket ) {     Connection<protocol> conn( p_socket );   // create new connection     if( AvailableConnections() == 0 ) {         defaulthandler::NoRoom( conn );      // ack! no room!         conn.CloseSocket();                  // just close it then     }     else {         m_connections.push_back( conn );     // add the connection         Connection<Telnet>& c = *m_connections.rbegin();         c.SetBlocking( false );              // nonblocking         m_set.AddSocket( c );                // add to set         c.AddHandler( new defaulthandler( c ) );     } } 

The function first creates a Connection out of the socket ( conn ). Then it checks to see if there is any more room for the connection within the manager.

Remember when I first showed you the concept of the protocol::handler classes? I said that some of them must have a static NoRoom function. Whatever class you decide to use as a default handler for a connection manager, it must have this function. Because the connection manager is essentially clueless about what protocol it's actually using, it doesn't know how to tell a connecting client that there is no more room for it. You could leave the job to the protocol class, but that's not really customizable. A protocol implementation is supposed to be general, not specific to the server you're going to be running, so it's not wise to give that responsibility to it. If you delegate the responsibility to the default protocol::handler , however, you gain flexibility and you can customize messages sent back to the client about how full the server is, or whatever else you might want to do. You'll see this implemented in Chapter 6, when I show you how to implement a Telnet handler. So, once the "no room" message has been sent, you need to close the socket; it's assumed that anyone sending sockets to this class is in "fire and forget" mode, which means that they shoot sockets off to the handler and then assume that the manager takes complete control of that socket.

On the other hand, if there is room for the connection, you need to do a little more work. First, the connection is added to the back of the list via the push_back function, and then a reference to the connection inside the list is created, named c . Remember, STL containers use copy-by-value , so conn is not the same connection as what is actually stored in the list; it's now a different connection. After that, it puts the socket into nonblocking mode, adds the connection into the connection managers' SocketSet , and finally adds a new defaulthandler to the connection's handler stack.

The connection is placed into nonblocking mode due to the buffering system; you don't want your connections blocking when you are trying to send data that might not get sent.

Closing Connections

There are two functions associated with closing connections: Close() and CloseConnections() . The first function is used to immediately close a connection, and the second is used to go through all of the connections to check if they need to be closed. Here's the first function:

 template<typename protocol, typename defaulthandler> void ConnectionManager<protocol, defaulthandler>:: Close( clistitr p_itr ) {     m_set.RemoveSocket( *p_itr );     p_itr->CloseSocket();     m_connections.erase( p_itr ); } 

The connection is removed from the socket set so that it is no longer polled for activity (because it will be closed in a moment!); then it's physically closed, and finally erased from the list of connections. Note that this function requires an iterator into the connection list, and because there's no way to get an iterator outside of the manager class, this function can only be called internally.

Here's the other function:

 template<typename protocol, typename defaulthandler> void ConnectionManager<protocol, defaulthandler>:: CloseConnections() {     clistitr itr = m_connections.begin();     clistitr c;     while( itr != m_connections.end() ) {         c = itr++;         if( c->Closed() )  Close( c );     } } 

This essentially loops through every connection that it is managing and checks to see if the connection should be closed. If so, then the function calls the Close helper function that I showed you previously.

You should notice that the function uses two iterators: itr , and c . At the beginning of the loop, c is set to the position pointed at by itr , and then itr is incremented to the next position. If you close a connection, the position pointed to by c disappears, and that iterator is completely invalid. You can't increment it or do anything else; your only viable option is to discard it. Luckily, because you have already saved the next position into itr , you can simply assign itr to c , and then move itr to the next position.

This process is shown in Figure 5.8. The method using only one iterator is on the left, and the method using two iterators is on the right.

Figure 5.8. You need two iterators to iterate through a list if you are also going to be removing items from the list.

graphic/05fig08.gif


Listening for Data

The Listen() function is quite long and complex, so I'm going to break it up into sections and explain them piece by piece. Here's the function:

 template<typename protocol, typename defaulthandler> void ConnectionManager<protocol, defaulthandler>::Listen() {     int socks = 0;     if( TotalConnections() > 0 ) {         socks = m_set.Poll();     } 

The previous segment checks to see if there are any connections in the manager, and if so, polls them. There's really no sense in polling an empty socket set, especially considering that you know it will eventually call select() , which is a system call, and will probably have more overhead than a simple if-statement at this level.

 if( socks > 0 ) {         clistitr itr = m_connections.begin();         clistitr c;         while( itr != m_connections.end() ) {             c = itr++;        // set itr to the next, and use c as current             if( m_set.HasActivity( *c ) ) {    // check activity                 try {                     c->Receive();              // try to receive data                     if( c->GetDataRate() > m_maxdatarate ) {                         c->Handler()->Flooded();   // connection flooded                         Close( c );                // close em!                     }                 } 

The previous code segment is executed whenever sockets have activity. It loops through every connection within the manager using two iterators itr and c just as you saw before with the CloseConnections function.

If the m_set reports that connections have activity, it tries to receive data from the connection. If it receives the data, it then checks to see if the datarate of the connection exceeds the m_maxdatarate variable. If it does, the connections' handler is notified that it was flooding, and then the connection is forcibly closed.

 catch( ... ) {                     c->Close();            // tell connection it's closed                     c->Handler()->Hungup();// tell handler it hung up                     Close( c );            // actually close connection                 }             }   // end activity check         }   // end socket loop     }   // end check for number of sockets returned by the poll } 

The catch-block catches exceptions that occurred when data was being received; it is assumed that if there was an exception, it was a fatal error. Even a would-block error is fatal in this case; it would be a signal that something was seriously messed up with the connection, since the socket set said there was something to receive on it. Therefore, the connection is told that it has been closed, the connection's current handler is notified that the connection hung up, and it is closed.

Sending Data

Because of the buffering system that all connections employ , to minimize the system calls, it is most efficient to buffer all the data you want to send and then send it all at once at a later time. The Send function does this by going through every connection and attempting to send the contents of each sending buffer. Here it is:

 template<typename protocol, typename defaulthandler> void ConnectionManager<protocol, defaulthandler>::Send() {     clistitr itr = m_connections.begin();     clistitr c;     while( itr != m_connections.end() ) {         c = itr++;     // move itr forward, keep c as current         try {             c->SendBuffer();    // try sending             if( c->GetBufferedBytes() > m_maxbuffered    // too much data                 c->GetLastSendTime() > m_sendtimeout ) {   // or send timeout                 c->Close();                // tell connection it has closed                 c->Handler()->Hungup();    // tell handler it hung up                 Close( c );                // close connection             }         }         catch( ... ) {        // catch all exceptions and hang up on them             c->Close();             c->Handler()->Hungup();             Close( c );         }     }   // end while-loop } 

Inside the try-block, the buffer for every connection is sent. Assuming the transmission succeeds, the next if-statement checks to see if there were any sending problems. It checks the buffer size and the amount of time that has passed since the connection started having sending problems (if any at all; see the Connection class from earlier in this chapter). If either problem exists, the connection's handler is told that the connection hung up, and then it is closed.

If an exception was thrown when sending the buffer, it is caught in the catch-block. In this case, any exception thrown will be a fatal error, so the connection's handler is told that it has hung up, and the connection is closed.

[ LiB ]


MUD Game Programming
MUD Game Programming (Premier Press Game Development)
ISBN: 1592000908
EAN: 2147483647
Year: 2003
Pages: 147
Authors: Ron Penton

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net