Demo 6.2SimpleChat

[ LiB ]

In an attempt to tie together the concepts of Telnet, protocol policies, and protocol handlers, I'm going to show you how to implement a simple Telnet chat server, known as SimpleChat.

The chat server is going to have two handlers: one to handle logging in, and one to handle chatting.

As well as these two handlers, the chat server is going to need a database structure, which will keep track of every user in the chat server. Figure 6.4 shows these classes.

Figure 6.4. Class diagrams for the two handlers and the user database class with functions that are underlined representing static functions.

graphic/06fig04.gif


The SimpleChat is going to need all three classes. The SimpleChat is Demo 6.2, and the directory \Demos\Chapter06\Demo06-02\ on the CD contains the source files needed for the program (SCUserDB.h/.cpp, SCLogon.h/.cpp, SCChat.h/.cpp, and Demo06-02.cpp).

Database

The most important part of the chat is the user database. The database stores simple User objects in a list that represents all the users who are currently connected. The database and User class are located within the files SCUserDB.h and SCUserDB.cpp.

Users

First let's look at the concept of a user. This chat isn't a persistent world program, because it doesn't save data on users anywhere . A user can log in with one name and then quit, and any other user can subsequently log in with that same name .

So users are simple objects; in fact, all that needs to be stored is the user's name and a pointer to his connection, as you saw from Figure 6.4. A constructor initializes a user using a connection pointer and a name as well.

Database Data

As you can see in Figure 6.4, the UserDatabase class has one data member: m_users , which is of type users . The users type is really just a typedef for a list of User objects:

 typedef std::list<User> users; typedef std::list<User>::iterator iterator; 

I've also taken the liberty to typedef the list<User>::iterator class as just plain iterator . As I have mentioned many times before, typedefs make programming easier.

Iterator Functions

There are three iterator functions inside the DB class, which are designed to make the DB class act somewhat like a regular STL container. Here are the first two functions:

 static iterator begin() { return m_users.begin(); } static iterator end()   { return m_users.end(); } 

The functions return iterators pointing to the beginning of the database and the end of the database. Just like STL, the end function returns an invalid iterator, one that you can use to test whether you've reached the end of the map.

Here's an example of using the iterator:

 UserDatabase::iterator itr = UserDatabase::begin(); string name = itr->name;       // get the name of the first user ++itr;                         // move to the next user bool b = ( itr == UserDatabase::end() )   // if true, iterator is invalid. 

You can see that the UserDatabase::iterator class acts just like a regular STL unidirectional iterator.

The other iterator function performs a search on the database for a given connection pointer:

 static iterator find( Connection<Telnet>* p_connection ) {     iterator itr = m_users.begin();           // start at front     while( itr != m_users.end() ) {           // loop while valid         if( itr->connection == p_connection ) // compare pointers             return itr;                       // match found, return itr         ++itr;                                // no match, keep looking     }     return itr;                               // no match, return itr } 

The function essentially loops through the entire database, looking for a connection that matches the pointer that was passed in. If a connection is found, an iterator pointing to that user is returned. By the time the function gets to the second-to-last line of code, the iterator should be equal to m_users.end() , so it is just returned. Remember: Whenever you use a function that returns an iterator, if it is equal to the end iterator, that means that the function didn't find what it was looking for.

Database Functions

There are a number of database functions that can add, remove, or check the existence of IDs or usernames. Here's the function to add users:

 bool UserDatabase:: AddUser( Connection<Telnet>* p_connection, string p_name ) {     if( !HasUser( p_name ) && IsValidName(p_name ) ) {         m_users.push_back( User( p_connection, p_name ) );         return true;     }     return false; } 

The function checks that the username doesn't exist in the DB and checks that the name is valid. If the username passes both of those checks, a new User object is created and pushed onto the back of the user list. true is returned on success, and false is returned on failure.

Next up is the user deletion function, which deletes a user based on his connection pointer:

 void UserDatabase::DeleteUser( Connection<Telnet>* p_connection ) {     iterator itr = find( p_connection ); // find the user     if( itr != m_users.end() )           // make sure user is valid         m_users.erase( itr );            // then delete the user } 

The function finds the User class associated with the connection, and then deletes it from the list. (Assuming it exists. If it doesn't, nothing happens.)

Now the function checks whether a username is being used within the DB:

 bool UserDatabase::HasUser( string& p_name ) {     iterator itr = m_users.begin();     while( itr != m_users.end() ) {         it( itr->name == p_name )  return true;         ++itr;     }     return false; } 

This is similar to the find function that searched based on connection pointers, but this compares names instead.

Valid Usernames

It's always a good idea to place restrictions on usernames. Otherwise, you'll end up having people with confusing names like "__()><?`%", and that's just madness. For this reason, I've included a function within the database that checks the validity of usernames. There are three stipulations. The user name must

Not contain any of the predetermined invalid characters

Not be longer than 16 characters, or shorter than 3 characters

Start with an alphabetic character

You don't want huge or tiny names; those just annoy everyone. It's also a personal preference of mine to force usernames to start with an alphabetic character, but it's not strictly necessary. Here's the function:

 bool UserDatabase::IsValidName( const string& p_name ) {     static string inv = " \"'~!@#$%^&*+/\[]{}<>()=.,?;:";     if(  p_name.find_first_of( inv ) != string::npos ) {         return false;    // has invalid characters     }     if( p_name.size() > 16  p_name.size() < 3 ) {         return false;    // too long or too short     }     if( !std::isalpha( p_name[0] ) ) {         return false;    // doesn't start with letter     }     return true; } 

The function maintains a static string named inv . This string contains all the characters that are invalid in usernames. The function first tries to find invalid characters within the string, and if it does, it returns false . Next the function checks the size of the name, and finally it checks to see if the first character is alphabetic.

If the string passes all those tests, you've got a valid name.

Logon Handler

Now I get to show you the two handlers that are used within the program. The first one the chat uses is a handler to manage the logon process. Overall, it's going to be a fairly simple handler, because all it needs to do is verify usernames and send them over to the chat handler.

This class is located within the SCLogon.h and SCLogon.cpp files. It inherits from the Telnet::handler class, which you should remember from earlier descriptions is just another name for a ConnectionHandler<Telnet, string> . This means that the SCLogon class needs to implement a constructor that takes a Connection<Telnet>* as its parameter, as well as the Handle , Enter , Leave , Hungup , and Flooded functions.

Last but not least, the class also has a NoRoom function, because it is going to be the default handler in the SimpleChat, so it needs to know how to tell connections when there's no more room.

No Room

When the connection manager has no more room for a new connection, it leaves it up to the default protocol handler to send an error to the connection, notifying the connection that there is no more room. For SimpleChat, the SCLogon handler is the default handler, so it must know how to send these messages. Here is the code:

 static void NoRoom( Connection<Telnet>& p_connection ) {     static string msg = "Sorry, there is no more room on this server.\r\n";     try {         p_connection.Send( msg.c_str(), (int)msg.size() );     }     catch( SocketLib::Exception ) {         // do nothing here; probably an exploiter if sending that data         // causes an exception.     } } 

Because the connection isn't added to the connection manager (there is no room!), the function tries to send the data directly to the connection, instead of buffering it. This is the important part: The data is enclosed within a try/catch block because the Send() function may throw exceptions if it cannot send data properly. You want to catch any exceptions before they crash your program. In this case, if an exception is thrown, it will probably be in the very rare circumstance that the person connecting to you is trying to crash your server. There's really no logical reason for the client to be unable to accept sends right after the client connects to the server, but you should always be on the safe side, and you shouldn't assume that the send will work.

New Connections

Whenever a new connection arrives, the connection manager automatically invokes the Enter function of a connection's handler.

Whenever the logon handler gets a new connection, it simply sends a welcoming string to the new connection:

 void SCLogon::Enter() {     m_connection->Protocol().SendString( *m_connection, green + bold +         "Welcome To SimpleChat!\r\n" +         "Please enter your username: " + reset + bold ); } 

Whenever the SDLogon class is constructed by the connection manager, it is automatically given a pointer to its connection, so you don't have to worry about the m_connection variable being invalid.

Basically, the function just uses the connection's protocol object to send a string. Note the usage of the VT100 color codes.

I showed you the Telnet::SendString() function earlier in this chapter, which buffers data on the connections buffer. This is generally the best behavior, because the connection manager handles all the sending details later (as long as you remember to tell it to), so you don't have to worry about timeouts and sending errors here.

Handling Commands

Whenever the Telnet protocol handler detects that a full command has been entered (anything ending in "\r" or "\n"), it sends the message to the handler. The logon handler treats any command as a person's desired username and tries to validate it.

 void SCLogon::Handle( connectionid p_connection, string p_data ) {     Connection<Telnet>* conn = m_connection;     if( !UserDatabase::IsValidName( p_data ) ) {         conn->Protocol().SendString( *conn, red + bold +             "Sorry, that is an invalid username.\r\n" +             "Please enter another username: " + reset + bold );         return;     } 

This first batch of code checks to see if the username is valid. If the username is not valid, the user is told so, and the function immediately returns.

 if( UserDatabase::HasUser( p_data ) ) {         conn->Protocol().SendString( *conn, red + bold +             "Sorry, that name is already in use.\r\n" +             "Please enter another username: " + reset );         return;     } 

The previous code segment then checks to see if the username has already been taken, and if so, it again tells the client that the name was rejected, and returns. Here's the final code segment:

 UserDatabase::AddUser( conn, p_data );     conn->Protocol().SendString( *conn, "Thank you for joining us, " +                                  p_data + newline );     conn->RemoveHandler();     conn->AddHandler( new SCChat( *conn ) ); } 

NOTE

There is one important thing I would like to mention: the RemoveHandler function. This function, when called from inside a handler, actually deletes the handler that is calling it. This is a tricky situation, but it's not all that bad if you pay attention to what you are doing. Whenever you call this function, all of the current handler's class data is deleted. For this simple handler, that means that the m_connection pointer no longer exists, and using its value causes some bad things to happen. Local variables , on the other hand, are still perfectly valid, which is why the pointer to m_connection is stored into conn at the beginning of the function. When you think about it, it's not really a huge problem. Whenever you are calling the RemoveHandler function, you're essentially leaving the state of that handler, so the next and only thing that should be done is either going back to a previous state, or entering a new state. In this case, the connection is entering the SCChat state. After that, the function should quit.

At this point, the username is acceptable, so the function simply adds the connection and the name to the user database and tells the user that he's entered the chat. After that, the logon handler is removed using the RemoveHandler function, and the connection is moved into the SCChat state, using AddHandler .

Other Functions

The other three handler functions Flooded , Hungup , and Leave are empty for the logon class:

 void Hungup() {}; void Flooded() {}; void Leave() {}; 

The handler functions are empty because the handler doesn't actually keep track of connections until a valid nickname is entered, at which time connections are automatically transferred over to the chat handler. So, essentially the logon handler doesn't care if the connections hang up, flood, or quit before it enters a valid username.

NOTE

You may one day want a logon handler to care about flooding. If you catch a user continually flooding your server, you should usually try banning that user. Some systems can do this automatically if you code for it.

Chat Handler

The chat handler is a little more complex than the logon handler, and it even adds two new functions to help you along. It is located in the SCChat.h and SCChat.cpp files.

New Connections

Handling new connections is a simple task for the chat handler. Since the logon handler has already added the user to the database, all you need to do here is announce that a new user has arrived:

 void SCChat::Enter() {     SendAll( bold + yellow + UserDatabase::find( m_connection )->name +              " has entered the room." ); } 

The function uses the SendAll() function, which I will examine later on. For now, all you need to know is that this function sends a string to every connection in the user database.

Exiting Connections

Whenever a connection leaves the SCChat state, you need to make sure that user is deleted from the user database:

 void SCChat::Leave() {     UserDatabase::DeleteUser( m_connection ); } 

Take note that this function is called whenever a connection moves to a different state, or simply leaves this state (for example, when a connection is being deleted), so you should never call this function on your own. This function basically exists to perform some house-cleaning whenever it's needed.

Handling Commands

There are two types of commands that the handler knows about: regular chatting and instructional commands. Whatever you type with a '/' character in front of it is interpreted as an instructional command; everything else is assumed to be regular chatting.

At this time, there are only two instructional commands supported: /who and /quit . The first command compiles a list of everyone on the server and sends it back to you. The second disconnects you from the server.

The function first gets the name of the user who is sending the text and determines if the text is a command:

 void SCChat::Handle( string p_data ) {     string name = UserDatabase::find( m_connection )->name;     if( p_data[0] == '/' ) {         string command = BasicLib::ParseWord( p_data, 0 );         string data = BasicLib::RemoveWord( p_data, 0 ); 

If the text is a command, the function creates two new strings: one represents the command ( command ); the other represents the rest of the string ( data ). The next part of the function handles /who commands:

 if( command == "/who" ) {             string wholist = magenta + bold + "Who is in the room: ";             UserDatabase::iterator itr = UserDatabase::begin();             while( itr != UserDatabase::end() ) {                 wholist += (*itr).name;  // add user's name to end                 ++itr;                   // go to next user                 if( itr != UserDatabase::end() ) {                     wholist += ", ";     // add comma if not last user                 }             }             wholist += newline;             m_connection->Protocol().SendString( *m_connection, wholist );         } 

If the command is a /who command, the function creates a new string, named wholist , as well as an iterator for the database. Then the function iterates through every user in the database and appends each user's name to the end of the list. For every user except the last, a comma and a space are added to the list as well. So you'll end up with, "Who is in the room: Bob, Sue, Zach", for example. Finally, the who-list string is sent back to the connection using the Telnet::SendString function.

Here's the code that handles quitting:

 else if( command == "/quit" ) {             CloseConnection( "has quit. Message: " + data );             m_connection->Close();         }     } 

If the command were to quit, the CloseConnection() function would be called. The function is a helper function that basically tells everyone in the room that the user has quit, and it even allows you to add a quit message at the end. /quit goodbye ! would result in "Bob has quit. Message: goodbye!" being printed to everyone.

After that, the connection is told to close. (But remember, it won't actually be closed until the connection manager gets around to it.)

The next section of code deals with anything that isn't a command:

 else {         if( BasicLib::TrimWhitespace( p_data ).size() > 0 ) {             SendAll( green + bold + "<" + name + "> " + reset + p_data );         }     } } 

First, it checks the size of the string to ensure that all whitespace has been trimmed . If the check returns 0, nothing happens. This prevents people from typing things like " " into the chatter, which would be extremely annoying.

So basically the code encases your nickname in angle brackets, puts it in green, and then adds your message at the end after resetting the colors. If your name is "Bob" and you typed "Hello", it would print out "<Bob> Hello" to everyone.

The SendAll Function

I've included a function here, which you've seen used earlier, that sends data to every connection in the database.

 void SCChat::SendAll( const string& p_message ) {     UserDatabase::iterator itr = UserDatabase::begin();     while( itr != UserDatabase::end() ) {         itr->connection->Protocol().SendString( *itr->connection,             p_message + newline );         ++itr;     } } 

The function simply performs a loop through the database using the iterator class, and sends the string in the parameter to every user, with a newline tacked onto the end.

Closing Function

The next important function in the chat manager is the CloseConnection function. This is essentially a helper function that makes it easier for you to close connections.

 void SCChat::CloseConnection( const string& p_reason ) {     SendAll( bold + red + UserDatabase::find( m_connection )->name +         " " + p_reason ); } 

In this case, the function simply sends a message to everyone saying that the user has quit. As I said, it's just a simple helper.

Other Functions

There are two other simple functions within the handler, all very simple. They are Flooded() and Hungup() , which are called when connections are forcibly closed by the connection manager:

 void SCChat::Hungup()  { CloseConnection( "has hung up!" ); } void SCChat::Flooded() { CloseConnection( "has been kicked for flooding!" ); 

These two functions simply utilize the CloseConnection helper function to notify everyone in the chatroom that the user has left.

Tying It All Together

Finally, you can integrate all the pieces together to form the chat server. This is done within the Demo06-01.cpp file.

 int main() {     SocketLib::ListeningManager<Telnet> lm;     SocketLib::ConnectionManager<Telnet> cm( 128 ); 

First, the two managers are created with the names lm and cm . The next step is to make the listening manager know about its connection manager, and then tell it to start listening on a port:

 lm.SetConnectionManager( &cm );     lm.AddPort( 5099 ); 

Here's the final part of the code:

 while( 1 ) {         lm.Listen();         cm.Manage();         ThreadLib::YieldThread();     } } 

The listening manager is told to listen on port 5099, and then the loop starts. The loop listens for incoming connections, and tells the connection manager to listen/send/close connections. Finally, it calls the thread library's yield function, so that the program doesn't consume 100% of your computer's resources.

That's pretty much it. Pretty easy, isn't it? You can compile this program using the instructions found in Appendix A, which is on the CD; the program uses the SocketLib, ThreadLib, and BasicLib.

Figure 6.5 shows a screenshot from the chat.

Figure 6.5. Screenshot from the SimpleChat.

graphic/06fig05.gif


[ 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