§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 实用程序,ActorFlow 将 ActorRef
转换为流。此实用程序采用一个函数,该函数将 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-idle
和pekko.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 模板引擎
发现此文档中的错误?此页面的源代码可以在 这里 找到。阅读完 文档指南 后,请随时贡献拉取请求。有疑问或建议要分享?前往 我们的社区论坛 与社区展开讨论。