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:
<request type> <UPC-code> <number>
where:
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:
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:
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.
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 */
}
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.
#include <sys/types.h>
#include <sys/wait.h>
main() {
........
while (1) { /* parent loops forever */
......
if ( (childpid = fork()) == 0) {
......
/* child process */
exit();
}
/* parent starts here and checks return code */
pid = waitpid(-1, ... , WNOHANG);
pid = waitpid(-1, ...., WNOHANG);
........
}
.......
After the parent forks off the child, it issues two non-blockingwaitpid()
system calls. (The -1 value indicates that the parent should wait for any
child process, and the WNOHANG makes this a non-blocking waitpid). Note
that a problem can arise if a parent only issues one waitpid call following
a fork call. Suppose a child process has not terminated by the time waitpid
is invoked. Then it will stick around in the form of a zombie. If later
on there is another fork followed by a waitpid, the old zombie will be
culled but the new child will remain as a zombie. The situation can only
get worse with each subsequent fork. If you issue not one but two waitpid
calls for each fork call, only one child process is created per loop but
more than one can be culled per loop. If there is a queue of zombies waiting
to be culled, eventually they all will be culled.
The parameters to waipid() vary greatly from one UNIX derivative
to another, so consult the man pages for more info on waitpid().
There may be a cleaner way to do this!
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.