Play-Websocket Iteration 1


/ Published in: Java
Save to your folder(s)



Copy this code and paste it in your HTML
  1. package play.server;
  2.  
  3. import org.jboss.netty.buffer.ChannelBuffer;
  4. import org.jboss.netty.buffer.ChannelBufferInputStream;
  5. import org.jboss.netty.buffer.ChannelBuffers;
  6. import org.jboss.netty.channel.*;
  7. import org.jboss.netty.handler.codec.http.*;
  8. import org.jboss.netty.handler.codec.http.websocket.*;
  9.  
  10. import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.*;
  11.  
  12. import play.Invoker;
  13. import play.Logger;
  14. import play.Play;
  15. import play.PlayPlugin;
  16. import play.mvc.ActionInvoker;
  17. import play.mvc.Http;
  18. import play.mvc.Http.Request;
  19. import play.mvc.Http.Response;
  20.  
  21. import java.io.*;
  22. import java.util.logging.Level;
  23. import java.util.concurrent.ConcurrentMap;
  24. import java.util.concurrent.ConcurrentSkipListMap;
  25. import java.security.MessageDigest;
  26.  
  27. public class PlayWebsocketHandler extends PlayHandler {
  28.  
  29. // Specify ws:// or wss:// here
  30. private static final String WEBSOCKET_SCHEME = "ws://";
  31.  
  32. // Specify PlayRouter method prefix here
  33. private static final String WEBSOCKET_METHOD_NAME = "WEBSOCKET";
  34.  
  35. /* A map that stores netty-requests that has been upgraded (via handshake) to websocket stream.
  36.   * ConcurrentMap provides good lookup performance.
  37.   * ChannelHandlerContext can be used as reference, as it does not change on every messageReceived.
  38.   * Refer to http://docs.jboss.org/netty/3.1/api/org/jboss/netty/channel/ChannelHandlerContext.html#setAttachment
  39.   * Alternatively, client ip address may be used as key.
  40.   */
  41. private final ConcurrentMap<ChannelHandlerContext, HttpRequest> activeWebsocketMap = new ConcurrentSkipListMap<ChannelHandlerContext, HttpRequest>();
  42.  
  43. @Override
  44. public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception {
  45. Logger.trace("messageReceived: begin");
  46.  
  47. final Object msg = e.getMessage();
  48. if (msg instanceof WebSocketFrame) {
  49. final WebSocketFrame nettyWebSocketFrame = (WebSocketFrame) msg;
  50.  
  51. // This is all that is needed to send arbitrary content over the wire.
  52. //ctx.getChannel().write(new DefaultWebSocketFrame(nettyWebSocketFrame.getTextData().toUpperCase()));
  53.  
  54. // Websocket frames do not give netty requests, and thus need to be retrieved from the handshake.
  55. HttpRequest nettyRequest = activeWebsocketMap.get(ctx);
  56. // Faster version of the above
  57. //HttpRequest nettyRequest = (HttpRequest) ctx.getAttachment();
  58.  
  59. // Reuse the original handshake request but replace the body.
  60. final Request request = parseRequest(ctx, nettyRequest);
  61. ChannelBuffer b = nettyWebSocketFrame.getBinaryData();
  62. // Use params.body in Play-Controller to retrieve the binary content.
  63. request.body = new ChannelBufferInputStream(b);
  64. // Mark this request as using Websocket method
  65. request.method = WEBSOCKET_METHOD_NAME;
  66. // Also provide channel reference in request object for use in PlayController
  67. request.args.put("CHANNEL", ctx.getChannel());
  68.  
  69. final Response response = new Response();
  70.  
  71. try {
  72. Http.Response.current.set(response);
  73. response.out = new ByteArrayOutputStream();
  74.  
  75. Invoker.invoke(new NettyInvocation(request, response, ctx, nettyRequest, e));
  76.  
  77. } catch (Exception ex) {
  78. serve500(ex, ctx, nettyRequest);
  79. }
  80.  
  81. } else {
  82. // Not a websocket frame, pass to PlayHandler.
  83. // As there is no way to override an inner class in java, a simple workaround is used here.
  84. // super.messageReceived(ctx, e);
  85. superClassMessageReceived(ctx, e);
  86. }
  87.  
  88. Logger.trace("messageReceived: end");
  89. }
  90.  
  91. public class NettyInvocation extends PlayHandler.NettyInvocation {
  92.  
  93. private final ChannelHandlerContext ctx;
  94. private final Request request;
  95. private final Response response;
  96. private final HttpRequest nettyRequest;
  97. private final MessageEvent event;
  98. private final boolean isWebsocketUpgradeRequest;
  99.  
  100. public NettyInvocation(Request request, Response response, ChannelHandlerContext ctx, HttpRequest nettyRequest, MessageEvent e) {
  101. super(request, response, ctx, nettyRequest, e);
  102. this.ctx = ctx;
  103. this.request = request;
  104. this.response = response;
  105. this.nettyRequest = nettyRequest;
  106. this.event = e;
  107.  
  108. this.isWebsocketUpgradeRequest = HttpHeaders.Values.UPGRADE.equalsIgnoreCase(nettyRequest.getHeader(HttpHeaders.Names.CONNECTION)) &&
  109. HttpHeaders.Values.WEBSOCKET.equalsIgnoreCase(nettyRequest.getHeader(HttpHeaders.Names.UPGRADE)) &&
  110. !WEBSOCKET_METHOD_NAME.equalsIgnoreCase(request.method);
  111. }
  112.  
  113. // !!!!!! We want this to be invoked by the original PlayHandler Class
  114. @Override
  115. public boolean init() {
  116.  
  117. if (isWebsocketUpgradeRequest) {
  118. Logger.trace("init: begin");
  119. Logger.trace("init: WebsocketUpgrade deferred to execute()");
  120. Logger.trace("init: end");
  121. return true; // Do not prevent execute()
  122.  
  123. } else {
  124. // Not a websocket upgrade request, pass to PlayHandler.
  125. return super.init();
  126. }
  127. }
  128.  
  129. @Override
  130. public void execute() throws Exception {
  131. Logger.trace("execute: begin");
  132.  
  133. if (isWebsocketUpgradeRequest) {
  134. // This block is to do deferred handshake.
  135. handleWebSocketHandshakeRequest(ctx, nettyRequest);
  136. // Keep nettyRequest reference in Map.
  137. activeWebsocketMap.put(ctx, nettyRequest);
  138. // Faster version of the above
  139. //ctx.setAttachment(nettyRequest);
  140.  
  141. } else if (WEBSOCKET_METHOD_NAME.equalsIgnoreCase(request.method)) {
  142. // Only do this block for replies to upgraded websocket streams.
  143. ActionInvoker.invoke(request, response);
  144. ctx.getChannel().write(new DefaultWebSocketFrame(response.out.toString()));
  145.  
  146. } else {
  147. super.execute();
  148. }
  149.  
  150. Logger.trace("execute: end");
  151. }
  152. }
  153.  
  154. /*
  155.   * Returns the websocket endpoint's Uniform Resource Identifier (URI)
  156.   */
  157. private static String getWebSocketURI(HttpRequest nettyRequest) {
  158. return WEBSOCKET_SCHEME + nettyRequest.getHeader(HttpHeaders.Names.HOST) + nettyRequest.getUri();
  159. }
  160.  
  161. /*
  162.   * This method is an almost exact copy of Netty's sample-code for Websocket.
  163.   * This is a proven implementation. You do not need to look into it.
  164.   */
  165. private static void handleWebSocketHandshakeRequest(ChannelHandlerContext ctx, HttpRequest nettyRequest) {
  166. // Create the WebSocket handshake response.
  167. HttpResponse res = new DefaultHttpResponse(
  168. HttpVersion.HTTP_1_1,
  169. new HttpResponseStatus(101, "Web Socket Protocol Handshake"));
  170. res.addHeader(HttpHeaders.Names.UPGRADE, HttpHeaders.Values.WEBSOCKET);
  171. res.addHeader(HttpHeaders.Names.CONNECTION, HttpHeaders.Values.UPGRADE);
  172.  
  173. // Fill in the headers and contents depending on handshake method.
  174. if (nettyRequest.containsHeader(SEC_WEBSOCKET_KEY1) &&
  175. nettyRequest.containsHeader(SEC_WEBSOCKET_KEY2)) {
  176. // New handshake method with a challenge:
  177. res.addHeader(SEC_WEBSOCKET_ORIGIN, nettyRequest.getHeader(ORIGIN));
  178. res.addHeader(SEC_WEBSOCKET_LOCATION, getWebSocketURI(nettyRequest));
  179. String protocol = nettyRequest.getHeader(SEC_WEBSOCKET_PROTOCOL);
  180. if (protocol != null) {
  181. res.addHeader(SEC_WEBSOCKET_PROTOCOL, protocol);
  182. }
  183.  
  184. // Calculate the answer of the challenge.
  185. String key1 = nettyRequest.getHeader(SEC_WEBSOCKET_KEY1);
  186. String key2 = nettyRequest.getHeader(SEC_WEBSOCKET_KEY2);
  187. int a = (int) (Long.parseLong(key1.replaceAll("[^0-9]", "")) / key1.replaceAll("[^ ]", "").length());
  188. int b = (int) (Long.parseLong(key2.replaceAll("[^0-9]", "")) / key2.replaceAll("[^ ]", "").length());
  189. long c = nettyRequest.getContent().readLong();
  190. ChannelBuffer input = ChannelBuffers.buffer(16);
  191. input.writeInt(a);
  192. input.writeInt(b);
  193. input.writeLong(c);
  194. byte[] digest = null;
  195. try {
  196. digest = MessageDigest.getInstance("MD5").digest(input.array());
  197. } catch (Exception ex) {
  198. java.util.logging.Logger.getLogger(PlayHandler.class.getName()).log(Level.SEVERE, null, ex);
  199. }
  200. ChannelBuffer output = ChannelBuffers.wrappedBuffer(digest);
  201. res.setContent(output);
  202. } else {
  203. // Old handshake method with no challenge:
  204. res.addHeader(WEBSOCKET_ORIGIN, nettyRequest.getHeader(ORIGIN));
  205. res.addHeader(WEBSOCKET_LOCATION, getWebSocketURI(nettyRequest));
  206. String protocol = nettyRequest.getHeader(WEBSOCKET_PROTOCOL);
  207. if (protocol != null) {
  208. res.addHeader(WEBSOCKET_PROTOCOL, protocol);
  209. }
  210. }
  211.  
  212. // Upgrade the connection and send the handshake response.
  213. ChannelPipeline p = ctx.getChannel().getPipeline();
  214. p.remove("aggregator");
  215. p.replace("decoder", "wsdecoder", new WebSocketFrameDecoder());
  216.  
  217. ctx.getChannel().write(res);
  218.  
  219. p.replace("encoder", "wsencoder", new WebSocketFrameEncoder());
  220. }
  221.  
  222. /*
  223.   * This method is an exact copy of PlayHandler.messageReceived()
  224.   * This is a simple workaround. You do not need to look into it.
  225.   */
  226. public void superClassMessageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception {
  227. Logger.trace("messageReceived: begin");
  228.  
  229. final Object msg = e.getMessage();
  230. if (msg instanceof HttpRequest) {
  231. final HttpRequest nettyRequest = (HttpRequest) msg;
  232. try {
  233. Request request = parseRequest(ctx, nettyRequest);
  234. request = processRequest(request);
  235.  
  236. final Response response = new Response();
  237.  
  238. Http.Response.current.set(response);
  239. response.out = new ByteArrayOutputStream();
  240. boolean raw = false;
  241. for (PlayPlugin plugin : Play.plugins) {
  242. if (plugin.rawInvocation(request, response)) {
  243. raw = true;
  244. break;
  245. }
  246. }
  247. if (raw) {
  248. copyResponse(ctx, request, response, nettyRequest);
  249. } else {
  250. Invoker.invoke(new NettyInvocation(request, response, ctx, nettyRequest, e));
  251. }
  252.  
  253. } catch (Exception ex) {
  254. serve500(ex, ctx, nettyRequest);
  255. }
  256. }
  257. Logger.trace("messageReceived: end");
  258. }
  259.  
  260. }

Report this snippet


Comments

RSS Icon Subscribe to comments

You need to login to post a comment.