ドキュメント

§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を構築するためのいくつかのファクトリメソッドを提供しています。

§アクターを使用したWebSocketの処理

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

import play.libs.streams.ActorFlow;
import play.mvc.*;
import org.apache.pekko.actor.*;
import org.apache.pekko.stream.*;
import javax.inject.Inject;

public class HomeController extends Controller {

  private final ActorSystem actorSystem;
  private final Materializer materializer;

  @Inject
  public HomeController(ActorSystem actorSystem, Materializer materializer) {
    this.actorSystem = actorSystem;
    this.materializer = materializer;
  }

  public WebSocket socket() {
    return WebSocket.Text.accept(
        request -> ActorFlow.actorRef(MyWebSocketActor::props, actorSystem, materializer));
  }
}

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

import org.apache.pekko.actor.*;

public class MyWebSocketActor extends AbstractActor {

  public static Props props(ActorRef out) {
    return Props.create(MyWebSocketActor.class, out);
  }

  private final ActorRef out;

  public MyWebSocketActor(ActorRef out) {
    this.out = out;
  }

  @Override
  public Receive createReceive() {
    return receiveBuilder()
        .match(String.class, message -> out.tell("I received your message: " + message, self()))
        .build();
  }
}

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

§WebSocketが閉じられたときの検出

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

public void postStop() throws Exception {
  someResource.close();
}

§WebSocketを閉じる

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

self().tell(PoisonPill.getInstance(), self());

§WebSocketを拒否する

WebSocketリクエストを拒否したい場合があります。たとえば、ユーザーがWebSocketに接続するために認証されている必要がある場合、またはWebSocketがパスに渡されるIDを持つリソースに関連付けられているが、そのIDを持つリソースが存在しない場合などです。Playはこの目的のために`acceptOrResult` WebSocketビルダーを提供しています。

public WebSocket socket() {
  return WebSocket.Text.acceptOrResult(
      request ->
          CompletableFuture.completedFuture(
              request
                  .session()
                  .get("user")
                  .map(
                      user ->
                          F.Either.<Result, Flow<String, String, ?>>Right(
                              ActorFlow.actorRef(
                                  MyWebSocketActor::props, actorSystem, materializer)))
                  .orElseGet(() -> F.Either.Left(forbidden()))));
}

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

§WebSocketを非同期的に受け入れる

アクターを作成したりWebSocketを拒否したりする準備ができる前に、非同期処理を行う必要がある場合があります。その場合は、`WebSocket`の代わりに`CompletionStage<WebSocket>`を返すことができます。

§さまざまな種類のメッセージの処理

これまでは、`Text`ビルダーを使用して`String`フレームを処理することのみを見てきました。Playには、`Binary`ビルダーを使用した`ByteString`フレームと、`Json`ビルダーを使用した`String`フレームから解析された`JSONNode`メッセージの組み込みハンドラーもあります。`Json`ビルダーの使用例を次に示します。

public WebSocket socket() {
  return WebSocket.Json.accept(
      request -> ActorFlow.actorRef(MyWebSocketActor::props, actorSystem, materializer));
}

Playは、`JSONNode`メッセージを上位レベルのオブジェクトとの間で変換するための組み込みサポートも提供しています。入力イベントを表す`InEvent`クラスと、出力イベントを表す別のクラス`OutEvent`がある場合、次のように使用できます。

public WebSocket socket() {
  return WebSocket.json(InEvent.class)
      .accept(
          request -> ActorFlow.actorRef(MyWebSocketActor::props, actorSystem, materializer));
}

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

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

代わりに、Akkaストリームを直接使用してWebSocketを処理できます。Akkaストリームを使用するには、最初にAkkaストリームjavadslをインポートします。

import org.apache.pekko.stream.javadsl.*;

これで、次のように使用できます。

public WebSocket socket() {
  return WebSocket.Text.accept(
      request -> {
        // Log events to the console
        Sink<String, ?> in = Sink.foreach(System.out::println);

        // Send a single 'Hello!' message and then leave the socket open
        Source<String, ?> out = Source.single("Hello!").concat(Source.maybe());

        return Flow.fromSinkAndSource(in, out);
      });
}

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

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

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

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

public WebSocket socket() {
  return WebSocket.Text.accept(
      request -> {
        // Just ignore the input
        Sink<String, ?> in = Sink.ignore();

        // Send a single 'Hello!' message and close
        Source<String, ?> out = Source.single("Hello!");

        return Flow.fromSinkAndSource(in, out);
      });
}

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

public WebSocket socket() {
  return WebSocket.Text.accept(
      request -> {

        // log the message to stdout and send response back to client
        return Flow.<String>create()
            .map(
                msg -> {
                  System.out.println(msg);
                  return "I received your message: " + msg;
                });
      });
}

§WebSocketへのアクセス

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

GET      /ws                                   controllers.Application.socket 

§WebSocketフレーム長の構成

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

sbt -Dwebsocket.frame.maxLength=64k run

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

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

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

デフォルトでは、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実装を使用し、フレームを自分で処理します。

§WebSocketとアクションコンポジション

アクションコンポジションを利用する以下のコントローラーの例を考えてみましょう。

@Security.Authenticated
public class HomeController extends Controller {

    @Restrict({ @Group({"admin"}) })
    public WebSocket socket() {
        return WebSocket.Text.acceptOrResult(request -> /* ... */);
    }
}

デフォルトでは、上記に示したsocket()メソッドなどのWebSocketを処理する場合、アクションコンポジションは適用されません。その結果、@Security.Authenticated@RestrictDeadbolt 2ライブラリから)のようなアノテーションは効果がなく、実行されません。

Play 2.9以降、設定オプションplay.http.actionComposition.includeWebSocketActionstrueに設定することで有効にできます。WebSocketアクションメソッドをアクションコンポジションに含めることで、@Security.Authenticated@Restrictでアノテーションが付けられたアクションが期待どおりに実行されるようになります。このアプローチの利点は、アノテーションアクションメソッドに既に実装されているacceptOrResultメソッドで認証または承認コードを複製する必要がないことです。

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


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