Revision: 40492
Initial Code
Initial URL
Initial Description
Initial Title
Initial Tags
Initial Language
at February 3, 2011 01:00 by heri16
Initial Code
package play.server; import org.jboss.netty.buffer.ChannelBuffer; import org.jboss.netty.buffer.ChannelBufferInputStream; import org.jboss.netty.buffer.ChannelBuffers; import org.jboss.netty.channel.*; import org.jboss.netty.handler.codec.http.*; import org.jboss.netty.handler.codec.http.websocket.*; import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.*; import play.Invoker; import play.Logger; import play.Play; import play.PlayPlugin; import play.mvc.ActionInvoker; import play.mvc.Http; import play.mvc.Http.Request; import play.mvc.Http.Response; import java.io.*; import java.util.logging.Level; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ConcurrentSkipListMap; import java.security.MessageDigest; public class PlayWebsocketHandler extends PlayHandler { // Specify ws:// or wss:// here private static final String WEBSOCKET_SCHEME = "ws://"; // Specify PlayRouter method prefix here private static final String WEBSOCKET_METHOD_NAME = "WEBSOCKET"; /* A map that stores netty-requests that has been upgraded (via handshake) to websocket stream. * ConcurrentMap provides good lookup performance. * ChannelHandlerContext can be used as reference, as it does not change on every messageReceived. * Refer to http://docs.jboss.org/netty/3.1/api/org/jboss/netty/channel/ChannelHandlerContext.html#setAttachment * Alternatively, client ip address may be used as key. */ private final ConcurrentMap<ChannelHandlerContext, HttpRequest> activeWebsocketMap = new ConcurrentSkipListMap<ChannelHandlerContext, HttpRequest>(); @Override public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception { Logger.trace("messageReceived: begin"); final Object msg = e.getMessage(); if (msg instanceof WebSocketFrame) { final WebSocketFrame nettyWebSocketFrame = (WebSocketFrame) msg; // This is all that is needed to send arbitrary content over the wire. //ctx.getChannel().write(new DefaultWebSocketFrame(nettyWebSocketFrame.getTextData().toUpperCase())); // Websocket frames do not give netty requests, and thus need to be retrieved from the handshake. HttpRequest nettyRequest = activeWebsocketMap.get(ctx); // Faster version of the above //HttpRequest nettyRequest = (HttpRequest) ctx.getAttachment(); // Reuse the original handshake request but replace the body. final Request request = parseRequest(ctx, nettyRequest); ChannelBuffer b = nettyWebSocketFrame.getBinaryData(); // Use params.body in Play-Controller to retrieve the binary content. request.body = new ChannelBufferInputStream(b); // Mark this request as using Websocket method request.method = WEBSOCKET_METHOD_NAME; // Also provide channel reference in request object for use in PlayController request.args.put("CHANNEL", ctx.getChannel()); final Response response = new Response(); try { Http.Response.current.set(response); response.out = new ByteArrayOutputStream(); Invoker.invoke(new NettyInvocation(request, response, ctx, nettyRequest, e)); } catch (Exception ex) { serve500(ex, ctx, nettyRequest); } } else { // Not a websocket frame, pass to PlayHandler. // As there is no way to override an inner class in java, a simple workaround is used here. // super.messageReceived(ctx, e); superClassMessageReceived(ctx, e); } Logger.trace("messageReceived: end"); } public class NettyInvocation extends PlayHandler.NettyInvocation { private final ChannelHandlerContext ctx; private final Request request; private final Response response; private final HttpRequest nettyRequest; private final MessageEvent event; private final boolean isWebsocketUpgradeRequest; public NettyInvocation(Request request, Response response, ChannelHandlerContext ctx, HttpRequest nettyRequest, MessageEvent e) { super(request, response, ctx, nettyRequest, e); this.ctx = ctx; this.request = request; this.response = response; this.nettyRequest = nettyRequest; this.event = e; this.isWebsocketUpgradeRequest = HttpHeaders.Values.UPGRADE.equalsIgnoreCase(nettyRequest.getHeader(HttpHeaders.Names.CONNECTION)) && HttpHeaders.Values.WEBSOCKET.equalsIgnoreCase(nettyRequest.getHeader(HttpHeaders.Names.UPGRADE)) && !WEBSOCKET_METHOD_NAME.equalsIgnoreCase(request.method); } // !!!!!! We want this to be invoked by the original PlayHandler Class @Override public boolean init() { if (isWebsocketUpgradeRequest) { Logger.trace("init: begin"); Logger.trace("init: WebsocketUpgrade deferred to execute()"); Logger.trace("init: end"); return true; // Do not prevent execute() } else { // Not a websocket upgrade request, pass to PlayHandler. return super.init(); } } @Override public void execute() throws Exception { Logger.trace("execute: begin"); if (isWebsocketUpgradeRequest) { // This block is to do deferred handshake. handleWebSocketHandshakeRequest(ctx, nettyRequest); // Keep nettyRequest reference in Map. activeWebsocketMap.put(ctx, nettyRequest); // Faster version of the above //ctx.setAttachment(nettyRequest); } else if (WEBSOCKET_METHOD_NAME.equalsIgnoreCase(request.method)) { // Only do this block for replies to upgraded websocket streams. ActionInvoker.invoke(request, response); ctx.getChannel().write(new DefaultWebSocketFrame(response.out.toString())); } else { super.execute(); } Logger.trace("execute: end"); } } /* * Returns the websocket endpoint's Uniform Resource Identifier (URI) */ private static String getWebSocketURI(HttpRequest nettyRequest) { return WEBSOCKET_SCHEME + nettyRequest.getHeader(HttpHeaders.Names.HOST) + nettyRequest.getUri(); } /* * This method is an almost exact copy of Netty's sample-code for Websocket. * This is a proven implementation. You do not need to look into it. */ private static void handleWebSocketHandshakeRequest(ChannelHandlerContext ctx, HttpRequest nettyRequest) { // Create the WebSocket handshake response. HttpResponse res = new DefaultHttpResponse( HttpVersion.HTTP_1_1, new HttpResponseStatus(101, "Web Socket Protocol Handshake")); res.addHeader(HttpHeaders.Names.UPGRADE, HttpHeaders.Values.WEBSOCKET); res.addHeader(HttpHeaders.Names.CONNECTION, HttpHeaders.Values.UPGRADE); // Fill in the headers and contents depending on handshake method. if (nettyRequest.containsHeader(SEC_WEBSOCKET_KEY1) && nettyRequest.containsHeader(SEC_WEBSOCKET_KEY2)) { // New handshake method with a challenge: res.addHeader(SEC_WEBSOCKET_ORIGIN, nettyRequest.getHeader(ORIGIN)); res.addHeader(SEC_WEBSOCKET_LOCATION, getWebSocketURI(nettyRequest)); String protocol = nettyRequest.getHeader(SEC_WEBSOCKET_PROTOCOL); if (protocol != null) { res.addHeader(SEC_WEBSOCKET_PROTOCOL, protocol); } // Calculate the answer of the challenge. String key1 = nettyRequest.getHeader(SEC_WEBSOCKET_KEY1); String key2 = nettyRequest.getHeader(SEC_WEBSOCKET_KEY2); int a = (int) (Long.parseLong(key1.replaceAll("[^0-9]", "")) / key1.replaceAll("[^ ]", "").length()); int b = (int) (Long.parseLong(key2.replaceAll("[^0-9]", "")) / key2.replaceAll("[^ ]", "").length()); long c = nettyRequest.getContent().readLong(); ChannelBuffer input = ChannelBuffers.buffer(16); input.writeInt(a); input.writeInt(b); input.writeLong(c); byte[] digest = null; try { digest = MessageDigest.getInstance("MD5").digest(input.array()); } catch (Exception ex) { java.util.logging.Logger.getLogger(PlayHandler.class.getName()).log(Level.SEVERE, null, ex); } ChannelBuffer output = ChannelBuffers.wrappedBuffer(digest); res.setContent(output); } else { // Old handshake method with no challenge: res.addHeader(WEBSOCKET_ORIGIN, nettyRequest.getHeader(ORIGIN)); res.addHeader(WEBSOCKET_LOCATION, getWebSocketURI(nettyRequest)); String protocol = nettyRequest.getHeader(WEBSOCKET_PROTOCOL); if (protocol != null) { res.addHeader(WEBSOCKET_PROTOCOL, protocol); } } // Upgrade the connection and send the handshake response. ChannelPipeline p = ctx.getChannel().getPipeline(); p.remove("aggregator"); p.replace("decoder", "wsdecoder", new WebSocketFrameDecoder()); ctx.getChannel().write(res); p.replace("encoder", "wsencoder", new WebSocketFrameEncoder()); } /* * This method is an exact copy of PlayHandler.messageReceived() * This is a simple workaround. You do not need to look into it. */ public void superClassMessageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception { Logger.trace("messageReceived: begin"); final Object msg = e.getMessage(); if (msg instanceof HttpRequest) { final HttpRequest nettyRequest = (HttpRequest) msg; try { Request request = parseRequest(ctx, nettyRequest); request = processRequest(request); final Response response = new Response(); Http.Response.current.set(response); response.out = new ByteArrayOutputStream(); boolean raw = false; for (PlayPlugin plugin : Play.plugins) { if (plugin.rawInvocation(request, response)) { raw = true; break; } } if (raw) { copyResponse(ctx, request, response, nettyRequest); } else { Invoker.invoke(new NettyInvocation(request, response, ctx, nettyRequest, e)); } } catch (Exception ex) { serve500(ex, ctx, nettyRequest); } } Logger.trace("messageReceived: end"); } }
Initial URL
Initial Description
Initial Title
Play-Websocket Iteration 1
Initial Tags
Initial Language
Java