文档

§WebSockets

WebSockets 是基于允许双向全双工通信的协议的,可以在 Web 浏览器中使用的套接字。只要服务器和客户端之间存在活动的 WebSocket 连接,客户端就可以随时发送消息,服务器也可以随时接收消息。

现代符合 HTML5 标准的 Web 浏览器通过 JavaScript WebSocket API 原生支持 WebSockets。但是,WebSockets 不仅限于 Web 浏览器使用,还有许多可用的 WebSocket 客户端库,例如允许服务器相互通信,以及原生移动应用程序使用 WebSockets。在这些情况下使用 WebSockets 的优势是可以重用 Play 服务器使用的现有 TCP 端口。

提示:查看 caniuse.com 以了解有关哪些浏览器支持 WebSockets、已知问题以及更多信息的详细信息。

§处理 WebSockets

到目前为止,我们一直在使用 Action 实例来处理标准 HTTP 请求并发送回标准 HTTP 响应。WebSockets 是完全不同的东西,无法通过标准 Action 处理。

Play 的 WebSocket 处理机制建立在 Pekko 流的基础上。WebSocket 被建模为一个 Flow,传入的 WebSocket 消息被馈送到流中,而流产生的消息被发送到客户端。

请注意,虽然从概念上讲,流通常被视为接收消息、对其进行一些处理,然后产生处理后的消息的东西 - 但没有理由必须如此,流的输入可能与流的输出完全断开连接。Pekko 流提供了一个构造函数,Flow.fromSinkAndSource,专门用于此目的,并且在处理 WebSocket 时,输入和输出通常不会连接。

Play 在 WebSocket 中提供了一些用于构建 WebSocket 的工厂方法。

§使用 actor 处理 WebSocket

要使用 actor 处理 WebSocket,我们可以使用 Play 实用程序,ActorFlowActorRef 转换为流。此实用程序采用一个函数,该函数将 ActorRef 转换为发送消息到一个 pekko.actor.Props 对象,该对象描述了 Play 在接收到 WebSocket 连接时应该创建的 actor。

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));
  }
}

在这种情况下,我们发送到这里的 actor 看起来像这样

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();
  }
}

从客户端接收到的任何消息都将发送到 actor,而发送到 Play 提供的 actor 的任何消息都将发送到客户端。上面的 actor 只是将从客户端接收到的每条消息都发送回去,并在其前面加上 我收到了你的消息:

§检测 WebSocket 何时关闭

当 WebSocket 关闭时,Play 将自动停止 actor。这意味着您可以通过实现 actor 的 postStop 方法来处理这种情况,以清理 WebSocket 可能消耗的任何资源。例如

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

§关闭 WebSocket

当处理 WebSocket 的 actor 终止时,Play 将自动关闭 WebSocket。因此,要关闭 WebSocket,请向您自己的 actor 发送一个 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

您可能需要进行一些异步处理,然后才能准备好创建 actor 或拒绝 WebSocket,如果是这种情况,您可以简单地返回 CompletionStage<WebSocket> 而不是 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));
}

§直接使用 Pekko 流处理 WebSockets

Actor 并不总是处理 WebSockets 的正确抽象,尤其是在 WebSocket 更像流时。

相反,您可以直接使用 Pekko 流来处理 WebSockets。要使用 Pekko 流,首先导入 Pekko 流 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! 消息。我们还需要连接一个永远不会发送任何内容的源,否则我们的单个源将终止流,从而终止连接。

提示:您可以在 https://www.websocket.org/echo.html 上测试 WebSockets。只需将位置设置为 ws://127.0.0.1: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 帧长度

您可以使用 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,则 Play 会在达到play.server.http[s].idleTimeout后关闭空闲的 WebSocket 连接。

为了避免这种情况,您可以让 Play 后端服务器在达到空闲超时时间(在服务器没有从客户端收到任何消息的情况下)后 ping 客户端。

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

Play 然后会向客户端发送一个空的ping帧。通常这意味着一个活动的客户端会用一个pong帧进行响应,您可以根据需要在您的应用程序中处理该帧。

您可以让 Play 发送一个空的pong帧来进行单向 pong 保持活动心跳,而不是使用双向 ping/pong 保持活动心跳来发送一个ping帧,这意味着客户端不应该进行响应。

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

注意:请注意,如果您只在application.conf中设置这些配置,它们不会在开发模式(使用sbt run时)生效。因为这些是后端服务器配置,您必须通过PlayKeys.devSettings在您的build.sbt中设置它们,才能使它们在开发模式下生效。更多关于为什么以及如何操作的详细信息,请参见这里

为了在开发过程中测试这些保持活动帧,我们建议使用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

如果客户端向您的 Play 应用程序发送的关闭状态码不是默认的 1000,请确保它们使用根据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 实现,因此它自己处理帧。

§WebSockets 和 Action 组合

考虑以下使用动作组合的控制器示例。

@Security.Authenticated
public class HomeController extends Controller {

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

默认情况下,在处理WebSocket(例如上面显示的socket()方法)时,不会应用动作组合。因此,诸如@Security.Authenticated@Restrict(来自Deadbolt 2 库)之类的注释无效,并且永远不会执行。

从 Play 2.9 开始,您可以通过将配置选项 play.http.actionComposition.includeWebSocketActions 设置为 true 来启用它。将 WebSocket 操作方法包含在操作组合中,确保使用 @Security.Authenticated@Restrict 注释的操作现在按预期执行。这种方法的优点是您可能不需要在注释操作方法中已实现的 acceptOrResult 方法中重复身份验证或授权代码。

下一步:Twirl 模板引擎


发现此文档中的错误?此页面的源代码可以在 这里 找到。阅读完 文档指南 后,请随时贡献拉取请求。有疑问或建议要分享?前往 我们的社区论坛 与社区展开讨论。