URL: http://www.ccs.neu.edu/home/rraj/Courses/1337/S03/Programs/pa1.html

COM 1337 / 3501 - Computer Networks                                                         April 10, 2003
Spring 2003

Programming Assignment 1: Client-Server programming through sockets

Deadline:

Electronic copy due Monday, April 28, 2003, at 11:59 PM
Hard copy due Tuesday, April 29, in class

In this assignment you will learn to use UNIX sockets for communicating over the Internet. The goal is to familiarize you with socket programming. You will use UNIX sockets to implement a simple client and server that communicate over the network and implement a simple application involving cash registers.

To complete this assignment, you may use any UNIX system that is connected to the College's network. You are to write both the client and server programs; their specifications are given below.  The client implements a simple cash register that opens a session with the server and then supplies a sequence of codes for products.  The server returns the price of each one, and also keeps a running total of purchases for each client.  When the client closes the session, the server returns the total cost.  This is how point-of-sale terminals work, especially with scanners.  This way, the database only has to be kept one place.

The client and server programs should be compiled separately, so that each have their own executable image. The two programs (processes) will communicate with each other in a connection-oriented (stream) manner using sockets. Thus, this assignment requires that you have some understanding of the use of the sockets API for connection-oriented interprocess communication. You will definitely need to read carefully the manual ("man") pages for the socket(), connect(), send(), recv(), close(), bind(), listen(), and accept() routines, if you are not already familiar with them.  Please also go over the tutorials available from the Resources webpage

Application-level Message Formats & Operations

The client and server communicate according to a protocol through which they exchange lines of ASCII characters via the reliable byte-stream service provided by TCP. The information exchange between the client and server proceeds as follows:

The format of the request string sent by the client is as follows:

    <request type> <UPC-code> <number>

where:

Examples of  client request are:

    0 00104 40  requesting cost of 40 items of product 00104
    0 89135 9    requesting cost of 9 items of product 89135
    1 00000 0    close
    1 10982 9    close
    2 00910 1    invalid

The server has a database containing 1 table with three fields.  The first field is the UPC code, the second field is the name of the product (you can assume that it is at most 25 characters and has no white space),  and the price per unit of the product, represented as an integer.  An sample database, which you can use for testing, is

    00104   PostGreatGrains        3
    10982   HormelFoodsSpam    2
    89135   MrClean                        4
    23272   IBMThinkpad600E    2000
    13400   HPLaserJet                  700

For the close command, the server returns an integer, which is the total cost of the transaction.  For the item command, the server returns:

    <response-type> <response>

where:


Client Operation

The client does the following:

  1. Create a socket (of type SOCK_STREAM and family AF_INET) using socket().
  2. Fill in a sockaddr_in data structure with the server's host IP address and port number. (To convert a "dotted quad-port" string to a socket address, i.e. to fill in a sockaddr_in structure, use the StringToSockaddr() routine provided in the skeleton code. You can also use the inet_addr() routine to convert a "dotted quad" string to the correct 32-bit unsigned integer representation, and atol() to convert the port number.)
  3. Call connect() with the filled-in sockaddr_in data structure to initiate a connection to the server. If unsuccessful, print out the reason for the error and exit. (Note that the pointer to the sockaddr_in structure that is passed as a parameter needs to be "type cast" to a plain sockaddr structure in the call.)
  4. Start a loop that repeatedly reads a UPC code and number of items from standard input, communicates with the server, writes the response to standard output
    1. If the UPC code entered in standard input is -1, then exit the loop.
    2. Construct a request string using the UPC code and the number of items entered in the standard input. 
    3. Write the request message to the socket (using send()).
    4. Read response from the socket (using recv()).  Parse the message and print the response on standard output on a new line.
  5. Close the connection (using close()).
Server Operation

The server program should take as argument the port number on which it listens for incoming connection requests. It fills in a sockaddr_in structure with the port number and its IP address. It uses the socket, bind, and listen system calls to create a socket, bind the port number to it, and to listen for a request. On getting the request, it accepts it and receives the request string sent to it. It should then enter a loop in which it continually serves the requests with the appropriate responses, sends the responses, and then closes the socket created by the accept() once the client closes the connection (i.e., when recv() returns a zero value.).

You should implement a "concurrent server", i.e., a server that accept()'s connections and then fork()'s off a child or creates a thread to communicate back with the client.

Helpful hints and comments:

  1. For an introduction to socket programming see this primer or other links on the Resources page.

  2.  
  3. It is strongly encouraged that the everyone begin by writing a sequential server and test it with your client. Finally, write a concurrent server.

  4.  
  5. Because different computers may use different byte ordering conventions ("big endian" or "little endian"), care must be taken to ensure that multi-byte integers sent onto the network are laid out in a standard network byte order, which may or may not be the same as host byte order. So remember to use the htons, htonl, ntohs, and ntohl calls, particularly when filling up the fields in the sockaddr_in structure to ensure that all machines interpret the byte orderings in the same manner.

  6.  
  7. High-level skeleton code for a concurrent server are available here: Java, C using threads, and C using processes (fork())

  8.  
  9. To read the man page for the "foo" routine, type man foo. This will print the first man page encountered that is titled "foo". However, if you want the man page for the "foo" system call and there's also a command foo, you have to specify the section of the manual you want. Thus if you want the system call "foo", you type man 2 foo, because section 2 contains manual pages for system calls. (To learn how to use the command man, you should type man man. But there's a better way to browse manual pages if you use the X window system: xman provides a nice point-and-click interface, with search capability.)

  10.  
  11. On Solaris systems, the routines making up the socket interface are not system calls, but library routines, which means that they are not included in the standard C library. This means that you have to do something special at compile time, and that their manual pages are not in section 2 but in section 3 (actually section 3N). During compilation, your code must be linked with the sockets and nsl libraries. This is done as part of the gcc command:

  12. gcc [flags] your_file.c -lsocket -lnsl   ...

    Note that "Solaris" is the same as "SunOS 5". If you run the command uname -a on a Solaris system, it will say "SunOS...5.5.1" or something of that form.
     

  13. Remember to error check each system call. You may use perror to notify the user of errors. Also, the system declared global variable, errno, gives you information about the type of error that occurred.

  14.  
  15. Your processes should communicate using the reliable stream protocol (SOCK_STREAM) and the Internet domain protocols (AF_INET). If you need to know your host's IP address, you can telnet to your own machine and see the dotted decimal address displayed by the telnet program. You can also use the UNIX nslookup command.

  16.  
  17. Some of you may be running both the client and server on the same machine (by starting up the server and running it in the background, and then starting the client). Recall the use of the ampersand to start a process in the background. If you need to kill your server after you have started it, you can use the UNIX kill command. Use the UNIX  ps command to find the process id of your server. Always make sure that you don't leave processes running behind!

  18.  
  19. Make sure you close every socket that you use in your program. If you abort your program, the socket may still hang around and the next time you try and bind a new socket to the port number you previously used (but never closed), you may get an error. Also, please be aware that port numbers, when bound to sockets, are system-wide values and thus other students may be using the port number you are trying to use.

  20. If your server is designed only to terminate upon receipt of a control-C from the keyboard or of a kill signal, then you will want to use the signal system call (in C) to make sure all sockets are closed before your server terminates. (A call to exit will ensure that these sockets are closed gracefully before the server terminates.) The signal system can be used to call a function that you designate to be executed as soon as a control-C or a kill signal is received. In the code fragment below, the user-supplied function cleanExit() will be called when a control-C or kill signal are received. The two calls to signal in the main routine let the operating system know that cleanExit should be called on a control-C or a kill signal.  Here is the C code for this operation.

    #include <signal.h>
    ........
    main() {
    ........
    signal(SIGTERM,cleanExit);
    signal(SIGINT,cleanExit);
    .......
    } /* end of main */
     

    void cleanExit() /* called on control-C or kill */
    {
    /* exit below will close open sockets*/
    exit(); /* all done, so die */
    }
     

  21. If you are writing the server in C and using fork() to create child processes, the creation of child processes carries with it the responsibility to "cleanup after your children." When a parent process uses fork() to create a child process and that child process later terminates, the child does not go away entirely until the parent allows it to. Specifically, the child sticks around in the form of a "zombie" process until it is released from zombiedom by a wait() or (as discussed below) a waitpid() call from its parent. It is good programming practice to free up resources by cleaning up zombie processes.

  22. Of course, we don't want the parent to block and wait until a child process dies - that would defeat the purpose of having a concurrent server. One somewhat ugly workaround is the following.

What to submit:

The programs you submit should work correctly and be well documented with a record of the information exchanged between your client and server. It would be a good idea to test your client program with the server program implemented by a classmate, or vice versa. This should be possible as long as your programs adhere to the exchange protocol defined above. This is only an interesting way to see how protocol modules can "interoperate" if they accurately implement the protocol specification. You must, however, implement both the client and server on your own. You should submit both a hard copy and an electronic copy. See guidelines for submission.


Back to course home page