Friday, January 13, 2012

Comet in a socket: implementing web chat in Jetty

This is part 2 in WebSocket series. Read part 1 here. If you are impatient, you can find a very brief WebSocket implementation cheat sheet at the end of this post.

In one of my previous posts, I have briefly described the history of real-time communication techniques used in web pages. As much as it is good to know, however, it will not help you much in implementing a working, modern solution to the problem. So, this time we are going to do just that - implement a simple website chat using HTML5 WebSockets and a Jetty server.

Preparations

Before we start, make sure you have all the prerequisites. I will be doing this in Eclipse; if you prefer some other Java IDE, do not fear, as there will be nothing important that is Eclipse-specific here. What is important, however, is to get the correct Jetty version. I will be using the latest (at the time of writing) release version, Jetty 8.0.4, which you can download here. Be wary of getting the 7.x.x version; since so far there is no J2EE WebSockets standard, every server implements it in its own way and the way 7.x.x and 8.x.x Jetty versions do it is, unfortunately, not the same.

After your Jetty server is downloaded, make sure to register it in your Eclipse's Servers tab. If you do not see Jetty 8 on the list of available server runtimes, download the Jetty 8 plugin. After that, you are good to go - just make sure you get the correct plugin, as there is another outdated one available on the Web which will not support version 8.

Creating the servlet

The first thing you should do after creating your dynamic web project targeting Jetty 8 runtime is to create the WebSockets servlet which will handle all the incoming calls. This servlet must extend Jetty's WebSocketServlet class and its initial implementation can look like this:
@WebServlet("/chat")
public class ChatServlet extends WebSocketServlet 
 implements Servlet {

 @Override
 public WebSocket doWebSocketConnect(HttpServletRequest req,
   String protocol) {
  // TODO Auto-generated method stub
  return null;
 }
}
Note: if your IDE is unable to find WebSocketServlet class then it most probably means that the Jetty 8 runtime is not correctly associated with your project; the class resides in jetty-websocket-*.jar

As can be seen here, the class extends the aforementioned WebSocketServlet and defines one method only. All that is expected of this method is to return an instance of a class implementing WebSocket interface, which will implement all the necessary callbacks for WebSocket communication. Like this:


@WebServlet("/chat")
public class ChatServlet extends WebSocketServlet 
 implements Servlet {

 @Override
 public WebSocket doWebSocketConnect(HttpServletRequest req,
   String protocol) {
  return new WebSocket() {
   @Override
   public void onOpen(Connection conn) {
    // TODO Auto-generated method stub
   }

   @Override
   public void onClose(int closeCode, String closeMessage) {
    // TODO Auto-generated method stub
   }
  };
 }
}
Wait a moment! Are there really only two callbacks? What about a callback for incoming messages? As it turns out, implementing WebSocket interface directly is not terribly useful - the only things you will be notified about is establishing and closing the connection. If you want to create any useful WebSocket code, you should rather implement one of the four interfaces nested inside WebSocket interface. Which one to implement depends on the kind of notifications you want to receive; in our case, we are interested in text messages (WebSockets standard also supports binary messages, but JavaScript doesn't, besides we are not really interested in going low-level).

Implementing the chat room

Before we further change our ChatServlet, lets create a simple chat room implementation.
public class ChatRoom {
 private Set<ChatUser> users = new HashSet<ChatUser>();

 public synchronized void messageAll(String msg) {
  for (ChatUser user : users) {
   user.sendMessage(msg);
  }
 }
 
 public synchronized void addUser(ChatUser user) {
  users.add(user);
 }

 public synchronized void removeUser(ChatUser user) {
  users.remove(user);
 }
}
This is pretty bare-bones and not safe at all; we don't even escape the HTML in the message being sent. When implementing a real chat service, make sure to do it! As it stands, our server is nothing more than a glorified container for ChatUser objects. One thing of note is that all three of its methods are synchronized; this is to ensure we do not get any ConcurrentModificationExceptions, as all methods may be called from different threads.

ChatUser class is where the most interesting stuff happens. It will actually implement the WebSocket interface and handle all the inbound and outbound communication.

class ChatUser implements WebSocket.OnTextMessage {
 private final ChatRoom room;
 private final String name;
 private Connection connection;

 public ChatUser(ChatRoom room, String name) {
  this.name = name;
  this.room = room;
 }

 public void sendMessage(String message) {
  try {
   connection.sendMessage(message);
  } catch (IOException e) {
  }
 }

 @Override
 public void onOpen(Connection conn) {
  this.connection = conn;
  room.addUser(this);
 }

 @Override
 public void onMessage(String msg) {
  room.messageAll("" + name + ": " + msg);
 }

 @Override
 public void onClose(int exitCode, String exitMsg) {
  room.removeUser(this);
 }
}
The four methods it implements are pretty self-descriptive. sendMessage is called by the chat room to send a text message to the chat client. onOpen receives and stores the Connection object for further use (we need it if we want to send anything) and registers the user in their chat room.onMessage is the callback that is called by Jetty when any message is sent from the client to the server. Finally, onClose cleans up after the user by removing the object from the associated chat room.

Wrapping it up

Armed with our ChatRoom and ChatUser class implementations, we can go back to the ChatServlet and beef it up a little. To keep things simple, we will assume there is only one global chat room where all the users end up. We are also not going to validate user names in any way, so there can be two users using the same nickname - something not acceptable in a real chat application.
@WebServlet("/chat")
public class ChatServlet extends WebSocketServlet 
 implements Servlet {
 private static final ChatRoom room = new ChatRoom();
 
 @Override
 public WebSocket doWebSocketConnect(HttpServletRequest req,
   String protocol) {
  String name = req.getParameter("name");
  return new ChatUser(room, name);
 }
}
As you can see, I decided to use the 'name' parameter from the request URL as the username. And since ChatUser already implements the WebSocket interface, we can just return it and Jetty will make sure to call all the required callbacks for us. Simple!

And finally, we are done with the server. Now the only thing left is...

Creating a simple client

If you expect some state-of-the-art HTML/CSS/JavaScript code here, you may be disappointed. What I am going to create is just as bare-bones as the server implementation; however, it has one big advantage: it is fully-functional. Therefore, you can use it as a starting point to implementing your fully-fledged chat service.

That being said, lets create a simple index.html file inside our project.

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" 
  "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" 
 content="text/html; charset=ISO-8859-1">
<title>WebSockets Chat</title>
</head>
<body>
 Name: <input id="name" type="text"></input> 
 <button id="login">Login</button>
 <button id="logout">Logout</button>
 <div id="chat" style="width: 640px; height: 480px; 
  overflow-y: auto; border: 1px solid;"></div>
 <input id="input" type="text" style="width: 580px"></input>
 <button style="width: 60px;" id="send">Send</button>
</body>
</html>
As you can see, we have a name input field, a main chat area and a textfield allowing the user to send chat messages. With this basic structure in place, time to add some JavaScript. I will be using some very basic jQuery in this case.
<script type="text/javascript" 
src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js">
</script>
<script type="text/javascript">

$(document).ready(function() {
 /* Bind the buttons to their handlers */
 $('#login').click(login);
 $('#send').click(send);
 toggleButtons(false);
});

/* Open a new WebSocket connection and login the user */
function login() { ... }

/* Close the current WebSocket connection */
function logout() { ... }

/* Append the given message to the chat window */
function appendChat(msg) { ... }

/* Send currently entered text to the server */
function send() { ... }

/* Enable/disable buttons depending on connect state */
function toggleButtons(connected) {
 if (connected) {
  $('#login').attr('disabled', 'disabled');
  $('#logout').removeAttr('disabled');
  $('#send').removeAttr('disabled');
 } else {
  $('#login').removeAttr('disabled');
  $('#logout').attr('disabled', 'disabled');
  $('#send').attr('disabled', 'disabled');
 }
}

</script>

Now we just need to implement the four missing functions. Lets start with login(); we want to retrieve the entered name, connect to the server and register our WebSockets handlers for all the server events. We can do it like this:
function login() {
 /* If we are already connected, disconnect first */
 if (window.ws)
  ws.close();
 /* Retrieve the username */
 var name = $('#name').val();
 if (name.length == 0)
  return; /* No name; do nothing */
 /* Connect to the server; absolute URL required here! */
 ws = new WebSocket("ws://" + window.location.host 
  + "/Chat/chat?name=" + name);
 /* Register the WebSocket event handlers */
 ws.onopen = function() {
  toggleButtons(true);
  appendChat('<b>CONNECTED</b>');
 };
 ws.onmessage = function(msg) {
  appendChat(msg.data);
 };
 ws.onerror = function(err) {
  appendChat('<b>ERROR: ' + err + '</b>');
 };
 ws.onclose = function() {
  toggleButtons(false);
  appendChat('<b>DISCONNECT</b>');
 };
}
The most interesting thing here is the instatiation of a new WebSocket object and assigning its event handlers. Please note that, unfortunately, we have to pass the absolute URL of our chat servlet here; if your project has a different context root than /Chat, you will need to adjust this accordingly. One potential improvement we could do here is retrieving current document's URL and constructing the WebSocket URL based on it. That way, our code would be much more portable and independent of the server configuration.

The remaining three functions are much simpler.

function logout() {
 if (window.ws)
  ws.close();
}

function appendChat(msg) {
 var chat = $('#chat');
 chat.append('<div>' + msg + '</div>');
 chat.scrollTop(chat.prop('scrollHeight'));
}

function send() {
 var msg = $('#input').val();
 if (ws && msg) {
  $('#input').val('');
  ws.send(msg);
 }
}
Of note here are three things:
  1. To close a WebSocket connection, call the close() method on the WebSocket object.
  2. To send a WebSocket message, simply call the send() method on the WebSocket object, passing the message string as its argument.
  3. To receive a WebSocket message, access the data field of the msg object passed to ws.onmessage event handler.

And just to remind you again, in a real application you should not accept user input without escaping the HTML first. Doing it like in the above example is a surefire way to get into trouble.

Starting the server

When you start Jetty, you may encounter an error message about the classloader being unable to find WebSocketServlet message. This is because, apparently, in the current Jetty version it is not by default exposed to the webapps. You can easily avoid the error by copying the jetty-websocket-*.jar from Jetty's lib directory to WEB-INF\lib of your application. Alternatively, you can fiddle with Jetty configuration to expose it.

That's it

We are done implementing our chat server. If you deploy your application and start Jetty, and then open the index.html from a WebSocket-compliant browser (such as Chrome), you should be able to connect to the chat and send and receive chat messages. In fact, you can even open the page in a few browser tabs and each one will be able to do it independently; there are no session cookies or the like here.


Our web chat in action

Cheat sheet

Steps needed to use WebSockets using Jetty server and JavaScript client in a nutshell:
  • Create a new servlet extending WebSocketServlet class
  • Create a new class implementing WebSocket.OnTextMessage interface
  • Return an instance of this class in the servlet's doWebSocketConnect method; it will be used as a callback by Jetty
  • In your JavaScript code, create a new WebSocket('ws://yourserver/servlet') object
  • Assign callback functions to this object's onopen, onmessage, onerror and onclose fields

We are not done yet!

In the next part of this series I show how the same JavaScript code (with only minimal changes) can also work in the browsers not yet implementing the WebSockets draft.

4 comments:

  1. I will keep it in mind, thanks for sharing the information. Keep updating, looking forward for more posts. Thanks.

    日本NCH

    ReplyDelete
  2. Thanks for the amazing content on your blog I am very interested in this article and you have really helped me.

    commercial waste receptacles

    ReplyDelete
  3. Totally a great page you've got in here. Impressive indeed.


    bagless upright vacuum cleaner

    ReplyDelete
  4. When you start to develop applications using Google Web Toolkit, you relatively quickly learn that its support of Java standard library is... lacking... to say the least. Among the things that, to me personally, were most lacking is the support of Java's reflection API. This becomes very apparent when you try to dynamically instantiate an object knowing only its class name - there simply is no support for Class.newInstance() call in GWT. Similarly, you don't have a generic way to retrieve a given field's value knowing only the field's name (stored in a string), you need to hardcode the appropriate getter call in your application.


    CMS Design

    ReplyDelete