伺服器推送事件:一種從伺服器流式推送事件的簡易方法
哈嘍!昨天我見識到了一種我以前從沒見過的從伺服器推送事件的炫酷方法: 伺服器推送事件 !如果你只需要讓伺服器發送事件,相較於 Websockets,它們或許是一個更簡便的選擇。
我會聊一聊它們的用途、運作原理,以及我昨日在試著運行它們的過程中遇到的幾個錯誤。
問題:從伺服器流式推送更新
現在,我有一個啟動虛擬機的 Web 服務,客戶端輪詢伺服器,直到虛擬機啟動。但我並不想使用輪詢方式。
相反,我想讓伺服器流式推送更新。我跟 Kamal 說我要用 Websockets 來實現它,而他建議使用伺服器推送事件不失為一個更簡便的選擇!
我登時就愣住了——那什麼玩意???聽起來像是些我從來沒見過的稀罕玩意兒。於是乎我就查了查。
伺服器推送事件就是個 HTTP 請求協議
下文便是伺服器推送事件的運作流程。我-很-高-興-地了解到它們就是個 HTTP 請求協議。
1.客戶端提出一個 GET 請求(舉個例子)https://yoursite.com/events
2.客戶端設置 Connection: keep-alive
,這樣我們就能有一個長連接 3.伺服器設置設置一個 Content-Type: text/event-stream
響應頭 4.伺服器開始推送事件,就比如下文這樣:
event: status
data: one
舉個例子,這裡是當我藉助 curl
發送請求時,一些伺服器推送事件的樣子:
$ curl -N 'http://localhost:3000/sessions/15/stream'
event: panda
data: one
event: panda
data: two
event: panda
data: three
event: elephant
data: four
伺服器可以根據時間推移緩慢推送事件,並且客戶端也能夠在它們到來時讀取它們。你也可以將 JSON 或任何你想要的東西放在事件當中,就比如 data: {'name': 'ahmed'}
。
線路協議真的很簡單(只需要設置 event:
和 data:
,或者如果你願意,可設置為 id:
和 retry:
),所以你並不需要任何花里胡哨的伺服器庫來實現伺服器推送事件。
JavaScript 的代碼也超級簡單(僅使用 EventSource)
以下是用於流式伺服器推送事件的瀏覽器 JavaScript 的代碼。(我從 伺服器推送事件的 MND 頁面 得到的這個範例)
你可以訂閱所有事件,也可以為不同類型的事件使用不同的處理程序。這裡我有一個只接受類型為 panda
的事件的處理程序(就像我們的伺服器在上一節中推送的那樣)。
const evtSource = new EventSource("/sessions/15/stream", { withCredentials: true })
evtSource.addEventListener("panda", function(event) {
console.log("status", event)
});
客戶端在中途不能推送更新
不同於 Websockets,伺服器推送事件不允許大量的來回事件通訊。(這體現在它的字眼中 —— 伺服器 推送所有事件)。初始的時候客戶端發出一個請求,然後伺服器發出一連串響應。
如果 HTTP 連接結束,它會自動重連
使用 EventSource
發出的 HTTP 請求和常規 HTTP 請求有一個很大的區別,MDN 文檔中對此有所說明:
默認情況下,如果客戶端和伺服器之間的連接斷開,則連接會重啟。請使用
.close()
方法來終止連接。
很奇怪,一開始我真的被它嚇到了:我打開了一個連接,然後在伺服器端將其關閉,然後兩秒過後客戶端向我的傳送終端發送了另一條請求!
我覺得這裡可能是因為連接在完成之前意外斷開了,所以客戶端自動重新打開了它以防止類似情況再發生。
所以如果你不想讓客戶端繼續重試,你就得通過調用 .close()
直截了當地關閉連接。
這裡還有些其它特性
你還能在伺服器推送事件中設置 id:
和 retry:
欄位。似乎,如果你在伺服器推送事件上設置,那麼當重新連接時,客戶端將發送一個 Last-Event-ID
響應頭,帶有它收到的最後一個 ID。酷!
我發現 W3C 的伺服器推送事件頁面 令人驚訝地容易理解。
在設置伺服器推送事件的時候我遇到了兩個錯誤
我在 Rails 中使用伺服器推送事件時遇到了幾個問題,我認為這些問題挺有趣的。其中一個緣於 Nginx,另一個是由 Rails 引起的。
問題一:我不能在事件推送的過程中暫停
這個奇怪的錯誤是在我做以下操作時出現的:
def handler
# SSE is Rails' built in server-sent events thing
sse = SSE.new(response.stream, event: "status")
sse.write('event')
sleep 1
sse.write('another event')
end
它會寫入第一個事件,但不能寫入第二個事件。我對此-非-常-困-惑,然後放開腦洞,試著理解 Ruby 中的 sleep
是如何運作的。但是 Cass 將我引領到一個與我有著相同困惑的 Stack Overflow 問答帖,而這裡包含了讓我為之震驚的回答!
事實證明,問題出在我的 Rails 伺服器位於 Nginx 之後,似乎 Nginx 默認使用 HTTP/1.0 向上游伺服器發起請求(為啥?都 2021 年了,還這麼干?我相信這其中一定有合乎情理的解釋,也許是為了向下兼容之類的)。
所以客戶端(Nginx)會在伺服器推送第一個事件之後直接關閉連接。我覺得如果在我推送第二個事件的過程中 沒有 暫停,它繼續正常工作,基本上就是伺服器在連接關閉之前和客戶端在爭速度,爭著推送第二部分響應,如果我這邊推送速度足夠快,那麼伺服器就會贏得比賽。
我不確定為什麼使用 HTTP/1.0 會使客戶端的連接關閉(可能是因為伺服器在每個事件結尾寫入了兩個換行符?),但因為伺服器推送事件是一個比較新的玩意兒,HTTP/1.0 (這種老舊協議)不支持它一點都會不意外。
設置 proxy_http_version 1.1
從而解決那個麻煩。好欸!
問題二:事件被緩衝
這個事情解決完,第二個麻煩接踵而至。不過這個問題實際上非常好解決,因為 Cass 已經建議將 stackoverflow 里另一篇帖的回答 作為前一個問題的解決方案,雖然它並沒有是導致問題一出現的源頭,但它-確-實-解-釋-了問題二。
問題在這個示例代碼中:
def handler
response.headers['Content-Type'] = 'text/event-stream'
# Turn off buffering in nginx
response.headers['X-Accel-Buffering'] = 'no'
sse = SSE.new(response.stream, event: "status")
10.times do
sse.write('event')
sleep 1
end
end
我本來期望它每秒返回 1 個事件,持續 10 秒,但實際上它等了 10 秒才把 10 個事件一起返回。這不是我們想要的流式傳輸方式!
原來這是因為 Rack ETag 中間件想要計算 ETag(響應的哈希值),為此它需要整個響應為它服務。因此,我需要禁用 ETag 生成。
Stack Overflow 的回答建議完全禁用 Rack ETag 中間件,但我不想這樣做,於是我去看了 鏈接至 GitHub 上的議題。
那個 GitHub 議題建議我可以針對僅流式傳輸終端應用一個解決方法,即 Last-Modified
響應頭,顯然,這麼做可以繞過 ETag 中間件。
所以我設置為:
headers['Last-Modified'] = Time.now.httpdate
然後它起作用了!!!
我還通過設置響應頭 X-Accel-Buffering: no
關閉了位於 Nginx 中的緩衝區。我並沒有百分百確定我要那樣做,但這麼做似乎更安全。
Stack Overflow 很棒
起初,我全身心致力於從頭開始調試這兩個錯誤。Cass 為我指向了那兩個 Stack Overflow 帖子,一開始我對那些帖下提出的解決方案持懷疑態度(我想:「我沒有使用 HTTP/1.0 啊!ETag 響應頭什麼玩意,跟這一切有關係嗎??」)。
但結果證明,我確實無意中使用 了 HTTP/1.0,並且 Rack ETag 中間件確實給我帶來了問題。
因此,也許這個故事告訴我,有時候計算機就是會以奇怪的方式相互作用,其它人在過去也遇到過計算機以完全相同的奇怪方式相互作用的問題,而 Stack Overflow 有時會提供關於為什麼會發生這些情況的答案 : )
我認為重要的是不要隨意從 Stack Overflow 中嘗試各種解決方案(當然,在這種情況下不會有人建議這樣做!)。對於這兩個問題,我確實需要去仔細思考,了解發生了什麼,還有為什麼更改這些設置會起作用。
就是這樣!
今天我要繼續著手實現伺服器推送事件,因為昨天一整天我都沉浸在上述這些錯誤里。好在我學到了一個以前從未聽說過的易學易用的網路技術,心裡還是很高興的。
(題圖:MJ/4c08a193-086e-4efe-a662-00401c928c41)
via: https://jvns.ca/blog/2021/01/12/day-36--server-sent-events-are-cool--and-a-fun-bug/
作者:Julia Evans 選題:lujun9972 譯者:Drwhooooo 校對:wxy
本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive