Unix Network Programming Manual
Multi-client Servers
The bottom of this page displays a program
that uses the fork command to implement a multi-client server. This
is a relatively "stripped-down" program to not do much, but it is a
working example.
The fork command has the following syntax:
pid = fork ()
which is about as simple as you can get. When the fork call is executed,
it creates a near duplicate of the program making the call and readies it
to run.
When the new process is created, it has the same
relative program counter as the old process, so it begins by returning from
the fork call, just as the original process will do.
So now there are two copies of the same program and you might wonder how they can be differentiated. The return value, pid,
for the program making the call (the parent) is the process id of the newly
created process (the child).
For the child the return value is always zero. You might be wondering how
this is useful, so a simple example is in order. Most uses of fork look
something like this:
pid = fork ();
if (pid < 0)
{
fprintf (stderr, "Error in fork call\n");
close (client_sock);
}
if (pid == 0)
{
/* Do the child stuff - and NO FORK CALLS */
close (client_sock);
exit (0); /* THERE MUST BE ONE OF THESE IN THE CHILD CODE */
}
/*
* Do the parent stuff, including a non-blocking wait. If you
* don't do this, you create zombies and that is BAD, BAD, BAD.
*/
wait3 (NULL, WNOHANG, NULL);
After the return from the fork call, the value of pid is checked.
Remember that both processes will do exactly the same thing. One will find
that the value of pid is greater than zero, and in this case, it
will behave like the parent process. The other will see a pid of zero and
behave like the child. Normally, the parent behavior is to wait around
for another connection by using an accept call. Meanwhile, the child
handles the connection with the client and does all of the real work.
One important note is that the code for the child process
should not contain a fork call. If it does, the child will create
a child, which will create a child and so on, until the operating system
process table is full. The machine will crash or have to be shut down
and everyone concerned will be quite unhappy.
To better understand what happens here, look at the part of the example
that is executed when the pid is zero. While all this does is
close the socket and exit, this could be anything. Normally, it is a
call to a function that handles all the client activities. The
child code in the example below is a call to the
function client_server, which performs all ensuing work. Note that
it is expected that the child will never return from this call, because the
child should never be allowed to execute the code that is meant for the
parent. The child process doesn't return, it exits.
The
parent code in the example below
entails remaining in the do loop, executing wait3, accept
and then the fork again. So the parent just continues to wait for
more connections while the child is handling the client requests for
service.
The wait3 call
wait3 call is described in the comments and is required to insure
that child processes don't stay around in the system forever. If you don't
have one of these, you will hear from your system administrator shortly.
When starting out using fork, the best advice is don't get too adventurous.
Use the standard forms and use a few print statements to understand what is
going on before trying to do anything fancy. Keep in mind that for beginners:
- The parent and child should never execute each others' code.
- The child should never execute a fork call.
- The parent must use a wait call of some form to clear terminated
children.
/* ==================================================================
This simple example demonstrates a server that processes accepts
and creates a child process to handle all of the communication
with the client. You have to remember that the child
inherits an environment that is virtually identical to the
parent, including all file descriptors. So the child process
has access to the sockets created by the parent.
The function of this server is to allow the client to have
access to certain operating system functions that might not
otherwise be available. This is the sort of thing a distributed
operating system or database might do, although, hopefully, in a
more robust and complete way.
Syntax:
fork_server port
================================================================== */
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <sys/wait.h>
#define NAMELEN 64
#define CMDLEN 32
typedef int BOOLEAN;
void client_wait ();
void client_server ();
BOOLEAN legal_name ();
void main
(
int argc,
char *argv []
)
{
int child_id;
int status; /* the returned child status */
int l_sock, chct, addrlen;
char ch, buf[80];
char hostname [NAMELEN];
struct hostent *hostp;
struct sockaddr_in addr;
/*
* Check the args - client_server port
*/
if (argc < 2)
{
fprintf (stderr, "Syntax: fork_server portnum\n");
exit (0);
}
/*
* Open a stream socket for public access and listen on it
* for new clients.
*/
if ((l_sock = socket (AF_INET, SOCK_STREAM, 0)) < 0)
{
fprintf (stderr, "Could not open the socket\n");
exit (0);
}
bzero (addr, sizeof (addr));
addr . sin_family = AF_INET;
addr . sin_port = htons (atoi (argv[1]));
addr . sin_addr . s_addr = htonl (INADDR_ANY);
if (bind (l_sock, &addr, sizeof (addr)) < 0)
{
fprintf (stderr, "Could not bind the address\n");
exit (0);
}
if ((listen (l_sock, 1)) < 0)
{
fprintf (stderr, "Unable to listen\n");
close (l_sock);
exit (0);
}
/*
* client_wait does the work.
*/
client_wait (l_sock);
fprintf (stderr, "cmd_server terminating\n");
close (l_sock);
}
/* ============================== client_wait ============================
Function: Accept client_wait requests and spawn client servers.
Usage: client_wait (l_sock)
Arguments: int l_sock - the advertised socket
Return: None.
Notes:
================================================================== */
void client_wait (l_sock)
int l_sock;
{
int child_id, client_sock;
struct sockaddr addr;
int addrlen;
/*
* Wait for connections. If there is a failure, simply go on.
* When a connection is made, fork a child process to do the work.
*/
do
{
/*
* When a child process dies, it must be recognized by the parent, or
* the child hangs around as a zombie. This is handled with a wait.
* In this case a plain wait () won't work, because we don't want to
* wait forever. Instead, use wait3, which clears the child
* termination signal, but if there isn't anything, will return if
* the WNOHANG flag is set.
*/
wait3 (NULL, WNOHANG, NULL);
if ((client_sock = accept (l_sock, &addr, &addrlen)) < 0)
{
fprintf (stderr, "Failed connection with client\n");
continue;
}
/*
* Create the child process to do the actual client communications.
* Again, the child sees a return of zero and the parent sees the
* child id being returned.
*/
if ((child_id = fork ()) < 0)
{
fprintf (stderr, "Failure in creating child process\n");
close (client_sock);
}
else
if (child_id == 0) /* Do the client stuff */
client_server (client_sock);
/*
* The server (parent) has no more use for the client
* socket and can close it. Remember that the child
* client server still has it open.
*/
close (client_sock);
} while (1);
/*
* The server never terminates, so don't worry about anything here.
*/
return;
}
/* ============================== client_server ============================
Function: Handle client communications. The task of the server
is to execute simple commands over the protected
directory, which would normally be prohibited.
Usage: client_server (client_sock)
Arguments: int client_sock - the advertised socket
Return: None.
Notes:
================================================================== */
void client_server (client_sock)
int client_sock;
{
char buf [CMDLEN];
int buflen;
static char *ack = "\06";
static char *logout_msg = "Pleased to be of service - Goodbye\n";
/*
* First, make sure that the client address is acceptable. When the
* connection is successful, the client should send it's name.
*/
buflen = recv (client_sock, buf, CMDLEN, 0);
buf [buflen] = '\0';
if ( ! legalname (buf))
{
fprintf (stderr, "Attempt to access by an illegal client named ");
buf [buflen] = '\0';
fprintf (stderr, "%s\n", buf);
exit (0);
}
if (send (client_sock, ack, 1, 0) < 0)
{
perror ("On connection acknowledgement");
close (client_sock);
exit (0);
}
/*
* Set it up so that standard output for this client server
* will be the client socket. Then whatever output would
* normally go to standard output will go back to the client.
* dup2 does this by closing stdout (descriptor 1) and making
* it a duplicate of the client socket.
*/
dup2 (client_sock, 1);
/*
* As long as the client is interested, accept communications,
* and execute the desired commands. At the end of each
* command, return an end-of-command character to the client.
* A command of "logout" indicates a desire to terminate the connection.
*/
do
{
buflen = recv (client_sock, buf, CMDLEN, 0);
if (strncmp (buf, "logout", 6) == 0)
{
/*
* Send a nice goodbye and send an ack just in case.
*/
send (client_sock, logout_msg, strlen (logout_msg), 0);
send (client_sock, ack, 1, 0);
break;
}
buf [buflen] = '\0'; /* Supply a terminator */
system (buf);
send (client_sock, ack, 1, 0);
} while (1);
/*
* Terminate the client socket and the process
*/
close (client_sock);
exit (0);
}
/* ============================== legal_name =======================
Function: Check the legality of a client address/name.
Usage: if (legal_name (client))
Arguments: char *client - the client name
Return: TRUE if legal, FALSE otherwise.
Notes:
===================================================================== */
BOOLEAN legalname (client_name)
char *client_name;
{
char client [NAMELEN], *cp;
FILE *clientfile;
BOOLEAN found;
if ((clientfile = fopen ("client_file", "r")) == NULL)
{
perror ("Opening the client file");
exit (0);
}
found = FALSE;
while (! feof (clientfile))
{
/*
* Get a line from the file, replace the newline, if any, with a
* null and then match it to the client name.
*/
fgets (client, NAMELEN, clientfile);
cp = index (client, '\n');
if (cp != NULL)
*cp = '\0';
if (strcmp (client, client_name) != 0)
continue;
/*
* Name is located. Close the file and return.
*/
fclose (clientfile);
return (TRUE);
}
return (FALSE); /* No where to be found. */
}