1. 程式人生 > >Play 2.6 WebSocket

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

處理不同型別的訊息

目前只是展示了使用TextBuilder對字串的處理。Play也支援通過BinaryBuilder來處理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中使用的模板