JVM上の WebSocket サーバープログラム - Jetty, Grizzly, Netty, EM-WebSocket を試す
WebSocket の簡単なサーバープログラムを Jetty, Grizzly, Netty, EM-WebSocket をそれぞれ使って、Groovy や JRuby で実装してみました。
WebSocket のプロトコル仕様は確定しておらず、互換性の無い改訂が行われているようなので、今回は draft-ietf-hybi-thewebsocketprotocol-00 をサポートした Google Chrome 12.0.742.100 の WebSocket クライアントと接続可能なサーバープログラムを作成する事にします。
実際に、draft-ietf-hybi-thewebsocketprotocol-00 で使う Sec-WebSocket-Key1 と Sec-WebSocket-Key2 は、draft-ietf-hybi-thewebsocketprotocol-06 で使わなくなっていたりする等、サーバー・クライアントでサポートしているプロトコル仕様に注意する必要がありました。(現時点での最新仕様は draft-ietf-hybi-thewebsocketprotocol-09 の模様)
使用した環境は以下の通りです。
- クライアント
- Google Chrome 12.0.742.100
- サーバー
ちなみに、EM-WebSocket を使うのが最も簡単で Netty を使うのが最も面倒でした。
また、Grizzly 2.1 以降は draft-ietf-hybi-thewebsocketprotocol-06 に対応しているものの、draft-ietf-hybi-thewebsocketprotocol-00 に対応していないため、今回の用途では使用できませんでした。
サンプルソースは http://github.com/fits/try_samples/tree/master/blog/20110618/
WebSocket クライアント
まずは Google Chrome 上で実行する WebSocket クライアントです。
ローカルファイルを Chrome 上で実行してサーバープログラムの動作確認に使います。
index.html
<!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <script> var ws = new WebSocket("ws://localhost:8080/"); ws.onopen = function(event) { console.log("websocket open"); stateChange("opened") }; ws.onmessage = function(event) { document.getElementById("log").innerHTML += "<li>" + event.data + "</li>"; }; ws.onclose = function(event) { console.log("websocket close"); stateChange("closed") }; ws.onerror = function(event) { console.log("error"); stateChange("error") }; function sendMessage() { var msg = document.getElementById("message").value; ws.send(msg); } function stateChange(state) { document.getElementById("state").innerHTML = state; } </script> </head> <body> <input id="message" type="text" /> <input type="button" value="send" onclick="sendMessage()" /> <span id="state">closed</span> <ul id="log"></ul> </body> </html>
Jetty による WebSocket サーバー
クライアントが送信してきた文字列の先頭に "echo : " という文字列を加えて返すだけの単純な処理を実装します。
jetty_groovy/echo_server.groovy
import javax.servlet.http.HttpServletRequest import org.eclipse.jetty.server.Server import org.eclipse.jetty.websocket.WebSocket import org.eclipse.jetty.websocket.WebSocket.Connection import org.eclipse.jetty.websocket.WebSocketHandler class EchoWebSocket implements WebSocket.OnTextMessage { def outbound void onOpen(Connection outbound) { println("onopen : ${this}") this.outbound = outbound } void onMessage(String data) { println("onmessage : ${this} - ${data}") this.outbound.sendMessage("echo: ${data}") } void onClose(int closeCode, String message) { println("onclose : ${this} - ${closeCode}, ${message}") } } def server = new Server(8080) //WebSocket用の Handler を設定 server.handler = new WebSocketHandler() { WebSocket doWebSocketConnect(HttpServletRequest request, String protocol) { println("websocket connect : ${protocol} - ${request}") new EchoWebSocket() } } server.start() server.join()
ユーザーホームディレクトリの .groovy/lib ディレクトリに Jetty の lib ディレクトリ内の JAR ファイルを配置しておき実行します。(Groovy の conf/groovy-starter.conf を編集しても可)
実行例
> groovy echo_server.groovy
Grizzly による WebSocket サーバー
Grizzly 2.1 は draft-ietf-hybi-thewebsocketprotocol-00 に対応していないため、Grizzly 2.0.1 を使う必要があります。
実装内容は Jetty 版と同じような感じです。
grizzly_groovy/echo_server.groovy
import org.glassfish.grizzly.http.server.* import org.glassfish.grizzly.http.HttpRequestPacket import org.glassfish.grizzly.websockets.* import org.glassfish.grizzly.websockets.frame.* class EchoWebSocketApplication extends WebSocketApplication { boolean isApplicationRequest(HttpRequestPacket req) { println("${req}") true } void onConnect(WebSocket websocket) { println("onConnect : ${websocket}") super.onConnect(websocket) } void onMessage(WebSocket websocket, Frame data) { println("onMessage : ${data}") websocket.send(Frame.createTextFrame("echo : ${data.asText}")) } void onClose(WebSocket websocket) { println("onClose : ${websocket}") super.onClose(websocket) } } def server = HttpServer.createSimpleServer() server.getListener("grizzly").registerAddOn(new WebSocketAddOn()) WebSocketEngine.engine.registerApplication("/", new EchoWebSocketApplication()) server.start() System.in.read() server.stop()
ユーザーホームディレクトリの .groovy/lib ディレクトリに以下の JAR ファイルを配置しておき実行します。(Groovy の conf/groovy-starter.conf を編集しても可)
実行例
> groovy echo_server.groovy
Netty による WebSocket サーバー
Netty で実装する場合、Jetty や Grizzly とは異なり、ハンドシェイク処理を自前で実装する事になります。
まず HTTP でハンドシェイクを処理した後に WebSocket 用にパイプラインの構成を変更します。
netty_groovy/echo_server.groovy
import java.net.InetSocketAddress import java.security.MessageDigest import java.util.concurrent.Executors import static org.jboss.netty.handler.codec.http.HttpHeaders.* import org.jboss.netty.bootstrap.ServerBootstrap import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory import org.jboss.netty.buffer.ChannelBuffers import org.jboss.netty.channel.* import org.jboss.netty.channel.Channels import org.jboss.netty.handler.codec.http.* import org.jboss.netty.handler.codec.http.websocket.* class ChatServerHandler extends SimpleChannelUpstreamHandler { //メッセージ受信処理 public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) { def msg = e.message println("message received : ${msg}") handleRequest(ctx, msg) } //WebSocket draft-ietf-hybi-thewebsocketprotocol-00 用の //ハンドシェイク処理(HTTP リクエストの処理) def handleRequest(ChannelHandlerContext ctx, HttpRequest req) { //ハンドシェイクのレスポンス作成 def res = new DefaultHttpResponse(HttpVersion.HTTP_1_1, new HttpResponseStatus(101, "Web Socket Protocol Handshake")) res.addHeader(Names.UPGRADE, Values.WEBSOCKET) res.addHeader(Names.CONNECTION, Values.UPGRADE) res.addHeader(Names.SEC_WEBSOCKET_ORIGIN, req.getHeader(Names.ORIGIN)) res.addHeader(Names.SEC_WEBSOCKET_LOCATION, "ws://localhost:8080/") def key1 = req.getHeader(Names.SEC_WEBSOCKET_KEY1) def key2 = req.getHeader(Names.SEC_WEBSOCKET_KEY2) //キー内の数値のみを取り出し数値化したものをキー内の空白数で割る int key1res = (int)Long.parseLong(key1.replaceAll("[^0-9]", "")) / key1.replaceAll("[^ ]", "").length() int key2res = (int)Long.parseLong(key2.replaceAll("[^0-9]", "")) / key2.replaceAll("[^ ]", "").length() long content = req.content.readLong() def input = ChannelBuffers.buffer(16) input.writeInt(key1res) input.writeInt(key2res) input.writeLong(content) res.content = ChannelBuffers.wrappedBuffer(MessageDigest.getInstance("MD5").digest(input.array)) //接続をアップグレード //(WebSocket 用に decoder と encoder を変更する) def pipeline = ctx.channel.pipeline pipeline.replace("decoder", "wsdecoder", new WebSocketFrameDecoder()) //レスポンス送信 ctx.channel.write(res) //encoder はレスポンス送信に使用するため送信後に WebSocket 用に変更 pipeline.replace("encoder", "wsencoder", new WebSocketFrameEncoder()) } //WebSocket 処理 def handleRequest(ChannelHandlerContext ctx, WebSocketFrame msg) { ctx.channel.write(new DefaultWebSocketFrame("echo : ${msg.textData}")) } } def server = new ServerBootstrap(new NioServerSocketChannelFactory( Executors.newCachedThreadPool(), Executors.newCachedThreadPool() )) //WebSocket を使うには、まず HTTP で処理する必要があるため //HTTP 用の decoder と encoder の構成を用意する server.setPipelineFactory({ def pipeline = Channels.pipeline() pipeline.addLast("decoder", new HttpRequestDecoder()) pipeline.addLast("encoder", new HttpResponseEncoder()) pipeline.addLast("handler", new ChatServerHandler()) pipeline } as ChannelPipelineFactory) server.bind(new InetSocketAddress(8080))
ユーザーホームディレクトリの .groovy/lib ディレクトリに以下の JAR ファイルを配置しておき実行します。(Groovy の conf/groovy-starter.conf を編集しても可)
- netty-3.2.4.Final.jar
実行例
> groovy echo_server.groovy
EM-WebSocket による WebSocket サーバー
まず、EM-WebSocket をインストールしておきます。
EM-WebSocket インストール
> gem install em-websocket
EM-WebSocket を使った WebSocket サーバーは以下のようになります。これまでのサンプルに比べると非常に簡単になっています。
em-websocket_jruby/echo_server.rb
require 'rubygems' require 'em-websocket' EventMachine::WebSocket.start(:host => "localhost", :port => 8080, :debug => true) do |ws| ws.onopen {puts "onopen"} ws.onmessage {|msg| ws.send "echo : #{msg}"} ws.onclose {puts "onclose"} end
実行例
> jruby echo_server.rb