Sockets

If objects A and B are separated by a machine boundary, then they must communicate with each other by sending messages across a network. The simplest method is to equip A and B with connected sockets. A socket is basically a transceiver. A transceiver combines a transmitter (an object that can be used to send or transmit messages) with a receiver (an object that can receive messages). Telephones, CB radios, and mailboxes are examples of transceivers. Of course a transceiver is only useful if it is connected to another transceiver in such a way that messages sent by one are received by the other and vice versa:

Assume the following Socket class has been implemented:

class Socket
{
public:
   Socket(int port = 0, string name = "localhost");
   Socket(SocketOID sock) { peer = sock; }
   ~Socket() { closesocket(oid); }
   void send(string msg); // uses TCP
   string receive();
private:
   SocketOID peer; // socket descriptor
};

From the network's perspective a server socket is identified by a two-part address: the domain name (or IP address) of the host computer, and a unique port number between 0 and 64000. These are the parameters to our Socket constructor. If successful, the constructor creates a client-side socket connected to the specified server.

Like threads and locks, sockets are really managed by the host platform. Instances of our Socket class encapsulate a descriptor or object identifier of an associated system-level socket. The send() and receive() functions delegate to corresponding member functions of this socket. The complete details are given in Programming Note 7.4.

Example: A Date Client

Most standard Internet servers— ftp servers, telnet servers, web servers, etc.— perpetually listen for clients at well known port numbers below 100. For example, most computers are equipped with a date server that listens for clients at port 13. When a connection is made, the date server sends the local date and time to the client.

When an instance of our DateClient class is created, the constructor attempts to create a socket connected to the data server of a specified host. Much can go wrong. The client machine may be disconnected from the network, the server machine may be down, the date server may not be running, or the domain name address may be bad. Our Socket constructor throws an exception if anything like this happens. Once connected, the date client gets the date by calling the socket's receive function:

class DateClient
{
public:
   DateClient(string serverLoc = "localhost")
   {
      try { server = new Socket(13, serverLoc); }
      catch(runtime_error e) { error(e.what()); }
   }
   string getDate() { return server->receive(); }
protected:
   Socket* server;
};

Here and elsewhere we are using the error() function defined in appendix 3 (util.h). Recall that this function prints its argument to cerr, then either terminates the application or throws an exception.

A Server Framework

Most servers follow the same basic design, so instead of developing a single example of a server, we develop a server framework that can be easily customized.

Our server framework uses the Master-Slave design pattern. The server is the master. It perpetually listens for client requests. When a client request arrives, the server creates a slave to service the client, then goes back to listening for more clients. If the server attempted to service clients by itself, then many clients would get a "busy signal" when they attempted to connect to the server.

What type of server slave should the server create? Our Server class is equipped with a virtual factory method that will be implemented in various derived classes to produce the right types of slaves:

class Server
{
public:
   Server(int port = 5001);
   void listen();
protected:
   SocketOID peer; // server socket descriptor
   virtual ServerSlave* makeSlave(Socket* cs, Server* m) = 0;
};

The complete details of Server::listen() are given in Programming Note 7.6. For now, it is sufficient to know that listen() perpetually removes descriptors of sockets connected to clients from a queue of clients waiting for service (using the Socket API accept() function), creates a slave with a socket connected to the client, then starts the slave:

while(true)
{
   SocketOID client = accept(...);
   ServerSlave* slave = makeSlave(new Socket(client), this);
   slave->start();
}

The ServerSlave class merely serves as a polymorphic base class for all concrete server slave classes:

class ServerSlave: public Thread
{
public:
   ServerSlave(Socket* s = 0, Server* m = 0)
   {
      sock = s;
      master = m;
   }
   virtual ~ServerSlave() { delete sock; }
protected:
   Socket* sock; // connected to a client
   Server* master;
};

Example: A Command Server Framework

We can extend our server framework to a command server. A command slave interprets client messages as commands that need to be executed. The slave executes each command, then sends the result back to the client. Of course execute() is a virtual function that needs to be re-implemented in derived server slave classes:

class CommandSlave: public ServerSlave
{
public:
   CommandSlave(Socket* cs = 0, Server* m = 0)
   : ServerSlave(cs, m) {}
   bool update();
protected:
   virtual string execute(string cmmd)
   {
      return string("echo: ") + cmmd; // for now
   }
};

Server slaves are threads, hence we need to provide them with an update() function. A command slave's function receives a command from the client, executes the command, sends the result back, then returns true. Recall that this will cause Thread::run() to repeatedly call update(). The loop terminates when the client sends the "quit" command:

bool CommandSlave::update()
{
   string command, result;
   command = sock->receive();
   cout << "command = " << command << endl;
   if (command == "quit")
   {
      cout << "slave quitting ...\n";
      return false; // terminate thread
   }
   try
   {
      result = execute(command);
      cout << "result = " << result << endl;
   }
   catch(runtime_error e)
   {
      result = e.what();
   }
   sock->send(result);
   return true; // keep thread going
}

Of course we also need a command slave factory method. This is implemented in a Server-derived class:

class CommandServer: public Server
{
public:
   ServerSlave* makeSlave(Socket* cs)
   {
      return new CommandSlave(cs, this);
   }
};

Many commercial servers implement slave factories so that they allocate slaves from a slave pool instead of creating new ones. This implementation is explored in the problem section.

Example: A Command Client

Command client constructor attempts to create a socket connection to the command server. 

class CommandClient
{
public:
   CommandClient(string serverLoc = "localhost",
                 int serverPort = 5001)
   {
      try
      {
         server = new Socket(serverPort, serverLoc);
      }
      catch(SockException e) { error(e.what()); }
   }
   void controlLoop();
protected:
   Socket* server;
};

Once a command client is created, we can simply start its control loop, which perpetually prompts the user for a command, sends the command to the server, then receives and displays the result returned by the server:

void CommandClient::controlLoop()
{
   bool more = true;
   string msg, response;

   while(more)
   {
      cout << "command -> ";
      getline(cin, msg); // VC++ getline() is broken[1]
      try
      {
         if (msg == "quit")
         {
            more = false;
            server->send(msg);
         }
         else
         {
            server->send(msg);
            response = server->receive();
            cout << "result = " << response << endl;
         }
      }
      catch(SockException e)
      {
         cerr << e.what() << endl;
         more = false;
      }

   } // while

   cout << "bye\n";
}

 



[1] Astonishingly, the Visual C++ getline() function is broken. Programmers may need to use the getLine() replacement in Appendix 3.