Dropwizard 0.8.0でWebSocketに挑戦

今回は現在開発が進んでいる Dropwizard 0.8.x 系に WebSocket を組み込むことに挑戦する。

Dropwizard 0.7.x が組み込む Jetty は少しバージョンが古く 9.0 系統。
Jetty はこの 9.0.x 系と 9.1 以降では WebSocket の実装が違う。JSRで仕様が固まったタイミングとの兼ね合いから仕方がないところか。

Dropwizard 0.8 系が組み込む Jetty は 9.2 系。WebSocket も当然最近の仕様となっている。

[こちらのページ](http://stackoverflow.com/questions/23234801/implementing-long-polling-server-using-dropwizard-0-7-0 "Implementing long polling server using Dropwizard 0.7.0") を参考にして実装してみた。
内容は単純にブロードキャストするだけのチャットサーバーとなっている。

ソースは以下の通り。Java8～

```java:SimpleChatServerApplication.java
import io.dropwizard.Application;
import io.dropwizard.setup.Environment;

import javax.servlet.ServletRegistration;

public class SimpleChatServerApplication extends Application<SimpleChatServerConfiguration> {

	public static void main(String[] args) throws Exception {
		new SimpleChatServerApplication().run(args);
	}

	@Override
	public void run(SimpleChatServerConfiguration configuration, Environment environment) throws Exception {
		final ServletRegistration.Dynamic websocket = environment.servlets().addServlet("ws", new ChatServerServlet());
		websocket.setAsyncSupported(true);
		websocket.addMapping("/ws/*");

		final TemplateHealthCheck healthCheck = new TemplateHealthCheck();
		environment.healthChecks().register("template", healthCheck);
	}
}
```

```java:SimpleChatServerConfiguration.java
import io.dropwizard.Configuration;

public class SimpleChatServerConfiguration extends Configuration {
}
```

```java:ChatServerServlet.java
import org.eclipse.jetty.websocket.servlet.WebSocketServlet;
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;

import simple.chat.server.EndpointHandler.EndpointHandlerBuilder;

public class ChatServerServlet extends WebSocketServlet {
	
	private static final long serialVersionUID = 1L;

	public ChatServerServlet() {
		EndpointHandlerBuilder.buildEndpointHandler();
	}

	@Override
	public void configure(WebSocketServletFactory factory) {
		factory.register(Endpoint.class);
	}
}
```

```java:Endpoint.java
import java.io.IOException;

import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;

@WebSocket
public class Endpoint {
	private Session session_;
	
	public Session getSession() {
		return session_;
	}

	@OnWebSocketConnect
    public void onConnect(Session session) {
        this.session_ = session;
		EndpointHandler handler = EndpointHandler.getInstance();
		handler.register(this);
    }

	@OnWebSocketMessage
	public void onMessage(Session session, String message) throws IOException {
		EndpointHandler handler = EndpointHandler.getInstance();
		handler.broadcastMessage(message);
	}

	@OnWebSocketClose
    public void onClose(int statusCode, String reason) {
		EndpointHandler handler = EndpointHandler.getInstance();
		handler.removeByInstance(this);
    }
	
	@OnWebSocketError
	public void onError(Throwable cause) {
		EndpointHandler handler = EndpointHandler.getInstance();
		handler.removeByInstance(this);
		cause.printStackTrace(System.err);
	}
}
```

```java:EndpointHandler.java
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class EndpointHandler {
	public static class EndpointHandlerBuilder {
		public static void buildEndpointHandler() {
			instance_ = new EndpointHandler();
		}
	}

	private static EndpointHandler instance_;
	
	public static EndpointHandler getInstance() {
		return instance_;
	}
	
	private Map<String, Endpoint> endpoints_ = new ConcurrentHashMap<>();
	
	public void register(Endpoint endpoint) {
		endpoints_.put(String.valueOf(endpoint.hashCode()), endpoint);
	}

	public Endpoint remove(String sessionId) {
		return endpoints_.remove(sessionId);
	}

	public Endpoint removeByInstance(Endpoint endpoint) {
		return remove(String.valueOf(endpoint.hashCode()));
	}

	public Endpoint get(String sessionId) {
		return endpoints_.get(sessionId);
	}
	
	public void broadcastMessage(String message) {
		endpoints_.values().forEach(p -> {
			try {
				p.getSession().getRemote().sendString(message);
			} catch (IOException e) {
				e.printStackTrace();
			}
		});
	}
}
```

```java:TemplateHealthCheck.java
import com.codahale.metrics.health.HealthCheck;

public class TemplateHealthCheck extends HealthCheck {
	@Override
	protected Result check() throws Exception {
		return Result.healthy();
	}
}
```

クライアントに Chrome の拡張機能 Simple WebSocket Client を使ってざっくりと動作確認を行っている。
接続urlは次のパターン。（ポートは適宜読み替え）

```text
ws://localhost:8080/ws
```

Gradleで設定する依存関係は以下の通り。

```groovy
dependencies {
    compile "io.dropwizard:dropwizard-core:0.8.0"
    compile "org.eclipse.jetty.websocket:websocket-server:9.2.9.v20150224"
}
```

##### 2015/03/11 依存設定修正

dropwizard 0.8が正式になったようなので、依存設定を修正。
コードの方は特に変更する必要も無く以前と同じ動作。

参考リンク

[Implementing long polling server using Dropwizard 0.7.0 - stackoverflow.com](http://stackoverflow.com/questions/23234801/implementing-long-polling-server-using-dropwizard-0-7-0 "Implementing long polling server using Dropwizard 0.7.0")