Play 2.6 WebSocket
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中使用的模板