Sockets API Wrapper Classes and Functions

[ LiB ]

There are basically four classes that know about the implementation of the underlying Sockets API. Since I've shown you in Chapter 2 how to use the API, I'm going to focus in this chapter on showing you how to design the classes, and not the code behind them. In addition, I'll dig deeper into subjects I outlined in Chapter 2. After all, this book is about MUDs, not a super- awesome Socket Library!

Socket Wrapper

My main problem with the Sockets API is that it handles listening sockets and data sockets the same way; you can't tell them apart. This is an awkward design, and it can lead to many types of problems. For example, you can inadvertently call send on a listening socket; obviously, you should only be doing that on data sockets, but it's so easy to accidentally do something like that.

Using a proper class hierarchy can prevent stupid things such as that from happening. You can make sure that a call to send or recv is never made on a listening socket. With good design, you can completely remove some of the errors you had to check for in the past. Figure 5.1 shows the design I'll be using for the sockets. All three of the classes shown in the figure can be found on the CD in the files /Libraries/SocketLib/SocketLibSocket.h and /Libraries/SocketLib/SocketLibSocket.cpp.

Figure 5.1. Design for the hierarchy of sockets used in this book.

graphic/05fig01.gif


Sockets

The base class is Socket . You'll notice it has a variable of type sock , which is just my typedef for a socket descriptor. I like typedefing things like that, because it makes programs more readable. When you see a function that takes a sock as a parameter, instead of just an int , you know that it wants a socket descriptor, and not some other type of data. The class also holds a sockaddr_in structure, which, if you remember from Chapter 2, holds information about a socket. For this class, it holds the local information about the socket (port number and address). All sockets have local information.

The last variable is a Boolean, m_blocking . This determines if the socket is blocking. Think back to Chapter 2, when you learned that sockets block by default. There are many exploits that are possible on blocking sockets.

NOTE

UML

Figure 5.1 is drawn in a diagram style known as a Unified Modeling Language ( UML) Class Diagram . UML is a pretty standard method of designing classes in object-oriented languages, so you should at least be familiar with it if you plan on doing some serious programming. It's not too difficult to understand. For example, you can see that the class name is in the top box, the datatypes are in the middle box, and the functions are in the third box.

  • The hash (#) symbol means that the item is protected.

  • The plus (+) means that it's public.

  • The minus (-) means that it's private .

One aspect may look weird to youthe type of variable, or the return type of a function, is listed after the function/variable name, after the colon (:). There are also several types of arrows used in UML class diagrams; you only see the open -head inheritance arrows in this diagram. The other common arrow type is a plain arrowhead , which is the uses arrow. For a more comprehensive look at UML, there's a great chapter about it in Game Programming Tricks of the Trade , by Lorenzo Phillips (Editor).

In particular, there's an "exploit" that allows you to connect to a listening socket and then disconnect immediately; in the server, the select() function will detect the new connection, but by the time it gets to the accept() call, the connection no longer exists. If you've got the listening sockets in the same thread as everything else, this can cause your entire game to hang, because the call to accept() will block until someone tries to connect again. Therefore, you need the ability to set a socket to nonblocking mode.

Unfortunately, the method of setting the blocking mode of a socket is different with both Winsock and BSD Sockets. (Typical!) The SetBlocking() function takes a Boolean as its parameter: true if you want the socket to block, false if you don't want it to block.

The Winsock method is pretty easy:

 void Socket::SetBlocking( bool p_blockmode ) {     int err;     #ifdef WIN32         unsigned long mode = !p_blockmode;         err = ioctlsocket( m_sock, FIONBIO, &mode ); 

Just one call to the ioctlsocket function is required; however, the blocking Boolean needs to be reversed for this function, because you're setting whether or not you want to enable nonblocking mode. It's just a minor difference in semantics, that's all.

The Linux method is a little trickier, because the function to set the blocking mode sets a whole bunch of other modes as well. Therefore, you need to retrieve the current flags of the socket, set or clear the nonblocking bit, and then set the new flags. It's a bit of a pain:

 #else         int flags = fcntl( m_sock, F_GETFL, 0 );         if( p_blockmode == false ) {             flags = O_NONBLOCK;         }         else {             flags &= ~O_NONBLOCK;         }         err = fcntl( m_sock, F_SETFL, flags );     #endif 

So the flags are retrieved, and depending on what mode you want, the nonblocking flag is either set (using logical-or), or cleared (using logical-and). Finally, the new flags are sent back to the socket. Here's the conclusion:

 if( err == -1 ) {         throw( Exception( GetError() ) );     }     m_isblocking = p_blockmode; } 

The SocketLib library has some auxiliary functions and classes built into it, such as the Exception class and the GetError() function. I'll go over these in more detail when I get to the parts that use them more, but for now, you should just know that the SocketLib throws exceptions when errors occur, and GetError() is a wrapper that gets error codes from the Sockets API.

I won't show you the code for the other functions in this class, since I've already covered the basics in Chapter 2. If you're really interested, you can take a look at the source, but, as I've said, these socket classes are just a wrapper around the Sockets API that clean up the interface for you.

I need to make one other important point: The constructor for the Socket class is protected. This was done so that you could not create plain Socket classes on your own; they are pointless. The class only provides useful functions for the ListeningSocket and DataSocket classes.

Listening Sockets

As a subclass of the Socket class, the ListeningSocket class inherits all of the functions and data of Socket and adds a few new functions of its own. I don't introduce much that is new in the code, so I won't show you any of the internals, but I'll show you how to use a listening socket in a bit.

NOTE

I've put one important concept into listening sockets that I haven't told you about before. Whenever you tell a listening socket to listen on a port, the function turns on the socket's SO_REUSEADDR option. Whenever you start up a program and make it listen on a port, then close the program, and try to run it again, you probably receive an "address already in use" error if this option isn't turned on. For some odd reason, the operating system still thinks the address is in use, but turning on this option makes it work correctly, so that you receive only the address already in use error when you try opening two active listening sockets on the same port.

Data Sockets

Data sockets are the other subclass. They also inherit from the plain sockets, but they need more data and have more complex functions. For example, data sockets add another sockaddr_in structure; in this case, it represents the remote address, which the socket is connected to. Because listening sockets can't be connected to anything, they don't have remote addresses, but data sockets do. Also, data sockets need information on how to connect to remote addresses and send and receive data. Once again, the class is basically just a wrapper around what I showed you in Chapter 2, so I'm not going to show you the internal code; that would be redundant.

Using Sockets

Because of the way my Socket Library is set up, it's incredibly easy to use. Observe:

 using namespace SocketLib; ListeningSocket lsock; DataSocket dsock; lsock.Listen( 5000 );          // start listening on port 5000 dsock = lsock.Accept();        // wait for an incomming connection 

Ta-da! Five lines of code for something that would take around 30-40 in the Sockets API. After this code has executed, dsock will contain a socket (if a client connects to you, of course) that you can send and receive data from:

 char buffer[128] = "Hello there!"; dsock.Send( buffer, strlen( buffer ) ); dsock.Receive( buffer, 128 ); 

This simple program sends a bunch of text"Hello there!"and then waits for the client to send something back. Here again, stuff like this makes your life so much easier.

Socket Sets

The select() function is the other main feature of the Sockets API that I will frequently use. The select() function polls a set of sockets activity. For this, I've created the SocketSet class, which will keep track of an fd_set of sockets. It will essentially allow you to add sockets, remove sockets, "poll" the sockets (call select() ), and check if a socket has activity. Figure 5.2 shows the class diagram for this class.

Figure 5.2. Class diagram for the SocketSet class, which tracks an fd_set of sockets.

graphic/05fig02.gif


The SocketSet class is fairly simple, but it does have one extra optimization that I did not discuss in Chapter 2. Remember the first parameter of the select() function? It is supposed to be the value of the highest socket descriptor within the set, plus 1. Windows ignores this value and does its own thing, but if you don't put in a valid value for Linux, you're going to have problems.

In Chapter 2, I told you that you could just put 0x7FFFFFFF into it. That's a really dumb thing to do if you're looking for speed, however, and I did that just for simplicity's sake. If you put a huge number such as 0x7FFFFFFF into the function, it's going to take a long time to check the sockets (because it's basically a for-loop underneath in Linux). If you use too small a value, you might end up ignoring some sockets. As much as I hate doing extra work for tedious things such as this, I was forced to add the capability to detect the largest socket descriptor into the Linux branch of the code.

Note the last variable within the class diagram in Figure 5.2: m_socketdescs . I made the code so that variable doesn't exist in Windows, but it exists in Linux. It's basically an std::set that keeps track of all the descriptors that have been added to the fd_set .

The internal implementation of storing sockets inside an fd_set isn't standard, so there really is no way to check the largest value in a set without calling FD_ISSET() on every possible socket descriptor, and as you can probably imagine, that would take forever. I just keep an std::set of socket descriptors handy, so I can search through them whenever a socket is removed. If you know how std::set s work, you'll realize that the highest value is always at the end of the container if you're using regular numeric types, so to get the value of the largest descriptor, you simply need to get an iterator to the last item and dereference it. The code is simple, so I won't bother showing it to you here.

Here's an example of its use:

 using namespace SocketLib; DataSocket socks[3]; SocketSet sset; // assume the sockets are connected here somewhere sset.AddSocket( socks[0] ); sset.AddSocket( socks[1] ); sset.AddSocket( socks[2] ); int active = sset.Poll( 1000 );    // wait 1 second for activity if( sset.HasActivity( socks[0] ) );// check if there is activity     // handle activity // later on: sset.RemoveSocket( socks[0] ); 

And so on. It's pretty easy to use. However, you must remember a few things when using this class. First of all, different operating systems have different numbers of sockets that you can store inside an fd_set . For example, Windows is set at 64 sockets, while Red Hat 8, which I am running, is set at 1024. Other implementations differ as well, I imagine.

To handle this, I've included a handy define inside of the Socket Library, the MAX constant. SocketLib::MAX will hold the maximum number of sockets you can store in an fd_set .

So, you should remember to check the SocketLib::MAX variable to see how many sockets you can fit in a set. Also, the SocketSet class doesn't actually count how many sockets it contains; it's really up to whomever uses it to keep track.

Function Wrappers

I've included four functions within the SocketLib namespace to help you with network programming. They are located within the SocketLibSystem.h and .cpp files. Here are the functions:

 ipaddress GetIPAddress( const std::string p_address ); std::string GetIPString( ipaddress p_address ); std::string GetHostNameString( ipaddress p_address ); bool IsIPAddress( const std::string p_address ); 

The ipaddress type is just a typedef for an unsigned long int , but, as I've told you before, turning items into typedefs makes your programs much more readable. Also, there's one little qualification I've made: ipaddresses must be in Network-Byte-Order (NBO). All socket functions assume that ipaddresses are in NBO, and if you play it right, you'll never have to convert addresses to and from host-byte-order.

So, you can use the GetIPAddress() function to convert string addresses into a binary IP address like this:

 ipaddress addr = SocketLib::GetIPAddress( "www.google.com" ); addr = SocketLib::GetIPAddress( "127.0.0.1" ); 

Note that this function is smart. It can convert either DNS-capable addresses such as www.google.com, or numeric addresses such as 127.0.0.1. However, you'll have to be careful when converting a DNS-capable address; this function may block. (Remember from Chap-ter 2 that this function needs to contact your DNS server.) So it's usually wise to call this function in a thread separate from your main game, unless you know for sure that you're converting a plain numeric address.

The next function takes an IP address and converts it into its numeric string:

 std::string str = SocketLib::GetIPString( addr ); 

After the code has executed, that string should contain "127.0.0.1" ( assuming addr still contains the same address from the previous code example).

The other string conversion function does a reverse-DNS lookup:

 addr = SocketLib::GetIPAddress( "www.google.com" ); str = SocketLib::GetHostNameString( addr );  // "www.google.com" 

And finally, we come to a function that detects whether a string contains a numeric IP address, or an address that you should try to look up through DNS. This can help you determine if the GetIPAddress function might block or not:

 bool b; b = SocketLib::IsIPAddress( "www.google.com" );  // false b = SocketLib::IsIPAddress( "127.0.0.1" );       // true 

So if the function returns false, GetIPAddress will probably block.

Errors

A Socket Library wouldn't be complete without an error reporting system, because many

things can go wrong with sockets, even when you prevent most errors with proper design.

Error Codes

The first thing I did was to create an enumerated type representing all the possible error codes:

 enum Error {     // errors that shouldn't happen; if they do, something is wrong:     ESeriousError,     // these errors are common     ENetworkDown,     ENoSocketsAvailable,     ENoMemory,     EAddressNotAvailable,     EAlreadyConnected,     ENotConnected,     EConnectionRefused,     ENetworkUnreachable,     ENetworkReset,     EShutDown,     EHostUnreachable,     EHostDown,     EConnectionAborted,     EConnectionReset,     EOperationWouldBlock,     // DNS errors     EDNSNotFound,     EDNSError,     ENoDNSData,     // These errors are specific errors that should never or rarely occur.     EInProgress,     EInterrupted,     EAccessDenied,     EInvalidParameter,     EAddressFamilyNotSupported,     EProtocolFamilyNotSupported,     EProtocolNotSupported,     EProtocolNotSupportedBySocket,     EOperationNotSupported,     EInvalidSocketType,     EInvalidSocket,     EAddressRequired,     EMessageTooLong,     EBadProtocolOption,     EOptionNotSupported,     EAddressInUse,     ETimedOut,     EShutDown,     // auxiliary socketlib errors     ESocketLimitReached,     ENotAvailable,     EConnectionClosed }; 

As you can see, there are a plethora of possibilities, possibly even a cornucopia? In either case, there are a lot of them! I'm not going to take the time to explain them here, because most are equivalent to the errors I've shown you in Chapter 2 and are detailed in Appendix B, "Socket Error Codes" on the CD. Notice the third grouping of errors. Most of those errors have been eliminated through the design of the socket library. Theoretically, those errors should never occur. Theoretically, of course. Things might not work out that way in real life, so if those errors occur, something is seriously wrong with your program.

NOTE

If you ever plan to make a better error-handling system, you might want to think about implementing a severity system, in which errors are assigned values that indicate how severe they are. For example, you can make certain errors have a severity level that tells your game that the connection needs to be closed, and others that tell your system that the entire network just isn't working, and the game must be shut down.

Translating Errors

Like all things in life, there are problems with the native error codes in the Sockets API. Windows is actually helpful here by giving each error its own specific value that you can retrieve using its WSAGetLastError() function. On the other hand, Linux muddles its system by using two different error-reporting mechanisms: errno and h_errno , which retrieve regular errors and host-lookup errors respectively. The problem is that both variables use different error-numbering systems, and some error codes conflict with each other on some systems.

To get around this problem, I've created a function to get errors that takes a Boolean parameter and determines which error source it should get errors from:

 Error GetError( bool p_errno = true ); 

If the parameter is true (the default value), the function gets errors from errno ; if false , it gets errors from h_errno . The WIN32 version of this function just ignores the parameter.

Then there is a function that actually does the translating:

 Error TranslateError( int p_error, bool p_errno ); 

This function really shouldn't be used anywhere except by the GetError() function, so it's mainly just a helper function that takes an integer error code and translates it into an actual Error type. It's a huge and ugly function, so I recommend looking at the source only if you like seeing huge, ugly functions.

The SocketLib::Exception Class

And finally, here's the SocketLib::Exception class:

 class Exception : public std::exception { public:     Exception( Error p_code );     Error ErrorCode();     std::string PrintError(); protected:     Error m_code; }; 

This class is very simple; it contains just a single error code and has functions to get the error code, as well as print the error message to a string. You can throw exceptions like this:

 throw SocketLib::Exception( SocketLib::GetError() ); 

And then catch them like this:

 try {     // write socket code here } catch( SocketLib::Exception& e ) {     // handle error here } 

If you don't know how to use exceptions, see Appendix C, "C++ Primer," on the CD for more information.

The Winsock Initializer

In Chapter 2, you learned that Winsock must be initialized and shut down whenever you use it. I'm going to use a clever trick to make the program automatically initialize the Winsock library by creating a global object, and initializing the library in that object's constructor.

In addition, I will shut down Winsock in the destructor of this object. The class is located in SocketLibSystem.cpp and is called System . The class has a member variable of type WSADATA , which, as you learned in Chapter 2, stores information about Winsock. Here's the class definition:

 class System { public:     System()  { WSAStartup( MAKEWORD( 2, 2 ), &m_WSAData ); }     ~System() { WSACleanup(); } protected:     WSADATA m_WSAData; }; 

The constructor automatically initializes Winsock, and the destructor shuts it down. Error checking is not performed on either function, because it is dangerous. Throwing exceptions in constructors and destructors is generally a bad idea, and it doesn't help you notify the user about what is wrong (because there's nothing to catch the exceptions). If you threw an exception in the constructor, the program would immediately exit, and it wouldn't be able to notify the person running the program that there was a problem with Winsock. So, it's better to let it be, and detect the Winsock error when you try using the network system.

Also, if you throw an exception in the destructor, there is a very possibility that many things in your program may not shut down correctly. So, if shutting down Winsock fails, we really don't care, because the program is exiting anyway.

Finally, the global instance of the system object is declared:

 System g_system; 

This name appears only within the SocketLibSystem module, so it shouldn't cause name conflicts anywhere else in the program.

And that about sums up the portion of the library that wraps the Sockets API/Winsock.

[ 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