"Linux Gazette...making Linux just a little more fun!"


Fun with Client/Server Computing

By David Nelson


Psst, wanna have some fun? Try client/server computing. It's like talking through two tin cans and a taut string, upgraded to the computer era. Linux has all the tools you need. You are already using client/server computing in applications such as Netscape, telnet, and ftp. And it's easy to write your own client/server apps, maybe even useful ones.

Client/server computing links two different programs (the client and the server) over a network. For practice you can even skip the network by letting Linux talk to itself. So read on even if you aren't attached to a network. (But your Linux installation needs to be configured for networking.)

A very common form of client/server computing uses BSD sockets. BSD stands for Berkeley Software Distribution, an early version of Unix. Logically, a BSD socket is a combination of IP address and port number. The IP address defines the computer, and the port number defines the logical communication channel in that computer. (In this usage a port is not a physical device. One physical device, e.g. an Ethernet card, can access all the ports in the computer.)

Linux Journal ran a nice three-part series on network programming by Ivan Griffin and John Nelson in the February, March, and April, 1998, issues. The February article contains the code to set up a skeleton client/server pair using BSD sockets; it includes all the plumbing needed to get started. You can download the code from SSC, then use this article to start playing with more content.

After downloading the file 2333.tgz, expand it with the command tar&nsbp;-xzvf 2333.tgz. Rename the resultant file 2333l1.txt to server.c, and the file 2333l2.txt to client.c. Edit server.c to delete the extraneous characters @cx: from the start of the first line, and either delete the last line or make it a comment by enclosing it between the characters /* and */. Similarly, delete the last line of client.c, or make it a comment. Compile server.c with the command gcc -oserver server.c; similarly compile client.c using gcc -oclient client.c.

The server runs on the local computer, so it only needs to know its port number to define a socket. The client runs on any computer, so it needs to know both its target server computer and the server's port number. You have thousands of port numbers to play with. Just don't use a port that is already taken. Your file /etc/services lists most of the ports in use. I found that port 1024 worked fine.

Now I said you didn't need to be connected to a network, but you do need to have your computer configured for networking to try this out. In fact, this code won't run for me if I use the generic name localhost; I have to give the explicit name of my computer. So assuming you are set up for networking, start the sever by typing

server 1024 &
and then start the client by typing
client hostname 1024
where hostname is the name or the IP address of your computer. If things work right, you will see output similar to the following:
Connection request from 192.168.1.1
14: Hello, World!
The first line gives the IP address of the client, and the second line is the message from the server to the client. Considering all the code involved, this would be a good entry for the World's Most Complex "Hello, World" Program Contest! Note that the server keeps running in the background until you kill it with the commands fg and ^C (ctrl-C).

Example of Query-Respone Client/Server

Now let's do something more useful. Debugging two programs simultaneously is no fun, so let's start simple by simulating a client/server pair in a single program. Then when you understand how things work we can divide the code between the client and the server. In the following program the client is simulated by the function client. The main routine simulates the server:
/* local test of client-server code */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

char name[256] = "";
char buffer[256] = "";

void client(char *buffer)
{
 printf("%s", buffer);
 fgets(buffer, 256, stdin);
}

 int main(int argc, char *argv[])
{
 int year, age;

 sprintf(buffer, "Please enter your name: ");

 client(buffer);

 strcpy(name, buffer);
 sprintf(buffer, "Hi, %sPlease enter your year of birth: ", name);

 client(buffer);

 year = atoi(buffer);
 age = 1998 - year;
 sprintf(buffer, "Your approximate age is %d.\nEnter q to quit: ", age);

 client(buffer);

 return(0);
}
You don't have to be an expert at C code to see how this works. The simulated server (main) sends the string "Please enter your name" to the simulated client (client) through the array buffer. The client prints the string, reads the name as a string from keyboard, and returns that string through buffer. Then the server asks for the year of birth. When the client collects it as a string, the server converts it to a number and subtracts it from 1998. It sends the resultant approximate age back to the client. We are done now, but because the client needs a keyboard entry before returning, the server requests that a "q" be entered. More sophisticated coding could eliminate this unnecessary awkwardness. This simulated client/server illustrates passing strings between server and client, asking and responding to questions, and doing arithmetic.

Copy the above code into an editor and save it as localtest.c. Compile it with the command gcc -olocaltest localtest.c. When you run it you should get output like:

Please enter your name: joe
Hi, joe
Please enter your year of birth: 1960
Your approximate age is 38.
Enter q to quit: q
Now let's turn this into a real client/server pair. Insert declarations into server.c by changing the beginning statements of main to read:
int main(int argc, char *argv[])
{
int i, year, age;
char name[256] = "";
char buffer[256] = "";
char null_buffer[256] = "";
    int serverSocket = 0,
The application-specific code in server.c is towards the end. Replace it with the following:
/*
* Server application specific code goes here,
* e.g. perform some action, respond to client etc.
*/

sprintf(buffer, "Please enter your name: ");
write(slaveSocket, buffer, strlen(buffer));
for (i = 0; i <= 255; i++) buffer[i] = 0;

/* get name */
read(slaveSocket, buffer, sizeof(buffer));
strcpy(name, buffer);
sprintf(buffer, "Hi, %sPlease enter your year of birth: ", name);
write(slaveSocket, buffer, strlen(buffer));
for (i = 0; i <= 255; i++) buffer[i] = 0;

/* get year of birth */
read(slaveSocket, buffer, sizeof(buffer));
year = atoi(buffer);
age = 1998 - year;
sprintf(buffer, "Your approximate age is %d.\nEnter q to quit: ", age);
write(slaveSocket, buffer, strlen(buffer));

close(slaveSocket);
exit(0);
This is almost the same as the server code in the simulated client/server, except that we read and write slaveSocket instead of calling the function client. You can think of slaveSocket as the connection through the socket between the server and client.

The client code is very simple. Insert declarations into client.c by changing the beginning statements of main to read

int main(int argc, char *argv[])
{
  int i;
  int clientSocket,
Find the application specific code near the end of client.c and replace it with the following:
/*
* Client application specific code goes here
* e.g. receive messages from server, respond, etc.
* Receive and respond until server stops sending messages
*/

while (0 < (status = read(clientSocket, buffer, sizeof(buffer))))
  {
    printf("%s", buffer);
    for (i = 0; i <= 255; i++) buffer[i] = 0;
    fgets(buffer, 256, stdin);
    write(clientSocket, buffer, strlen(buffer));
  }
    close(clientSocket);
    return 0;
  }
Again, this is almost the same as the client code in the simulated client/server. The main differences are the use of clientSocket, the other end of slaveSocket in the server, and the while statement for program control. The while statement closes the client when the server stops sending messages.

Recompile server.c and client.c and run them again as before. This time the output should be something like:

Connection request from 192.168.1.1
Please enter your name: joe
Hi, joe.
Please enter your year of birth: 1960
Your approximate age is 38.
Enter q to quit: q
Now you can really play: try running multiple client sessions that call the same server, and if you are on a network try running the server on a different computer from the client. The server code is designed to handle multiple simultaneous requests by starting a process for each client session. This is done by the fork call in server.c. Read the man page for fork to learn more.

Chat Program as a Client/Server

As a final example, let's look at a chat program for sending messages between users. It's primitive, because it only allows alternating lines between each person, and it requires the server to keep a window open. But it shows how a client/server pair can carry on an unlimited dialog, and it could be extended into a practical program.

Insert declarations into server.c by changing the beginning statements of main to read:

 int main(int argc, char *argv[])
{
  char buffer[256] = "";
  int i, serverquit = 1, clientquit = 1;
    int serverSocket = 0,
Replace the application-specific code towards the end of server.c with the following:
/*
* Server application specific code goes here,
* e.g. perform some action, respond to client etc.
*/

printf("Send q to quit.\n");
sprintf(buffer, "Hi, %s\nS: Please start chat. Send q to quit.\n", inet_ntoa(clientName.sin_addr));
write(slaveSocket, buffer, strlen(buffer));
for (i = 0; i <= 255; i++) buffer[i] = 0;

while (serverquit != 0 && clientquit != 0)
{
 status = 0;
 while (status == 0)
  status = read(slaveSocket, buffer, sizeof(buffer));
 clientquit = strcmp(buffer, "q\n");

 if (clientquit != 0)
 {
  printf("C: %s", buffer);
  for (i = 0; i <= 255; i++) buffer[i] = 0;

  printf("S: ");
  fgets(buffer, 256, stdin);
  serverquit  = strcmp(buffer, "q\n");
  write(slaveSocket, buffer, strlen(buffer));
  for (i = 0; i <= 255; i++) buffer[i] = 0;
 }
}
printf("Goodbye\n");
close(slaveSocket);
exit(0);
Insert declarations into client.c by changing the beginning statements of main to read:
int main(int argc, char *argv[])
{
 int i, serverquit = 1, clientquit = 1;
    int clientSocket,
Replace the application-specific code toward the end of client.c with the following
/*
* Client application specific code goes here
* e.g. receive messages from server, respond, etc.
*/

while (serverquit != 0 && clientquit != 0)
{
  status = 0;
  while (status == 0)
    status = read(clientSocket, buffer, sizeof(buffer));
  serverquit = strcmp(buffer, "q\n");

  if (serverquit != 0)
  {
    printf("S: %s", buffer);
    for (i = 0; i <= 255; i++) buffer[i] = 0;

    printf("C: ");
    fgets(buffer, 256, stdin);
    clientquit = strcmp(buffer, "q\n");
    write(clientSocket, buffer, strlen(buffer));
    for (i = 0; i <= 255; i++) buffer[i] = 0;
   }
 }
 printf("Goodbye\n");
 close(clientSocket);
 return 0;
 }
Recompile both server.c and client.c and you are ready to try it out. To simulate two computers on one, open two windows in X or use two different consoles (e.g. alt-1 and alt-2.) Start the server in one window using the command
server 1024
and the client in the other using the command
client hostname 1024
where hostname is replaced by your actual hostname or IP address.

Server and client code for this chat program are almost identical, and very similar to the previous example. There are two main differences. The first is the test to see whether either party has entered a "q" to quit. The flags serverquit and clientquit signal this. The second is the tight loop waiting for response from the other party. The function read returns the number of character read from the socket; this is stored into status. A non-zero number of characters indicates the other side has sent a message.

Here is an example session as printed by the server:

Connection request from 192.168.1.1
Send q to quit.
C: Hi server
S: Hi client
C: Bye server
S: Bye client
Goodbye
And here is the same session as printed by the client:
S: Hi, 192.168.1.1
S: Please start chat. Send q to quit.
C: Hi server
S: Hi client
C: Bye server
S: Bye client
C: q
Goodbye
I hope these examples have shown how easy it is to set up client/server computing. May your appetite be whetted to try your own applications. If you cook up something tasty, let the rest of us know. And don't forget to keep that string taut!


Copyright © 1998, David Nelson
Published in Issue 33 of Linux Gazette, October 1998


[ TABLE OF CONTENTS ] [ FRONT PAGE ]  Back  Next