Jetty8のWebSocketサーバ/クライアントを使ってみる

ついに RFC6455 (WebSocket) が出ましたね!ぼくも興味津々です。
ということで今回は Jetty の WebSocket サーバ/クライアント実装を使ってみます。

サーバ

今回作ったサーバのソースは以下の通りです。

import java.io.IOException;
import javax.servlet.http.HttpServletRequest;
import org.eclipse.jetty.websocket.WebSocket;
import org.eclipse.jetty.websocket.WebSocket.Connection;
import org.eclipse.jetty.websocket.WebSocketServlet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class JettyWebSocketServlet extends WebSocketServlet {

	Logger log = LoggerFactory.getLogger(JettyWebSocketServlet.class);

	// string には WebSocket のサブプロトコルが入る
	@Override
	public WebSocket doWebSocketConnect(HttpServletRequest hsr, String string) {
		// WebSocket インタフェースを実装したクラスのインスタンスを返す
		return new WebSocket.OnTextMessage() {
			
			protected Connection connection;

			// テキストのメッセージが届いたとき
			@Override
			public void onMessage(String data) {
				log.info("onMessage: " + data);
				// エコーする
				this.send(data);
			}

			// コネクションが開かれたとき
			@Override
			public void onOpen(Connection connection) {
				log.info("onOpen");
				this.setConnection(connection);
				this.send("Hello");
			}

			// コネクションが閉じられたとき
			@Override
			public void onClose(int closeCode, String message) {
				this.send("Bye");
				log.info("onClose");
			}

			public Connection getConnection() {
				return connection;
			}

			public void setConnection(Connection connection) {
				this.connection = connection;
			}

			protected void send(String message) {
				try {
					this.getConnection().sendMessage(message);
				} catch (IOException ex) {
					ex.printStackTrace();
				}
			}
		};
	}
}

Jetty の WebSocket サーバ実装では、まず WebSocketServlet クラスが軸になります。この WebSocketServlet は HttpServlet を継承しているので、普通のサーブレットとして web.xml に登録するだけで Jetty にデプロイできます。
ただし WebSocketServlet クラスは WebSocketFactory.Acceptor インタフェースを実装していますが、抽象クラスになっていて実際には doWebSocketConnect メソッドを実装していません。この doWebSocketConnect メソッドは WebSocket のリクエストがあったときに呼び出されます。
この doWebSocketConnect メソッドの返り値は WebSocket インタフェースになっています。つまり WebSocket インタフェースを実装した自分好みの具象クラスを作ってリクエストごとに返してやるわけです。

WebSocket インタフェースには onOpen, onClose メソッドが定義されています。はて、クライアント (対向) からデータを受け取るメソッドはいずこ。ソースコードで既に正解が出ていますが WebSocket インタフェースのフィールドにネストする形で OnTextMessage インタフェースが定義されていて、これを使うみたいです。同列に OnBinaryMessage インタフェースや OnFrame インタフェースとかもあるので、必要なメソッドを選んで実装していくんでしょうね。
あとは WebSocket インタフェースを好きに実装したクラスを作るだけです。今回はめんどくさいので無名クラスで済ませました。

念のため web.xml も貼っておきます。

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">

	<display-name>test-jetty-websocket</display-name>

	<servlet>
		<servlet-name>JettyWebSocketServlet</servlet-name>
		<servlet-class>test.jetty.websocket.JettyWebSocketServlet</servlet-class>
	</servlet>
	
	<servlet-mapping>
		<servlet-name>JettyWebSocketServlet</servlet-name>
		<url-pattern>/websocket/*</url-pattern>
	</servlet-mapping>
	
</web-app>

普通のサーブレットと全く変わりませんね。

クライアント

世の中を見渡すとクライアントに HTML/JavaScript を使ったサンプルが溢れています。が、ぼくは主にテストの観点からサーバと同じ言語/実装でクライアントも用意されていないと使う気になれません。その点 Jetty はクライアントも用意されているので完璧です。

import java.net.URI;
import java.util.concurrent.Future;
import org.eclipse.jetty.websocket.WebSocket;
import org.eclipse.jetty.websocket.WebSocket.Connection;
import org.eclipse.jetty.websocket.WebSocketClient;
import org.eclipse.jetty.websocket.WebSocketClientFactory;
import org.junit.Test;

public class JettyWebSocketServletTest {

	@Test
	public void test() throws Exception {
		WebSocketClientFactory webSocketClientFactory = new WebSocketClientFactory();
		webSocketClientFactory.start();
		WebSocketClient client = webSocketClientFactory.newWebSocketClient();
		Future<Connection> futureConnection = 
		    client.open(new URI("ws://localhost:8080/websocket/"), new WebSocket.OnTextMessage() {

			@Override
			public void onMessage(String data) {
			}

			@Override
			public void onOpen(Connection connection) {
			}

			@Override
			public void onClose(int closeCode, String message) {
			}
		});
		Connection connection = futureConnection.get();
		connection.sendMessage("Hello, WebSocket!!");
		connection.disconnect();
	}
}

クライアントでは WebSocketClientFactory から WebSocketClient を手に入れて使います。WebSocketClientFactory はあらかじめ start メソッドを叩いておかないと使えないみたいなので注意です。
WebSocketClient を手に入れたら open メソッドで Connection インタフェースを手に入れます。open メソッドには WebSocket サーバのエンドポイントと WebSocket インタフェースを実装したクラスを渡します。
Connection インタフェースのインスタンスは Future インタフェースの中に入った状態で得られるので get メソッドで取り出した上で使います。きっと WebSocket のコネクションが開いて使えるようになるまでブロックするんでしょうね。
今回はシンプルにコネクションを開いて一言喋ってサヨナラする、ほぼピンポンダッシュの実装にしました。

pom.xml

今回のプロジェクトは Maven で作りました。使った pom.xml は以下です。

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>

	<groupId>test.jetty.websocket</groupId>
	<artifactId>test-jetty-websocket</artifactId>
	<packaging>war</packaging>
	<version>1.0-SNAPSHOT</version>

	<name>test-jetty-websocket</name>
	<url>http://maven.apache.org</url>

	<dependencies>
		<dependency>
			<groupId>junit</groupId>
			<artifactId>junit</artifactId>
			<version>4.10</version>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>javax.servlet</groupId>
			<artifactId>servlet-api</artifactId>
			<version>2.5</version>
			<scope>provided</scope>
		</dependency>
		<dependency>
			<groupId>javax.servlet.jsp</groupId>
			<artifactId>jsp-api</artifactId>
			<version>2.1</version>
			<scope>provided</scope>
		</dependency>
		<dependency>
			<groupId>org.eclipse.jetty</groupId>
			<artifactId>jetty-websocket</artifactId>
			<version>8.0.4.v20111024</version>
		</dependency>
		<dependency>
			<groupId>org.slf4j</groupId>
			<artifactId>slf4j-api</artifactId>
			<version>1.6.4</version>
		</dependency>
		<dependency>
			<groupId>ch.qos.logback</groupId>
			<artifactId>logback-core</artifactId>
			<version>1.0.0</version>
		</dependency>
		<dependency>
			<groupId>ch.qos.logback</groupId>
			<artifactId>logback-classic</artifactId>
			<version>1.0.0</version>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<version>2.3.2</version>
				<configuration>
					<source>1.6</source>
					<target>1.6</target>
				</configuration>
			</plugin>
			<plugin>
				<groupId>org.mortbay.jetty</groupId>
				<artifactId>jetty-maven-plugin</artifactId>
				<version>8.0.4.v20111024</version>
			</plugin>
		</plugins>
	</build>
</project>

実行する

あとは2つコンソールを開いて Maven を実行するだけです。1つはサーバの起動、1つはテストの実行に使います。
こういうとき、サーバを裏で起動しておいてテストを走らせて終わったら落とすみたいなのを一発でやるのってどうやると良いんですかね?ご存知の方がいらっしゃったら教えてください!

$ mvn clean compile jetty:run
$ mvn test

上手くいけばサーバ側のロガー出力に以下のような出力が出るはずです。(ロガーとかよく分からんという場合は、サーバ側の log#info() を System.out.println() に書き換えても良いかもです)

23:28:11 [qtp1734564525-22 - /websocket/] INFO t.j.websocket.JettyWebSocketServlet - onOpen
23:28:11 [qtp1734564525-23] INFO  t.j.websocket.JettyWebSocketServlet - onMessage: Hello, WebSocket!!
23:28:11 [qtp1734564525-23] INFO  t.j.websocket.JettyWebSocketServlet - onClose

ピンポンダッシュされてますね!上手く動いてるみたいです!

ドキュメント

今回使った Jetty の WebSocket パッケージの JavaDoc は以下にあるみたいです。
http://download.eclipse.org/jetty/stable-8/apidocs/org/eclipse/jetty/websocket/package-summary.html

まとめ

今回は Jetty の API を使ってサーバとクライアントの両方を実装してみました。Jetty の API はシンプルでキレイですね。個人的には気に入りました。あと、サーバだけでなくクライアントの実装も用意されている点もステキで、開発フェーズのテストがすごくやりやすいだろうなと思います。