ドキュメント

§WebSocket

WebSocketは、双方向全二重通信を可能にするプロトコルに基づいて、Webブラウザから使用できるソケットです。サーバーとクライアントの間にアクティブなWebSocket接続がある限り、クライアントはいつでもメッセージを送信でき、サーバーはいつでもメッセージを受信できます。

最新のHTML5準拠のWebブラウザは、JavaScript WebSocket APIを介してWebSocketをネイティブにサポートしています。ただし、WebSocketはWebブラウザで使用されるだけではありません。多くのWebSocketクライアントライブラリが利用可能であり、たとえばサーバーが相互に通信したり、ネイティブモバイルアプリがWebSocketを使用したりできます。これらのコンテキストでWebSocketを使用すると、Playサーバーが使用する既存のTCPポートを再利用できるという利点があります。

ヒント: caniuse.comで、WebSocketをサポートするブラウザ、既知の問題、その他の情報を確認してください。

§WebSocketの処理

これまで、標準のHTTPリクエストを処理し、標準のHTTPレスポンスを送り返すために、Actionインスタンスを使用していました。WebSocketはまったく異なるものであり、標準のActionでは処理できません。

PlayのWebSocket処理メカニズムは、Akkaストリームに基づいて構築されています。WebSocketはFlowとしてモデル化され、着信WebSocketメッセージはフローに供給され、フローによって生成されたメッセージはクライアントに送信されます。

概念的には、フローはメッセージを受信し、それらに何らかの処理を行い、処理されたメッセージを生成するものとして見られることがよくありますが、これが当てはまる必要はありません。フローの入力は、フローの出力と完全に切断される場合があります。Akkaストリームは、まさにこの目的のために、コンストラクタ`Flow.fromSinkAndSource`を提供しており、WebSocketを処理する場合、入力と出力はまったく接続されないことがよくあります。

Playは、WebSocketでWebSocketを構築するためのファクトリメソッドをいくつか提供しています。

§Akkaストリームとアクターを使用したWebSocketの処理

アクターでWebSocketを処理するには、PlayユーティリティであるActorFlowを使用して、ActorRefをフローに変換できます。このユーティリティは、メッセージを送信するActorRefを、WebSocket接続を受信したときにPlayが作成する必要があるアクターを記述するakka.actor.Propsオブジェクトに変換する関数を受け取ります。

import javax.inject.Inject

import org.apache.pekko.actor.ActorSystem
import org.apache.pekko.stream.Materializer
import play.api.libs.streams.ActorFlow
import play.api.mvc._

class Application @Inject() (cc: ControllerComponents)(implicit system: ActorSystem, mat: Materializer)
    extends AbstractController(cc) {
  def socket = WebSocket.accept[String, String] { request =>
    ActorFlow.actorRef { out => MyWebSocketActor.props(out) }
  }
}

`ActorFlow.actorRef(...)`は、Akkaストリーム`Flow[In, Out, _]`に置き換えることができますが、アクターは一般的に最も簡単な方法です。

この場合に送信しているアクターは次のようになります。

import org.apache.pekko.actor._

object MyWebSocketActor {
  def props(out: ActorRef) = Props(new MyWebSocketActor(out))
}

class MyWebSocketActor(out: ActorRef) extends Actor {
  def receive = {
    case msg: String =>
      out ! ("I received your message: " + msg)
  }
}

クライアントから受信したメッセージはすべてアクターに送信され、Playによって提供されたアクターに送信されたメッセージはすべてクライアントに送信されます。上記のアクターは、クライアントから受信したすべてのメッセージの先頭に`I received your message:`を付けて送り返します。

§WebSocketが閉じられたことを検出する

WebSocketが閉じられると、Playは自動的にアクターを停止します。これは、WebSocketが消費した可能性のあるリソースをクリーンアップするために、アクターの`postStop`メソッドを実装することで、この状況を処理できることを意味します。例えば

override def postStop() = {
  someResource.close()
}

§WebSocketを閉じる

WebSocketを処理するアクターが終了すると、Playは自動的にWebSocketを閉じます。そのため、WebSocketを閉じるには、独自のアクターに`PoisonPill`を送信します。


import org.apache.pekko.actor.PoisonPill self ! PoisonPill

§WebSocketを拒否する

WebSocketリクエストを拒否したい場合があります。たとえば、ユーザーがWebSocketに接続するために認証されている必要がある場合、またはWebSocketがパスに渡されるIDを持つ一部のリソースに関連付けられているが、そのIDを持つリソースが存在しない場合などです。Playは、これに対処するために`acceptOrResult`を提供し、結果(禁止や見つかりませんなど)を返すか、WebSocketを処理するアクターを返すことができます。

  import javax.inject.Inject

  import org.apache.pekko.actor.ActorSystem
  import org.apache.pekko.stream.Materializer
  import play.api.libs.streams.ActorFlow
  import play.api.mvc._

  class Application @Inject() (cc: ControllerComponents)(implicit system: ActorSystem, mat: Materializer)
      extends AbstractController(cc) {
    def socket = WebSocket.acceptOrResult[String, String] { request =>
      Future.successful(request.session.get("user") match {
        case None => Left(Forbidden)
        case Some(_) =>
          Right(ActorFlow.actorRef { out => MyWebSocketActor.props(out) })
      })
    }
  }
}

**注**: WebSocketプロトコルは同一オリジンポリシーを実装していないため、クロスサイトWebSocketハイジャックから保護しません。ハイジャックからwebsocketを保護するには、リクエストの`Origin`ヘッダーをサーバーのオリジンと照合し、手動認証(CSRFトークンを含む)を実装する必要があります。WebSocketリクエストがセキュリティチェックに合格しない場合、`acceptOrResult`はForbidden結果を返すことによってリクエストを拒否する必要があります。

§異なるタイプのメッセージの処理

これまでは、`String`フレームの処理のみを見てきました。Playには、`Array[Byte]`フレームと、`String`フレームから解析された`JsValue`メッセージの組み込みハンドラもあります。これらをWebSocket作成メソッドの型パラメーターとして渡すことができます。たとえば、

import javax.inject.Inject

import org.apache.pekko.actor.ActorSystem
import org.apache.pekko.stream.Materializer
import play.api.libs.json._
import play.api.libs.streams.ActorFlow
import play.api.mvc._

class Application @Inject() (cc: ControllerComponents)(implicit system: ActorSystem, mat: Materializer)
    extends AbstractController(cc) {
  def socket = WebSocket.accept[JsValue, JsValue] { request =>
    ActorFlow.actorRef { out => MyWebSocketActor.props(out) }
  }
}

2つの型パラメーターがあることに気付いたかもしれません。これにより、着信メッセージと発信メッセージで異なる型のメッセージを処理できます。これは通常、低レベルのフレームタイプでは役に立ちませんが、メッセージをより高レベルのタイプに解析する場合に役立ちます。

たとえば、JSONメッセージを受信し、着信メッセージを`InEvent`として解析し、発信メッセージを`OutEvent`としてフォーマットしたいとします。最初に行うことは、`InEvent`および`OutEvent`タイプのJSONフォーマットを作成することです。

import play.api.libs.json._

implicit val inEventFormat: Format[InEvent]   = Json.format[InEvent]
implicit val outEventFormat: Format[OutEvent] = Json.format[OutEvent]

これで、これらのタイプのWebSocket `MessageFlowTransformer`を作成できます。

import play.api.mvc.WebSocket.MessageFlowTransformer

implicit val messageFlowTransformer: MessageFlowTransformer[InEvent, OutEvent] =
  MessageFlowTransformer.jsonMessageFlowTransformer[InEvent, OutEvent]

最後に、これらをWebSocketで使用できます。

import javax.inject.Inject

import org.apache.pekko.actor.ActorSystem
import org.apache.pekko.stream.Materializer
import play.api.libs.streams.ActorFlow
import play.api.mvc._

class Application @Inject() (cc: ControllerComponents)(implicit system: ActorSystem, mat: Materializer)
    extends AbstractController(cc) {
  def socket = WebSocket.accept[InEvent, OutEvent] { request =>
    ActorFlow.actorRef { out => MyWebSocketActor.props(out) }
  }
}

これで、アクターでは`InEvent`タイプのメッセージを受信し、`OutEvent`タイプのメッセージを送信できます。

§Akkaストリームを直接使用したWebSocketの処理

アクターは、WebSocketを処理するための適切な抽象化とは限りません。特に、WebSocketがストリームのように動作する場合です。

import org.apache.pekko.stream.scaladsl._
import play.api.mvc._

def socket = WebSocket.accept[String, String] { request =>
  // Log events to the console
  val in = Sink.foreach[String](println)

  // Send a single 'Hello!' message and then leave the socket open
  val out = Source.single("Hello!").concat(Source.maybe)

  Flow.fromSinkAndSource(in, out)
}

`WebSocket`は、リクエストヘッダー(WebSocket接続を開始するHTTPリクエストからの)にアクセスできるため、標準ヘッダーとセッションデータを取得できます。ただし、リクエスト本文やHTTPレスポンスにはアクセスできません。

この例では、各メッセージをコンソールに出力する単純なシンクを作成しています。メッセージを送信するために、単一の**Hello!**メッセージを送信する単純なソースを作成します。また、何も送信しないソースを連結する必要があります。そうでない場合、単一のソースはフローを終了し、接続も終了します。

**ヒント**: https://www.websocket.org/echo.htmlでWebSocketをテストできます。場所を`ws://localhost:9000`に設定するだけです。

入力データを破棄し、**Hello!**メッセージを送信した直後にソケットを閉じる別の例を書いてみましょう。

import org.apache.pekko.stream.scaladsl._
import play.api.mvc._

def socket = WebSocket.accept[String, String] { request =>
  // Just ignore the input
  val in = Sink.ignore

  // Send a single 'Hello!' message and close
  val out = Source.single("Hello!")

  Flow.fromSinkAndSource(in, out)
}

入力データが標準出力に記録され、マップされたフローを使用してクライアントに送り返される別の例を次に示します。

import org.apache.pekko.stream.scaladsl._
import play.api.mvc._

def socket = WebSocket.accept[String, String] { request =>
  // log the message to stdout and send response back to client
  Flow[String].map { msg =>
    println(msg)
    "I received your message: " + msg
  }
}

§WebSocketへのアクセス

データを送信したり、websocketにアクセスするには、ルートファイルにwebsocketのルートを追加する必要があります。例えば

GET      /ws                                   controllers.Application.socket 

§WebSocketフレーム長の構成

`play.server.websocket.frame.maxLength`を使用するか、アプリケーションの実行時にシステムプロパティ`-Dwebsocket.frame.maxLength`を渡すことで、WebSocketデータフレームの最大長を設定できます。例えば

sbt -Dwebsocket.frame.maxLength=64k run

この構成により、WebSocketフレーム長をより詳細に制御できるようになり、アプリケーションの要件に合わせて調整できます。また、長いデータフレームを使用したサービス拒否攻撃を軽減することもできます。

§キープアライブフレームの構成

まず、クライアントがPlayバックエンドサーバーに`ping`フレームを送信すると、自動的に`pong`フレームで応答します。これはRFC 6455セクション5.5.2に従って必須事項であるため、Play内でハードコードされており、何も設定する必要はありません。
それに関連して、Webブラウザをクライアントとして使用する場合、定期的に`ping`フレームを送信せず、JavaScript APIもサポートしていないことに注意してください(Firefoxのみ`network.websocket.timeout.ping.request`設定があり、`about:config`で手動で設定できますが、実際には役に立ちません)。

デフォルトでは、Playバックエンドサーバーはクライアントに定期的にpingフレームを送信しません。つまり、サーバーとクライアントのどちらも定期的にpingまたはpongを送信しない場合、アイドル状態のWebSocket接続はplay.server.http[s].idleTimeoutに達した後、Playによって閉じられます。

これを回避するには、アイドルタイムアウト(サーバーがクライアントから何も聞いていない時間)に達した後、Playバックエンドサーバーがクライアントにpingを送信するように設定できます。

play.server.websocket.periodic-keep-alive-max-idle = 10 seconds

Playは、クライアントに空のpingフレームを送信します。通常、これはアクティブなクライアントがpongフレームで応答することを意味し、必要に応じてアプリケーションで処理できます。

pingフレームを送信することによる双方向のping/pongキープアライブハートビートを使用する代わりに、Playに空のpongフレームを送信させて、単方向のpongキープアライブハートビートを実行させることができます。これは、クライアントが応答する必要がないことを意味します。

play.server.websocket.periodic-keep-alive-mode = "pong"

注意: これらの設定は、application.confにのみ設定した場合、開発モード(sbt runを使用する場合)では有効になりません。これらはバックエンドサーバーの設定であるため、開発モードで動作させるには、build.sbtPlayKeys.devSettingsを介して設定する必要があります。理由と方法の詳細はこちらをご覧ください。

開発において、これらのキープアライブフレームをテストするには、監視にWireshark(例:(http or websocket)のような表示フィルターを使用)を使用し、websocatを使用してサーバーにフレームを送信することをお勧めします。例えば、

# Add --ping-interval 5 if you want to ping the server every 5 seconds
websocat -vv --close-status-code 1000 --close-reason "bye bye" ws://127.0.0.1:9000/websocket

クライアントがデフォルトの1000以外のクローズステータスコードをPlayアプリケーションに送信する場合、問題を回避するために、RFC 6455 セクション7.4.1に従って定義され、有効なステータスコードを使用していることを確認してください。たとえば、Webブラウザは通常、そのようなステータスコードを使用しようとすると例外をスローし、一部のサーバー実装(例:Netty)はそれらを受信すると例外で失敗します(そして接続を閉じます)。

注意: pekko-http固有の設定pekko.http.server.websocket.periodic-keep-alive-max-idlepekko.http.server.websocket.periodic-keep-alive-modeは、Playには影響しません。バックエンドサーバーに依存しないように、Playは独自の低レベルWebSocket実装を使用し、フレームを独自に処理します。

次: Twirlテンプレートエンジン


このドキュメントに誤りを見つけましたか?このページのソースコードはこちらにあります。ドキュメントガイドラインを読んだ後、プルリクエストを送信してください。質問やアドバイスがあれば、コミュニティフォーラムにアクセスして、コミュニティとの会話を始めてください。