§WebSocket
WebSocket 是可從網路瀏覽器使用的 socket,基於允許雙向全雙工通訊的協定。只要伺服器與用戶端之間有 active WebSocket 連線,用戶端就能隨時傳送訊息,而伺服器也能隨時接收訊息。
現代符合 HTML5 規範的網路瀏覽器透過 JavaScript WebSocket API 原生支援 WebSocket。不過 WebSocket 不僅限於網路瀏覽器使用,有許多 WebSocket 用戶端程式庫可用,例如允許伺服器彼此通訊,以及原生行動應用程式使用 WebSocket。在這些情況下使用 WebSocket 的優點是可以重複使用 Play 伺服器使用的現有 TCP 埠。
提示:查看 caniuse.com 以深入了解哪些瀏覽器支援 WebSocket、已知問題等更多資訊。
§處理 WebSocket
到目前為止,我們使用 Action
執行個體來處理標準 HTTP 要求並傳回標準 HTTP 回應。WebSocket 完全不同,無法透過標準 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,請將 PoisonPill
傳送給您自己的 Actor
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 也內建處理 ByteString
框架的處理常式,使用 Binary
建構器,以及使用 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 串流直接處理 WebSocket
Actor 並不總是處理 WebSocket 的正確抽象化,特別是 WebSocket 的行為更像串流時。
相反地,你可以直接使用 Pekko 串流來處理 WebSocket。若要使用 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 上測試 WebSocket。只要將位置設定為
ws://127.0.0.1:9000
即可。
我們來寫另一個範例,捨棄輸入資料,並在傳送 Hello! 訊息後立即關閉 socket
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
設定 WebSocket 資料框架 的最大長度,或是在執行應用程式時傳遞 -Dwebsocket.frame.maxLength
系統屬性。例如
sbt -Dwebsocket.frame.maxLength=64k run
此組態可讓您更能控制 WebSocket 框架長度,且可調整以符合您的應用程式需求。它還可以減少使用長資料框架的阻斷服務攻擊。
§組態保持連線框架
首先,如果客戶端傳送 ping
框架至 Play 後端伺服器,它會自動以 pong
框架回應。根據 RFC 6455 第 5.5.2 節 的要求,這已內建於 Play 中,您不需要設定或組態任何內容。
相關地,請注意,當使用網路瀏覽器作為客戶端時,它們不會定期傳送 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
框架回應,您可以在應用程式中處理此框架(如果需要)。
與其使用雙向 ping/pong 保持連線心跳,藉由傳送 ping
訊息框,您可以讓 Play 傳送空的 pong
訊息框,以進行單向 pong 保持連線心跳,這表示客戶端不應回答
play.server.websocket.periodic-keep-alive-mode = "pong"
注意:請注意,如果您只在
application.conf
中設定這些設定檔,則在開發模式(使用sbt run
時)不會擷取這些設定檔。由於這些是後端伺服器設定檔,因此您必須在build.sbt
中透過PlayKeys.devSettings
設定它們,才能在開發模式中讓它們運作。更多關於原因和方法的詳細資訊,請參閱這裡。
對於開發,若要測試這些保持連線訊息框,我們建議使用 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
如果客戶端傳送除了預設 1000 以外的關閉狀態碼給您的 Play 應用程式,請務必使用根據 RFC 6455 第 7.4.1 節 定義且有效的狀態碼,以避免任何問題。例如,網頁瀏覽器在嘗試使用此類狀態碼時通常會擲回例外,而某些伺服器實作(例如 Netty)在收到它們時會失敗並擲回例外(並關閉連線)。
注意:特定於 pekko-http 的設定檔
pekko.http.server.websocket.periodic-keep-alive-max-idle
和pekko.http.server.websocket.periodic-keep-alive-mode
不會影響 Play。為了與後端伺服器無關,Play 使用其自己的低階 WebSocket 實作,因此會自行處理訊息框。
§WebSocket 與動作組合
考慮以下利用 動作組合 的控制器範例
@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 範本引擎
在此文件檔中發現錯誤?此頁面的原始程式碼可以在 這裡 找到。在閱讀 文件檔指南 後,請隨時提交拉取請求。有問題或建議要分享嗎?請前往 我們的社群論壇 與社群展開對話。