§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。
§使用 Pekko 串流和 Actor 處理 WebSocket
若要使用 Actor 處理 WebSocket,我們可以使用 Play 工具程式 ActorFlow,將 ActorRef
轉換為串流。此工具程式會取得一個函式,將 ActorRef
轉換為 pekko.actor.Props
物件,用來描述 Play 在收到 WebSocket 連線時應該建立的 Actor
import javax.inject.Inject
import org.apache.pekko.actor.ActorSystem
import org.apache.pekko.stream.Materializer
import play.api.libs.streams.ActorFlow
import play.api.mvc._
class Application @Inject() (cc: ControllerComponents)(implicit system: ActorSystem, mat: Materializer)
extends AbstractController(cc) {
def socket = WebSocket.accept[String, String] { request =>
ActorFlow.actorRef { out => MyWebSocketActor.props(out) }
}
}
請注意,ActorFlow.actorRef(...)
可以替換為任何 Pekko 串流 Flow[In, Out, _]
,但 Actor 通常是最直接的方式。
我們在此傳送的 Actor 在這種情況下看起來像這樣
import org.apache.pekko.actor._
object MyWebSocketActor {
def props(out: ActorRef) = Props(new MyWebSocketActor(out))
}
class MyWebSocketActor(out: ActorRef) extends Actor {
def receive = {
case msg: String =>
out ! ("I received your message: " + msg)
}
}
從用戶端接收到的任何訊息都會傳送給 actor,而 Play 提供的 actor 傳送的任何訊息都會傳送給用戶端。上述的 actor 只會將從用戶端接收到的每則訊息傳送回去,並在訊息前加上 我收到你的訊息:
。
§偵測 WebSocket 何時關閉
當 WebSocket 關閉時,Play 會自動停止 actor。這表示你可以透過實作 actor 的 postStop
方法來處理這個情況,以清理 WebSocket 可能已耗用的任何資源。例如
override def postStop() = {
someResource.close()
}
§關閉 WebSocket
當處理 WebSocket 的 actor 終止時,Play 會自動關閉 WebSocket。因此,要關閉 WebSocket,請將 PoisonPill
傳送給自己的 actor
import org.apache.pekko.actor.PoisonPill
self ! PoisonPill
§拒絕 WebSocket
有時你可能想要拒絕 WebSocket 要求,例如,如果使用者必須經過驗證才能連線到 WebSocket,或如果 WebSocket 與某個資源相關聯,其 ID 傳遞在路徑中,但不存在具有該 ID 的資源。Play 提供 acceptOrResult
來解決這個問題,讓你能夠傳回結果(例如禁止或找不到),或傳回要使用 actor 來處理 WebSocket 的結果
import javax.inject.Inject
import org.apache.pekko.actor.ActorSystem
import org.apache.pekko.stream.Materializer
import play.api.libs.streams.ActorFlow
import play.api.mvc._
class Application @Inject() (cc: ControllerComponents)(implicit system: ActorSystem, mat: Materializer)
extends AbstractController(cc) {
def socket = WebSocket.acceptOrResult[String, String] { request =>
Future.successful(request.session.get("user") match {
case None => Left(Forbidden)
case Some(_) =>
Right(ActorFlow.actorRef { out => MyWebSocketActor.props(out) })
})
}
}
}
注意:WebSocket 協定未實作 同源政策,因此無法防範 跨網站 WebSocket 劫持。若要保護 websocket 不受劫持,必須將要求中的
Origin
標頭與伺服器的來源進行比對,並實作手動驗證(包括 CSRF 令牌)。如果 WebSocket 要求未通過安全性檢查,則acceptOrResult
應透過傳回禁止結果來拒絕要求。
§處理不同類型的訊息
到目前為止,我們只看過處理 String
框架。Play 也內建處理 Array[Byte]
框架和從 String
框架解析的 JsValue
訊息的處理常式。您可以將這些作為 WebSocket 建立方法的類型參數傳遞,例如
import javax.inject.Inject
import org.apache.pekko.actor.ActorSystem
import org.apache.pekko.stream.Materializer
import play.api.libs.json._
import play.api.libs.streams.ActorFlow
import play.api.mvc._
class Application @Inject() (cc: ControllerComponents)(implicit system: ActorSystem, mat: Materializer)
extends AbstractController(cc) {
def socket = WebSocket.accept[JsValue, JsValue] { request =>
ActorFlow.actorRef { out => MyWebSocketActor.props(out) }
}
}
您可能已經注意到有兩個類型參數,這讓我們可以處理傳入訊息和傳出訊息的不同類型。這通常對較低層級的框架類型沒有用,但如果您將訊息解析為較高層級的類型,則可能有用。
例如,假設我們要接收 JSON 訊息,並且我們要將傳入訊息解析為 InEvent
,並將傳出訊息格式化為 OutEvent
。我們要做的第一件事是為我們的 InEvent
和 OutEvent
類型建立 JSON 格式
import play.api.libs.json._
implicit val inEventFormat: Format[InEvent] = Json.format[InEvent]
implicit val outEventFormat: Format[OutEvent] = Json.format[OutEvent]
現在,我們可以為這些類型建立一個 WebSocket MessageFlowTransformer
import play.api.mvc.WebSocket.MessageFlowTransformer
implicit val messageFlowTransformer: MessageFlowTransformer[InEvent, OutEvent] =
MessageFlowTransformer.jsonMessageFlowTransformer[InEvent, OutEvent]
最後,我們可以在我們的 WebSocket 中使用這些
import javax.inject.Inject
import org.apache.pekko.actor.ActorSystem
import org.apache.pekko.stream.Materializer
import play.api.libs.streams.ActorFlow
import play.api.mvc._
class Application @Inject() (cc: ControllerComponents)(implicit system: ActorSystem, mat: Materializer)
extends AbstractController(cc) {
def socket = WebSocket.accept[InEvent, OutEvent] { request =>
ActorFlow.actorRef { out => MyWebSocketActor.props(out) }
}
}
現在,在我們的 actor 中,我們將接收 InEvent
類型的訊息,並且我們可以傳送 OutEvent
類型的訊息。
§直接使用 Pekko 串流處理 WebSocket
Actor 並非總是處理 WebSocket 的正確抽象,特別是如果 WebSocket 的行為更像串流。
import org.apache.pekko.stream.scaladsl._
import play.api.mvc._
def socket = WebSocket.accept[String, String] { request =>
// Log events to the console
val in = Sink.foreach[String](println)
// Send a single 'Hello!' message and then leave the socket open
val out = Source.single("Hello!").concat(Source.maybe)
Flow.fromSinkAndSource(in, out)
}
WebSocket
可以存取要求標頭(來自啟動 WebSocket 連線的 HTTP 要求),讓您可以擷取標準標頭和階段資料。但是,它無法存取要求主體,也無法存取 HTTP 回應。
在此範例中,我們正在建立一個簡單的接收器,將每個訊息列印到主控台。若要傳送訊息,我們建立一個簡單的來源,它將傳送單一的 Hello! 訊息。我們還需要串接一個永遠不會傳送任何內容的來源,否則我們的單一來源將終止串流,進而終止連線。
提示:您可以在 https://www.websocket.org/echo.html 上測試 WebSocket。只需將位置設定為
ws://127.0.0.1:9000
。
讓我們撰寫另一個範例,在傳送 Hello! 訊息後,會捨棄輸入資料並關閉 socket
import org.apache.pekko.stream.scaladsl._
import play.api.mvc._
def socket = WebSocket.accept[String, String] { request =>
// Just ignore the input
val in = Sink.ignore
// Send a single 'Hello!' message and close
val out = Source.single("Hello!")
Flow.fromSinkAndSource(in, out)
}
這是另一個範例,其中輸入資料會記錄到標準輸出,然後使用對應的流程傳回給客戶端
import org.apache.pekko.stream.scaladsl._
import play.api.mvc._
def socket = WebSocket.accept[String, String] { request =>
// log the message to stdout and send response back to client
Flow[String].map { msg =>
println(msg)
"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.server.http[s].idleTimeout
之後,Play 會關閉閒置的 WebSocket 連線。
為避免此情況,您可以在閒置逾時後(伺服器未收到任何來自用戶端的訊息)讓 Play 後端伺服器對用戶端進行 ping。
play.server.websocket.periodic-keep-alive-max-idle = 10 seconds
Play 接著會將一個空的 ping
框架傳送給用戶端。通常這表示一個活動中的用戶端會以一個 pong
框架回應,您可以在應用程式中處理這個框架(如果需要的話)。
您可以讓 Play 傳送一個空的 pong
框架來進行單向 pong 保持連線心跳,而不是透過傳送 ping
框架來使用雙向 ping/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 實作,因此會自行處理框架。
下一步:Twirl 範本引擎
在這個文件中發現錯誤?此頁面的原始碼可以在 這裡 找到。在閱讀 文件指南 後,請隨時貢獻拉取請求。有問題或建議要分享?前往 我們的社群論壇 與社群展開對話。