WebSockets
WebSockets是一种支持全双工通信的套接字。现代的html5通过js api使得浏览器天生支持webSocket。但是Websockets在移动端以及服务器之间的通信也非常有用,在这些情况下可以复用一个已经存在的TCP连接。
处理WebSockets
一般Play通过action来处理http请求,但是WebSockets是完全不同的,没法使用action来处理。
Play处理WebSockets的机制是建立在Akka Streams之上的。一个WebSockets被抽象为Flow
,接收的信息被添加到flow中,flow产生的信息将发送到客户端中。
在理论上flow可以被视为一个接收某些信息,对信息进行一些处理再将信息传输出去的实体,- there is no reason why this has to be the case, the input of the flow may be completely disconnected from the output of the flow. Akka stream为了这个目的提供了一个构造器,Flow.fromSinkAndSource。并且在处理WebSockets时,输入与输出往往是不连接的。
Play在WebSocket中提供了一些工厂方法用来构建WebSockets。
使用actors来处理WebSockets
我们可以使用Play的工具ActorFlow来将一个ActorRef转换为一个flow。这个工具接收一个函数,该函数将ActorRef转换为一个akka.actor.Props对象,这个对象描述了Play需要创建用来接收WebSocket链接的actor(翻译的不是很准确,可以参考原文和事例代码,原文function that converts the ActorRef to send messages to a akka.actor.Props object that describes the actor that Play should create when it receives the WebSocket connection):
import play.libs.streams.ActorFlow;
import play.mvc.*;
import akka.actor.*;
import akka.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 akka.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提供的消息都会被发送到客户端中。
检测WebSocket的关闭
当WebSocket关闭时,Play会自动的停止actor。这意味着你可以实现actor的popStop方法来处理这一情况。下面例子关闭了一些资源:
public void postStop() throws Exception {
someResource.close();
}
关闭WebSocket
当actor处理WebSocket terminates时,Play会自动关闭WebSocket。所以如果需要关闭,发送一个PoisonPill
毒药信息给你的actor
self().tell(PoisonPill.getInstance(), self());
拒绝一个WebSocket
有些情况可能需要判断是否接受一个WebSocket请求,这种情况下可以用acceptOrResult
方法
public WebSocket socket() {
return WebSocket.Text.acceptOrResult(request -> {
if (session().get("user") != null) {
return CompletableFuture.completedFuture(
F.Either.Right(ActorFlow.actorRef(MyWebSocketActor::props,
actorSystem, materializer)));
} else {
return CompletableFuture.completedFuture(F.Either.Left(forbidden()));
}
});
}
Note: WebSocket协议没有实现[同源策略](https://en.wikipedia.org/wiki/Same-origin_policy),所以没有办法抵御WebSocket跨站劫持(http://www.christian-schneider.net/CrossSiteWebSocketHijacking.html)。为了保证WebSocket在劫持攻击下保持安全,需要检查request中的origin与服务端是否相同(防止跨域),并且进行验证(包括CSRF token)。如果没有通过验证可以拒绝该请求 |
异步接收WebSocket请求
返回值使用CompletionStage
处理不同类型的消息
目前只是展示了使用Text
Builder对字符串的处理。Play也支持通过Binary
Builder来处理ByteSting,使用Json
来将字符串解析为JSONNode。
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 streams来处理WebSocket
Actor并不总是合适的模型,特别是当WebSocket表现的更像是一个流。这时可以使用Akka Steams来处理。
import akka.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可以获取请求头部信息,这允许你读取标准的头部及session信息。但是无法获取请求体及响应信息。
这个例子中我们创建了一个sink将所有的信息打印到控制台中。为了发送信息,创建了一个source金金发送一个hello。我们也需要连接一个什么都不做的source,否则单个source会关闭flow,进而关闭链接。
可以在 https://www.websocket.org/echo.html上测试WebSocket,值需要将地址设为ws://localhost: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);
});
}
另外一个例子会将输入打印成标准输出,然后使用一个mapped flow返回给客户端
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;
});
});
}
配置帧长度
可以通过配置play.server.websocket.frame.maxLength或在启动时添加参数-Dwebsocket.frame.maxLength来配置帧的最大长度
sbt -Dwebsocket.frame.maxLength=64k run
HTTP的介绍基本就到这里了,后面会介绍Play中使用的模板