CLOVER🍀

That was when it all began.

UndertowでWebSocketを使って遊ぶ

WebSocketを軽く触ってみようと思いまして。

WebSocketのAPIは、Java EE 7にJSR-356があるので、こちらを使って試すことを前提に
考えたいと思います。

The Java Community Process(SM) Program - JSRs: Java Specification Requests - detail JSR# 356

また、実装にはUndertowを使います。これでかなりてこずりましたが…。

Undertow · JBoss Community

お題は、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のものを参考に…したかったのですが、ほとんどなにも書いていません。

Undertow

仕方がないので、このあたりを参考に。

java - How to do websockets in embedded undertow? - Stack Overflow

undertow/JSRWebSocketServer.java at 1.4.10.Final · undertow-io/undertow · GitHub

https://github.com/undertow-io/undertow/blob/1.4.10.Final/websockets-jsr/src/test/java/io/undertow/websockets/jsr/test/TestMessagesReceivedInOrder.java

そういえば、ドキュメントは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