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
};
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.
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;
};
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.
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.