Websockets and Java EE
Work has begun on the Servlet Expert Group to come up with a standard set of atoms for WebSocket applications in a Java EE environment. I'm not 100% convinced that the servlet spec is the best place to do this but I suppose it saves us the hassle of submitting another JSR and all the work that would entail. However, that's where the work is being done for now, so whatever. This is something I've naturally thought a lot about over the last couple of years as I've worked to implement and promote WebSockets so here is roughly how I would do it.
There's a lot of "we have to have something in the servlet spec" comments floating around. I don't necessarily agree with that sentiment but there is one thing I would: support for the upgrade request header. This would have the servlet container automatically detect a request with this header. The container would then query the system for all implementations of upgrade request handlers registered. Then, based on which protocol each handler has said it deals with, hand off processing to that handler. At this point, the request is not necessarily in the servlet stack at all but a completely separate API that might not even be back by a JSR. This has multiple advantages in my mind.
- WebSockets uses this header and so this feature is already going to be needed for WebSockets to work.
- It provides a general hook and customization point at which developers can choose to extend the container without resorting to modifying the source of the container itself to handle the new protocol. More importantly, it does so in a standardized fashion allowing for portability at least among the higher levels of the API. Certain container specific code might be needed for such things as suspending/parking requests and the like. But these could easily be abstracted away such that the protocol logic itself could be fairly portable.
- This also decouples new APIs such as WebSockets from the constraints of the legacy(?) servlet API.
- Using this hook, someone could homebrew their own protocols using the upgrade mechanism and be able to integrate in to their container with little fuss.
Barring some tweaks here and there, that's all the servlet spec really needs to offer. So where does that leave WebSockets? Why, in its own JSR, of course! I really think it needs its own separate JSR with its own EG populated by folks who know and understand the technology. More importantly, this separate EG would feel less bound to make this new API consistent/compatible with the servlet API. While there is certainly a need for integration with the servlet spec for access to things like the session data, binding the two APIs would really hamstring the interesting things we could do with WebSockets. So what would all this look like? Let's look at some mostly java mockups.
First, the servlet side upgrade handler. It would look something like this. If you've used @WebServlet, this should look familiar to you.
@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface HttpUpgradeHandler { /** * The name of the target protocol */ String value() default {}; /** * The description of the handler */ String description() default ""; /** * The display name of the handler */ String displayName() default ""; } |
And the interface exposed for implementations:
public interface UpgradeHandler { public void doUpgrade(ServletRequest request, ServletResponse response) throws UpgradeException; } |
This provides a general mechanism for any application developer to register a new handler thus extending the functionality of the container without need for hacking the container itself. So what would one of these handlers look like? Maybe something like this:
@HttpUpgradeHandler(protocol ="websocket", urlPatterns={"/sample"}) public class SampleWebSocketContext extends WebSocketContext { @Override public void customizeHandshake(ServletRequest request, ServletResponse response) { super.customizeHandshake(request, response); // insert cookies here } @Override public WebSocketContext createContext(ServletRequest request, ServletResponse response) { return new SampleWebSocketContext(request, response); } private static class SampleWebSocket extends DefaultWebSocket { private String userName; public SampleWebSocketContext(ServletRequest request, ServletResponse response) { super(request, response); userName = request.getParameter("user"); } public String getUserName() { return userName; } } } |
By the time control gets handed off to WebSocketContext, the appropriate headers requesting the upgrade have already been found and validated against name provided in the annotation. All that's left to do here is perform the actual upgrade. In this case, I added a method to customize the handshake for cases where, perhaps, the developer wants to add a header field or validate an existing header value. So what comes from the servlet specification is that annotation, the interface, and an exception. There might be more as details are hashed out, but that's about all I would say that the servlet specification should deliver. That paves the way for a WebSocket JSR export group to define its own atoms.
Not entirely surprisingly, I imagine a WebSocket JSR would end up with something that approximates what I built in Grizzly. That said, there are certain ... rough points that need to be smoothed over but that can most easily be smoothed over by 1) an expert group and 2) user feedback. Still, here's how I see one approach at cracking this nut.
In Grizzly, we have a WebSocketApplication which is the center of the developer's world when building WebSocket applications. I've recently come to prefer WebSocketContext instead. It's largely semantic but whatever. This is where all the application logic, if there is any, would likely want to live. Between the WebSocketContext and any potential custom implementation of WebSocket, there's a lot of room of custom logic. But none of this is tied to the servlet spec beyond the initial use of the ServletRequest and ServletResponse which I think is key.
This separation allows the WebSocket JSR evolve without needing to worry about servlet evolution. More importantly, it allows innovation to happen without needing a JSR at all. Because there's a generic upgrade hand off, developers are free to extend their container in whatever crazy directions they choose. Imagine implementing a custom JMS transport over HTTP and tunneling through your web server. Or FTP. Or SNMP. Or some completely custom, proprietary protocol. This extension point takes the EE platform one huge step down the road of enabling developers to build applications on and extending the EE platform without the need for container specific hacks. It aslo, of course, paves the way for future JSRs to provide new functionality without extraneous requirements and constraints from external JSR lifecycles.
At the risk of repeating myself, I'd like to say again that this proposal is not perfect. It almost certainly has some corner cases I haven't thought of or addressed here. But it's at least along the lines of how I've been imagining something like this would work for the last couple of years. It's a start. I have the code I showed above and more in a git hub repository if you'd like to take a look in more detail. I'd love to hear any comments you might have.
Grizzly 2.0 and Comet
With the release of Grizzly 2.0, I'd like to highlight one of its newest features: comet support. When implementing support for comet in grizzly 2.0, I tried as best I could to make it API compatible with your 1.9 applications. That said, there are a handful of changes that were made to clean things up a little and simplify some of the implementation details. I'll outline the changes you'll need to make to existing apps before building a new one from scratch.
Most of the changes are largely cosmetic. The first change you'll notice is that the package names have changed. With the advent of 2.0, grizzly now lives under the org.glassfish.grizzly rather than com.sun.grizzly. CometHandler is the core of any comet application code and it, too, has received a minor makeover:
attach()has been removed from the interface. This won't actually affect your code unless you happen to have used@Override. This method has been typically used to attach things like theHttpRequestobject to yourCometHandlerinstance. In reality, this kind of information can be passed via the constructor or a method on your subclass. Grizzly doesn't actually reference this method or the attachment in any way so it has been removed from the interface. You are, of course, welcome to continue usingattach()if you'd like. But since Grizzly never uses it internally, there's no sense in forcing all implementations to implement it.- Four new methods have been added added to the interface:
Response getResponse();void setResponse(Response response);CometContext<E> getCometContext();void setCometContext(CometContext<E> context);
Your
CometHandlerimplementation needs only provide these getters/setters and the fields they imply. Grizzly will handle the setting of those values itself. It didn't make it into 2.0 (because I got sidetracked) but 2.0.1 will have a DefaultCometHandler that takes care of that for you if you choose. The type of<E>should be consistent with the data type you want to pass to your handlers when an event occurs. Typically, this will simply be aStringbut, of course, could be almost anything. - The type of comet events have been modernized. In 1.9 you had a set of int constants to manage. In 2.0, I've changed them to an enum. This probably won't affect your code overly much but you do need to change any thing like
CometEvent.READtoCometEvent.Type.READ. Enum comparisons being what they are, everything else should just compile as long as you used the named constants. - A great number of items have been deprecated as well. The compiler will highlight these for you. All the deprecated methods should continue to work as you would expect, but I would recommend changing over when you get the chance. (You're already having to tweak code anyway.) We've striven to keep things API compatible as much as possible to ease the transition, but these methods might go away in the future so it's best to be prepared.
The riskiest change, to my mind, is that the execution type has been removed. The execution model of grizzly 2.0 makes supporting that feature complicated. However, when examining every comet app and unit test I could find, I didn't find that this feature was really ever used that much anyway. Indeed, when asked about it some of the original authors weren't really entirely sure about the feature anyway. If you find that you really need that feature the decision can be readdressed. But I have to warn you, prepare yourself for some disappointment because it seems unlikely to happen. But you never know.
That should cover the changes you'll see. There are a number of other changes behind the scenes, of course, but hopefully we've done a good job of shielding you from those. Now that we've seen what changes you need to make to an existing application, let's see what it takes to write a new one. With 2.0, creating a grizzly instance to run your apps becomes much, much simpler. From here on out, we'll take a look at the comet click counter example you can find in the samples folder of the source repository.
To start grizzly, you just need a few lines of code:
HttpServer httpServer = HttpServer.createSimpleServer("./", PORT); httpServer.getServerConfiguration().addHttpHandler(new ServletHandler(new LongPollingServlet()), QUERY_PATH); final Collection<NetworkListener> listeners = httpServer.getListeners(); for (NetworkListener listener : listeners) { listener.registerAddOn(new CometAddOn()); } httpServer.start(); |
This snippet sets up all the HTTP bits you'll need to serve up your comet application. With that set up, the next piece is the servlet manage your requests and the handler that provides your application logic.
public class LongPollingServlet extends HttpServlet { final AtomicInteger counter = new AtomicInteger(); private static final long serialVersionUID = 1L; private String contextPath = null; @Override public void init(ServletConfig config) throws ServletException { super.init(config); ServletContext context = config.getServletContext(); contextPath = context.getContextPath() + "/long_polling"; CometEngine engine = CometEngine.getEngine(); CometContext cometContext = engine.register(contextPath); cometContext.setExpirationDelay(5 * 30 * 1000); } @Override protected void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { CometEngine engine = CometEngine.getEngine(); CometContext<HttpServletResponse> context = engine.getCometContext(contextPath); final int hash = context.addCometHandler(new CounterHandler(res, counter)); } @Override protected void doPost(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { counter.incrementAndGet(); CometContext<HttpServletResponse> context = CometEngine.getEngine().getCometContext(contextPath); context.notify(null); PrintWriter writer = res.getWriter(); writer.write("success"); writer.flush(); } } |
public class CounterHandler extends DefaultCometHandler<HttpServletResponse> { private HttpServletResponse httpResponse; private AtomicInteger counter; CounterHandler(HttpServletResponse httpResponse, final AtomicInteger counter) { this.httpResponse = httpResponse; this.counter = counter; } public void onEvent(CometEvent event) throws IOException { if (CometEvent.Type.NOTIFY == event.getType()) { httpResponse.addHeader("X-JSON", "{\"counter\":" + counter.get() + " }"); PrintWriter writer = httpResponse.getWriter(); writer.write("success"); writer.flush(); event.getCometContext().resumeCometHandler(this); } } public void onInterrupt(CometEvent event) throws IOException { httpResponse.addHeader("X-JSON", "{\"counter\":" + counter.get() + " }"); PrintWriter writer = httpResponse.getWriter(); writer.write("success"); writer.flush(); } } |
The init() in the servlet registers the comet context we'll use to track all the handlers that various requests will park. That's the java code you need. Without overdoing it, a quick look at the javascript side should cover it.
var counter = { 'poll' : function() { new Ajax.Request('long_polling', { method : 'GET', onSuccess : counter.update }); }, 'increment' : function() { new Ajax.Request('long_polling', { method : 'POST' }); }, 'update' : function(req, json) { $('count').innerHTML = json.counter; counter.poll(); } } |
When the page loads, it makes the initial GET request to the server. That request gets parked in doGet() above. That request will stay open until the user clicks on a link in the html from the sample application. That click triggers a POST post. In doPost(), we call notify() on the context which triggers the parked CometHandler to finally respond to that initial GET request. Once that request finally responds, the javascript code will make another GET which gets parked and the process repeats itself. In this way multiple clients can track and update the counter on the server.
This is just a basic example, of course, ut it covers all the basic building blocks you'll need for more complex applications. For more examples, you can look through the sample applications. There are more examples in 1.9 than 2.0 right but, changes listed above aside, should help you find the answers. We're putting more and more documentation up at the website and, of course, we have mailing lists where you can post your questions. I think you'll find that 2.0 is much more pleasant to work with. So please take a look and give us some feedback on the lists. We'd love to hear about your experiences.
Grizzly/Kenai update
Well, it took longer than we'd hoped but we're finally live on the kenai infrastructure. Grizzly is one of a handful of pilot migrations trying to work the kinks out so we may still have a few issues here and there but I think the worst is behind us. I'd like to run down the big changes at least to help you find your way in the new set up.
Issue Tracking
The first big change that I'm personally very excited about is the issue tracker. We're now using Jira instead of the ancient bugzilla instance we've been using. All the issues have been imported and none of the bug IDs should have changed. The URLs are all different of course. You can find the new tracker at http://java.net/jira/browse/GRIZZLY and it's also listed on the new home page location of http://grizzly.java.net. (All projects on the new kenai system have basically moved from *.dev.java.net to *.java.net.) Jira gives a us a clearer view of what's remaining and where each line of development stands. I've cleaned up the tracker where I thought it needed it so it should be much easier to track your favorite issues now.
Subversion
Subversion has also moved for those of you who have checked out the repository. The new URL is https://svn.java.net/svn/grizzly~svn so each line of development can now be found at:
- 1.9.x -> https://svn.java.net/svn/grizzly~svn/trunk/code
- 2.x -> https://svn.java.net/svn/grizzly~svn/branches/2dot0
- 1.0.x -> https://svn.java.net/svn/grizzly~svn/branches/grizzly1.0
To switch your local copy to the new url simply this from the root of your local workspace (using trunk as an example)
svn switch --relocate https://grizzly.dev.java.net/svn/grizzly/trunk/code https://svn.java.net/svn/grizzly~svn/trunk/code
Your password may or may not need to be reset. Sadly, this is one area where we've hit a number of bumps. Resetting is easy enough from the website just be aware that you might need to do this. If you have commit access at least. This is the same login you'd use for the jira so you might need to do this even as an observer.
Mailing Lists
The addresses have changed for the mailings. For dev, it's dev@grizzly.java.net. For users, it's users@grizzly.java.net. The old addresses forward to the new ones so if you send to the old ones, they'll continue to find their way to the list but I'm not sure how long those forwards will be around. It would be wise to take a second to update your address books just to be safe.
Web Site
Finally, as I mentioned, the website has a new location: http://grizzly.java.net. The old URL will forward so any bookmarks should be fine for a while. But again, there's been no indication of how long such forwarding will be in place so it wouldn't hurt to update your links. It should look pretty familiar as we've tried to retain the old look and feel but it is slightly different. Before anyone complains/comments, I know some of the source/javadoc links on the left don't work yet. We're still working on realigning to the new infrastructure and we'll have to clean those up as we go. We'll also be reworking the content on the landing page since 2.0 figures to loom larger in the coming year.
That's about it. Take some time to explore the new system. Let us know if you run into any issues. Rather than posting to the mailling list, it'd help us track things if you'd file issues against the www component in jira: http://java.net/jira/browse/GRIZZLY/component/10009. I think you'll find the new system much more pleasurable to work with, though. Cheers.
Scripts to Help with GlassFish Development
GlassFish is a big enough source base that sometimes you just need a little help managing the development lifecycle. Over time I've developed a number of scripts that I use while working on either grizzly or glassfish to help manage the load. After a number of discussions, I've decided to share them in the hopes they will help others, too. Not of all these scripts are really glassfish or grizzly related so you might find them useful in your own projects as well. You can check out these scripts using git from this url: git://kenai.com/schema-doc~git-repo.
There are several scripts but I'll try highlight the more interesting ones. Just a note, though. These are bash scripts that have grown organically for a long time. So they're not necessarily going to be pretty. Some might even consider some of the hoops I've jumped through "stupid." That's fine. I'm not getting a Ph.D. with these. They work and that's enough for me. But anyway.
General Scripts
The first set of scripts that should apply to almost any project.
| script | description |
|---|---|
| changed.sh unknown.sh |
These scripts will show you any changed (or unknown) files for whichever VCS you're using. They currently support subversion, mercurial, and git. The git support is new-ish so let me know if something's off |
| findInJar.sh | As the name implies, this script will find every jar in or under the current director and grep for, well, really whatever you tell it. I wrote it with looking for classes in mind but since it just greps the contents, it will find anything that matches. It seems like everyone eventually writes a similar script so maybe this will save some people a little time. |
| failedTests.sh | This script requires that you use maven. It will run mvn surefire-report:report-only and scan for any failed tests. If it finds any, it will use the open command to open the report html in your default browser. It can, optionally, run your tests before looking for failures. If you'd like it to do this, simply pass --run to the script. |
GlassFish/Grizzly Related Scripts
Obviously, these scripts will be of little interest to those not working with some aspect of glassfish development. But if you're not, you're probably reading the wrong blog entry anyway. All of these scripts rely on the presence of environment variables. The scripts are set up to check for the variables and prompt you to define them so I won't go into them here. Just be aware that at first you'll have to define a few variable before these scripts will work for you. And these scripts need a UNIXy environment so if you're on Windows, you'll need something like cygwin to make these work. But even then, I've not tried these with cygwin so you might have issues even still. I'll refer to some of these variables by name below, but the script will walk you through what to set to what.
| script | description |
|---|---|
| distro.sh | This script will build the glassfish distribution bundles. Most of the heavy lifting is really done by maven but this script goes a step further and extracts the "glassfish" distribution of into ${GF_INSTALL}. Executed without parameters, it will build the distro, remove the current install, and unzip the new one. There are 3 options you can pass to this one:
|
| devtests.sh | This one is really specific. This will run the webtier devtests after reconfiguring the glassfish install in ${GF_INSTALL}. It can be run from anywhere so you don't need to worry about where to launch this one. Once the devtests finish, it will open the test_results.html displaying the results of the tests. This one takes a while to run. There are a lot of tests... |
| single.sh | Similar to devtests.sh, this test will run a single test in the webtier devtests. Just pass it the name of the directory while in v2/appserv-tests/devtests/web and it'll do the rest. |
| quicklook.sh | This script runs the quicklook tests to test your v3 tree. This script takes --debug and will run the quicklook tests using the mvnDebug script. |
| updateBundle.sh | This script compiles the current maven module and copies the resultant jar into your glassfish install. This should work for any glassfish-related project. I use it with grizzly, too, for example. |
| rebundle.sh | This script will scan svn looking for changed files and try to determine the module owning each file. It will then call updateBundle for each of those modules and update your glassfish install. Passing --start will then launch glassfish with your updated code. |
| startgf.sh stopgf.sh |
These scripts will start/stop glassfish from wherever you may be in the filesystem. Passing --debug will launch glassfish in a debug VM. It will also update your domain.xml such that the launch will pause until you connect to it with a debugger so be aware that things will appear to hang until you do. |
| tailgf.sh | This will tail your glassfish's server.log from wherever you are in the filesystem. If glassfish is not yet running, it will also truncate the logs first. |
That about does it. I use these scripts daily and I find them quite useful. Hopefully, you do as well.
GlassFish Web Sockets Sample
A few weeks back I blogged about the impending GlassFish support for the emerging Web Sockets standard. To update, I have some good news and some some ok news. The good news is that the implementation has stabilized enough to show some concrete code. The ok news is that, despite my best efforts and overly persistent petitioning, this support won't show up in 3.0.1. You'll need to use the GlassFish nightly builds to play with it. Hopefully that's OK given that web sockets are still evolving anyway and browser support is spotty at best. With that out of the way, let's look at a simple example.
For familiarity's sake, and for simple comparison, I ported the comet chat sample in grizzly to use web sockets. I wanted to see how different the code would look between two similar applications. So if you're familiar with the comet example, the web socket example should be very familiar. At the heart of grizzly's/glassfish's web socket support is the WebSocketApplication. For our chat, it's pretty simple: every time someone sends a message, broadcast it to everyone. The code is pretty simple:
package com.sun.grizzly.samples.websockets; import com.sun.grizzly.tcp.Request; import com.sun.grizzly.tcp.Response; import com.sun.grizzly.websockets.DataFrame; import com.sun.grizzly.websockets.WebSocket; import com.sun.grizzly.websockets.WebSocketApplication; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.logging.Level; public class ChatApplication extends WebSocketApplication { List<webSocket> sockets = new ArrayList<webSocket>(); @Override public WebSocket createSocket(Request request, Response response) throws IOException { final ChatWebSocket socket = new ChatWebSocket(this, request, response); sockets.add(socket); return socket; } public void onMessage(WebSocket socket, DataFrame frame) { final String data = frame.getTextPayload(); if (data.startsWith("login:")) { login((ChatWebSocket) socket, frame); } else { broadcast(data); } } public void onConnect(WebSocket socket) { } private void broadcast(String text) { WebSocketsServlet.logger.info("Broadcasting : " + text); for (WebSocket webSocket : sockets) { send(webSocket, text); } } private void send(WebSocket socket, String text) { try { socket.send(text); } catch (IOException e) { WebSocketsServlet.logger.log(Level.SEVERE, "Removing chat client: " + e.getMessage(), e); onClose(socket); } } public void onClose(WebSocket socket) { sockets.remove(socket); } private void login(ChatWebSocket socket, DataFrame frame) { if (socket.getUser() == null) { WebSocketsServlet.logger.info("ChatApplication.login"); socket.setUser(frame.getTextPayload().split(":")[1].trim()); broadcast(socket.getUser() + " has joined the chat."); } } } |
As you can see, we implement some very basic logic here. This, of course, could be as complex as you need it it to be. In a real chat application, for example, we'd have rooms/channels and the like but we're keeping it simple here. The other class to note is ChatWebSocket. There's not much need to provide custom WebSocket implementations but in this case, I chose to store the chat user's user name on the WebSocket. I could've used a Map in ChatApplication and used the default implementation but this seemed a bit more ... OOPish. The implementation there is, again, quite simple:
package com.sun.grizzly.samples.websockets; import com.sun.grizzly.tcp.Request; import com.sun.grizzly.tcp.Response; import com.sun.grizzly.websockets.BaseServerWebSocket; import java.io.IOException; public class ChatWebSocket extends BaseServerWebSocket { private String user; private final ChatApplication app; public ChatWebSocket(final ChatApplication listener, Request request, Response response) { super(listener, request, response); app = listener; } public String getUser() { return user; } public void setUser(String user) { this.user = user; } @Override public void send(String data) { super.send( toJsonp(getUser(), data) ); } @Override public void close() throws IOException { WebSocketsServlet.logger.info("closing : " + getUser()); app.remove(this); super.close(); } private String toJsonp(String name, String message) { return "window.parent.app.update({ name: \"" + escape(name) + "\", message: \"" + escape(message) + "\" });\n"; } private String escape(String orig) { StringBuilder buffer = new StringBuilder(orig.length()); for (int i = 0; i < orig.length(); i++) { char c = orig.charAt(i); switch (c) { case '\b': buffer.append("\\b"); break; case '\f': buffer.append("\\f"); break; case '\n': buffer.append("<br />"); break; case '\r': // ignore break; case '\t': buffer.append("\\t"); break; case '\'': buffer.append("\\'"); break; case '\"': buffer.append("\\\""); break; case '\\': buffer.append("\\\\"); break; case '<': buffer.append("<"); break; case '>': buffer.append(">"); break; case '&': buffer.append("&"); break; default: buffer.append(c); } } return buffer.toString(); } } |
Again, it's pretty simple. There's some extra code for escaping the text since in our javascript, we'll simply eval() the text and let the browser work its magic. With these two pieces in place, we're almost done on the server. In order to let the web sockets system know of our application, we need to register it. (This is an optional step that we'll cover later.) In our case, we have a servlet in our web app. The servlet actually doesn't do anything. It doesn't override doGet() or doPost(). It only really exists for this one method:
@Override
public void init(ServletConfig config) throws ServletException {
WebSocketEngine.getEngine().register(config.getServletContext().getContextPath() + "/chat", app);
} |
This line registers the request URI with the WebSocketEngine so that it knows that when it sees a web socket request come in that matches that URI, to hand things off to the appropriate application. For our chat sample, we need an application to handle the logic related to running a chat system. However, if you don't need such logic, you don't even have to register (or write) and application class at all.
When a web socket request comes in, grizzly will look for any registered application for that URI. If it can be found, handling is handed off and grizzly's request handling is done. However, if no application can be found, grizzly can still process the web socket request by deferring the request to whatever the ultimate endpoint is. Grizzly will still frame anything written back to the client from, say, the servlet such that your web socket client gets a proper response. This scenario doesn't support bidirectional conversations, but it does allow you to expose any URL to a web socket client with no additional work.
There is one final step before we can try out the sample. Web socket support is disabled by default in grizzly and in glassfish so we must enable it first. In glassfish, we can do this by issuing this command:
asadmin set configs.config.server-config.network-config.protocols.protocol.http-listener-1.http.websockets-support-enabled=true |
You will need to adjust the name of the protocol element to match your system if you changed any of that, but if you use the standard configuration, that's all you need. If you're using grizzly outside of glassfish, you have neither asadmin nor a domain.xml. For that, you'll need to enable web socket support on your SelectorThread:
SelectorThread st = new SelectorThread(); /* whatever other settings you might need */ st.setAsyncHandler(new DefaultAsyncHandler()); st.setEnableAsyncExecution(true); st.getAsyncHandler().addAsyncFilter(new WebSocketAsyncFilter()); |
That does it for the server side. On the client side we have the usual mix of HTML and javascript. I don't want to go in to depth on most of that but I do want to take a look at one particular piece. In application.js, we have this code:
var app = { url: 'ws://localhost:8080/grizzly-websockets-chat/chat', initialize: function() { if ("WebSocket" in window) { $('login-name').focus(); app.listen(); } else { $('missing-sockets').style.display = 'inherit'; $('login-name').style.display = 'none'; $('login-button').style.display = 'none'; $('display').style.display = 'none'; } }, listen: function() { $('websockets-frame').src = app.url + '?' + count; count ++; }, login: function() { 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 = name + ':'; $('login-button').disabled = true; $('login-form').style.display = 'none'; $('message-form').style.display = ''; websocket = new WebSocket(app.url); websocket.onopen = function() { // Web Socket is connected. You can send data by send() method websocket.send('login:' + name); }; websocket.onmessage = function (evt) { eval(evt.data); $('message').disabled = false; $('post-button').disabled = false; $('message').focus(); $('message').value = ''; }; websocket.onclose = function() { var p = document.createElement('p'); p.innerHTML = name + ': has left the chat'; $('display').appendChild(p); new Fx.Scroll('display').down(); }; }, post: function() { var message = $F('message'); if (!message > 0) { return; } $('message').disabled = true; $('post-button').disabled = true; websocket.send(message); }, update: function(data) { if (data) { var p = document.createElement('p'); p.innerHTML = data.name + ': ' + data.message; $('display').appendChild(p); new Fx.Scroll('display').down(); } } }; |
Most of that is prototype/scriptaculous code for managing the UI but notice in login() how we connect to the server and define our callbacks for when we receive various events. In post() we send our entry to the server and in update() we take what the server gives us and update the UI accordingly. It's a very simple and naive system to be sure but serves to illustrate the basics without getting mired down in details of building a large scale, fault tolerant system. Yes, I know there are things that could be improved with this or that detail. But I'm not building a replace for Yahoo! chat.
If you want to see the code in full, you can browse the grizzly subversion repository here.
Now, there *is* some bad news but it's temporary at least. This code is in the trunk of grizzly's subversion repository and will be included in the 1.9.19-beta2 release. This hasn't been integrated into glassfish 3.1 yet but will be soon. Until then, you'll need to build your own grizzly tree and glassfish distribution with the root pom updated to use 1.9.19-SNAPSHOT. This is, indeed, quite ugly but we should be integrating a new 1.9.19 beta soon and then you can simply use the nightly builds.
If you have any questions, please leave a comment or, better yet, join the dev and/or users mailing lists here. There's still some tweaking left to do here and there but things are shaping up quite nicely, I think. But I won't know what's missing or wrong unless you tell me. So, please, play. Comment. Let me know what you think.
Grizzly and WebSockets
As HTML5 lumbers its way through the standardization process, more and more developers are starting to play with the emerging features. One feature getting some serious attention is that of the websocket. Full details of the websocket protocol can be found here but I'll lay out the basics of it. Essentially a websocket is a TCP based socket that can be opened from a webpage via, typically, javascript code allowing bidirectional communication between the browser and the server. This allows for comet-like interactions but with an extra benefit (or two). Once the websocket connection is established there are no more protocol negotiations and handshakes unlike your typical AJAX conversations. And unlike (most?) comet implementations, a websocket can process multiple requests from the client. Obviously, this can lead to some rather interesting use cases.
It's early yet so support for websockets on either end of the browser/server connection is spotty at best. But we're staring to see browser support emerge and a number of server side options are popping up as well. This morning I committed an early rough draft, if you will, for support inside the grizzly project and soon glassfish itself. It's a mostly complete implementation and is ready for some experimentation. At the moment, the sample in the unit tests that will be of the most interest is a servlet based approach. What's nice about the current approach is that the servlet is largely unaware that it's involved in a websocket transaction at all. There's a lot of value in something like that but might limit some other, more powerful, use cases.
For now, though, you can play with the unit tests and see what can be done. It's preliminary but working and the API will evolve as we get feedback from the community. We'll be discussing how best to expose this functionality without needing to know all that much about grizzly internals. We'll be looking at other cases such as jetty and atmosphere to make sure we can provide a smooth, useful API that most people are comfortable with. There's been talk at various levels of building some form of standard interface to plug in to various websockets implementations on the server side but until then, we'd love your help in building something with grizzly and glassfish that we can all use.
So please join us on the mailing lists and give us a hand.