Thursday, February 4, 2010

UDP Multicasting Clients and a Name Server









UDP Multicasting Clients and a Name Server


Figure 30-4 shows the various objects in the multicasting chat application.



Figure 30-4. Multicasting chat system



Each user is represented by a MultiChat object that can send and receive messages from the multicast group. However, this isn't a pure peer-to-peer example since a client must first log in to the NameServer object and receive the address of the group. Upon leaving the group, the client must log out from the server. Each client has a name that is checked by NameServer for uniqueness. If the name is being used by a group member, then NameServer rejects the request to log in.


As a side effect of storing user names, NameServer can process who messages. Instead of a client multicasting a who message to all the other group members and receiving multiple replies, a single who message is directed to the NameServer followed by a single response. This approach significantly reduces the number of messages circulating in the system.


As discussed in the context of the threaded chat server, the main drawback of using NameServer is that it represents a single point of failure. However, NameServer's disappearance only prevents new clients from joining the group. Communication between existing members can continue. The other consequence of NameServer's demise is that who messages will no longer be answered.


NameServer must be able to handle multiple concurrent accesses from clients (to log in, log out, or request who information). The server uses UDP as its communication protocol, permitting it to multiplex easily between the various requests (since each request is a datagram).


The messages that a client can send to NameServer are:



hi <client name>


This is the login message, which results in NameServer sending the client the multicast address (as a string) or in a no message.


bye <client name>


This is the logout message, which allows NameServer to discard the client's name from its group members list.


who


This is the server, which returns the names of all the current group members.


A client can send messages to the group for transmission to the other members. The message format is:



( <client name>): <message> [ / <toClientName> ]



The client's name is prepended to the front of the message. The square brackets mean the message may have an optional / extension, which makes the message visible only to that particular client. For example:



Andrew: come in for dinner / John



is the message "come in for dinner" sent from Andrew to John. Whereas:



Andrew: has anyone seen John's football?



broadcasts the message "has anyone seen John's football?" from Andrew to everyone (including himself).


The class diagrams for the application are given in Figure 30-5 but are uninformative. Almost no public methods are required since communication is datagram-based.



Figure 30-5. Class diagrams for the multicasting chat system




The Name Server


The NameServer class is similar to the top-level to the UDP-based server, ScoreUDPServer, in Chapter 29. It creates a DatagramSocket for receiving client packets and then enters a loop waiting for them to arrive. The client's address, port, and the text of the message are extracted from an arriving packet and from the information passed to a processClient( ) method.


processClient( ) employs a four-way branch to handle the various kinds of message (hi, bye, who) along with a default catch-all branch:



// globals
private static final String GROUP_HOST = "228.5.6.7";
// the multicast group address sent to new members

private ArrayList groupMembers;
// holds the names of the current members of the multicast group


private void processClient(String msg, InetAddress addr, int port)
{
if (msg.startsWith("hi")) {
String name = msg.substring(2).trim( );
if (name != null && isUniqueName(name)) {
groupMembers.add(name);
sendMessage(GROUP_HOST, addr, port); // send multicast addr
}
else
sendMessage("no", addr, port);
}
else if (msg.startsWith("bye")) {
String name = msg.substring(3).trim( );
if (name != null)
removeName(name); // removes name from list
}
else if (msg.equals("who"))
sendMessage( listNames( ), addr, port);
else
System.out.println("Do not understand the message");

} // end of processClient( )



The client's name is added to an ArrayList called groupMembers if it isn't present in the list. A bye message removes the name. A who message triggers a call to listNames( ), which builds a string of all the groupMembers names.


sendMessage( ) creates a datagram that's sent back to the client's address and port:



private void sendMessage(String msg, InetAddress addr, int port)
{
try {
DatagramPacket sendPacket =
new DatagramPacket( msg.getBytes( ), msg.length( ), addr, port);
serverSock.send( sendPacket );
}
catch(IOException ioe)
{ System.out.println(ioe); }
}





Improving Client Login and Logout


The simplicity of processClient( ) in NameServer shows that the login and logout mechanisms could be improved. There's no password associated with the name used in hi, so a client can use any name to log in. A bye message can come from anywhere, so it's possible for any client to log out another client. In any case, the removal of a client from the NameServer doesn't force a client to leave the multicast group.


The introduction of a password, together with encryption of the message inside the datagram would solve many of these problems. The remaining concern is that the server cannot force a client to leave the multicast group. The protocol is supported at the IP level, so beyond the reach of the Java MuticastSocket API. Indeed, the server has no control over the group at all, aside from deciding who will be given the multicast group address. This is hardly effective since the address can be shared between clients once one user has it. This lack of control is a crucial difference between the client/server approach and peer-to-peer.




Multicast Chatting


A MultiChat object is capable of two forms of network communication: it can send ordinary UDP datagrams to the NameServer (and receive replies) and can post datagrams to the multicast group (and receive messages from the group).


A MultiChat object is invoked with the client name supplied on the command line:



$ java MultiChat andy



Its GUI is similar to the one for the threaded chat client, as shown in Figure 30-6.


One obvious difference is that messages are now prefixed with the client's name rather than an address and port.



Figure 30-6. The multicasting chat client



The MultiChat constructor creates a DatagramSocket for the NameServer and sends it a hi <name> message requesting the multicast group's address. If the address is sent back, the group will be joined and a hi message sent to it (the message could be anything):



public MultiChat(String nm)
{
super( "Multicasting Chat Client for " + nm);
userName = nm;
initializeGUI( );

/* Attempt to register name and get multicast group
address from the NameServer */
makeClientSock( );
sendServerMessage("hi " + userName); // say "hi" to NameServer
checkHiResponse( );

// Connect to the multicast group; say hi to everyone
joinChatGroup( );
sendPacket("hi");

addWindowListener( new WindowAdapter( ) {
public void windowClosing(WindowEvent e)
{ sayBye( ); } // say bye at termination time
});

setSize(300,450);
show( );

waitForPackets( );
} // end of MultiChat( );




Having the client talk to the name server

makeClientSock( ) creates a DatagramSocket for sending and receiving communication from the NameServer. However, it's given a timeout of five seconds:



clientSock = new DatagramSocket( );
clientSock.setSoTimeout(5000);



A timeout is useful when the client is waiting for a datagram inside receive( ). This occurs after a hi message has been sent and after who queries. If the server has died, the timeout will limit the wait to five seconds before a SocketTimeoutException is raised.


The drawback is deciding on a reasonable timeout value. For instance, five seconds would probably be too short if the link was across the internet, through slow modems. This is why most programmers prefer to put blocking calls into separate threads so they can wait as long as they like. An alternative for TCP is to use nonblocking sockets, as illustrated in Chapter 29.


Messages are sent to the server from sendServerMessage( ) by creating a datagram and sending it through clientSock. Messages coming from the server are read by readServerMessage( ). Its clientSock.receive( ) call is wrapped in a try-catch block in case of a timeout (or other network problems).


A call is made to readServerMessage( ) immediately after a message is sent to the server. There's no attempt to use a separate thread for server processing. For example, a who message is followed by a readServerMessage( ) call:



sendServerMessage("who");
String whoResponse = readServerMessage( );





Having the client talk to the multicast group

The multicast group address string is extracted from the hi response and used by joinChatGroup( ) to create the multicast socket. A code fragment that does this is:



groupAddr = InetAddress.getByName(hiResponse);

groupSock = new MulticastSocket(GROUP_PORT); // port 5555
groupSock.joinGroup(groupAddr);



sendPacket( ) is used to send a message to the group and adds the (userName): prefix prior to delivery:



private void sendPacket(String msg)
{ String labelledMsg = "(" + userName + "): " + msg;
try {
DatagramPacket sendPacket =
new DatagramPacket(labelledMsg.getBytes( ),
labelledMsg.length( ), groupAddr, GROUP_PORT);
groupSock.send(sendPacket);
}
catch(IOException ioe)
{ System.out.println(ioe); }
}



Text messages are sent to the group when the user hits Enter in the messages text field. actionPerformed( ) calls sendMessage( ):



public void actionPerformed(ActionEvent e)
{ if (e.getSource( ) == jbWho)
doWho( );
else if (e.getSource( ) == jtfMsg)
sendMessage( );
}



sendMessage( ) extracts the string from the text field, checks it for validity, and passes it to sendPacket( ). All of this is done inside the GUI thread of the MultiChat application.




Hearing from the multicast group

Multicast messages may arrive at any time, so must be handled in a separate thread. Instead of creating a new Thread object, MultiChat uses the application thread by entering an infinite loop inside waitForPackets( ):



private void waitForPackets( )
{ DatagramPacket packet;
byte data[];

try {
while (true) {
data = new byte[PACKET_SIZE]; // set up an empty packet
packet = new DatagramPacket(data, data.length);
groupSock.receive(packet); // wait for a packet
processPacket(packet);
}
}
catch(IOException ioe)
{ System.out.println(ioe); }
}



This is the same approach as in the ScoreUDPClient in Chapter 29.


processPacket( ) extracts the text from the incoming packet and displays it (if it's visible to this client):



private void processPacket(DatagramPacket dp)
{ String msg = new String( dp.getData( ), 0, dp.getLength( ) );
if (isVisibleMsg(msg, userName))
showMsg(msg + "\n");
}



A message is visible if it has no / <name> part if / <name> contains the client's name, or if the message is originally from the client. This latter condition can be checked by looking at the start of the message that has the sender's name in brackets.





How Invisible Are Invisible Messages?


A message won't appear in a client's text display area if it has a / name extension that refers to a different client. Is this sufficient for private communication between two users? Unfortunately, this is not sufficient since the message is still being multicast to every client. This means that a hacker could modify the MultiChat code to display everything that arrives. Privacy requires that the messages be encrypted.


The other misleading thing about private communication is that it appears to be point-to-point between users and seemingly more efficient than multicasting. Unfortunately, this is false. True point-to-point communication would require another communication link, perhaps utilizing the DatagramSockets utilized for client/server interaction.




Ways to Implement who


MultiChat handles a who message by querying the server for a list of names. Another approach would be to multicast the message to all the clients to gather responses. The main benefit would be communication without relying on the server, which can fail or otherwise stop responding.


I didn't use multicasting to process a who message because of the large number of messages it would add to the system. With multicasting, the client's who query would be sent to n clients. Each client would send a reply, multicasting to every client, not just the one interested in the answer. This would mean a total of nxn messages circulating through the group. The grand total for a single who query is n + n2. By using the server, I replaced this large number with two messages: the request sent to NameServer and its reply. Alternatives to multicasting should always be explored since bandwidth is such a crucial commodity.










    No comments: