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 の模様)


使用した環境は以下の通りです。

ちなみに、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 を編集しても可)

  • gmbal-api-only-3.0.0-b023.jar
  • grizzly-framework-2.0.1-b2.jar
  • grizzly-http-2.0.1-b2.jar
  • grizzly-http-server-2.0.1-b2.jar
  • grizzly-http-servlet-2.0.1-b2.jar
  • grizzly-rcm-2.0.1-b2.jar
  • grizzly-websockets-2.0.1-b2.jar
  • management-api-3.0.0-b012.jar

実行例

> 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