|
|
|
|||||
|
|
AsyncFW users, please also see the CometWorld
tutorial. Quick and Dirty Comet!
In 60 minutes or less! Comet is simply a mechanism for suspending a request on
the server. Combine that with an
Asynchronous Ajax request from the client, and you have a “fire and forget”
interface to the server. The server
will reply when new data is available, without the client (seemingly) needing
to ask for it. The caveat being
“Seemingly” - In reality, the client is simply staging the request ahead of
time. Why use Comet?
That is the tricky part – If you are currently “polling”
the server for data Comet may provide a better model. If you search for ‘Comet’ on the web, you might
soon think that all Comet is good for is “Chat”
applications. While I don’t think I
would use Comet on every site, and certainly not on every page, I do think it
has some merit. When a user accesses a web page, they may sit idly reading
for some time, 99.9% of the time that is perfect, but what if they are
looking at a Temperature Gauge, Stock Price, Airline fare, Ticket
availability, etc. These are values
that can change at any time. Normally, you would simply set up a timed poll
of the server to keep the information accurate (at least accurate to the time
of the last poll). With Comet, you can broadcast the change to all online
users in real-time. While polling is a workable solution, you have to balance
the needs of the users, versus the load on the server. Setting a sub-one
second poll, would place a tremendous burden on a
server with many users. Setting a long polling time reduces that burden but
also reduces the “potential” accuracy of the data. Comet resolves this conflict. By setting up a “tell me
when the data changes” connection, you don’t need to poll. The data will
simply update when there is need. But, it too has it’s
costs. With Comet, you have to maintain the connection state on the server,
so every user will have one permanent connection (dedicated to Comet), and at
least one additional transient connection for normal processing. Using Comet
Searching the web for information on Comet can be a bit of
a challenge. You will find the obligatory “Chat” examples (like the world needs
more of those) and a bunch of theory and arcane discussions. If you are like
me, and just want to make it work, it can be a challenge. I spent over week trying to wedge Comet
into the FrameWork,
I thought this would be nice little niche addition that some would find
useful. While most of the issues and difficulties I had were mine, it was a
bit of a challenge to find any examples other than JSP “Chat” with iFrames. I had no
interest in JSP, nor a Chat application (as I am a servlet
purist, and I already more chat applications than I need) ,
and additionally - I didn’t want to require the use of iFrames. I also wanted to maintain state, return
something other than static HTML and I wanted to make the entire process
painless for those that use the Framework
for web site creation. Setup
Glass Fish v2
Version Info: Sun Java System Application
Server 9.1 Update 2 Open ESB v2 Portlet
Container 2.0 Update 1 Sun Java System Access Manager
7.1 Patch 1 Java BluePrints Java Engineering Edition 5
Samples Your First Cup: An Introduction
to the Java EE Platform API Documentation Java SE Development Kit 6 Update
10 First, I have only used Comet on Sun Application Server/GlassFish, how well it works in other servers - I have no
idea. So for this tutorial I will assume GlassFish
v2 (v3 should also work, and seems to be the recommended solution). Modify domain.xml
The first think you must do is enable
Comet for your site. This is done by modifying your domain.xml
file located in the /config directory of your site.
For my installation that means going to C:\Sun\SDK\domains\domain1\config
and opening the file domain.xml.
Locate the “http-listener” entry for port 8080, and add
the “commetSupport” property: <property
name="cometSupport"
value="true" /> Optionally I also added the following Java-config setting: <jvm-options>-Dcom.sun.enterprise.server.ss.ASQuickStartup=false</jvm-options> Verify that you
have the Comet Libraries
There are at least 2 versions of the Comet libraries. I am
using the default libraries that I got with Glassfish: import com.sun.enterprise.web.connector.grizzly.comet.*; Make the above import will resolve in your IDE by simply
creating an empty servlet and adding the above
import, and compiling. Comet de-mystified:
Ok,
de-mystified may be a bit aggressive, but I will give it a
try. If you don’t understand
client-side From the client (Java Script) you will want to establish
two connections. The first, is the long waiting
Comet connection. The other will be your transient,
or active connection. As a general
rule, you may want to establish your long-lived Comet connection on load of
your page. This connection should be
Asynchronous so that the browser does not lock waiting for a reply. req.open(method, url, true); The key to opening an asynchronous javascript
connection is to use “true” as the third parameter of the req.open
command. NOTE: ‘req’ is an instance of the XMLHttpRequest
or ActiveX object. Server Side: FrameWork Users First…
FrameWork users: please also see the CometWorld
tutorial. For FrameWork users,
this is simple. Simply create a servlet and extend
the FWCometServiceLet. public class AjaxDataServer
extends FWCometServiceLet
implements FWServiceLetInt
{ Then in your FrameWork Execute
method, you simply need to handle the two types of connections public void Execute(FWSession fwSess) throws
FWException { //fwSess.getSessionReq().toConsole(); try { String
stAction = fwSess.getRequest().getParameter("sysAction").toLowerCase().trim(); if ("init".equalsIgnoreCase(stAction)){ fwSess.getSessionResp().setValue("City",
"Servlet Initialized"); }else if ("openchannel".equalsIgnoreCase(stAction)){ fwSess.getSessionResp().AddNode("State",
"open chat"); }else if ("postdata".equalsIgnoreCase(stAction)){ ++cnt; fwSess.getSessionResp().setText("City",
"POST:"+cnt); fwSess.getSessionResp().AddNode("State",
"post message"); }else{ fwSess.getSessionResp().AddNode("State",
"FAILED"); } }catch(Exception
err){ fwSess.getSessionResp().AddNode("sysError", err.getMessage()); } } …and you are done.
Your long-lived Comet connection will establish a non-blocking,
asynchronous connection to the servlet when your
client side Java Script executes the following command; AjaxStartListener(“FWCometServiceLetName”, sysAction); The FWCometServiceLetName is
simply the name of the Servlet which extends the FWCometServiceLet class on the server. The sysAction is whatever meaningful value you wish to send
(see the java code above to see how sysAction is
used). For non-Framework Users
Assumptions: You are using Eclipse and GlassFish.
If you need help setting up this environment, see Workstation
Setup. First let me say, I would not implement Comet in a pure servlet, see the Mini-Servlet Framework for more details – that said, let’s
use a pure servlet example J
just to keep things simple. Setup: 1) Create
a new ‘Dynamic Web Project’ in Eclipse called ‘NativeComet’ 2) Add
a package named com.test 3) Add
a servlet to the new package called ‘Comet’ 4) Create
a simple POJO class called CometState 5) Under
Webcontent create an Inde.html page 6) Under
Webcontent create a JavaScript file called Comet.js
Let’s flesh these all out, then
discuss them in some detail. Index.html <!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>Comet basic</title> <!--
http://localhost:8080/NativeComet/Index.html --> <script
type='text/javascript' src='Comet.js'></script> </head> <body> <form
name="index" id="index"> <table> <tr> <td>Field One </td> <td> <Input name="fieldone" id="fieldone" value="Send This to
all"/> </td> </tr> <tr> <td>click on input<br>to send notify<br>w/field 1 data. </td> <td><Input name="fieldtwo" id="fieldtwo" onmousedown="NotifyListeners('Comet','NotifyListeners');" value="Click
Me"/> </td> </tr> </table> <script
type="text/javascript"> try{ AjaxStartListener("Comet","StartListener"); }catch(err){ alert("[Start.html].script:
Failed to load FRAMEWORK:"+err); } </script> </form> </body> </html> Comet.js function getRequestObj(){ var req = null; if (window.XMLHttpRequest) { // browser
has native support for XMLHttpRequest object req = new XMLHttpRequest(); } else if (window.ActiveXObject) { // try XMLHTTP
ActiveX (Internet Explorer) version req = new ActiveXObject("Microsoft.XMLHTTP"); if (! req){ req=new ActiveXObject("Msxml2.XMLHTTP"); } } return req; } function AjaxStartListener(servletName, Action){ try{ servletName = "/NativeComet/"+servletName; var sysAction = Action; var sysAccess = "StartListener"; var method = 'POST'; var url = servletName.replace(/^http:\/\/[^\/]+\//i, "http://"+window.location.hostname+":"+window.location.port+"/"); var req = getRequestObj(); if(req){ req.onreadystatechange=function(){ ListenerReturn(Action, req); } toSend = "sysAction="+sysAction+"&sysAccess="+sysAccess+"&"+"FieldOne="+document.getElementById("fieldone").value; req.open(method, url, true); req.setRequestHeader("Content-length", toSend.length); req.setRequestHeader("content-type","application/x-www-form-urlencoded");
req.send(toSend); } }catch(err){ alert("[Comet].[AjaxStartListener] ERROR 0100:"+err); } } function NotifyListeners(servletName, Action){ try{ servletName = "/NativeComet/"+servletName; var sysAction = Action; var sysAccess = "NotifyListeners"; var method = 'POST'; var url = servletName.replace(/^http:\/\/[^\/]+\//i, "http://"+window.location.hostname+":"+window.location.port+"/"); var req = getRequestObj(); if(req){ req.onreadystatechange=function(){ NotifyReturn(Action, req); } toSend = "sysAction="+sysAction+"&sysAccess="+sysAccess+"&"+"FieldOne="+document.getElementById("fieldone").value; req.open(method, url, true); req.setRequestHeader("Content-length", toSend.length); req.setRequestHeader("content-type","application/x-www-form-urlencoded");
req.send(toSend); } }catch(err){ alert("[Comet].[NotifyListeners] ERROR 0100:"+err); } } function ListenerReturn(Action, req){ if (req.readyState == 4 && (req.status
== 200)) { ProcessXMLResp(req.responseXML); } else if (req.readyState == 4 && (req.status
!= 200)) { alert("[Comet].[ListenerReturn]:
ERROR 0300: Your request may have timed out. Please try again."+req.status); } } function NotifyReturn(Action, req){ if (req.readyState == 4 && (req.status
== 200)) { //just eat it! } else if (req.readyState == 4 && (req.status
!= 200)) { alert("[Comet].[ListenerReturn]:
ERROR 0300: Your request may have timed out. Please try again."+req.status); } } function ProcessXMLResp(inXML){ document.forms['index'].elements["fieldone"].value = inXML.getElementsByTagName("FieldOne")[0].childNodes[0].nodeValue; AjaxStartListener("Comet","StartListener") } Comet.java package com.test; import java.io.IOException; import javax.servlet.ServletConfig; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import com.sun.enterprise.web.connector.grizzly.comet.*; /** * Servlet
implementation class Comet */ public class Comet extends HttpServlet { private static
final long serialVersionUID = 1L; private String contextPath; /** * @see HttpServlet#HttpServlet() */ public Comet() { super(); // TODO Auto-generated
constructor stub } public void init(ServletConfig config) throws ServletException { super.init(config); contextPath = config.getServletContext().getContextPath()+"/Comet"; CometEngine cometEngine = CometEngine.getEngine(); CometContext context = cometEngine.register(contextPath,cometEngine.AFTER_SERVLET_PROCESSING); context.setExpirationDelay(120000); } /** * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse
response) */ protected void doGet(HttpServletRequest
request, HttpServletResponse response) throws ServletException, IOException { doPost(request,response); } /** * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse
response) */ protected void doPost(HttpServletRequest
request, HttpServletResponse response) throws ServletException, IOException { CometState cs = new CometState(request, response); CometEngine cometEngine = CometEngine.getEngine(); CometContext cometContext = cometEngine.getCometContext(contextPath); System.out.println("Entered Servlet:"+cs.getReply()); if(request.getParameter("sysAccess").equalsIgnoreCase("StartListener")){ try{ CometRequestHandler handler = new CometRequestHandler(); handler.clientIP =
request.getRemoteAddr(); handler.attach(cs); cometContext.addCometHandler(handler); }catch(Exception
err){ err.printStackTrace(); } }else
if(request.getParameter("sysAccess").equalsIgnoreCase("NotifyListeners")){ try{ cometContext.notify(cs); }catch(Exception
err){ err.printStackTrace(); } } } private class CometRequestHandler implements CometHandler<CometState> { private
CometState cometState; public
String clientIP; @Override public
void attach(CometState arg0) { System.out.println("\n**Attached CometState."); cometState = arg0; } @Override public
void onEvent(CometEvent
event) throws IOException { try
{ if (event.getType()
== CometEvent.NOTIFY) { System.out.println("\n**Notify Client:\n"+((CometState)event.attachment()).getReply()); cometState.getResponse().setContentType("text/xml"); cometState.getResponse().setHeader("Cache-Control",
"no-cache"); cometState.getResponse().getWriter().write(((CometState)event.attachment()).getReply()); cometState.getResponse().getWriter().flush(); event.getCometContext().resumeCometHandler(this); } }
catch (Throwable t) { t.printStackTrace(); } } @Override public
void onInitialize(CometEvent
event) throws IOException { //
TODO Auto-generated method stub System.out.println("\n**Initialize CometState."); } @Override public
void onInterrupt(CometEvent
event) throws IOException { try{ System.out.println("\n**Interrupt CometState."); cometState.getResponse().setContentType("text/xml"); cometState.getResponse().setHeader("Cache-Control",
"no-cache"); cometState.getResponse().getWriter().write("<INTERRUPTED>FAILED</INTERRUPTED>"); cometState.getResponse().getWriter().flush(); event.getCometContext().resumeCometHandler(this); }catch(Exception
err){ err.printStackTrace(); } } @Override public
void onTerminate(CometEvent
event) throws IOException { onInterrupt(event); } } } CometState.java package com.test; import java.util.Enumeration; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; public class CometState { HttpServletRequest request; HttpServletResponse response; String reply; public CometState(HttpServletRequest inRequest, HttpServletResponse inResponse){ request
= inRequest; response
= inResponse; reply
= BuildReply(); } public HttpServletRequest getRequest()
{ return
request; } public HttpServletResponse getResponse()
{ return
response; } public String getReply(){ return
reply; } private String BuildReply(){ StringBuffer sbReply = new StringBuffer(); try{ sbReply.append("<?xml version=\"1.0\"
encoding=\"ISO-8859-1\" ?>\n<Start>"); Enumeration
e = getRequest().getParameterNames(); while(e.hasMoreElements()){ String
key = (String)e.nextElement(); String[]
value = getRequest().getParameterValues(key); sbReply.append("<"+key+"
value='"+value[0]+"'>"+value[0]+"</"+key+">"); } sbReply.append("</Start>"); }catch(Exception
err){ err.printStackTrace(); } return
sbReply.toString(); } } The NativeComet
Application
The NativeComet application is a
good exercise for both FrameWork and non-FrameWork users. It is a painfully simple and
ugly application, but I think it provides a better Comet-framework to build
upon than the “chat” examples. The Index.html is pretty straight forward (Look mom, no iFrames!). It imports the comet.js
script, and has two input fields. Input FieldOne is
the value that will be broadcast to users. Input FieldTwo
is essentially a button, when clicked it will submit the value of FieldOne to be sent to all other clients. If you haven’t
guessed, this application is best tested with multiple client instances running J. The comet.js JavaScript file is
where the rubber starts to meet the road. This contains the Java Script
Functions: function
getRequestObj() Returns the
HTTP Object for the function
AjaxStartListener(servletName,
Action) Called to
initiate the listener on the Server function
NotifyListeners(servletName,
Action) Called to
force client updates for all registered listeners function
ListenerReturn(Action, req) Called when
the client has received an update function
NotifyReturn(Action, req) Called when
the notification has been issued. Nothing is done here. function
ProcessXMLResp(inXML) Process
the returned XML. In this case, simply put the new FieldOne
node value in the input box Comet Java Code
The Comet java class is simply a servlet
with the init(), doGet(),
and doPost() overridden and an inner class named CometRequestHandler. The CometRequestHandler
implements CometHandler, this is the key to the
entire process – more on that in a minute. public void init(ServletConfig config) throws ServletException In the init method contains a
number of key setup activities, so let’s take them one line at a time; contextPath = config.getServletContext().getContextPath()+"/Comet"; The ContextPath
refers to the group you are subscribing too.
This is important if you are planning on having multiple
conversations. Think of it as the
phone number of the party-line you are joining, or setting up. It reality it
is the key to the Hashmap which is the CometContext. CometEngine cometEngine = CometEngine.getEngine(); Get an instance of the CometEngine. The Comet Engine is a Singleton, so it is
not initialized until it is first invoked. CometContext context = cometEngine.register(contextPath,cometEngine.AFTER_SERVLET_PROCESSING);
Create a CometContext
context.setExpirationDelay(120000); Set the delay 1000 x Number of
seconds (the above is 120 seconds). The default value is 30 seconds. After the specified time, the connection
will expire, and an informational Exception will be thrown. doPost The doPost
method does the heavy lifting, the doGet just
forwards to post. Here again we need
to get an instance of the CometEngine, and the CometContext. We also see the CometState
class for the first time (more on that later). CometState cs =
new CometState(request,
response); CometEngine cometEngine = CometEngine.getEngine(); CometContext cometContext = cometEngine.getCometContext(contextPath); if(request.getParameter("sysAccess").equalsIgnoreCase("StartListener")){ try{ CometRequestHandler handler = new CometRequestHandler(); handler.clientIP = request.getRemoteAddr(); handler.attach(cs); cometContext.addCometHandler(handler); }catch(Exception err){ err.printStackTrace(); } }else if(request.getParameter("sysAccess").equalsIgnoreCase("NotifyListeners")){ try{ cometContext.notify(cs); }catch(Exception err){ err.printStackTrace(); } } Add Listener The rest of the code controls the
activites which our servlet
can perform, namely “Start Listening”, and “Notify All Listeners of a
change”. Here we use the “sysAccess” value that we passed from the JavaScript, to
control which of the two activities to perform. When sysAccess=”StartListener”, we are telling to server to register this
client instance with the CometEngine, and to notify
it when any of the other participants issues a “NotifyListeners”
command. When a “StartListening”
request is received, the servlet creates a new CometRequestHandler (defined as an in-line class), it then attaches an instance of the CometState
class (more on this class later) to the new Hander, and adds the handler to
the CometContext object. The client connection is
suspended, and is now awaiting notification of a change. Notify When the client issues a “NotifyListeners” request, it simply calls the .notify method of the CometContext. The CometContext, then calls the CometRequestHandler.notify method for every
registered listener. Notice that the .notify
method is taking an instance of the CometState (cs) for the requestor. This will be the source for the
message that will be broadcast to all listeners. CometRequestHandler
It is the job of the CometContext
to marshal the incoming request to all listeners via .notify. For every
listener there is an instance of the CometRequestHandler
handler class. Each instance of the CometRequestHandler class has a copy of its original
request, and response object stored in the CometState
class. So when the CometRequestHandler receives a
notification (i.e .notify is invoked) the CometState.response
object is written too, and the thread is resumed. This causes the update to occur on the client. . . cometState.getResponse().getWriter().write(((CometState)event.attachment()).getReply()); . . In the above line of code, taken from the .notify method, we are writing to an
instance of the listener (CometRequestHander) in
the CometContext hash - by accessing the instance
variable cometState,
which was set in the .attach
method. What we are writing, is coming from the request event which triggered
the .notify. The Event.Attachment
is assigned using the cometContext.notify(cs) (where cs = CometState object created
in the doPost). And that completes the circle. If
you are confused, simply walk the code a few times, it will all become clear(er). Running the Sample
Start 3
instances of your browser, and run the following URL; http://localhost:8080/NativeComet/Index.html
Change one of the “Field One”
text fields, and then click on Field Two.
If all is well, you should see
the updates on the page. Conclusion
Comet is a very functional framework for doing server side
push, however, I have not done any detailed analysis of the burden imposed on
the server. I believe the need for something
like Comet is real, though limited, but I would caution against over
using it. Make sure you really have a need and use it with caution. The source for Comet is available, and would recommend
taking a look. FrameWork users, please also see the CometWorld
tutorial. |
|||||
|
|
Copyright 2009. All rights reserved by S. Chappell |