ЭЛЕКТРОННАЯ БИБЛИОТЕКА КОАПП
Сборники Художественной, Технической, Справочной, Английской, Нормативной, Исторической, и др. литературы.



12.3 Handling Multiple Clients

The fact that accept, read, and sysread are blocking calls has more implications for the server.[1] A single-threaded process can invoke only one of these calls at a time, which is fine if there aren't too many clients clamoring for the server's attention and if no client ties the server up for too long a time. The real world is ugly, and you have to resolve this issue. There are three ways of doing this:

  1. Create multiple threads of control (processes or threads) and have each call block in its own thread.

  2. Make these calls only when you are absolutely sure they won't block. We'll call this the "select" approach, because we use the select call to ensure that a socket has something to offer.

  3. Make these calls nonblocking using fcntl or ioctl.

[1] accept blocks until someone tries to connect.

As we shall see, option 2 should be used in conjunction with option 3 in production systems. In all cases, the client code remains unaffected while we try out these options.

Incidentally, there is a fourth option. Some systems support an asynchronous I/O notification: a SIGIO signal is sent to the process if a specified socket is ready to do I/O. We will not pay attention to this approach because there is no way for a signal handler to know which socket is ready for reading or writing.

12.3.1 Multiple Threads of Execution

Perl doesn't have threads yet (at least not officially[2]), but on Unix and similarly empowered systems, it supports fork, the way to get process-level parallelism. The server process acts as a full-time receptionist: it blocks on accept, and when a connection request comes in, it spawns a child process and goes back to accept. The newly created child process meanwhile has a copy of its parent's environment and shares all open file descriptors. Hence it is able to read from, and write to, the new socket returned by accept. When the child is done with the conversation, it simply exits. Each process is therefore dedicated to its own task and doesn't interfere with the other. The following code shows an example of a forking server:

[2] Malcolm Beattie has a working prototype of a threaded Perl interpreter, which will be incorporated into the mainstream in the Perl 5.005 release.

# Forking server
use IO::Socket;
$SIG{CHLD} = sub {wait ()};
$main_sock = new IO::Socket::INET (LocalHost => 'goldengate',
                                   LocalPort => 1200,
                                   Listen    => 5,
                                   Proto     => 'tcp',
                                   Reuse     => 1,
                                  );
die "Socket could not be created. Reason: $!\n" unless ($sock);
while ($new_sock = $main_sock->accept()) {
    $pid = fork();
    die "Cannot fork: $!" unless defined($pid);
    if ($pid == 0) { 
        # Child process
        while (defined ($buf = <$new_sock>)) {
           # do something with $buf ....
           print $new_sock "You said: $buf\n";
        }
        exit(0);   # Child process exits when it is done.
    } # else 'tis the parent process, which goes back to accept()
}
close ($main_sock);

The fork call results in two identical processes - the parent and child - starting from the statement following the fork. The parent gets a positive return value, the process ID ($pid) of the child process. Both processes check this return value and execute their own logic; the main process goes back to accept, and the child process reads a line from the socket and echoes it back to the client.

Incidentally, the CHLD signal has nothing to do with IPC per se. On Unix, when a child process exits (or terminates abnormally), the system gets rid of the memory, files, and other resources associated with it. But it retains a small amount of information (the exit status if the child was able to execute exit(), or a termination status otherwise), just in case the parent uses wait or waitpid to enquire about this status. The terminated child process is also known as a zombie process, and it is always a good thing to remove it using wait; otherwise, the process tables keep filling up with junk. In the preceding code, wait doesn't block, because it is called only when we know for sure that a child process has died - the CHLD signal arranges that for us. Be sure to read the online documentation for quirks associated with signals in general and SIGCHLD in particular.

12.3.2 Multiplexing Using select

The reason we forked off a different process in the preceding section was to avoid blocking during accept, read, or write for fear of missing out on what is happening on the other sockets. We can instead use the select call, introduced in BSD Unix, that returns control when a socket (any filehandle, in fact) can be read from or written to. This approach allows us to use a single-threaded process - somewhat akin to firing the receptionist and handling all the incoming calls and conversations ourselves.

The interface to the native select call is not very pretty, so we use the IO::Select wrapper module instead. Consider

use IO::Socket;
use IO::Select; 
$sock1 = new IO::Socket (....);
$sock2 = new IO::Socket (....);
$read_set = new IO::Select;
$read_set->add($sock1);
$write_set = new IO::Select;
$write_set->add($sock1, $sock2);

The IO::Select module's new method creates an object representing a collection of filehandles, and add and remove modify this set. The select method (which calls Perl's native select function) accepts three sets of filehandles, or IO::Select objects, which are monitored for readability, writability, and error conditions, respectively. In the preceding snippet of code, we create two such sets - a filehandle can be added to any or all of these sets if you so wish - and supply them to the select method as follows:

($r_ready, $w_ready, $error) = 
      IO::Select->select($read_set, $write_set, $error_set, $timeout);

select blocks until an interesting event occurs (one or more filehandles are ready for reading, writing, or reporting an error condition) or the time-out interval has elapsed. At this point, it creates three separate lists of ready filehandles and returns references to them. The time-out is in seconds but can be expressed as a floating-point number to get millisecond resolution.

Let us use this information to implement a program that retrieves messages from one or more clients:

# Create main socket ($main_socket) as before ...
# ....

use IO::Select;
$readable_handles = new IO::Select();
$readable_handles->add($main_socket);
while (1) {  #Infinite loop
    # select() blocks until a socket is ready to be read or written
    ($new_readable) = IO::Select->select($readable_handles,
                                         undef, undef, 0);
    # If it comes here, there is at least one handle
    # to read from or write to. For the moment, worry only about 
    # the read side.
    foreach $sock (@$new_readable) {
        if ($sock == $main_socket) {
            $new_sock = $sock->accept();
            # Add it to the list, and go back to select because the 
            # new socket may not be readable yet.
            $readable_handles->add($new_sock);
        } else {
            # It is an ordinary client socket, ready for reading.
            $buf = <$sock>;
            if ($buf) {
                # .... Do stuff with $buf
            } else {
                # Client closed socket. We do the same here, and remove
                # it from the readable_handles list
                $readable_handles->remove($sock);
                close($sock);
            }
        }
    }   
}

We create a listening socket, $main_socket, and configure it to listen on a well-known port. We then add this socket to a newly created IO::Select collection object. When select returns the first time, $main_socket has something to read from (or has an error, a possibility that we ignore for the moment); in other words, it has received a connection request and is guaranteed not to block when accept is called. Now, we are not interested in being blocked if the socket returned from accept has nothing to say, so we add it to the list of filehandles being monitored for readability. When select returns the next time, we know that one of the two sockets is ready for reading (or both are ready). If $main_socket is ready, we repeat the exercise above. If not, we have a socket with something to read.

select also returns if one or more remote sockets are closed. The corresponding sockets on the listening end return 0 when any of the I/O operators are used (0 bytes read or written). The server above removes these sockets from the IO::Select collections to prevent from select returning the same defunct sockets every time.

12.3.2.1 Blocking looms again

All we have done in this section is depend on select to tell us that a filehandle is ready for reading or writing before actually attempting to read or write from it. Unfortunately, we still don't know how much data has accumulated in the I/O buffers (for purposes of reading) or how much can be written to it (the other side may be reading slowly, and there's a limit to how much you can pump in from this side). Both sysread and syswrite return the number of bytes actually read or written, so you would have to invoke them in a loop until the entire message is read or written. Once you have drained the buffers (or filled them, as the case may be), there is the very real possibility that it might block the next time you attempt to read or write if the other side doesn't do something quick. One option is to invoke select in every iteration of the loop and proceed only if it confirms the socket's availability. This slows you down when the filehandle can accommodate your read or write requests. Besides, you have to quit the loop anyway when select tells you that a filehandle isn't ready and make the attempt later on when the file descriptor changes state.

For single-threaded programs the next option is to make the filehandle non-blocking. Read on.

12.3.3 Nonblocking Filehandles

Any filehandle can be made nonblocking by using the operating-system-specific fcntl or ioctl call, like this:

use POSIX;
fcntl($sock, F_SETFL(), O_NONBLOCK());

The Fcntl module (file control) makes the constants in the fcntl.h file available as functions. The fcntl function takes a command like F_SETFL ("set flag") and an argument that is specific to that command. Depending on the operating system, the flag to set nonblocking I/O may also be known as O_NDELAY or FNDELAY.

In any case, once this operation is carried out, sysread and syswrite return undef (not 0) and set $! to EAGAIN (or EWOULDBLOCK on BSD 4.3) if they cannot carry out the operation right away. The following code accounts for these return and error values when reading a socket:

# Want to read 1024 bytes
$bytes_to_read = 1024; $msg = '';
while ($bytes_to_read) {
    $bytes_read = sysread($sock, $buf, $bytes_to_read);
    if (defined($bytes_read)) {
        if ($bytes_read == 0) {
            # Remote socket closed connection
            close($sock);
            last;
        } else { 
            $msg .= $buf;
            $bytes_to_read -= $bytes_read;
        }
    } else {
        if ($! == EAGAIN()) {
            # Can return to select. Here we choose to 
            # spin around waiting for something to read.
        } else {
            last;
        }
    }
}

One simple option is to forget the select call and simply spin around in a polling loop, calling read (or sysread) on each socket (or accept on the main socket) to check whether it has anything to say, or calling write (or syswrite) if we have something to say, without fear that it would block. This approach is a constant drain on the CPU because the process is never idle. You should always strive to build a quiescent [3] server, in client/server parlance.

[3] "Marked by inactivity or repose," as Webster's Tenth Collegiate Dictionary puts it.

You might have noticed that we have ignored clients in all these discussion. If a client is willing to block, there is no issue at all, since, unlike the server side, it is not talking to more than one entity. But if it contains a GUI, it clearly cannot afford to block, and we have much the same problem. We will revisit this issue in Section 14.1, "Introduction to GUIs, Tk, and Perl/Tk". In a system in which there is no clear delineation between "clients" and "servers" - a cluster of bank computers is an example of such a peer-to-peer system - every process is modeled on the more general server framework described in the preceding pages.

You can now see that all three approaches to creating servers have their individual quirks and failings. The next section introduces us to techniques and strategies used in typical production servers.