A Multiplexed Psychiatrist Server


 
Network Programming with Perl
By Lincoln  D.  Stein
Slots : 1
Table of Contents
Chapter  12.   Multiplexed Applications

    Content

A Multiplexed Psychiatrist Server

In this section we develop a version of the Chatbot::Eliza server that uses multiplexing. It illustrates how a typical multiplexed server works. The basic strategy for a multiplexed server follows this general outline:

  1. Create a listen socket.

  2. Create an IO::Select set and add the listen socket to the list of sockets to be monitored for reading.

  3. Enter a select() loop.

  4. When the select() loop returns, examine the list of sockets ready for reading. If the listen socket is among them, call accept() and add the resulting connected socket to the IO::Select set.

  5. If other sockets are ready for reading, perform I/O on them.

  6. As client connections finish, remove them from the IO::Select set.

This version of the Eliza server illustrates how this works in practice.

The Main Server Program

The new server is called eliza_select.pl . It is broken into two parts , the main part, shown in Figure 12.2, and a module that subclasses Chatbot::Eliza called Chatbot::Eliza::Polite.

Figure 12.2. Multiplexed psychiatrist server

graphics/12fig02.gif

Compared to the previous versions of this server, the major design change is the need to break up the Chatbot::Eliza object's command_interface() method. The reason is that command_interface() has its own I/O loop, which doesn't relinquish connection until the conversation with the client is done. We can't allow this, because it would lock out other clients .

Instead, we again subclass Chatbot::Eliza to create a "polite" version, which adds three new methods named welcome() , one_line() , and done() . The first method returns a string containing the greeting that the user sees when he or she first connects. The second takes a line of user input, transforms it, and returns the string containing the psychiatrist's response, along with a new prompt for the user . done() returns a true value if the user's previous line consisted of one of the quit phrases such as "bye," " goodbye ," or "exit."

Another change is necessary to keep track of multiple Chatbot::Eliza instances. Because each object maintains an internal record of the user's utterances, we have to associate each connected socket with a unique Eliza object. We do this by creating a global hash named %SESSIONS in which the indexes are the socket objects and the values are the associated Chatbot::Eliza objects. When can_read() returns a socket that is ready for I/O, we use %SESSIONS to look up the corresponding Chatbot::Eliza object.

We'll walk through the main part first.

Lines 1 “6: Load modules We bring in IO::Socket, IO::Select, and Chatbot::Eliza::Polite. We also declare the %SESSIONS hash for mapping IO::Socket instances to Chatbot objects.

Lines 7 “12: Create listen socket We create a listen socket on our default port.

Lines 13 “15: Add listen socket to IO:: Select object We create a new IO::Select object and add the listen socket to it.

Lines 16 “18: Main select() loop We now enter the main loop. Each time through the loop, we call the IO::Select object's can_read() method. This blocks indefinitely until the listen socket becomes ready for accept() or a connected socket (none of which have yet been added) becomes ready for reading.

Line 18: Loop through ready handles When can_read() returns, its result is a list of handles that are ready for reading. It's now our job to loop through this list and figure out what to do with each one.

Lines 19 “24: Handle the listen socket If the handle is the listen socket, then we call its accept() method, returning a new connected socket. We create a new Chatbot::Eliza::Polite object to handle the connection and add the socket and the Chatbot object to the %SESSIONS hash. By indexing the hash with the unique name of the socket object, we can recover the corresponding Chatbot object whenever we need to do I/O on that particular socket.

After creating the Chatbot object, we invoke its welcome() method. This returns a welcome message that we syswrite() to the newly connected client. After this is done, we add the connected socket to the IO::Select object by calling IO::Select->add() . The connected socket will now be monitored for incoming data the next time through the loop.

Lines 25 “27: Handle I/O on connected socket If a handle is ready for reading, but it is not the listen socket, then it must be a connected socket accepted during a previous iteration of the loop. We recover the corresponding Chatbot object from the %SESSIONS hash. If the lookup is unsuccessful (which logically shouldn't happen), we just ignore the socket and go on to the next ready socket.

Otherwise, we want to read a line of input from the client. Reading a line of input from the user is actually a bit of a nuisance because Perl's line-oriented reads, including the socket's getline() method, use stdio buffering and are thus incompatible with calls to select() .

We'll see in the next chapter how to roll our own line-reading function that is compatible with select() , but in this case we punt on the issue by doing a byte-oriented sysread () of up to 1,024 characters and treating that as if it were a full line. This is usually the case if the user is interactively typing messages, so it's good enough for this server.

Lines 28 “32: Send response to client sysread() returns either the number of bytes read, or on end of file. Because the call is unbuffered, the number of bytes returned may be greater than but less than the number we requested .

If $bytes is positive, then we have data to process. We clean the data up and pass it to the Eliza object's one_line() method, which takes a line of user input and returns a response. We call syswrite() to send this response back to the client. If $bytes is or undef , then we treat it as an end of file and allow the next section of code to close the session.

Lines 33 “38: Handle session termination The last part of the loop is responsible for closing down sessions.

A session should be closed when either of two things occur. First, a result code of 0 from sysread() signifies that the client has closed its end of the connection. Second, the user may enter one of several termination phrases that Eliza recognizes, such as "bye," "quit," or "goodbye." In this case, Eliza's done() method returns true.

We check for both eventualities. In either case, we remove the socket from the list of handles being monitored by the IO::Select object, close it, and remove it from the %SESSIONS hash.

Note that we treat a return code of undef from sysread() , which indicates an I/O error of some sort , in the same way as an end of file. This is often sufficient, but a server that was processing mission-critical data would want to distinguish between a client that deliberately shut down the connection and an error. In this case, you could pass $bytes to defined() to distinguish the two possibilities.

The Eliza::Chatbot::Polite Module

Figure 12.3 shows the code for Eliza::Chatbot::Polite. Like the earlier modification for threads, this module was created by cutting and pasting from the original Eliza::Chatbot source code.

Figure 12.3. Multiplexed psychiatrist server

graphics/12fig03.gif

Lines 1 “3: Module setup We load the Chatbot::Eliza module and declare the current package to be its subclass by placing the name of the parent in the @ISA array.

Lines 4 “14: The welcome() method The welcome() method is copied from the top part of the old command_interface() method. It sets the two prompts (the one printed before the psychiatrist's utterances and the one printed in front of the user's inputs) to reasonable defaults and then returns a greeting randomly selected from an internally defined list. The user prompt is appended to this string.

Lines 15 “34: The one_line() method The one_line() method takes a string as input and returns a response. We start by checking the user input for one of the quit phrases. If there is a quit phrase, then we generate an exiting remark from a random list of final phrases, set an internal flag that the user is done, and return the reply. Otherwise, we invoke our inherited transform() method to turn the user's input into a suitably cryptic utterance and return the response along with the next prompt.

Lines 35 “36: The done() method In this method we simply check the internal exit flag set in one_line() and return true if the user wants to exit.

Problems with the Psychiatrist Server

You can run this version of the server, telnet to it (or use one of the gab clients developed in this or previous chapters), and have a nice conversation. While one session is open, you can open new sessions and verify that they correctly maintain the thread of the conversation.

Unfortunately, this server is not quite correct. It starts to display problems as soon as you try to use it noninteractively, for example, by running one of the clients in batch mode with standard input redirected from a file. The responses may appear garbled or contain embedded newlines. The reason for this is that we incorrectly assume that sysread() will return a full line of text each time it's called. In fact, sysread() is not line oriented and just happens to behave that way when used with a client that transmits data in line-length chunks . If the client is not behaving this way, then sysread() may return a small chunk of data that's shorter than a line or may return data that contains multiple newlines.

An obvious solution, since we must avoid the <> operator, is to write our own readline() routine. When called, it buffers calls to sysread() , returning only the section of the buffer up to the first newline. If a newline isn't seen at first, readline() calls sysread() as many times as needed.

However, this solution just moves the problem about because only the first call to sysread() is guaranteed not to block. A poorly written or malicious client could hang the entire server by sending a single byte of data that wasn't a newline. Our readline() routine would read the byte, call sysread() again in an attempt to get to the newline, and block indefinitely. We could work around this by calling select() again within readline() or by putting a timeout on the read using the idiom described in Chapter 2's section Timing Out Slow System Calls.

However, there's a related problem in the multiplexed server that's not so easily fixed. What happens if a client connects to the server, sends us a lot of data, but never reads what we send back? Eventually the socket buffer at the client's side of the connection fills up, causing further TCP transmissions to stall. This in turn propagates back to us via TCP's flow control and eventually causes our server to block in syswrite() . This makes all current sessions hang, and prevents the server from accepting incoming connections.

To solve these problems we must use nonblocking I/O. The next chapter presents two useful modules. One, called IO::Getline, provides a wrapper around filehandles that allows you to perform line-oriented reads safely in conjunction with select() . The other, IO::SessionSet, adds the ability to buffer partial writes and solves the problem of the server stalling on a blocked write.

Win32 Issues

Another problem arises when using multiplexed applications on Microsoft Windows platforms. When I originally developed the scripts in these chapters, I tested them on Windows98 using ActiveState Perl, and everything seemed to work fine. Later, however, I discovered that the gab5.pl script (Figure 12.1) was consuming a large amount of CPU time, even when it was apparently doing nothing but waiting for keyboard input.

When I tracked down this problem, I learned that the Win32 port of Perl does not support select() on non-socket filehandles, including STDIN and STDOUT. So client-side scripts that multiplex across STDIN do not wait for the filehandle to be ready, but just loop. This problem affects the scripts in Figure 13.1 and 13.7. The IO: Poll call is affected as well (Chapter 16), and Figure 16.1 will exhibit the same problem.

Multiplexing across sockets works just fine on Win32 platforms, and so all the server examples work as expected. The Macintosh port of Perl has no problem with select() .


   
Top


Network Programming with Perl
Network Programming with Perl
ISBN: 0201615711
EAN: 2147483647
Year: 2000
Pages: 173

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net