We look at the client program first (Figure 19.2). It has four tasks . It accepts commands from the user and transmits them in the proper format to the chat server, and it accepts messages from the server and transforms them into human-readable output for the user .
The client uses two dispatch tables to handle user commands and server events. %COMMANDS dispatches on commands typed by the user. Each key is the text of a command (e.g., " join "), and each value is an anonymous subroutine that is invoked when the command is issued. In most cases, the subroutine simply sends the appropriate event code to the server. Whenever the user types a command, the client parses out the command and any optional arguments, and then passes the command to the dispatch table.
Lines 1 “7: Import modules The client turns on strict type checking and brings in the IO::Socket and IO::Select modules. It then brings in two application-specific modules. ChatObjects::ChatCodes contains the numeric constants for server messages, and ChatObjects::Comm defines a wrapper that packs and unpacks the messages exchanged with the server.
Lines 8 “9: Install signal handlers We want the client to log out politely even if it is killed with the interrupt key. For this reason we install INT and TERM handlers that call exit() to perform a clean shutdown. An END{} clause defined at the bottom of the script logs out of the server before the client shuts down.
We also define two globals . $nickname contains the user's nickname, and $server contains the ChatObjects::Comm wrapper.
Lines 10 “33: Define dispatch tables These lines create the %COMMANDS and %MESSAGES dispatch tables. When the main loop dispatches on a user command, it looks the command up in the %COMMAND table and calls the anonymous subroutine it finds there, passing it any text that followed the command on the line. Here is a typical %COMMANDS entry:
join => sub { $server->send_event(JOIN_REQ,shift) },
This is saying that when the user issues the /join command, the client should call the $server object's send_event() method with an event code of JOIN_REQ and whatever argument followed the command. In this case, the argument is expected to be the name of a channel to join.
A typical entry in %MESSAGES is this one:
PUBLIC_MSG() => \&public_msg,
This entry tells the script to invoke the subroutine public_msg() when the event code PUBLIC_MSG is received. The parentheses following the PUBLIC_MSG constant are necessary because otherwise Perl assumes that anything to the left of a => symbol is a string.
When the script dispatches to one of these subroutines, it passes the event code as the first argument and the message text as the second. Passing the event code allows the same subroutine to handle different messages. For example, handling of the USER_JOINS and USER_PARTS messages, which are sent to notify the client that another user has joined or departed a channel, respectively, is sufficiently similar that it is handled by the same subroutine, join_part() .
Lines 34 “37: Create the UDP socket and the server wrapper We get the server name and port number from the command line. If they are not given, we choose some defaults. This data is passed to the ChatObjects::Comm->new() method. When we address this module, we will see that its new() method is a thin wrapper that takes whatever parameters are passed to it, adds Proto => 'udp' , and passes the arguments to IO::Socket::INET->new() .
Notice that we are passing the PeerAddr argument to the IO::Socket::INET->new() , causing IO::Socket to attempt a connect() with the indicated server host. This address will be used as the destination whenever we call send() , ignoring any destination address that we provide on the argument list. Recall from Chapter 18 that the other effect of connecting a UDP socket is to filter out messages sent to the socket from arbitrary hosts . Since the client is going to exchange messages with one server, both of these behaviors are desirable.
Lines 38 “40: Log in We invoke an internal subroutine named do_login() to prompt the user to log in and send the appropriate login message to the server. If successful, this subroutine returns the user's chosen nickname.
Lines 41 “53: Dispatch Loop We'll be reading user commands from standard input and receiving messages from the server socket. select() lets us watch both handles for incoming data. We create a new IO::Select object initialized to a set containing both the server socket and STDIN . The server socket is wrapped inside the ChatObjects::Comm object, so we must retrieve the handle by calling the object's socket() method.
Each time through the loop we call $select->can_read() to recover those handles that have data to read. If one of the handles is STDIN , then we invoke the subroutine do_user() to process user commands. Otherwise, we invoke do_server() to process messages received on the socket.
Notice that the can_read() method call will indicate that STDIN is ready for reading if the user happened to close the stream by pressing the end-of-file key. do_user() specifically checks for the EOF condition and returns false. When this happens, we exit the loop, terminating the program.
Lines 54 “66: Handle user commands The do_user() subroutine reads commands from standard input and dispatches on them. Its argument is the \*STDIN glob reference returned by select() . Because of the bad interactions between select() and standard I/O buffering, we don't use the angle- bracket operator to read from STDIN . Instead, we use sysread () to fetch the longest plausible line from standard input and assume that it will correspond to a line of input. This is a valid assumption provided that the user is typing at a terminal. If we wanted to take commands from a file or pipe, we would use the IO::Getline wrapper from Chapter 13.
Each command is parsed into a command and its argument. Any command that doesn't begin with a " / " is assumed to be a public message to send to the current channel. Internally we treat this as a command named " public " and use the entire command line as its arguments.
We look up the command in the %COMMANDS dispatch table, and if it isn't found, we issue an error message. Otherwise, we invoke the returned subroutine, passing it the command arguments, if any. Most commands end up sending a message to the server by calling the global $server object's send_event() method.
Lines 67 “75: Handle server messages >The do_server() method is called to handle an incoming message from the server. The argument it receives from the select() loop is the socket handle. We don't want to work with the socket directly, so we call the static method sock2server() in the ChatObjects::Comm module in order to retrieve the corresponding ChatObjects::Comm object.
We call the ChatObjects::Comm object's recv_event() method to receive a message from the server and parse it into an event code and data. We use the code to look up a handler in the %MESSAGES dispatch table. If one is found, we invoke it. Otherwise, we print a warning. After invoking the subroutine, do_server() returns the event code as its function result. [1]
[1] It would be simpler to use the global $server object directly here, but this indirect method bears dividends in the multicast version of the chat system developed in Chapter 21.
Lines 76 “88: Log in The do_login() subroutine first sends a LOGOFF event if the $nickname global is already defined. It then prompts the user for a login name by calling the get_nickname() subroutine, and sends a LOGIN_REQ message to the server.
The subroutine now waits for a LOGIN_ACK from the server. It is possible for either the request or the acknowledgment to get lost in transit, so do_login() repeats the login several times, each time using select() with a 6-second timeout to wait for a response. If no LOGIN_ACK is received after five tries , do_login() gives up.
Lines 89 “158: Handle server events Most of the remainder of the client consists of subroutines that handle server events. Each of them parses the server event data (when need be) and prints a message for the user. A typical example is the list_channel() subroutine, which is called when the client receives a CHANNEL_ITEM message carrying information about a chat channel that the user can join. The event data in this case consists of the channel title, a count of the users subscribed to it, and a brief description of the channel's topic. The subroutine converts this information into a nicely formatted table entry and prints it to standard output.
Notice that the event code is provided as the first argument to list_channel() and similar routines. This allows some subroutines to handle similar messages, such as the join_part() subroutine, which handles both JOIN_ACK and PART_ACK messages.
Lines 159 “164: Log out and clean up Because there's no connection involved, the server can't tell that a user has gone offline unless the client explicitly tells it so. The script ends with an END{} block that is executed just before the program terminates. It sends a LOGOFF event to the server and closes the socket.
Notice that with the exception of the login message, the client in Figure 19.2 doesn't retransmit messages or explicitly wait for particular responses. Because this is an interactive application, we rely on the user to notice that the occasional command didn't "take" and reissue it. Nor do we mind if an occasional public message doesn't get through.
If necessary, we could add reliability to each outgoing message by retransmitting it until we receive an acknowledgment from the server. The do_login() subroutine illustrates a simple way to do this. Of course, this raises the risk of sending the server duplicate messages in the event that the original message got through and it was the acknowledgment that was lost in transit. However, duplicate messages don't matter to the server, because actions such as joining a channel have no ill effect if repeated.
Let's look at the ChatObjects::Comm module now (Figure 19.3). It is a wrapper around the UDP socket that provides the ability to encode and decode chat system messages.
Lines 1 “5: Bring in required modules We turn on strict type checking and bring in the Carp and IO::Socket modules. We also define a package global, %SERVERS , that will be used to do the reverse association between an IO::Socket object and the ChatObjects::Comm object that wraps it.
Lines 6 “10: Object constructor The new() method creates and initializes a new ChatObjects::Comm object. We call another method, create_socket() , to create the appropriate socket object, and wrap it in a blessed hash. Before returning the new object, we remember it in the %SERVERS global.
Line 11: The create_socket() method This method returns an appropriately initialized IO::Socket::INET object. We call IO::Socket::INET->new() with a Proto argument of "udp" and any other arguments that were passed to us.
Line 12: Look up a ChatObjects::Comm object based on its socket The sock2server() class method uses %SERVERS to look up a ChatObjects::Comm object based on its IO::Socket object.
Line 13: Look up a socket based on a ChatObjects::Comm object The socket() method does exactly the opposite , returning the IO::Socket object corresponding to a ChatObjects::Comm object.
Lines 14 “18: Close the socket The close() method closes the socket and deletes the ChatObjects::Comm object from %SERVERS .
Lines 19 “29: Send an event The client can use the send_event() method to send a command to the server, or the server can use it to send an event code to the client. It takes three arguments containing the event code, the event data, and the destination address. The subroutine invokes pack() to pack the event code and data into the binary form used by the protocol and sends it down the socket using send() . If a destination address is provided, we use the four-argument form of send() . Otherwise, we assume that the socket has had a default destination assigned using connect() , and call the three-argument form of send() . Since send() is the last call in the subroutine, its result code is implicitly returned by send_event() .
Lines 30 “36: Receive an event The recv_event() function calls recv() to retrieve an event from the server. The event is unpacked into the event code and data, and these values are returned along with the peer address.
For completeness, we show the ChatObjects::ChatCodes module in Figure 19.4. It just defines the various constant event codes used by the chat client and server.