CPS 214 Programming Project 3 - Fall 1997

Due: Friday, November 21 (midnight)

25 points

Dynamic Access of User Services

This assignment serves as an introduction to client-server programming using the TCP/IP protocols. In this assignment, you will write both a client and a server. Your client and server will communicate using TCP. Your server may implement any service you choose. The ground rules are simple: the server reads and writes data to and from the TCP connection. The server may prompt the client for input, or simply print a random message and exit. The exact details are your choice. Once you have a working server, you should leave it running in background (even after you log out) waiting for service requests from a client.

To access a service, the client opens a TCP connection to the server, sends and receives data, and then closes the connection. Conceptually, the client runs as a (very!) simplified version of telnet between your terminal and the server, copying data sent by the server to standard output and sending data read from standard input to the server. The client terminates the connection with the server when it receives an end-of-file from either the server or standard input.

The above scenario omits a key aspect of client-server programming: How does the client find out where the server is? That is, what transport-level address (i.e., Internet address and port number) should the client connect to?

One solution is to use a name server that dynamically maps service names into their transport-level addresses. We will supply you with a such a server, called oracle, allowing you to register the service you provide and advertise it to other students in the class. Conceptually, the oracle is like the white pages in your phone book. A server registers the name and transport address of its service in the phone book, and clients use the phone book to map service names to transport addresses.

When your server starts, the operating system will assign it an unused transport address (e.g., XXX) on which it can wait for incoming connection requests. The server then advertises the availability of its service by sending a short message to the oracle containing the name of the service (e.g., ``fortune'') together with the transport address (e.g., XXX). The oracle server records the name-to-address mapping in its local database.

When a client wishes to connect to a server, it first sends a message containing the desired service name (e.g., ``fortune'') to the oracle, and the oracle returns a message with the appropriate transport address (e.g., XXX). The client then opens a TCP connection to that service. Exact details for communicating with the oracle are described below.

Client

Your client should accept the following set of commands from standard input: When the client wishes to connect to a server, it performs the following steps:
  1. Contact the oracle to find the transport-level address of the service you are trying to locate (details below).
  2. Open a TCP connection to the server supplying the desired service.
  3. Ready standard input and send it to the server, and copy all data sent by the server to standard output. Note that the client cannot predict in advance whether data will come from the keyboard or server next, but must be prepared to handle either case. The use of select(2) may be helpful here.
  4. After receiving an end-of-file from either standard input or the TCP connection, close the TCP connection. Note: if your client receives an end-of-file from standard input while it is connected to a server, it should terminate the connection to the server, but should continue reading additional commands from standard input because the user may want to connect to another service (see clearerr3)). If the client receives EOF while waiting for the next command (i.e., it is not currently connected to a server), the EOF should terminate the client (i.e., same effect as ``quit'' command).

Server

Your server should perform the following steps when making a service available:
  1. Create a TCP socket (similar to a UNIX file descriptor --- see socket(2).)
  2. Bind the socket to a sockaddr_in structure with family AF_INET, port number 0, and address INADDR_ANY. This directs the Unix kernel to faccept TCP connection requests from any machine (INADDR_ANY) in the Internet, and specifying a port number of 0 (meaning ``don't care'') directs the kernel to allocate an unused port number (see bind(2)).
  3. Extract the port number allocated in the previous step (see getsockname(2)), and fetch the Internet address of the host on which your server resides (see gethostname(2) and gethostbyaddr(3). Fill in the appropriate fields in the om structure (described below) and register the service with oracle.
  4. Specify the backlog of incoming connection requests you are willing to tolerate (see listen(2).
  5. Finally, wait for a connection request and service it. When you have serviced the request, repeat the process by waiting for the next connection attempt (see accept(2), and close(2)).

Interacting With Oracle

Oracle resides on machine spike.cs.duke.edu at well-known UDP port oracle. All communication with oracle is via UDP messages containing a structure called an om ``oracle message''), whose definition can be found in the file ~narten/csp214/p3/oracle.h The file is reproduced below: #define luid 8 #define cchMaxServ 10 #define cchMaxDesc 40 #define verCur 'D' #define cmdErr 1 /* An error occurred. See sbDesc for details */ #define cmdGet 2 /* Get the address of a service */ #define cmdAckGet 3 /* ACK for cmdGet message */ #define cmdEnd 4 /* Last response to a cmdGet message */ #define cmdPut 5 /* Register a new service */ #define cmdAckPut 6 /* ACK for cmdPut message */ #define cmdClr 7 /* Unregister a service */ #define cmdAckClr 8 /* ACk for cmdClr message */ struct om { /* oracle message */ unsigned long ti; /* time of registration */ unsigned char cmd; /* command/reply code */ unsigned char ver; /* version number of this structure */ unsigned short int port; /* TCP port number of service */ unsigned long int addr; /* Internet Address of server */ char uid[luid]; /* login of provider id (e.g., 'narten') */ char sbDesc[cchMaxDesc]; /* description of service (or error reply) */ char sbServ[cchMaxServ]; /* name of service requested/provided */ }; #define lom (sizeof (struct om))

Locating a Service

To find a service, your client program fills in the fields of the om structure as follows: In response to a cmdGet message, oracle returns two or more messages. Response messages have a cmd type of cmdAckGet, and the end of cmdAck responses is signaled by a cmdEnd message. CmdEnd messages do not contain the name of a service; they simply signal the end the last response. If only one service matches the client's request, the server will return two messages: a cmdAckGet, followed by a cmdEnd. Each cmdAck message contains the following fields:

Registering a Service with Oracle

When the server wishes to register a service with the oracle, it sends an om message with the following fields: In response to a cmdPut message, oracle returns a message of type cmdAckPut if the registration succeeds. In the case of errors, the oracle returns a message of type cmdErr, and then sets the field sbDesc to contain a short explanation of the error.

Exchanging Datagrams with the Oracle

  1. Create a UDP socket (see socket(2)).
  2. Get the Internet address and port number of the oracle (see gethostbyname(3) and getservbyname(3)).
  3. Open a UDP connection to the oracle (see connect(2)).
  4. Send an om message of the appropriate type to the oracle (see send(2)).
  5. Wait for an om reply from the oracle (see recv(2)).

Hints

The following steps may be helpful in getting started:
  1. Start small. Write a procedure that sends a datagram to the oracle, and receives a datagram response from the oracle If you made the mistake, the oracle will return an om with cmd set to cmderr, and sbDesc will contain a brief description of the problem.
  2. Write a client that connects to a server registered with the oracle. We will register a few simple commands (``fortune'' and ``date'' seem to be staples) that you can use to test your client.
  3. Once you have the client working, begin work on the server.
As an aid to debugging, as well as to trace use of your server, you should print trace output when connections are made. For example, you might print the host name and port number of every user that connects to your server, along with the time of the connection.

Requests to the oracle are actually a bit more general than we've described so far. Regular expressions can be used as service names or users ids to effect a kind of ``wild-card search'' for services. For example, specifying a service name of ``.*'' matches all services. Specifying a user name of ``...'' matches all services provided by users with three-character ids. The format of regular expressions is the same as that of ed(1).

Use your imagination and creativity in designing server programs.

The following library procedures and system calls will be helpful: clearerr(3), gethostname(2), listen(2), close(2), getpwent(3), getuid(2), send(2), recv(2),getservbyname(3), gethostbyname(3), gethostbyaddr(3), getsockname(2), bind(2), socket(2), connect(2), accept(2), and listen(2). The routine select(2) can be used to read from both standard input and a TCP socket at the same time.

For more information about the Unix networking-related library routines and system calls, consult the chapter on sockets in Comer's book.