Sample Multicast Applications We'll look at two example multicast applications. One is a simple time-of-day server, which intermittently broadcasts the current time to whoever is interested. The other is a reworking of Chapter 19's chat system. Time-of-Day Multicasting Server The first example application is a server that intermittently transmits its hostname and the time of day to a predetermined port and multicast address. Client applications that wish to receive these time-of-day messages join the group and echo what they receive to standard output. You might use something like this to monitor the status of your organization's servers; if a server stops sending status messages, it might be an early warning that it had gone offline. Thanks to the IO::Socket::Multicast module, both client and server applications are less than 25 lines of code. We'll look at the server first (Figure 21.4). Figure 21.4. Multicast time-of-day server Lines 1 “4: Load modules We load the IO::Socket and IO::Socket::Multicast modules. We also bring in the Sys::Hostname module, a standard part of the Perl distribution that allows you to determine the hostname in a OS-independent way. Lines 5 “8: Get arguments We choose an interval of 15 seconds between transmissions. We then read the port, multicast group address, and the TTL for transmissions from the command line; if they're not defined, we assume reasonable defaults. For the port, we arbitrarily choose 2070. For the multicast group, we choose 224.225.226.227, one of the many unassigned groups. For TTL, we choose 31, which, by convention, is an organization-wide scope (messages will stay within the organization but will not be forwarded to the outside world). Lines 9 “12: Set up socket We create a new multicasting UDP socket by calling IO::Socket::Multicast->new() and set the multicast TTL for outgoing messages by calling the socket's mcast_ttl() method. Lines 13 “16: Prepare to transmit messages We create a packed destination address using inet_aton() and sockaddr_in(>) , using the multicast address and port specified on the command line. We also retrieve the name of the host and store it in a variable for later use. Lines 17 “24: Main loop The server now enters its main loop. We want to transmit on even multiples of PERIOD seconds, so we use the % operator to compute the modulus of time() over PERIOD . If we are at an even multiple of PERIOD , then we create a status message consisting of the local time followed by a slash and the hostname, producing this type of format: Mon May 29 19:05:15 2000/pesto.cshl.org We send a copy of the message to the socket using send() with the multicast destination set up previously. After transmitting the message, we sleep for 1 second and loop again. Time-of-Day Multicast Client We'll now look at a client that can receive messages from the server (Figure 21.5). Figure 21.5. Time-of-day multicast client Lines 1 “3: Load modules We bring in IO::Socket and IO::Socket::Multicast modules as before. Lines 4 “5: Retrieve command-line arguments We fetch the port and multicast address from the command line. If these arguments are not provided, we default to the values used by the server. Lines 7 “10: Set up socket Next we set up the socket we'll use for receiving multicast messages. We create a UDP socket using IO::Socket::Multicast->new , passing the LocalPort argument to bind() the socket to the desired port. The newly created socket is now ready to receive unicast messages directed to that port, but not multicasts. To enable reception of group messages, we call mcast_add() with the specified multicast group address. Lines 11 “16: Client main loop The remainder of the client is a simple loop that calls recv() to receive messages on the socket. We unpack the sender's address using sockaddr_in() and print the address and the message body to standard output. To test the client, I ran the server on several machines on my LAN, and the client on my desktop system. The client's output over a period of 45 seconds was this (blank lines have been inserted between intervals to aid readability): % time_of_day_cli.pl 143.48.31.66: Wed Aug 23 13:31:00 2000/swiss 143.48.31.45: Wed Aug 23 13:31:00 2000/feta.cshl.org 143.48.31.54: Wed Aug 23 10:31:00 2000/pesto 143.48.31.47: Wed Aug 23 13:31:00 2000/turunmaa.cshl.org 143.48.31.43: Wed Aug 23 13:31:00 2000/romano.cshl.org 143.48.31.69: Wed Aug 23 13:31:00 2000/munster.cshl.org 143.48.31.63: Wed Aug 23 13:31:00 2000/whey.cshl.org 143.48.31.66: Wed Aug 23 13:31:15 2000/swiss 143.48.31.69: Wed Aug 23 13:31:15 2000/munster.cshl.org 143.48.31.63: Wed Aug 23 13:31:15 2000/whey.cshl.org 143.48.31.44: Wed Aug 23 13:31:15 2000/edam.cshl.org 143.48.31.45: Wed Aug 23 13:31:15 2000/feta.cshl.org 143.48.31.54: Wed Aug 23 10:31:15 2000/pesto 143.48.31.47: Wed Aug 23 13:31:15 2000/turunmaa.cshl.org 143.48.31.43: Wed Aug 23 13:31:15 2000/romano.cshl.org 143.48.31.66: Wed Aug 23 13:31:30 2000/swiss 143.48.31.43: Wed Aug 23 13:31:30 2000/romano.cshl.org 143.48.31.69: Wed Aug 23 13:31:30 2000/munster.cshl.org 143.48.31.63: Wed Aug 23 13:31:30 2000/whey.cshl.org 143.48.31.44: Wed Aug 23 13:31:30 2000/edam.cshl.org 143.48.31.45: Wed Aug 23 13:31:30 2000/feta.cshl.org 143.48.31.54: Wed Aug 23 10:31:30 2000/pesto 143.48.31.47: Wed Aug 23 13:31:30 2000/turunmaa.cshl.org All the machines on my office network are supposed to have their internal clocks synchronized by the network time protocol. The fact that " pesto " is off by several hours relative to the others suggests that something is wrong with this machine's time-zone setting. The example client was unexpectedly useful in identifying a problem. Another thing to notice is that we don't see a transmission from edam.cshl.org in the first group but transmissions from it appear later. It may have missed a time interval (the sleep() function is only accurate to plus or minus 1 second), or the multicast message from that machine may have been lost. Multicast messages, like other UDP messages, are unreliable. Multicast Chat System We'll now use multicasting to redesign the architecture of the UDP-based Internet chat system developed in Chapter 19. Recall that the heart of the system was five lines of code from the server's ChatObjects::Channel module: sub send_to_all { my $self = shift; my ($code,$text) = @_; $_->send($code,$text) foreach $self->users; } Given a message code and message body, send_to_all() looks up each registered user and sends it a copy of the message. The socket transmission is done by a ChatObjects:: User object, which maintains a copy of the client's address and port number. The weakness of this system is that if there are a great many registered users, the server sends out an equally large number of UDP packets, loading its local network and routers. This system can probably scale to support thousands of registered users, but not tens of thousands (depending on how "chatty" they are). In the reimplemented version, we'll replace the server's send_to_all() method with a version that looks like this: sub send_to_all { my $self = shift; my ($code,$text) = @_; my $dest = $self->mcast_dest; my $comm = $self->comm; $comm->send_event($code,$text,$dest) warn $!; } Instead of looking up each client and sending it a unicast message, we make one call to the communication object's send_event() method, using as the destination a multicast group address. We'll go over the details of this method when we walk through the code. Let's look at the revised chat protocol from the client's point of view. In the original version of this system, the client did all its communication via a single UDP socket permanently assigned to the server. In the new version, we alter this paradigm: -
The client creates a socket for communicating with the server. This is the same as the original application. One socket will be used for all messages sent by the client to the server; we'll call this the control socket. -
The client creates a second socket for receiving multicasts. When the client logs in, the server responds with two messages, one acknowledging successful login and the other providing the port number on which to listen for multicasts. The client responds by creating a second socket and binding it to the indicated port. The client now select() s over the multicast socket as well as over standard input and the control socket. -
The client adds multicast groups to subscribe to channels. There is a one-to-one correspondence between chat channels and multicast groups. When the client subscribes to a new chat channel, the server responds with an acknowledgment that contains the multicast group address on which public messages to that group will be transmitted. The client adds the group to the socket using mcast_add() . -
The client drops multicast groups to depart channels. The client calls mcast_drop() when it wants to depart a channel. -
The client sends public messages as before. To send a public message, the client sends it to the server and the server retransmits it as a multicast. Therefore, the client code for sending a public message is unchanged from the original version. From the server's point of view, the following changes are needed: -
The server has both a port and a multicast port. In addition to the port used to receive control messages from clients , the server is configured with a port used for its multicast messages. This could have been the same as the control port, but it was cleaner to keep the two distinct. -
The multicast port is sent to the client at login time. We need a new message to send to the client at login time to tell it what port to use for receiving multicasts. -
Each chat channel has a multicast group address. Each chat group has a distinct multicast address. To send a message to all members of a channel, the server looks up its corresponding group address and sends a single message to that address. A feature of this design is that the client sends public messages to the server using conventional unicasting , and the server retransmits the message to members of the channel via multicast. A reasonable alternative would be to make the client responsible for sending public messages directly to the relevant multicast address. Either architecture would work, and both would achieve the main goal of avoiding congestion on the server's side of the connection. I chose the first architecture for two reasons. First, I wanted to avoid too radical a rewriting of the client, which would have been necessary if the burden of keeping track of which channels the user belonged to had been shifted to the client side. Second, I wanted to leave the way open for the server to exercise editorial control over the clients' content. Many chat systems have a "muzzling" function that allows the server administrator to silence a user who is becoming abusive . Because all public messages are forced to pass through the server, it would be possible to add this feature later. A final consideration is the TTL on outgoing multicasts, which could have different meanings on different clients' networks. Having the server issue all the multicasts enforces uniformity on the scope of public messages. We'll walk through the server first, and then the client. The first change is very minor (Figure 21.6). We add a new event code constant named SET_MCAST_PORT to ChatObjects::ChatCodes. This is the message sent by the server to the client to tell it what port to bind to in order to receive multicast transmissions. Figure 21.6. Revised ChatObjects::ChatCodes Next we look at the server script (Figure 21.7). It is very similar to the original version, so we'll just go over the parts that are different. Figure 21.7. Multicast server Lines 4 “7: Load multicast subclasses of modules Instead of loading the ChatObjects::Channel and ChatObjects::Comm modules, we load slightly modified subclasses named ChatObjects::MChannel and ChatObjects::MComm respectively. Lines 19 “21: Read command-line arguments We read three arguments from the command line corresponding to the control port, the multicast port, and the TTL on outgoing public messages. If the multicast port isn't provided, we use the control port plus one. If the TTL isn't provided, we choose the organization-wide scope of 31. Line 22: Create a new communications object We call ChatObjects::MComm->new() to create a new communications (comm) object. As in the original version of this server, we use the comm object as an intermediary for sending and receiving events from clients. Its primary job is to pack and unpack chat system messages using the binary format we designed. This subclass of the original ChatObjects::Comm takes three arguments: the control port, the multicast port, and the TTL for outgoing multicast messages. Lines 23 “30: Create a bunch of channels We create several chat channels in the form of ChatObjects::MChannel objects. The constructor for this subclass takes four arguments, the title and description of the channel, as before, and two new arguments consisting of a multicast group address for the channel and the comm object. We arbitrarily use group addresses in the range 225.1.0.1 through 225.1.0.5 for this purpose. Lines 32 “43: Main loop The server main loop is identical to the earlier version. Lines 44 “50: Handle logins The do_login() is slightly modified. After successfully logging in the user and creating a corresponding ChatObjects::User object, we call the user object's send() method to send the client a SET_MCAST_PORT event. The argument for this event is the multicast port, which we retrieve from the comm object's mport() method (we could also get the value from the $mport global variable). Figure 21.8 lists the code for the ChatObjects::MComm module. It is a subclass of ChatObjects::Comm that overrides the new() constructor and adds one method, mport() . Figure 21.8. ChatObjects::MComm module Lines 1 “6: Load modules We tell Perl that ChatObjects::MComm is a subclass of ChatObjects::Comm and load ChatObjects::Comm and IO::Socket. We also load IO::Socket::Multicast so as to have access to the various mcast_ methods . Lines 7 “15: Override new() method We replace ChatObjects::Comm->new() with a new version. We begin this version by invoking the parent class's new() method to construct the control socket. When this is done, we remember the multicast port argument in the object hash and set the TTL on outgoing messages by calling mcast_ttl() on the control socket. Line 16: The create_socket() method We override our parent's create_socket() method with one that creates a suitable IO::Socket::Multicast object, rather than IO::Socket::INET. Line 17: The mport() method This new method looks up the multicast port in the object hash and returns it. Lines 18 “23: The mcast_event() method This new method is responsible for sending an event message, given the event code, the event text, and the multicast destination address. We use sockaddr_in() to create a suitable packed destination address using our multicast port and multicast IP address, and pass the event code, text, and address to our inherited send_event() method. We turn now to the ChatObjects::MChannel module (Figure 21.9). This module, which is responsible for transmitting public messages to all currently enrolled members of a channel, requires the most extensive changes. Figure 21.9. ChatObjects::MChannel module Lines 2 “6: Load modules We declare ChatObjects::MChannel as a subclass of ChatObjects::Channel, so that Perl falls back to the parent class for any methods that aren't explicitly defined in this class. Lines 7 “13: Override new() method We override the new() method to save information about the channel's multicast address and the comm object to use for outgoing messages. We begin by invoking the parent class's new() method. We then copy the method's third and fourth arguments into hash keys named mcast_addr and comm , respectively. Lines 14 “15: mcast_addr() and comm() accessors We define two accessors named mcast_addr() and comm() , to retrieve the multicast address for the channel and the comm object, respectively. Lines 16 “20: info () method We override the channel's info() method, which sends descriptive information about the channel to the client. Previously this method returned the name of the channel, the number of users enrolled, and the description. We modify this slightly so that the dotted -quad multicast IP address for the channel occupies a position between the user count and the description. Lines 21 “26: mcast_dest() method The mcast_dest() method returns the packed binary destination address for the multicast group. It retrieves the multicast port from the server object and uses sockaddr_in() to combine it with the dotted-quad address returned by mcast_addr() . We explicitly put sockaddr_in() into a scalar context so that it packs the port and IP address together, rather than attempting to unpack its argument. Lines 27 “33: send_to_all() method The send_to_all() method is called whenever it's necessary to send a message to all members of a channel. Such messages are sent when a user joins or departs a channel, as well as when a user sends a public message to the channel. We call mcast_dest() to get the packed binary address for multicasts directed to the channel, and then pass this destination, along with the event code and content, to the comm object's send_event() method. Note that the ChatObject::MComm class doesn't itself define the send_event() method. This is inherited from the parent class and is used to send both unicast messages to individual clients and multicast messages to all channel subscribers. Only a few parts of the client application need to be modified to support multicasting, so we list only the relevant portions of the source code (Figure 21.10). The full source code for the modified client is in Appendix A. Figure 21.10. Internet chat client using multicast Lines 1 “9: Load modules In addition to the IO::Socket and IO::Select modules, we now load ChatObjects::MComm and IO::Socket::Multicast in order to gain access to mcast_add() and friends . Lines 23 “36: Define handlers for server events The %MESSAGES hash maps server events to subroutines that are invoked to handle the events. We add SET_MCAST_PORT to the list of handled events, making its handler the new create_msocket() subroutine. Lines 37 “42: Initialize the control and multicast sockets We read the command-line arguments to get the default server address and control port. We then create a standard ChatObjects::Comm object, which holds the server unicast address and port. We store this in $comm . This will be used to exchange chat messages with the server. For multicast messages we will later create a ChatObjects::MComm object. Lines 41 “54: Log in and enter select loop We now attempt to log into the server. If successful, we create an IO::Select object on the control socket and STDIN and enter the main loop of the client, handling user commands and server messages. This part of the program hasn't changed from the original but is repeated here in order to provide context. Lines 59 “67: Handle the SET_MCAST_PORT message The create_msocket() subroutine is responsible for handling SET_MCAST_PORT messages sent from the server. It must do two things: create a new ChatObjects::MComm object bound to the indicated port and add the new comm object's socket to the list of filehandles monitored by the client's main select() loop. The function first examines the port number sent by the server in the message body and refuses to handle the message unless it is numeric. If the $msocket global variable is already defined, the function removes it from the list of handles monitored by the global IO::Select object (currently, this never happens, but a future iteration of this server might change the multicast port dynamically). The next step is to create a new comm object to handle incoming multicasts. We call ChatObjects::MComm->new() to create a new communications object wrapped around a multicasting UDP socket. The last step is to add the newly created socket to the list that the global IO::Select object monitors . Lines 124 “136: Join and part channels The join_part() subroutine is called to handle the server's JOIN_ACK and PART_ACK message codes. The subroutine parses the message from the server, which contains the affected channel's multicast address. In the case of a JOIN_ACK message, we tell the multicast socket to join the group by calling its mcast_add() method. Otherwise, we call mcast_drop() . Lines 137 “142: List a channel A last, trivial change is to the list_channel() method, which lists information about a channel in response to a CHANNEL_ITEM message. The format of this message was changed to include the channel's multicast address, so the regular expression that parses it must change accordingly . The new multicast-enabled version of the chat server works well on a local area network and between subnets separated by multicast routers. It will not work across the Internet unless the ISPs at both ends route multicast packets or you set up a multicast tunnel with mrouted or equivalent. One limitation of this client is that only one user can run it on the same machine at the same time. This is because only one socket can be bound to the multicast port at a time. We could work around this limitation by setting the Reuse option during creation of the multicast socket. This would allow multiple sockets to bind to the same port but would create a situation in which, whenever one user subscribed to a channel, all other users on the machine would start to receive messages on that channel as well. To prevent this, the client would have to keep track of the channels it subscribed to and filter out messages coming from irrelevant ones. Perhaps a better solution would be to allocate a range of ports for use by the chat system and have each client run through the allowed ports until it finds a free one that it can bind to. Alternatively, the server could keep track of the ports and IP addresses used by each client and use the SET_MCAST_PORT message to direct the client toward an unclaimed port. |