Project 1 : Introduction to Socket Programming

CS219 - Programming for the World Wide Web

Due : Friday, January 15, 1999. Midnight.

1. Introduction

In this assignment, you will learn about programming with sockets by implementing a somewhat modified version of the Trivial File Transfer Protocol (TFTP) for transfering files between two machines. You will implement two programs; a server that listens for incoming connections and provides read and write access to a directory of files, and a client that users can run to connect to the server. Both the client and server must be written in C or C++ and operate under Unix.

The purpose of this assignment is to introduce you to the socket programming API as well as a variety of operating system concepts that are widely used in the construction of networked applications.

Please read this entire handout before you begin work.


2. Overview

You will first write a server program that allows users (clients) to upload and download files to and from a specified directory. The server will be run as follows:

server directory port
where "directory" is the location of the files to be accessed and "port" is the TCP port number that clients will use to locate the server. For example,
% server /home/beazley/myfiles 10000
File server listening on classes.cs.uchicago.edu port 10000
After the server has started, it simply spins in an infinite loop waiting for incoming connections until you decide to kill it. Needless to say, the server isn't too terribly exciting to watch (despite the fact that it is the most interesting part of the project to implement).

To connect to the server, you must write a client program that operates as follows:

client server port
where "server" is the hostname of the machine running the server and "port" is the port number. For example:

% client classes.cs.uchicago.edu 10000
Connected to classes.cs.uchicago.edu
Welcome to Dave's file server.
client>
Once the client has connected to the server, it should allow the user to interactively get a directory listing, download and upload files, and close the connection. For example:
Welcome to Dave's file server.
client> dir 
total 229
-rw-rw-r--   1 beazley  beazley       134 Dec 19 17:59 Makefile
-rw-rw-r--   1 beazley  beazley        75 Dec 19 17:55 Makefile
-rwxrwxr-x   1 beazley  beazley      5876 Dec 16 22:59 a.out
-rwxrwxr-x   1 beazley  beazley      9130 Dec 19 21:28 client
-rw-rw-r--   1 beazley  beazley       580 Dec 16 22:59 client.c
client> get client.c 
Received 'client.c' 580 bytes.
client> get foobar 
File not found.
client> get /etc/passwd 
File not found.
client> put spam
Sent spam, 10672 bytes.
client> quit 
Goodbye!
% 
Although the project is (hopefully) simple to describe, there are a number of issues that you need to worry about. First, the server must be able to handle multiple client connections (all of which might be active at any given time). Second, the server needs to defend itself from attack and abuse. For example, the server should not allow clients to access forbidden files (such as the system password file), overwrite existing files on uploads, or crash if a client starts misbehaving. In addition, the server needs to properly manage system resources (since the server runs forever, it shouldn't leak memory or file descriptors). Finally, the server should limit the number of simultaneous client connections and automatically disconnnect clients that appear to be idle.

Most of the work on this project is focused on the implementation of the server. The client should be relatively easy.


3. The Protocol

The real TFTP protocol is implemented using UDP. Since UDP is unreliable, it adds a huge amount of coding complexity (that is better to avoid for now). Your version of TFTP will be slight variation of the TFTP protocol implemented over a TCP connection which is guaranteed to be reliable.

3.1. Packets

The client and server will communicate with each other by sending packets of data back and forth. The format of these packets is as follows:

Each packet begins with a 2-byte opcode. Filenames and error messages are specified in ASCII and terminated with a zero byte (EOS). Data is sent in blocks of 512 bytes or less.

Some care needs to be taken with the opcode, block number, and block size parameters. If you were to write down a 16 bit integer value on paper, you would probably write it with the most significant bit on the left and the least significant bit on the right (the same way you write numbers in decimal):

(most significant) 1101111100110101 (least significant)
When stored in computer memory however, the ordering of the bits varies depends on the machine being used. For example, the most significant part may appear first like this:
                     byte 1      byte 2    
(most significant) [11011111]  [00110101] (least significant)
or, it may appear last like this:
                     byte 1      byte 2    
(least significant) [00110101]  [11011111] (most significant)
The first case, in which the most significant bits appear in the first byte is known as "Big endian." The second case in which the least significant bits appear first is known as "Little endian". In the course of normal programming, you almost never have to worry about this. However, when sending data across a network, it is critical to choose a consistent ordering. If you don't do this, things will break in horrible manner when data is sent between machines with a different byte-ordering schemes (between a PC and a Macintosh for instance).

For this project, the opcode, block number, and block size should all be sent in big-endian format in which the most significant byte is given first and the least significant byte is given last. The following C++ code illustrates how you might convert a 32-bit integer into a big-endian encoding:

unsigned int  value;             // This is the integer you want to convert
unsigned char data[4];           // 4 bytes of storage

data[0] = (value >> 24) & 0xff;        // Most significant part
data[1] = (value >> 16) & 0xff;
data[2] = (value >> 8) & 0xff;
data[3] = value & 0xff;                // Least significant part

3.2. Sending and receiving files

When the client wants to retrieve a file it sends the server a RRQ packet. When the client wants to upload a file, it sends the server a WRQ packet. The receive/send operations work as follows:
The client asks to receive a file from the server (download)
Client (receiver)     Server (sender)
------------------    -----------------                        
  RRQ             --->
                 <---  DAT, block #1
  ACK,block #1    --->
                 <---  DAT, block #2
  ACK,block #2    --->
                 <---  DAT, block #3
  ACK,block #3    --->
  
  (etc.)

The client asks to send a file to the server (upload)

Client (sender)       Server (receiver)
------------------    -----------------                        
  WRQ             --->
                 <---  ACK, block #0
  DAT, block #1   --->
                 <---  ACK, block #1
  DAT, block #2   --->
                 <---  ACK, block #2
  DAT, block #3   --->
                 <---  ACK, block #3
  
  (etc.)
If the length of the data being sent in each packet is equal to the packet size (512 bytes), it means that more data will follow. If it is less, it means that the packet is the final one. If any other type of packet is received during transmission, it either means that an error has occurred (you should check for an ERR packet) or that there has been a protocol error of some sort. For this assignment, the connection should be closed if any of the following occur:

3.3 Getting a directory listing

If the server receives a RRQ packet with an empty filename(""), it should generate an ASCII formatted directory listing and send it back to the client as an ordinary file (using the same protocol as above).
The client asks for a directory from the server
Client (receiver)     Server (sender)
------------------    -----------------                        
  RRQ,filename="" --->
                 <---  DAT, block #1
  ACK,block #1    --->
                 <---  DAT, block #2
  ACK,block #2    --->
                 <---  DAT, block #3
  ACK,block #3    --->
  
  (etc.)
In this case, clients should simply redirect the incoming directory data to standard output instead of writing it to a file.

3.4 Error recovery

Both the client and server should be prepared to handle error cases. For example:

The client asks to receive a non-existent file from the server
Client (receiver)     Server (sender)
------------------    -----------------                        
  RRQ             --->
                 <---  ERR,"File not found"
If possible, the client and server should be able to handle ERR packets without terminating the connection. However, if the communication protocol is violated in any other manner, the server should simply print an error message "protocol error" and close the client connection. For example,
A client protocol error
Client (receiver)     Server (sender)
------------------    -----------------                        
  RRQ             --->
                 <---  DAT, block #1
  WRQ             --->
                      Protocol error. Connection closed.
  Lost connection.
Although you might consider ways to make the server recover from such events, this also may be the work of a malacious client trying to attack the server in some manner. Just to be on the safe side, it is better for the server to drop the connection on the first sign of trouble.

4. What to do.

You will be implementing a server and client that speak the protocol described in the last section. Do not begin to do anything until you are sure you fully understand the protocol.

4.1 The Server

4.1.1 Running the server

The server is started as follows:
server directory port
When the server is started. It should attempt to open up a socket on the given port. If successful, it should print a message such as the following :
% server /home/beazley/myfiles 10000
File server listening on classes.cs.uchicago.edu port 10000
If the server is unable to start because the given port is in use (or for any other reason), it should print an appropriate error message and exit. For example:
% server /home/beazley/myfiles 10000
Unable to bind to port 10000.  Sorry!
% 
If you get this message, it usually means that the requested port number is already in use. Pick another port number and try again. You should pick a port number that is greater than 1024 and less than 65535.

Note : If you kill the server and immediately restart it using the same port number, you may get this error message. Sometimes it takes a while for the operating system to reclaim port numbers that are no longer in use. If this happens to use, just pick a different port number or wait a little while.

4.1.2 Accepting a connection

When a client first connects to the server, the server should use fork() to make the client handler run as a different process. Meanwhile, the server should go back to listen for more client connections. For example, the main loop of the server will look roughly like this:
while(1) {
   int clientfd = accept( ... )      // Wait for a connection
   if (!fork()) {
         // Child process
         handle_client(clientfd);    // Go listen to the client 
         close(clientfd);            // Close the client 
         exit(0);
   } else {
         // Parent process.  Do nothing.
   }
   close(clientfd);
}
Keep in mind that fork() makes a copy of the running server program and returns two different values. In the process that called fork(), fork() returns a non-zero process id of the child. In the child, fork() returns zero.

4.1.3 Handshaking

After receiving a connection and forking the client handler, the server should send a message to the client identifying itself. This should be done by sending a DAT packet to the client that contains a greeting message. The client should respond by sending an ACK. Thus, the connection process works like this:
Client connecting to the server
Client (receiver)     Server (sender)
------------------    -----------------                        
  connect         --->
                       fork()
                 <---  DAT,block 1, "Hello from Your name's fileserver"
  ACK,block 1     --->
  print message       
  wait for user       wait for packets.
If the client does not respond with ACK, the server should immediately drop the connection. After the initial packet has been received and acknowledged by the client, the server should start waiting for incoming packets.

4.1.4 Waiting for packets

After a successful connection, the server should simply wait for packets to arrive from the client. After a packet is received, the server should check the opcode to see what to do next. The server must recognize all 5 opcodes (RRQ, WRQ, ACK, DAT, and ERR).

Keep in mind that not all packets are valid at any given time. For example, if the server were to receive a DAT packet without first receiving a WRQ packet, it should report a protocol error and immediately close the client connection. Thus, the client handling loop might look roughly like this:

handle_client() {
    ...
    while (1) {
         get_packet()
         switch(opcode) {
           case OP_RRQ:
	      if (strlen(filename) == 0) send_directory();
              else send_file();
              break;
           case OP_WRQ:
              receive_file();
              break;
           case OP_DRQ:
              send_directory();
              break;
           case OP_ERR:
           case OP_DAT:
           case OP_ACK:
              protocol_error()
              exit(0);
           default:
              unknown_opcode_error()
              exit(0)
         }
    } 
}
Likewise, functions to send files, receive files, and transmit directories would be programmed to recognize certain packets and report protocol errors in a similar manner.

4.1.5 Server security

Servers in the real world are subject to all sorts of abuse. You should take any precautionary measure that you feel is appropriate to prevent the server from crashing. You should also prevent the following:

4.1.6 Server Logging

To figure out what's going on, your server should print some diagnostic messages to standard output as it runs. In particular, it should print a message each time it receives a connection and a message for each command performed by the clients. Since more than one client might be connected at once, you should include the client process id (PID) in the messages. For example, the following output shows a sample server in operation:
% server /home/beazley/myfiles 10000
File server listening on port 10000
[1910] Received a connection from 127.0.0.1, port 4453
[1912] Received a connection from 127.0.0.1, port 4458
[1914] Received a connection from 127.0.0.1, port 4459
[1916] Received a connection from 127.0.0.1, port 4464
[1918] Received a connection from 127.0.0.1, port 4465
[0] Message. Too many people are connected. Go away!
[1910] Client timed out.
[1912] Client timed out.
[1914] Client timed out.
[1916] Client timed out.
[1918] Client timed out.
[1923] Received a connection from 127.0.0.1, port 4533
[1923] Directory
[1923] Sent client.cxx 1939
[1923] Directory
[1923] client terminated.
You are free to choose the format and content of the messages. The log will help you debug things.

4.1.7 Limiting the number of client connections

The server should allow no more than 5 simultaneous client connections. If 5 clients are already connected, new connections should either be refused with a sensible error message or placed in a queue to be serviced later. If you decide to mess around with a queue, you should limit the number of connections that can be placed in the queue to no more than 5 (allowing for 5 active connections and 5 pending connections). If you want to just send an error message, it might look like this:
% client classes.cs.uchicago.edu 10000
Too many people are connected. Go away!
%
When refusing a connection in this manner, the server should just send an ERR packet to the client and close the connection. For example:
Client connecting to a busy server
Client (receiver)     Server (sender)
------------------    -----------------                        
  connect         --->
                 <---  ERR, "I'm busy, go away"
  exit                close connection

Note : The implementation of this feature is somewhat tricky so make sure you have the rest of the server working first.

4.1.8 Client timeouts

If a client becomes idle for more than 60 seconds, the server should terminate its connection. See the implementation notes section for more details about how to do this.

Note : do not implement this feature until you have the rest of the server working.

4.2 The Client

4.2.1 Running the client

The client is run as follows:
client hostname port
where hostname is the name of the server machine (classes.cs.uchicago.edu for instance) and port is the port number. The client will need to convert the hostname into an IP address (128.135.20.107) and try to establish a connection. If no connection can be established, an error message should be generated:
% client classes.cs.uchicago.edu 10000
Unable to connect with the server.
%
If a bad hostname is given, the following error should be generated:
% client spam.cs.uchicago.edu 10000
Unknown host : spam.cs.uchicago.edu
%
If successful, the client should print a connection message, followed by a server identification message (sent by the server), and prompt the user for a command.
% client classes.cs.uchicago.edu 10000
Connected to rustler.cs.uchicago.edu            (generated by the client)      
Welcome to Dave's file server                   (sent by the server)
client> 
At the prompt, the user must be able to type the following commands: Options in brackets are optional. For example, the command "get foo bar" tells the server to send us the file 'foo' which we will save as 'bar'. The command "put foo bar" tells the client to read the file 'foo' and upload it to the server as 'bar.' If an invalid command is typed, an error should be displayed.
client> spam
Unrecognized command.
If the server should terminate the connection (or go down for any reason), the client should report a problem and exit. For example:
client> dir
Server not responding. Sorry.
%

4.4.2 Writing the client

Unlike the server, the client does not need to worry about multiple threads of execution, resource management, or security. Your client program will look roughly like this:
  1. Convert the hostname into an IP address. Report "hostname error" if unsuccessful.
  2. Open a socket connection with the server. Report "unable to connect" on failure.
  3. Establish the connection by receiving the server identification message and sending an ACK.
  4. Read a command from the user.
  5. Send data to the server (using your protocol). Report "server not responding" if the connection appears to be dead.
  6. Wait for the server's response.
  7. Handle data sent by the server (a directory, an error message, a file, etc...).
  8. Repeat steps 4-8 until the user quits or the server terminates the connection.
You should try to make the client as simple as possible. Most of the work in this assignment is focused on the server.

4.4.3 More about the client connection.

The reliability of the client is far less important than that of the server (consider the reliability of web-browsers compared to web-servers for instance). In particular, it is not necessary for the client to gracefully exit or inform the server that it is done. The server may also decide to simply terminate the client connection (if we have been idle for too long, if there are too many people connected, or if a protocol error occurred).

Although protecting the server is clearly more important, one shouldn't completely ignore security on the client. In particular, the client should not respond to incoming RRQ or WRQ packets that would allow a remote server to download or upload files on the user's machine (as protection against servers written by Microsoft perhaps).


5. Implementation Details

This section provides some implementation details to help you out with some of the more tricky parts of this assignment. I have implemented the project on both Solaris and Linux. You will be handing in the assignment on 'classes' which is a Solaris machine.

If you are new to Unix programming, I recommend consulting the following books :

5.1 Header Files

Your code should use the following header file:
/* tftp.h : header files for Project 1 */
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <ctype.h>
#include <signal.h>
#include <sys/wait.h>
#include <netdb.h>

/* A few constants */ 
#define  TIMEOUT      60
#define  PACKET_SIZE  512
#define  MAX_CLIENTS  5

A copy of this file is available in /usr/local/classes/current/CS219/projects/project1/.

5.2 Suggested files

Since the client and server will share a considerable amount of code, you might break the project up as follows:

5.3 Compilation

You will need to hand in the project with a Makefile that builds both the server and the client. Your makefile will look something like this :
# Makefile for Solaris
CC = cc
LIBS = -lsocket -lnsl 

server::
	$(CC) tftp.c server.c -o server $(LIBS)

client::
	$(CC) tftp.c client.c -o client $(LIBS)
You should be able to type 'make server' or 'make client' to build the server and client respectively.

A copy of this Makefile is available in /usr/local/classes/current/CS219/projects/project1/.

Note : If using the C++, the name of the compiler is 'CC'.

Note : If using Linux, the LIBS line should be empty.

5.4 Socket Connections

5.4.1 Opening a socket on the server

The following code fragment shows how to open a socket on the server :
  struct sockaddr_in servaddr;
  /* Create a socket */            
  int fd = socket(AF_INET, SOCK_STREAM, 0);
  if (fd < 0) {
    printf("Unable to open socket!\n");
    exit(1);
  }
  /* Bind it to a specific port number (contained in port) */
  memset(&servaddr, 0, sizeof(servaddr));
  servaddr.sin_family = AF_INET;
  servaddr.sin_port = htons(port);
  servaddr.sin_addr.s_addr = htonl(INADDR_ANY);

  if (bind(fd, (struct sockaddr *) &servaddr, sizeof(servaddr)) < 0) {
    printf("unable to bind to port %d\n", port);
    exit(1);
  }
  
  /* Tell the OS to allow no more than 5 pending connections. 
     Note : This is different from allowing 5 active client connections */
  
  listen(fd,5);

5.4.2 Opening a socket on the client

To open a socket on the client, do this
  int      fd;
  struct   sockaddr_in servaddr;
  struct   hostent *host;
  
  /* Create the socket */
  fd = socket(AF_INET, SOCK_STREAM, 0);
  if (fd < < 0) {
     printf("Unable to create socket!\n");
     exit(1);
  }
  /* Set its address :
        port     =  Server port number (e.g., 10000)
        hostname =  Hostname (e.g., "classes.cs.uchicago.edu")
  */
  memset(&servaddr,0,sizeof(servaddr));
  servaddr.sin_family = AF_INET;
  servaddr.sin_port = htons(port);

  /* Look up the hostname try to turn it into an IP address */
  if ((host = gethostbyname(hostname)) == (NULL)) {
    printf("Unknown hostname : %s\n", hostname);
    exit(1);
  }
  memcpy(&servaddr.sin_addr,host->h_addr,host->h_length);

  /* Connect with the server */  
  if (connect(sockfd, (struct sockaddr *) &servaddr, sizeof(servaddr)) < 0) {
    printf("Unable to connect with the server.\n");
    exit(1);
  }

5.4.3 Accepting a connection on the server

while(1) {    /* Server runs forever */
  /* Accept a client connection */
  struct sockaddr_in clientaddr;
  int len = sizeof(clientaddr);
  int clientfd = accept(fd, (struct sockaddr *) &clientaddr, &len);
  if (clientfd < 0) {
      if (errno != EINTR) {
         printf("Accept error!\n");
         printf("%s\n",strerror(errno));
         exit(1);
      }
  } else {
      /* Fork off the client handling process */
      if (!fork()) {
          /* I am the child */
          close(fd);
          /* Say where the connection came from */
	  printf("[%d] Received a connection from %s, port %d\n", getpid(),
		 inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));

          /* Go off and talk to the client for awhile */
      	  handle_client(clientfd);

          /* Done. Close the client connection and exit. */
          close(clientfd);
          exit(0);
          /* Child process terminated */
      } else {
          /* I am the parent */
          /* Do nothing interesting.  Just go back to the top of this
             loop and keeping listening for more connections */
      }
      close(clientfd);
  }
}

5.4.4 Reading and writing socket data

The Unix read() and write() functions can be used to communicate with a socket. However, you should use the following two functions for all socket communication in this project:

/* Write n bytes of data to the socket fd */
ssize_t
writen(int fd, void *vptr, ssize_t n)
{
  size_t  nbytes = n;
  char *ptr = (char *) vptr;
  while (nbytes) {
    ssize_t nwritten = write(fd,ptr,nbytes);
    if (nwritten <= 0) {
      if (errno == EINTR)
	nwritten = 0;
      else
	return nwritten;
    }
    nbytes -= nwritten;
    ptr += nwritten;
  }
  return (n - nbytes);
}

/* Read, at most maxlen bytes of data from the socket fd */
ssize_t readn(int fd, void *vptr, ssize_t maxlen)
{
  ssize_t nbytes = maxlen;
  char *ptr = (char *) vptr;
  while (nbytes) {
    ssize_t n = read(fd,ptr,nbytes);
    if (n < 0) {
      if (errno == EINTR)
	n = 0;
      else
	return n;
    } else if (n == 0) {
      break;
    }
    ptr += n;
    nbytes -= n;
  }
  return maxlen-nbytes;
}
Both functions return the actual number of bytes read or written, or a negative number if an error occurred. If both cases, the vptr parameter is a pointer to the actual data to be sent or a pointer to a buffer where received data is to be placed.

Now, a word about this funny EINTR business. In most cases, network operations can take an unpredictable amount of time to complete. For example, a client might send data to the server very slowly or become stalled due to network traffic. In such cases, the operating system may choose to interrupt any pending system call (like read(), write(), accept(), etc...) to go do something else. A system call may also be interrupted by a signal. In any case, the EINTR check in the readn() and writen() functions is checking to see if the read() or write() system calls have been interrupted. If so, we simply go back to the top of the loop to re-issue the read() or write() call again. As a result, these functions are guaranteed to read or write exactly n nbytes (unless some sort of unrecoverable network error occurs).

You may wish to write similar read and write functions for handling different parts of the TFTP protocol such as reading/writing 16 bit integers, strings, and so forth. In any event, all of your socket communication should utilize the two functions above.

5.5 Unix File I/O

Both the client and server will have to perform a limited amount of Unix file I/O. A short summary of what you need is below.

5.5.1 Opening files

To open a file for reading, do the following :
int fd = open("filename", O_RDONLY,0);
if (fd < 0) {
   printf("Can't open file.\n");
   return;
}
To create a new file for writing, do the following :
int fd = creat("filename", 0644);
if (fd < 0) {
  printf("Can't create file.\n");
  return;
}
When a file is opened or created, an integer file descriptor (fd) is returned. This file descriptor can be used in exactly the same way as a socket. In fact, the readn() and writen() functions previously given can also be used to read or write data from files. For example, the following code opens a file, and reads it in 512 byte blocks until the end of the file is reached:
char buffer[512];
int  fd = open(filename, O_RDONLY, 0);
if (fd < 0) return;
int nbytes = readn(fd,buffer,512);
while (nbytes > 0) {
   /* Read nbytes of data.  Do something with it */
   ...
   /* read the next chunk of data */
   nbytes = readn(fd,buffer,512);
}
/* Done */
To close a file, use the close() function.
close(fd);

5.5.2 Testing for regular files

Your server must not allow clients to access directories, symbolic links, and other types of special Unix files. To test for this, do the following:
int fd = open(filename, O_RDONLY, 0);
...
struct stat   fileinfo;
if (fstat(fd,&fileinfo) < 0) {
  close(fd);
  printf("I/O error.\n");
  return;
}
if (!S_ISREG(fileinfo.st_mode)) {
  close(fd);
  printf("Not a regular file. Sorry.\n");
  return;
}
...

5.6 How to generate a directory list

One way to generate the directory listing is to try to read the directory yourself using system functions such as readdir(). Another option, which is far easier, is to simply execute the 'ls -l' command and redirect its output through a pipe like this:
FILE *f = popen("ls -l /your/directory","r");
if (f) {
  int fd = fileno(f);
  /* fd is now a file descriptor that you can read just like a normal file */
  ...
  fclose(f);
} else {
  printf("Unable to generate directory.\n");
}

5.7 Signals and timeouts

To implement the server timeout that disconnects clients after 60 seconds, you will need to use a signal. First you will need to write a function like this:
void sig_alarm(int signo) {
  /* A timeout occurred. Deal with it */
  ...
}
Next, in your server, you will need to tell the operating system about your signal handler by putting the following statement somewhere in the code.
signal(SIGALRM,sig_alarm);
Finally, whenever you want to start the 60-second timer, you will need to execute the following statement.
alarm(60);
If you re-issue the alarm() function, the timer is reset to whatever new value you provide.

When the alarm expires, the system will interrupt your program and go immediately to the sig_alarm() function. In this case, the alarm should be used to terminate idle client connections.

5.8 Limiting the number of clients

Your server should not allow more than 5 client connections to be active at any given time. However, this is also the most tricky part of the project to implement and it is up to you to figure out a way to do it. There are a couple of general approaches you might take however.

One way to do it is for the server to keep a simple counter of how many clients have been created with fork(). When accepting a new connection, you simply check the counter to see if its value is too high. If so, an error is returned to the client and the connection is closed. Although simple, the tricky part is figuring out how to decrement the counter when a client handler terminates (and how to do it in a reliable manner).

Another option is to created a pre-forked server. With this approach, the server immediately forks five processes to handle clients when it starts. When new connections are received, they are handled by one of the available client handlers. If all of the client handlers are busy, new incoming connections will simply sit in a pending connection queue (this is handled by the operating system and is set by the value of the listen() function). If even more connections arrive (more than can fit in the queue), the operating system will start refusing incoming connections.

Again, it is up to you to figure out how to do this. Keep in mind that having a working server without this feature is better than no server at all.


6. How to start

To help you with your testing, a reference client has been provided in

/usr/local/classes/current/CS219/projects/project1/

While no source code is available for this client, it speaks the protocol described in section 3. You should be able to run it against your server to see if you are at least on the right track. Of course, you still have to write your own client to complete the assignment. Additional testing tools will be made available in due course.

DO NOT WAIT UNTIL THE NIGHT BEFORE THE DUE DATE TO BEGIN WORK!


7. What to hand in

The assignment is due Friday, January 15, Midnight.

You must handin the following files:

Everything must be handed in electronically on classes.cs.uchicago.edu. Since grading is based on how your program runs, hard-copies are not accepted. Details about the handin procedure will be given in class.

Please do not hesistate about sending e-mail or coming to see me if you run into any problems.