§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.sbt
でPlayKeys.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-idle
とpekko.http.server.websocket.periodic-keep-alive-mode
は、Playには影響しません。バックエンドサーバーに依存しないように、Playは独自の低レベルWebSocket実装を使用し、フレームを独自に処理します。
このドキュメントに誤りを見つけましたか?このページのソースコードはこちらにあります。ドキュメントガイドラインを読んだ後、プルリクエストを送信してください。質問やアドバイスがあれば、コミュニティフォーラムにアクセスして、コミュニティとの会話を始めてください。