 |
Writing a Twitter like application using Grizzly Comet part 1: The Servlet
Posted by jfarcand on November 30, 2008 at 09:01 PM | Comments (7)
Twitter is more and more popular and I've decided to write my own Twitter using Grizzly Comet. The result is amazing: 150 lines of Java code and an amazing grizzly transformed into a bird!
Grizzly Comet is a framework build on top of the Grizzly. With Grizzly Comet, you can create powerful asynchronous application. Grizzly Comet support: - Asynchronous read and write: read or write without blocking, waiting for an images to be fully uploaded or written. Instead, let the framework notify you when an async event is ready. This is particularly useful when your application needs to upload files.
- Long Polling: open a connection from the browser and wait for the server to push data only when available. That means the connection stay suspended until an event happens on the server side.
- Streaming: Same as long polling, but never resume the connection. When an event happens, leave the connection suspended "forever".
- Grouping of suspended connections: You can group suspended connection, and manipulate a groups instead of single connection. That makes building a Grizzly Comet application quite simple.
- Filtering/Aggregating/Throttling events per connection or group. Under high load, it might be better to aggregate events instead of sending them one by one (might be faster). Event can also be filtered or throttled before they get written on a suspended connection. When pushing event to a group, it is always important to make sure the push operations is not a bottleneck. Grizzly Comet ships with such mechanism, significantly improving performance of your asynchronous application
OK Enough charabia! Let's demonstrate the power of the framework by re-writing Twitter and make it a full real time and asynchronous application. No need to refresh the page anymore. In this first part, I will explain the server side...the client will comes next, but get ready, I'm far from an expert with JavaScript :-). But first, let's see what the end results will looks like...all of this using a single Servlet!
Now let's deep dive into the monster's Comet API
CometEngine: The CometEngine is the entry point to the framework. The first steps when writing a Grizzly Comet application is to first create a 'group' or 'topic' object that can be used to suspend, share, filter, throttle, aggregate and resume connections: /**
* Create a {@link CometContext}
* @param id - The topic associated with the {@link CometContext}
* @return {@link CometContext}
*/
private CometContext createCometContext(String id){
CometEngine cometEngine = CometEngine.getEngine();
CometContext ctx = cometEngine.register(id);
ctx.setExpirationDelay(FIVE_MINUTES_TIMEOUT);
return ctx;
}
In the above, we grab the static instance of CometEngine and call register(id) to create a CometContext, an object representing suspended connection based on a topic. Note that CometContext can be created from everywhere like EJB, POJo, etc. This is quite important if you are planning to push events from non web components CometContext: The most important object of the Grizzly Comet Framework. A CometContext represents a group of suspended connections. From a CometContext you can push events, define your own mechanism of filtering/aggregating/throttling, suspend and resume connection: // Create a CometContext based on this session id.
twitterContext =
createCometContext(sessionId);
// Create and register a CometHandler.
ReflectorCometHandler handler = new ReflectorCometHandler
(true,startingMessage,endingMessage);
handler.attach(response.getWriter());
twitterContext.addCometHandler(handler);
// Keep a reference to us so we can be updated directly.
twitterContext.addAttribute("twitterHandler", handler);
In the code above, we first create a CometContext, then add a ReflectorCometHandler (more on this below) and add some attributes. Invoking addCometHandler(handler) automatically suspend the connection. CometHandler: This interface represents your suspended connection. Defining a CometHandler allow a web application to handle the lifecycle of a suspended connection. Events are pushed to a CometHandler as soon as they occurs. Event like when the connection get suspended (onInitialize), when server event are pushed (onEvent), when your application decide to resume the connection (onTerminate) or when the client close a suspended connection, or the connection was idle for X times (onInterrupt). The most important method is the onEvent, where you are usually define what you will do with the event, e.g. store it, write it, discard it etc. As a very simple example (this is the one used by this Twitter application), Grizzly Comet ships with the RefectorCometHandler, which does nothing except writing all messages it gets: /**
* Write {@link CometEvent#attachment} and resume the connection if
* {@link ReflectorCometHandler#useStreaming} is false
* @param event
* @throws java.io.IOException
*/
public synchronized void onEvent(CometEvent event) throws IOException {
try {
if (event.getType() != CometEvent.READ) {
printWriter.println(event.attachment());
printWriter.flush();
if (!useStreaming){
event.getCometContext().resumeCometHandler(this);
}
}
} catch (Throwable t) {
throw new IOException(t.getMessage());
}
}
But how CometHandler gets invoked? CometHandler gets invoked when an application invoke CometContext.notify(CometEvent). As an example, let's take explain how a chat application works. First, users (browsers) enter a chat room, waiting for message. As soon as they enter the chatroom, a Grizzly Comet implementation will invoke CometContext.addCometHandler(). That means those connections are suspended, waiting for events. If a user enter some message, the way to share (or push) that information back to the users is by invoking CometContext.notify("Salut"). Automatically the CometHandler.onEvent() will be called, and if the ReflectorCometHandler described above is used, then the message will be directly written to the suspended connection. In effect, that operation will push back the message to the client. You can always filter messages before they reach CometHandler (details in part III) So, four simple steps: - CometEngine.register("topic|group") to create the group
- CometHandler ch = new CometHandler() to prepare for suspending the request/response
- CometContext.addCometListener(CometHandler) to suspend the connection
- CometContext.notify(Message) to push message to the group or CometContext.resumeCometHandler(ch) to resume the connection, or in short to commit the response
And now, Twitter Twitter Twitter Let's first define what the application will do: - First, the user will sign in.
- Next, the user is now able to micro blog (add updates and push them to peoples registered (followers) to receives such updates).
- User can follow another user's micro blogs/updates by entering their name inside the 'follow field'
- To be smarter than Twitter, we will allow the user to gets update from the person the follow directly on the screen
- To make the application more cool that the original Twitter.com, we will allow users to move their blogs/updates on the screen, via the JMaki Comet Extension
 So let's go steps by steps on how we can build a Twitter like application. For Twitter, we will use a single TwitterServlet, and we will define the basic as: /**
* Grab an instance of {@link ServletContext}
* @param config
* @throws javax.servlet.ServletException
*/
@Override
public void init(ServletConfig config) throws ServletException {
super.init(config);
servletContext = config.getServletContext();
}
/**
* Same as {@link TwitterServlet#doPost}
*
* @param request
* @param response
* @throws javax.servlet.ServletException
* @throws java.io.IOException
*/
@Override
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doPost(request, response);
}
All the logic happens inside the doPost. First, sign in Twitter: I will talk in more details in part II about how the client works, so let's just describe the basic. Below is the code that allow a user to sign in login: function() {
var name = $F('login-name');
if(! name.length > 0) {
$('system-message').style.color = 'red';
$('login-name').focus();
return;
}
$('system-message').style.color = '#2d2b3d';
$('system-message').innerHTML = '';
$('login-button').disabled = true;
$('login-form').style.display = 'none';
$('message-form').style.display = '';
$('follower').style.display = '';
var query =
'action=login' +
'&name=' + encodeURI($F('login-name'));
new Ajax.Request(app.url, {
postBody: query,
onSuccess: function() {
$('message').focus();
}
});
},
On the server side, this is as simple as: /**
* Based on the {@link HttpServletRequest#getParameter} action value, decide
* if the connection needs to be suspended (when the user logs in) or if the
* {@link CometContext} needs to be updated (by the user or by its follower.
*
* There is one {@link CometContext} per suspended connection, representing
* the user account. When one user B request to follow user A, the {@link CometHandler}
* associated with user B's {@link CometContext} is also added to user A
* {@link CometContext}. Hence when user A push message ({@link CometContext.notify()}
* all {@link CometHandler} gets the {@link CometEvent}, which means user B
* will be updated when user A update its micro blog.
*
* The suspended connection on the client side is multiplexed, e.g.
* messages sent by the server are not only for a single component, but
* shared amongs several components. The client side include a message board
* that is updated by notifying the owner of the {@link CometContext}. This
* is achieved by calling {@link CometContext.notify(CometEvent,CometHandler)}
*
* @param request
* @param response
* @throws javax.servlet.ServletException
* @throws java.io.IOException
*/
@Override
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String action = request.getParameter("action");
String sessionId = request.getSession().getId();
HttpSession session = request.getSession();
CometContext twitterContext = (CometContext) session.getAttribute(sessionId);
if (action != null) {
In short, we grab the action value from the doPost, and also we look for a CometContext. The CometContext here represents the user blogs (or update), and all CometHandlers added to this "user" CometContext will receive updates when the user micro blogs/updates his page. Of course on the first request the CometContext is null as their is no suspended connections. On the client/broser side, the first action sent is 'start' (when the page is loaded), and this is where we will suspend the connection: } else if ("start".equals(action)) {
String message = "{ message : 'Welcome'}";
response.setContentType("text/html");
String callback = request.getParameter("callback");
if (callback == null) {
callback = "alert";
}
response.getWriter().println("<script id='comet_" + counter++ + "'>"
+ "window.parent." + callback + "(" + message + ");</script>");
// Create a CometContext based on this session id.
twitterContext =
createCometContext(sessionId);
// Create and register a CometHandler.
ReflectorCometHandler handler = new ReflectorCometHandler
(true,startingMessage,endingMessage);
handler.attach(response.getWriter());
twitterContext.addCometHandler(handler);
// Keep a reference to us so we can be updated directly.
twitterContext.addAttribute("twitterHandler", handler);
session.setAttribute("handler", handler);
session.setAttribute(sessionId, twitterContext);
return;
The most important code above is when we create the CometContext (based on the session id), then create a ReflectorCometHandler that gets invoked when it is time to write back to the browser updates made to the CometContext. The CometContext as described above. Next, we suspend the connection/response by invoking addCometHandler. We use the session to store the CometContext and also the CometHandler representing the user suspended connection. As soon as the user sign in: /*
* Notify the submitter, via its CometHandler, that it has just logged in.
*/
if ("login".equals(action)) {
response.setContentType("text/plain");
response.setHeader("Cache-Control", "private");
response.setHeader("Pragma", "no-cache");
response.setCharacterEncoding("UTF-8");
String name = request.getParameter("name");
if (name == null) {
logger.severe("Name cannot be null");
return;
}
session.setAttribute("name", name);
CometHandler ch = (CometHandler)session.getAttribute("handler");
twitterContext.notify(BEGIN_SCRIPT_TAG
+ toJsonp("Welcome back", name)
+ END_SCRIPT_TAG, CometEvent.NOTIFY, ch);
// Store the CometContext associated with this user so
// we can retrieve it for supporting follower.
servletContext.setAttribute(name, twitterContext);
From the user's name, we push our first message back to this user using the suspended connection. As we noted above, we push the message using the CometContext.notify, which in turn will invoke the ReflectorCometHandler.onEvent(), which will write the message back to the client. Now the user is ready to micro blog/updates. Time to micro blog using Grizzzly!!!!! As soon as the user update his status, the client sent: } else if ("post".equals(action)) {
String message = request.getParameter("message");
String callback = request.getParameter("callback");
if (message == null) {
logger.severe("Message cannot be null");
return;
}
if (callback == null) {
callback = "alert";
}
if (twitterContext != null){
// Notify other registered CometHandler.
twitterContext.notify("<script id='comet_" + counter++ + "'>"
+ "window.parent." + callback + "(" + message + ");</script>");
}
response.getWriter().println("ok");
return;
This looks like way to simple, right? For any update, we just need to get the CometContext and invoke notify on it, and BINGO, all our followers will be updated in REAL-TIME!. What is a follower? A follower is another users that want to get updated when another user enter a new micro blog. Followers register themselves by entering the name of the user they want to follow: } else if ("following".equals(action)) {
response.setContentType("text/html");
String message = request.getParameter("message");
String name = (String)session.getAttribute("name");
// Retrive the user CometContext.
CometContext followerContext
= (CometContext) servletContext.getAttribute(message);
CometHandler ch = (CometHandler)session.getAttribute("handler");
if (followerContext == null){
twitterContext.notify(BEGIN_SCRIPT_TAG
+ toJsonp("Invalid Twitter user ", message)
+ END_SCRIPT_TAG, CometEvent.NOTIFY, ch);
return;
}
followerContext.addCometHandler(ch, true);
twitterContext.notify(BEGIN_SCRIPT_TAG
+ toJsonp("You are now following ", message)
+ END_SCRIPT_TAG, CometEvent.NOTIFY, ch);
CometHandler twitterHandler =
(CometHandler)followerContext.getAttribute("twitterHandler");
followerContext.notify(BEGIN_SCRIPT_TAG
+ toJsonp(name, " is now following " + message)
+ END_SCRIPT_TAG, CometEvent.NOTIFY, twitterHandler);
return;
}
The 'name' is the user which want to follow another user, which is represented by the 'message'. First, we get the CometContext representing the user we want to follow, and add our CometHandler to it. That means that every time the followerContext ill be updated, we will also get the update via our CometHandler. Too cool!. We also update our CometContext by pushing a message telling our follower that we are now following a new user. We also update the followerContext by pushing a message saying we are now following that user.THAT'S IT!!! Yes, that the only steps you have to write in order to build a Twitter like application using Grizzly Comet. One thing to remember is that everything happens using a CometContext. From that object, you can notify/filter/aggregate/ etc. a set of suspended connection represented by a CometHandler. In the current example I'm using the ReflectorCometHandler, but you can write your own by implementing the CometHandler interface. Now ready to try it? Two possibilities. For development, I recommend you use the embedded Grizzly Comet Server and just do % java - jar grizzly-comet-webserver-1.9.0.jar -p 8080 -a \
./grizzly-twitter.war com.sun.grizzly.samples.twitter.TwitterServlet
For production or if you need to uses Eclipse or Netbeans, download GlassFish v3 and deploy the grizzly-twitter.war application like any other web application. The application and source can be downloaded here. If you want to improve it or contribute, you are welcome to join the Grizzly community! OK next time I will explain in details how the client works (at least I will try :-)). Please post your questions on users@grizzly.dev.java.net so the Grizzly community can help and respond :-)
technorati: grizzly comet grizzly glassfish v3
comet Twitter
Bookmark blog post: del.icio.us Digg DZone Furl Reddit
Comments
Comments are listed in date ascending order (oldest first) | Post Comment
-
hi jean, if u can give me a favour.........please tell me..........how to set a jar file in to a window service..............i have a jar file.......and i don't want to execute it by double clicking..........rather i want to set this jar file as a windows service,,,,,,,,,,,,,do u have any idea..............please......
guide me .......ashvini007@gmail.com
Posted by: ashvini007 on December 01, 2008 at 01:17 AM
-
in comethandler you use :
public synchronized void onEvent(CometEvent event)
why do you let worker threads to block waiting for the current worker thread to finish its client io ?.
thats what your design allows for.
i did send you a patch where the comethandler queues io and allows other threads then the current one to return and not block and wait.
Posted by: gustav3d on December 01, 2008 at 12:15 PM
-
@Gustav, the CometHandler must be synchronized because two CometContext might invoke simultaneously the CometHandler. I still need to look at you patch :-)
Thanks!!
Posted by: jfarcand on December 01, 2008 at 12:21 PM
-
This is cool, I'm going to try it out
Posted by: caroljmcdonald on December 03, 2008 at 01:58 PM
-
hi jfrcand:
could grizzly comet work in tomcat?thanks
Posted by: pollux0505 on December 03, 2008 at 09:03 PM
-
Jean Francois,
I am having difficulties updating my current installation of Glassfish v3 Prelude with the latest Grizzly snapshot. Specifically, I am working on an application that uses the DeliverResponse object to to notify clients and I have posted my issue on nabble, etc. Would you please let me know what the latest version of Grizzly Glassfish v3 Prelude supports, how to download it, and how to install it?
Thank you,
Anthony
Posted by: aalcamo on December 11, 2008 at 09:07 AM
-
@ pollux0505 No only with GlassFish. Atmosphere (atmosphere.dev.java.net will bring portability to Comet application....A+
Posted by: jfarcand on December 15, 2008 at 01:11 PM
|