WebSocketを軽く触ってみようと思いまして。
WebSocketのAPIは、Java EE 7にJSR-356があるので、こちらを使って試すことを前提に
考えたいと思います。
The Java Community Process(SM) Program - JSRs: Java Specification Requests - detail JSR# 356
また、実装にはUndertowを使います。これでかなりてこずりましたが…。
お題は、echo、クライアントとサーバーそれぞれをUndertowとJSR-356のAPIを使って実装します。
準備
プロジェクトは、クライアントとサーバーでそれぞれ別にしました。
build.sbt
name := "undertow-websocket" lazy val commonSettings = Seq( version := "0.0.1-SNAPSHOT", organization := "org.littlewings", scalaVersion := "2.12.1", scalacOptions ++= Seq("-Xlint", "-deprecation", "-unchecked", "-feature"), updateOptions := updateOptions.value.withCachedResolution(true) ) lazy val server = (project in file("server")). settings( name := "undertow-websocket-server", commonSettings, libraryDependencies ++= Seq( "io.undertow" % "undertow-websockets-jsr" % "1.4.10.Final" ) ) lazy val client = (project in file("client")). settings( name := "undertow-websocket-client", commonSettings, libraryDependencies ++= Seq( "io.undertow" % "undertow-websockets-jsr" % "1.4.10.Final" ) )
ドキュメントは、Undertowのものを参考に…したかったのですが、ほとんどなにも書いていません。
仕方がないので、このあたりを参考に。
java - How to do websockets in embedded undertow? - Stack Overflow
undertow/JSRWebSocketServer.java at 1.4.10.Final · undertow-io/undertow · GitHub
そういえば、ドキュメントは1.3系ですけど、今の1.x系は1.4が最新なんですね。
サーバー側の実装
JSR-356のAPIを使ったサーバー側のエンドポイントの実装サンプルは調べるとけっこう出てくるので、簡単に。
server/src/main/scala/org/littlewings/javaee7/websocket/EchoServer.scala
package org.littlewings.javaee7.websocket import javax.websocket._ import javax.websocket.server.ServerEndpoint import org.jboss.logging.Logger @ServerEndpoint("/echo") class EchoServer { val logger: Logger = Logger.getLogger(getClass) @OnMessage def onMessage(session: Session, msg: String): Unit = { logger.infof("receive message = %s", msg) session.getBasicRemote.sendText(s"[$msg]") } @OnOpen def onOpen(session: Session, config: EndpointConfig): Unit = logger.infof("session open") @OnClose def onClose(session: Session, reason: CloseReason): Unit = logger.infof("close, reason = %s", reason.getReasonPhrase) @OnError def onError(session: Session, cause: Throwable): Unit = logger.errorf(cause, "error") }
@ServerEndpointで、WebSocketのエンドポイントであることと、パスを指定します。
@ServerEndpoint("/echo") class EchoServer {
@OnOpen、@OnClose、@OnErrorで、接続時、クローズ時、エラー発生時のイベントを受け取ります。
メッセージは@OnMessageで受け付け、今回はテキストメッセージを「[]」をくっつけて送り返すようにしています。
@OnMessage def onMessage(session: Session, msg: String): Unit = { logger.infof("receive message = %s", msg) session.getBasicRemote.sendText(s"[$msg]") }
続いて、起動クラス。ここが1番てこずったというか、情報がなかったというか。
server/src/main/scala/org/littlewings/javaee7/websocket/WebSocketServer.scala
package org.littlewings.javaee7.websocket import java.time.LocalDateTime import io.undertow.servlet.Servlets import io.undertow.servlet.api.DeploymentInfo import io.undertow.websockets.jsr.WebSocketDeploymentInfo import io.undertow.{Handlers, Undertow} import scala.io.StdIn object WebSocketServer { def main(args: Array[String]): Unit = { val webSocketDeploymentInfo = new WebSocketDeploymentInfo() .addEndpoint(classOf[EchoServer]) val builder = new DeploymentInfo() .setClassLoader(getClass.getClassLoader) .setContextPath("/") .setDeploymentName("myapp.war") .addServletContextAttribute(WebSocketDeploymentInfo.ATTRIBUTE_NAME, webSocketDeploymentInfo) val manager = Servlets.defaultContainer.addDeployment(builder) manager.deploy() val path = Handlers .path .addPrefixPath("/", manager.start()) val undertow = Undertow .builder .addHttpListener(8080, "localhost") .setHandler(path) .build() undertow.start() StdIn.readLine(s"[${LocalDateTime.now}] Please Enter, Stop.") undertow.stop() } }
JSR-356のAPIを使って実装したWebSocketエンドポイントを、Undertowにどうデプロイすればいいのかがわからずに
だいぶ困っていましたが、最終的にこんな感じになりました。
val webSocketDeploymentInfo = new WebSocketDeploymentInfo() .addEndpoint(classOf[EchoServer]) val builder = new DeploymentInfo() .setClassLoader(getClass.getClassLoader) .setContextPath("/") .setDeploymentName("myapp.war") .addServletContextAttribute(WebSocketDeploymentInfo.ATTRIBUTE_NAME, webSocketDeploymentInfo)
WebSocketDeploymentInfoを作成してエンドポイントを追加し、DeploymentInfoのServletContextの属性として設定すれば
OKみたいです。
あとはServletを使っていた時みたいにHandlerとして設定して、起動するだけですね。
val manager = Servlets.defaultContainer.addDeployment(builder) manager.deploy() val path = Handlers .path .addPrefixPath("/", manager.start()) val undertow = Undertow .builder .addHttpListener(8080, "localhost") .setHandler(path) .build() undertow.start()
ここまでで、サーバー側はおしまい。
クライアント側
あんまり情報がなさそうな、JSR-356のクライアント側。
こちらは、こんな感じの実装に。
client/src/main/scala/org/littlewings/javaee7/websocket/EchoClient.scala
package org.littlewings.javaee7.websocket import javax.websocket._ import org.jboss.logging.Logger @ClientEndpoint class EchoClient { val logger: Logger = Logger.getLogger(classOf[EchoClient]) @OnMessage def onMessage(session: Session, msg: String): Unit = { logger.infof("received from server: %s", msg) } @OnOpen def onOpen(session: Session, config: EndpointConfig): Unit = logger.infof("session open") @OnClose def onClose(session: Session, reason: CloseReason): Unit = logger.infof("close, reason = %s", reason.getReasonPhrase) @OnError def onError(session: Session, cause: Throwable): Unit = logger.errorf(cause, "error") }
@ClientEndpointをクラスに付与している以外は、ほとんどサーバー側のエンドポイントと変わりません。
起動用のクラスは、こちら。
client/src/main/scala/org/littlewings/javaee7/websocket/WebSocketClient.scala
package org.littlewings.javaee7.websocket import java.net.URI import javax.websocket.{CloseReason, ContainerProvider} import scala.io.StdIn object WebSocketClient { def main(args: Array[String]): Unit = { val container = ContainerProvider.getWebSocketContainer val session = container.connectToServer(classOf[EchoClient], URI.create("ws://localhost:8080/echo")) Iterator .continually(StdIn.readLine("enter text> ")) .takeWhile(_.trim != "exit") .filter(!_.trim.isEmpty) .foreach { message => session.getBasicRemote.sendText(message) } session.close(new CloseReason(CloseReason.CloseCodes.NORMAL_CLOSURE, "byebye!!")) } }
ContainerProviderからWebSocketContainerを取得し、あとは作成したクライアントのクラスとサーバー側のエンドポイントを指定して、
WebSocketのSessionを取得します。
あとはプロンプトっぽいものを簡単に作って、入力された文字列をサーバーに送信、「exit」と入力されると
終了するようにしています。
動作確認
それでは、試してみましょう。
まずはサーバー側を起動。
> run [info] Running org.littlewings.javaee7.websocket.WebSocketServer 2 11, 2017 4:22:50 午後 org.xnio.Xnio <clinit> INFO: XNIO version 3.3.6.Final 2 11, 2017 4:22:50 午後 org.xnio.nio.NioXnio <clinit> INFO: XNIO NIO Implementation Version 3.3.6.Final 2 11, 2017 4:22:50 午後 io.undertow.websockets.jsr.Bootstrap handleDeployment WARN: UT026009: XNIO worker was not set on WebSocketDeploymentInfo, the default worker will be used 2 11, 2017 4:22:50 午後 io.undertow.websockets.jsr.Bootstrap handleDeployment WARN: UT026010: Buffer pool was not set on WebSocketDeploymentInfo, the default pool will be used 2 11, 2017 4:22:50 午後 io.undertow.websockets.jsr.ServerWebSocketContainer addEndpointInternal INFO: UT026003: Adding annotated server endpoint class org.littlewings.javaee7.websocket.EchoServer for path /echo [2017-02-11T16:22:51.123] Please Enter, Stop.
クライアント側も起動。
> run [info] Running org.littlewings.javaee7.websocket.WebSocketClient 2 11, 2017 4:23:09 午後 org.xnio.Xnio <clinit> INFO: XNIO version 3.3.6.Final 2 11, 2017 4:23:09 午後 org.xnio.nio.NioXnio <clinit> INFO: XNIO NIO Implementation Version 3.3.6.Final 2 11, 2017 4:23:09 午後 io.undertow.websockets.jsr.ServerWebSocketContainer addEndpointInternal INFO: UT026004: Adding annotated client endpoint class org.littlewings.javaee7.websocket.EchoClient 2 11, 2017 4:23:10 午後 org.littlewings.javaee7.websocket.EchoClient onOpen INFO: session open enter text>
サーバー側にも、セッションをオープンしたログが出ます。
[2017-02-11T16:22:51.123] Please Enter, Stop.2 11, 2017 4:23:10 午後 org.littlewings.javaee7.websocket.EchoServer onOpen INFO: session open
クライアント側で、いくつか入力。
enter text> Hello WebSocket!! enter text> 2 11, 2017 4:23:53 午後 org.littlewings.javaee7.websocket.EchoClient onMessage INFO: received from server: [Hello WebSocket!!] enter text> こんにちは、世界 enter text> 2 11, 2017 4:24:00 午後 org.littlewings.javaee7.websocket.EchoClient onMessage INFO: received from server: [こんにちは、世界]
サーバー側の様子。
2 11, 2017 4:23:53 午後 org.littlewings.javaee7.websocket.EchoServer onMessage INFO: receive message = Hello WebSocket!! 2 11, 2017 4:23:59 午後 org.littlewings.javaee7.websocket.EchoServer onMessage INFO: receive message = こんにちは、世界
終了。
enter text> exit 2 11, 2017 4:24:26 午後 org.littlewings.javaee7.websocket.EchoClient onClose INFO: close, reason = byebye!!
サーバー側も、セッション完了。
2 11, 2017 4:24:26 午後 org.littlewings.javaee7.websocket.EchoServer onClose INFO: close, reason = byebye!!
OKそうです。
まとめ
Undertowを使って、Java EEのWebSocket API(JSR-356)を試してみました。
Echoくらいなら、JSR-356自体は簡単ですが、Undertowでの設定にだいぶてこずりました…。
まあ、動いたので良しとしましょう。
今回作成したソースコードは、こちらに置いています。
https://github.com/kazuhira-r/javaee7-scala-examples/tree/master/undertow-websocket