Detecting Dead Clients


 
Network Programming with Perl
By Lincoln  D.  Stein
Slots : 1
Table of Contents
Chapter  19.   UDP Servers

    Content

The Chat Server

The chat server is more complicated than the chat client because it must keep track of each user that logs in and each user's changing channel membership. When a user enters or leaves a channel, the server must transmit a notification to that effect to every remaining member of the channel. Likewise, when a user sends a public message while enrolled in a channel, that message must be duplicated and sent to each member of the channel in turn .

To simplify user management, we create two utility classes, ChatObjects::User and ChatObjects::Channel. A new ChatObjects::User object is created each time a user logs in to the system and destroyed when the user logs out. The class remembers the address and port number of the client's socket as well as the user's nickname, login time, and channel subscriptions. It also provides method calls for joining and departing channels, sending messages to other users, and listing users and channels. Since most of the server consists of sending the appropriate messages to users, most of the code is found in the ChatObjects::User class.

ChatObjects::Channel is a small class that keeps track of each channel. It maintains the channel's name and description, as well as the list of subscribers. The subscriber list is used in broadcasting public messages and notifying members when a user enters or leaves the channel.

The Main Server Script

Let's walk through the main body of the server first (Figure 19.5).

Figure 19.5. chat_server.pl main script

graphics/19fig05.gif

Lines 1 “8: Load modules The program begins by loading various ChatObjects modules, including ChatObjects::ChatCodes, ChatObjects::Comm, and ChatObjects::User. It also defines a DEBUG constant that can be set to a true value to turn on debug messages.

Lines 9 “14: Define channels We now create five channels by invoking the ChatObjects::Channel->new() method. The method takes two arguments corresponding to the channel title and description.

Lines 15 “24: Create the dispatch table We define a dispatch table, named %DISPATCH , similar to the ones used in the client application. Each key in the table is a numeric event code, and each value is the name of a ChatObject::User method. With the exception of the initial login, all interaction with the remote user goes through a ChatObjects::User object, so it makes sense to dispatch to method calls rather than to anonymous subroutines, as we did in the client.

Here is a typical entry in the dispatch table:

 SEND_PUBLIC() => 'send_public', 

This is interpreted to mean that whenever a client sends us a SEND_PUBLIC message, we will call the corresponding ChatObject::User object's send_public() method.

Lines 25 “28: Create a new ChatObjects:: Comm object We get the port from the command line and use it to initialize a new ChatObjects::Comm object with the arguments LocalPort=>$port . Internally this creates a UDP protocol IO::Socket object bound to the desired port. Unlike in the client code, in the server we do not specify a peer host or port to connect with, because this would disable our ability to receive messages from multiple hosts .

Lines 29 “32: Process incoming messages, handle login requests The main server loop calls the ChatObject::Server object's recv_event() repeatedly. This method calls recv() on the underlying socket, parses the message, and returns the event code, the event message, and the packed address of the client that sent the message.

Login requests receive special treatment because there isn't yet a ChatObjects::User object associated with the client's address. If the event code is LOGIN_REQ , then we pass the address, the event text, and our ChatObjects::Comm object to a do_login() subroutine. It will create a new ChatObjects::User object and send the client a LOGIN_ACK .

Lines 33 “35: Look up the user Any other event code must be from a user who has logged in earlier. We call the class method ChatObjects::User->lookup_byaddr() to find a ChatObjects::User object that is associated with the client's address. If there isn't one, it means that the client hasn't logged in, and we issue an error message by sending an event of type ERROR .

Lines 36 “39: Handle event If we were successful in identifying the user corresponding to the client address, we look up the event code in the dispatch table and treat it as a method call on the user object. The event data, if any, is passed to the method to deal with as appropriate. If the event code is unrecognized, we complain by issuing an ERROR event. In either case, we're finished processing the transaction, so we loop back and wait for another incoming request.

Lines 40 “45: Handle logins The do_login() subroutine is called to handle new user registration. It receives the peer's packed address, the ChatObjects::Comm object, and the LOGIN_REQ event data, which contains the nickname that the user desires to register under.

It is certainly possible for two users to request the same nickname. We check for this eventuality by calling the ChatObjects::User class method lookup_byname() . If there is already a user registered under this name, then we issue an error. Otherwise, we invoke ChatObjects::User->new() to create a new user object.

The ChatObjects::User Class

Most of the server application logic is contained in the ChatObjects::User module (Figure 19.6). This object mediates all events transmitted to a particular user and keeps track of the set of channels in which a user is enrolled.

Figure 19.6. The ChatObjects::User Module

graphics/19fig06.gif

The set of enrolled channels is implemented as an array. Although the user may belong to multiple channels, one of those channels is special because it receives all public messages that the user sends out. In this implementation, the current channel is the first element in the array; it is always the channel that the user subscribed to most recently.

Lines 1 “4: Bring in required modules The module turns on strict type checking and brings in the ChatObjects::ChatCodes and Socket modules.

Lines 5 “6: Overload the quote operator One of Perl's nicer features is the ability to overload certain operators so that a method call is invoked automatically. In the case of the ChatObjects::User class, it would be nice if the object were replaced with the user's nickname whenever the object is used in a string context. This would allow the string " Your name is $user " to interpolate automatically to " Your name is rufus " rather than to " Your name is ChatObjects::User=HASH(0x82b81b0). "

We use the overload pragma to implement this feature, telling Perl to interpolate the object into double-quoted strings by calling its nickname() method and to fall back to the default behavior for all other operators.

Lines 7 “9: Set up package globals The module needs to look up registered users in two ways: by their nicknames and by the addresses of their clients . Two in-memory globals keep track of users. The %NICKNAMES hash indexes the user objects by the users' nicknames. %ADDRESSES , in contrast, indexes the objects by the packed addresses of their clients. Initially these hashes are empty.

Lines 10 “22: The new() method The new() method creates new ChatObjects::User objects. It is passed three arguments: the packed address of the user's client, the user's nickname, and a ChatObjects::Comm object to use in sending messages to the user. We store these attributes into a blessed hash, along with a record of the user's login time and an empty anonymous array. This array will eventually contain the list of channels that the user belongs to.

Having created the object, we invoke the server object's send_event() method to return a LOGIN_ACK message to the user, being sure to use the three-argument form of send_event() so that the message goes to the correct client. We then stash the new object into the %NICKNAMES and %ADDRESSES hashes and return the object to the caller.

There turns out to be a slight trick required to make the %ADDRESSES hash work properly. Occasionally Perl's recv() call returns a packed socket address that contains extraneous junk in the unused fields of the underlying C data structure. This junk is ignored by the send() call and is discarded when sockaddr_in() is used to unpack the address into its port and IP address components .

The problem arises when comparing two addresses returned by recv() for equality, because differences in the junk data may cause the addresses to appear to be different, when in fact they share the same port numbers and IP addresses. To avoid this issue, we call a utility subroutine named key() , which turns the packed address into a reliable key containing the port number and IP address.

Lines 23 “32: Look up objects by name and address The lookup_byname() and lookup_byaddr() methods are class methods that are called to retrieve ChatObjects::User objects based on the nickname of the user and her client's address, respectively. These methods work by indexing into %NICKNAMES and %ADDRESSES . For the reasons already explained, we must pass the packed address to key() in order to turn it into a reliable value that can be used for indexing. The users() method returns a list of all currently logged-in users.

Lines 33 “38: Various accessors The next block of code provides access to user data. The address() , nickname() , timeon() , and channels() methods return the user's address, nickname, login time, and channel set. current_channel() returns the channel that the user subscribed to most recently.

Lines 39 “43: Send an event to the user The ChatObjects::User send() method is a convenience method that accepts an event code and the event data and passes that to the ChatObject::Server object's send_event() method. The third argument to send_event() is the user's stored address to be used as the destination for the datagram that carries the event.

Lines 44 “50: Handle user logout When the user logs out, the logout() method is invoked. This method removes the user from all subscribed channels and then deletes the object from the %NICKNAMES and %ADDRESSES hashes. These actions remove all memory references to the object and cause Perl to destroy the object and reclaim its space.

Lines 51 “65: The join() method The join() method is invoked when the user has requested to join a channel. It is passed the title of the channel.

The join() method begins by looking up the selected channel object using the ChatObjects::Channel lookup() method. If no channel with the indicated name is identified, we issue an error event by calling our send() method. Otherwise, we call our channels() method to retrieve the current list of channels that the user is enrolled in. If we are not already enrolled in the channel, we call the channel object's add() method to notify other users that we are joining the channel. If we already belong to the channel, we delete it from its current position in the channels array so that it will be moved to the top of the list in the next part of the code. We make the channel object current by making it the first element of the channels array, and send the client a JOIN_ACK event.

Lines 66 “80: The part() method The part() method is called when a user is departing a channel; it is similar to join() in structure and calling conventions.

If the user indeed belongs to the selected channel, we call the corresponding channel object's remove() method to notify other users that the user is leaving. We then remove the channel from the channels array and send the user a PART_ACK event. The removed channel may have been the current channel, in which case we issue a JOIN_ACK for the new current channel, if any.

Lines 81 “89: Send a public message The send_public() method handles the PUBLIC_MSG event. It takes a line of text, looks up the current channel, and calls the channel's message() method. If there is no current channel, indicating that the user is not enrolled in any channel, then we return an error message.

Lines 90 “101: Send a private message The send_private() method handles a request to send a private message to a user. We receive the data from a PRIVATE_MSG event and parse it into the recipient's nickname and the message text. We then call our lookup_byname() method to turn the nickname into a user object. If no one by that name is registered, we issue an error message. Otherwise, we call the user object's send() method to transmit a PRIVATE_MSG event directly to the user.

This method takes advantage of the fact that user objects call nickname() automatically when interpolated into strings. This is the result of overloading the double-quote operator at the beginning of the module.

Lines 102 “111: List users enrolled in the current channel The list_users() method generates and transmits a series of USER_ITEM events to the client. Each event contains information about users enrolled in the current channel (including the present user).

We begin by recovering the current channel. If none is defined (because the user is enrolled in no channels at all), we send an ERROR event. Otherwise, we retrieve all the users on the current channel by calling its users() method, and transmit a USER_ITEM event containing the user nickname, the length of time the user has been registered with the system (measured in seconds), and a space-delimited list of the channels the user is enrolled in.

Like the user class, ChatObjects::Channel overloads the double-quoted operator so that its title() method is called when the object is interpolated into double-quoted strings. This allows us to use the object reference directly in the data passed to send() .

Lines 112 “115: Listchannels list_channels() returns a list of the available channels by sending the user a series of CHANNEL_ITEM events. It calls the ChatObjects::Channel class's channels() method to retrieve the list of all channels, and incorporates each channel into a CHANNEL_ITEM event. The event contains the information returned by the channel objects' info () method. In the current implementation, this consists of the channel title, the number of enrolled users, and the human-readable description of the channel.

Line 116 “118: Turn a packed client address into a hash key As previously explained, the system recv() call can return random junk in the unused parts of the socket address structure, complicating the comparison of client addresses. The key() method normalizes the address into a string suitable for use as a hash key by unpacking the address with sockaddr_in() and then rejoining the host address and port with a " : " character. Two packets sent from the same host and socket will have identical keys.

Because we have a method named join() , we must qualify the built-in function of the same name as CORE::join() in order to avoid the ambiguity.

The ChatObjects::Channel Class

Last, we look at the ChatObjects::Channel class (Figure 19.7). The most important function of this class is to broadcast messages to all current members of the channel whenever a member joins, leaves, or sends a public message. The class does this by iterating across each currently enrolled user, invoking their send() methods to transmit the appropriate event.

Figure 19.7. The ChatObjects::Channel class

graphics/19fig07.gif

Lines 1 “3: Bring in modules The module begins by loading the ChatObjects::User and ChatObjects::ChatCodes modules.

Lines 4 “7: Overload double-quoted string operator As in ChatObjects::User, we want to be able to interpolate channel objects directly into strings. We overload the double-quoted string operator so that it invokes the object's title() method, and tell Perl to fall back to the default behavior for other operators.

At this point we also define a package global named %CHANNELS . It will hold the definitive list of channel objects indexed by title for later lookup operations.

Lines 8 “16: Object constructor The new() class method is called to create a new instance of the ChannelObjects::Channel class. We take the title and description for the new channel and incorporate them into a blessed hash, along with an empty anonymous hash that will eventually contain the list of users enrolled in the channel. We stash the new object in the %CHANNELS hash and return it.

Lines 17 “22: Look up a channel by title The lookup() method returns the ChatObjects::Channel object that has the indicated title. We retrieve the title from the subroutine argument array and use it to index into the %CHANNELS array. The channels() method fetches all the channel titles by returning the keys of the %CHANNELS hash.

Lines 23 “25: Various accessors The title() and description() methods return the channel's title and description, respectively. The users() method returns a list of all users enrolled in the channel. The keys of the users hash are users' nicknames, and its values are the corresponding ChatObjects::User objects.

Lines 26 “30: Return information for the CHANNEL_ITEM event The info() method provides data to be incorporated into the CHANNEL_ITEM event. In the current version of ChatObjects::Channel, info() returns a space-delimited string containing the channel title, the number of users currently enrolled, and the description of the channel. In the next chapter we will override info() to return a multicast address for the channel as well.

Lines 31 “35: Send an event to all enrolled users The send_to_all() method is the crux of the whole application. Given an event code and the data associated with it, this method sends the event to all enrolled users. We do this by calling users() to get the up-to-date list of ChatObject::User objects and sending the event code and data to each one via its send() method. This results in one datagram being sent for each enrolled user, with no issues of blocking or concurrency control.

Lines 36 “42: Enroll a user The add() method is called when a user wishes to join a channel. We first check that the user is not already a member, in which case we do nothing. Otherwise, we use the send_to_all() method to send a USER_JOINS event to each member and add the new user to the users hash.

Lines 43 “49: Remove a user The remove() method is called to remove a user from the channel. We check that the user is indeed a member of the channel, delete the user from the users hash, and then send a USER_PARTS message to all the remaining enrollees.

Lines 50 “55: Send a public message The message() method is called when a user sends a public message. We are called with the name of the user who is sending the message and retransmit the message to each of the members of the group (including the sender) with the send_to_all() method.

Notice that the server makes no attempt to verify that each user receives the events it transmits. This is typical of a UDP server, and appropriate for an application like this one, which doesn't require 100 percent precision.


   
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