|  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       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     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       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.    |