7.4.2.4 An Interactive Client-Server Set-up

7.4.2.4  An Interactive Client-Server Set-up

     Now, we discuss a client program and an associated server program that are interactive and run either on the same machine or two different machines. The user sits at the client and types in one of several commands the client understands. The client sends the command over to the server which responds appropriately. We assume that the server is running on a Unix machine. The server returns the results of running system commands for certain queries, and for certain other commands, it returns files on the server. The program is given below.

 Program 7.31

#!/usr/bin/perl
#interactiveServer.pl

use IO::Socket::INET;
use Net::hostent; 
use strict;

sub readFile {
    my ($fileName) = @_;
    open (IN, $fileName);
    my @contents = ;
    return @contents;
}

my $PORT = 2000;  #Pick something not used
my %socketDescription = (Proto => 'tcp',
                         LocalPort => $PORT,
                         Listen => SOMAXCONN,
                         Reuse => 1);

my $server = IO::Socket::INET -> new (%socketDescription);
die "Cannot set up server" unless $server;
print "[Interactive server $0 accepting clients]\n";

my $client;
while ($client = $server -> accept()){
    print $client "Welcome to server $0 on $ENV{'HOST'}\n";
    print $client " type ? for command list\n";
    my $hostInfo = gethostbyaddr ($client -> peeraddr);
    print "[Connect from ", $hostInfo -> name, "]\n";
    
    while (my $clientCommand = <$client>){
        if ($clientCommand =~ /localtime/){
            print $client scalar (`date`), "\n";
        }
        elsif ($clientCommand =~ /passwd/){
            print $client  readFile ('/etc/passwd');
        }
        elsif ($clientCommand =~ /services/){
            print $client  readFile ('/etc/services');
        }
        elsif ($clientCommand =~ /who/){
            print $client  scalar (`/usr/bin/who`), "\n";
        }
        elsif ($clientCommand =~ /quit|q/){
            print "[Disconnect with ", $hostInfo -> name, "]\n";
            last;
        }
        else {
            print $client 
                "Commands are:  passwd localtime services who quit\n";
        }
    }
    close $client;
}

The server program uses two Perl modules: IO::Socket::INET and Net::hostent. As we know from before, the IO::Socket::INET package allows us to build sockets for Internet communication. The

Net::hostent package allows us to obtain certain details about a computer or host on the Internet.

The server program has a subroutine called readFile that returns the contents of a file whose full name is given to it. The server program establishes a TCP socket connection at a certain port on the local machine. The maximum number of connections to this server socket is SOMAXCONN whose value is obtained from Perl’s Socket package which actually obtains it from some C language files available on the machine. The IO::Socket or
IO::Socket::INET package inherits this value from the Socket package. The value of SOMAXCONN on the Linux machine the author uses is 128.

If the server socket $socket cannot be established, the program dies. Otherwise, the server prints its name on its terminal and says it is accepting connections.

In the conditional of the main while loop, the program calls accept () method on the server socket. accept () accepts an incoming connection and creates a new socket called $client. $client is automatically unbuffered. The program prints a welcome message on the $client socket. This welcome message actually shows up at the other end of the communication line from the server: the actual client program, if a line of communication has been successfully set up with a client program. $0 gives the name of the script running, i.e., the server program name.

The $client socket obtains the name of the machine it is connected to at the other end by calling peeraddr on it: $client->peeraddr(). It then calls gethostbyaddr of the Net::hostent package to obtain details of this machine. The gethostbyaddr method takes as input an IP address, and returns what is called a Net::hostent or hostent (host entity) object in networking terminology. This object contains certain fields: name,
aliases, addrtype, length and addr_list. We use only the name field in our program.

 

    my $hostInfo = gethostbyaddr ($client -> peeraddr);

    print "[Connect from ", $hostInfo -> name, "]\n";

 

First, the argument sent to gethostbyaddr is given below.

 

$client -> peeraddr

 

$client is a socket created by

 

$server -> accept().

 

This socket is an IO::Socket::INET type object. An object of this type understands several methods, one of them being peeraddr(). peeraddr () returns the address component of the socket on the peer host. $client is a socket on the server. Therefore, its peer or counterpart is the socket on the client program. As a result,

 

$client -> peeraddr

 

returns the IP address of the client program that is attempting to talk to the server program. In Perl, when a method is called on an object, it is not necessary to use ( ) following the method name, if no arguments are being passed to the method.

Thus, gethostbyaddr gets as its argument the IP address of the machine on which the client program is running. The call to gethostbyaddr returns the Net::hostent or hostent object containing details about this machine. The reference to the object containing these details is stored in $hostInfo.

 

$hostInfo -> name

 

is a call to obtain the name associated with the IP address of the machine on which the client program is running. Thus, the statement

 

    print "[Connect from ", $hostInfo -> name, "]\n";

 

prints the name, not the IP address, of the client. The details that gethostbyaddr gathers about a host comes either by reading the file /etc/hosts on a Unix machine, or by consulting a Domain Name Server (DNS) database.

Next, we have another while loop inside the first while loop. In the conditional of the inner while loop, the program reads a command that comes to it from the user sitting at the client program. Depending on what the command is, it returns the appropriate answer. For the command localtime typed on the client, the server runs the date system command on the server machine and returns the string that
date returns. scalar is a unary operator that takes an expression and forces it to be interpreted in the scalar context. In this case, the date system command, run by enclosing date in backticks, returns a string containing the result of running date at the operating system level. The use of scalar here is actually useless. But, if the result returned by running a system command is a list of lines, scalar will make a string out of it.

If the command is passwd or services, the server returns the files /etc/passwd and /etc/services respectively, on the server, to the client. On a Unix machine, /etc/passwd contains brief details of accounts that users have on the system. In earlier days, this file used to contain an encrypted version of the login passwords for users on the system, but no more.
/etc/passwd
is a file readable by everyone on a Unix system, and now-a-days, the place where the encrypted password used to be contains one or more meaningless characters such as *. The encrypted passwords are now stored in the file /etc/shadow, a file that is readable only by the root user of a Unix system. The /etc/services file on a Unix system is readable by everyone. It contains a list of network facilities or services provided by a system, and the port number at which the service is available.

If the command typed in at the client and arriving at the server is who, the server runs /usr/bin/who and returns the server’s response to the client, giving the list of users logged in at the server at the time. If the client sends quit or q, the server’s control goes to the end of the inner loop which also ends the outer while loop. If a user sitting at the client types in something that the server does not understand, the server prints the list of acceptable commands on the client’s terminal. The server hangs if there are no requests arriving at it from client(s).

Let us now look at the client program that talks to the server program discussed above. The client program assumes that the server program is running on the machine www.cs.uccs.edu. This is where the server program must be running for this client-server pair to work correctly. The client program times out after 30 seconds if it does not get a response from the server.

 Program 7.32

#!/usr/bin/perl
#interactiveClient.pl
#Creates a socket, keeps it interactive so that a user types 
#commands and gets replies 
use strict;
use IO::Socket::INET;
use Net::hostent;
my ($interactiveSocket, $childProcessID);

$interactiveSocket = new IO::Socket::INET (
         PeerAddr => 'www.cs.uccs.edu',
         PeerPort => 2000,
         Proto => 'tcp',
                                  Timeout => 30
);

die "Couldn't connect: $!" unless $interactiveSocket;
my $hostInfo = gethostbyaddr ($interactiveSocket -> peeraddr);
my $hostName = $hostInfo->name();

print "[Created a TCP socket with $hostName]\n";

$childProcessID = fork ();
die "Fork failed: $!" unless defined ($childProcessID);

if ($childProcessID > 0){
    #parent
    #copy the socket to standard output. This lets us see what
    #comes back to the socket from the server
    while (defined (my $responseLine = <$interactiveSocket>)){
          print STDOUT $responseLine;  
    }

    #When the remote server closes its connection, send a TERM  signal
    #to the child process and kill it. 
    kill ("TERM", $childProcessID);
    #Eliminates the child as as soon as the server closes its end.
    #Shut the socket down for input as well as output
    shutdown ($interactiveSocket, 2);
    #exit the program normally
    exit 0;
}
else{
    #child process reads from STDIN
    #copy standard input to the socket. This lets us type a command
    #on the screen and have it written to the socket, i.e., sent to the server
    while (defined (my $command = )){
        chomp $command;
        print $interactiveSocket "$command\r\n"; #\r\n is needed
    }
}
close ($interactiveSocket);

The client program, after creating the socket, calls fork(). fork() works only on a Unix system. Following this call, the lines of code in the program are executed by two processes—the child and the parent. fork() returns 0 to the child process and the positive process ID of the child to the parent process. Thus, the if part is executed by the parent, and the else part is run by the child.

The socket $interactiveSocket was created before fork(). Therefore, there is only one socket that is accessed by the parent as well as the child process. The parent process reads the socket represented by

$interactiveSocket. The child process writes to the same socket. In other words, the socket is read as well as written into. However, the action of reading and the action of writing are divided between the two processes—the parent and the child, respectively.

The parent is the reader process. The parent executes a while loop. In the while loop, it continuously reads the socket, i.e., it reads what comes back from the server end of the socket. This is the result that the server sends back for a command typed in at the client at an earlier time. The parent process reads all the lines of response that comes back from the server.

The child process in the client program is the writer process. The child process executes another while loop inside the else block. The conditional of this while loop reads the standard input STDIN. If there is an input that can be read, i.e., if a user has typed in a command at the client’s terminal window, this command is written onto the socket $interactiveSocket. In other words, this command is sent to the
server. The server responds to this command by executing its code. What comes back from the server is read by the client in the parent process discussed earlier.

In reality, the server program does not do anything on its own unless asked by a client. Thus, although the reader (the parent) and the writer (the child) processes seem to be reading from and writing the socket in parallel, the effect of the reader-writer pair’s actions is that the user’s input at the client terminal is read first, and then the server’s results are printed next, and this continues giving a sense of synchronized sequential input and output at the client’s terminal window.

In the code that the parent process runs, we see the following statements at the very end.

 

    #When the remote server closes its connection, send a TERM signal

    #to the child process and kill it.

    kill ("TERM", $childProcessID);

    #Eliminates the child as as soon as the server closes its end.

    #Shut the socket down for input as well as output

    shutdown ($interactiveSocket, 2);

    #exit the program normally

    exit 0;

 

When processes are running on a Unix system, various signals can be sent back and forth among the processes. kill is a function that sends such a signal to a process. Although the name of the function is kill, it can send several types of signals such as TERM and KILL. Note that the kill function and the KILL signal are different things. The signals can be
given in terms of numbers or strings. The TERM signal kills a process that does not do any specialized processing of signals. It is possible for a process to have code that catches the TERM signal and does something with it. A good way to kill a process, in general, is to send a TERM signal first and then a KILL signal. The KILL signal is the 9 signal in terms of numbers. Thus,

 

    kill (9, $childProcessID);

 

is also acceptable. The KILL signal cannot be intercepted by the process receiving it. That is the KILL signal is final. Here, we send a TERM signal only because we know what the process exactly does because we wrote it ourselves.

In the parent process, we first kill the child process by sending the TERM signal. Next, we shut the socket down using the shutdown socket method. shutdown takes the socket as the first argument, and either 0, or 1, or 2, as the second argument. 0 closes a socket for reading, 1 closes a socket for writing, and 2 closes a socket for both reading and writing.

Finally, we exit the parent process with a 0 argument. This means that the parent process ends normally. Before exiting, the parent process killed the child process. As a result, both parent and child are dead, and the program’s execution is completely finished.

It is possible that sometimes when the client is still alive, we kill the server process, by say, typing an interrupt signal such as CONTROL-C. In such a case, the client also dies because the server is not sending anything to the client and as a result, the client has nothing defined to read from its end of the socket. In the client program, the parent process’s (or, the reader’s) while loop has nothing to read in such a case. The execution of the while loop of the parent is finished. This causes the
KILL signal to be sent, the socked shut down, and the parent exited.

The following discusses a simple interaction between the server program and the client program. The server is running on the machine www.cs.uccs.edu. The client can run on any machine. In this particular case, it is run once on the machine pikepeak.uccs.edu and then on the machine blanca.uccs.edu.

When the server starts, the output on its terminal looks like the following.

 

[Interactive server interactiveServer.pl accepting clients]

 

At this point, the server is waiting for input from one or more clients. Next, we start a terminal on

pikespeak.uccs.edu and start the client program. The output on the client’s terminal looks like the following.

 

[Created a TCP socket with cs.uccs.edu]

Welcome to server interactiveServer.pl on cs.uccs.edu

 type ? for command list

 

At this time, the output at the server terminal looks like the following.

 

[Interactive server interactiveServer.pl accepting clients]

[Connect from pikespeak.uccs.edu]

 

At the client, the user types in commands for the server to respond. While the first client is running on pikespeak.uccs.edu, we start a second client (i.e., the same program) on blanca.uccs.edu. The client on the second machine prints the following on the terminal at blanca.uccs.edu.

 

[Created a TCP socket with cs.uccs.edu]

Welcome to server interactiveServer.pl on cs.uccs.edu

 type ? for command list

 

But, then the second client blocks because our server can handle only one client at a time. Next, we do a few things on the client, and finally type quit at the client on pikespeak.uccs.edu. At this time, the output at the first client’s terminal looks like the following.

 

[Created a TCP socket with cs.uccs.edu]

Welcome to server interactiveServer.pl on cs.uccs.edu

 type ? for command list

?

Commands are:  passwd localtime services who quit

who

lli      pts/0    Jan 30 16:29

kalita   pts/1    Jan 29 11:08

lohmann  :0       Jan 30 09:18

lohmann  pts/2    Jan 30 09:19

lli      pts/4    Jan 30 16:34

 

Commands are:  passwd localtime services who quit

localtime

Tue Jan 30 17:00:00 MST 2001

 

Commands are:  passwd localtime services who quit

quit

 

When the user types in quit at the client on pikespeak.uccs.edu, the client on this machine dies. The output on the terminal above shows this. At this time, the output on the server at www.cs.uccs.edu looks like the following.

 

[Interactive server interactiveServer.pl accepting clients]

[Connect from pikespeak.uccs.edu]

[Disconnect with pikespeak.uccs.edu]

 

As soon as the first client dies, the server program can attend to the second client, the only interacting client. The output on the server changes to the following.

 

[Interactive server interactiveServer.pl accepting clients]

[Connect from pikespeak.uccs.edu]

[Disconnect with pikespeak.uccs.edu]

[Connect from blanca.uccs.edu]

 

This indicates that the client from blanca.uccs.edu is being serviced at this time. This second client can ask questions of the server and then finally quit. The output on the server end changes to the following.

 

[Interactive server interactiveServer.pl accepting clients]

[Connect from pikespeak.uccs.edu]

[Disconnect with pikespeak.uccs.edu]

[Connect from blanca.uccs.edu]

[Disconnect with blanca.uccs.edu]

 

The server is hanging at this point. We can either kill it by typing in an interrupt signal like CONTROL-C, or keep it hanging, waiting to hear from a third client, and so on.