Using server‐sent events - daniel-qa/C_Sharp GitHub Wiki

Using server-sent events

SSEClient 範例

  • 呼叫
 string url = "https://host/service/sse";
 CancellationTokenSource cts = new CancellationTokenSource();

 SSEClient sseClient = new SSEClient();
 sseClient.StartSSEConnection(url, cts.Token);

 cts.Cancel(); // Cancel SSE connection if needed

 // 取得資料
 Console.WriteLine(sseClient.sid);

  • SSEClient Class
 public class DataObject
 {        
     public string sid { get; set; }
 }

 public class SSEClient
 {
     private readonly HttpClient _httpClient;
     public string sid { get; set; }

     public SSEClient()
     {
         _httpClient = new HttpClient();
         _httpClient.DefaultRequestHeaders.Add("X-Auth-Name", "HiTeach");
         _httpClient.DefaultRequestHeaders.Add("X-Auth-DID", Program.device_id);
         _httpClient.DefaultRequestHeaders.Add("X-Auth-CID", Program.channel_id);
         _httpClient.DefaultRequestHeaders.Add("X-Auth-PIN", "1001");
         _httpClient.DefaultRequestHeaders.Add("X-Auth-APP", "irs");
     }

     public void StartSSEConnection(string url, CancellationToken cancellationToken)
     {
         try
         {
             var request = new HttpRequestMessage(HttpMethod.Get, url);
             HttpResponseMessage response = _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).Result;

             response.EnsureSuccessStatusCode();

             // Reading SSE messages from the response stream
             using (var responseStream = response.Content.ReadAsStreamAsync().Result)
             using (var reader = new StreamReader(responseStream))
             {
                 while (!reader.EndOfStream)
                 {
                     string line = reader.ReadLine();
                     if (string.IsNullOrWhiteSpace(line))
                         continue;

                     // Process the received SSE message (parse and handle)
                     Console.WriteLine("Received SSE message: " + line);

                     if (line.Contains("sid"))
                     {
                         // 取得 sid
                         string jsonString = line.Substring("data: ".Length);

                         // 反序列化 JSON 字符串到 DataObject 对象
                         DataObject dataObject = JsonConvert.DeserializeObject<DataObject>(jsonString);

                         // 访问 sid 的值
                         string sidValue = dataObject.sid;

                         sid = sidValue;

                         // 输出 sid 的值
                         Console.WriteLine("sid 的值为: " + sidValue);


                         cancellationToken.ThrowIfCancellationRequested();
                         break;
                     }
                     else
                     {
                         cancellationToken.ThrowIfCancellationRequested();

                         Console.WriteLine("未取得 sid");
                         sid = "";
                         break;
                     }
                 }
             }
         }
         catch (HttpRequestException ex)
         {
             Console.WriteLine($"HTTP request error: {ex.Message}");
         }
         catch (OperationCanceledException)
         {
             Console.WriteLine("SSE connection was canceled.");
         }
         catch (Exception ex)
         {
             Console.WriteLine($"Error during SSE connection: {ex.Message}");
         }
         finally
         {
             // Clean up or perform final actions as needed
             _httpClient.Dispose();
         }
     }
 }


#

與 Server 互動

一般的 get/post request 都是基於 HTTP 的標準根據請求來回覆回應的,一旦回應完成連線就會中斷。但如果有些更複雜的需求需要讓後端發送訊息給前端,比較好的方式就是建立 websocket,讓 request 保持連線。

不過有時候為了一個需求要建立 websocket 會有點麻煩。因此 Server-Sent Events 就是為了這種需求而誕生的

Server-Sent Events

以下簡稱 SSE。若說 websocket 是雙向溝通的話,SSE 其實是單向溝通的,也就是說,一但連線建立之後,就只能接收 Server 端來的訊息

實際的使用情境例如:

上傳超大檔案需要通知 client 端上傳處理進度。

轉檔需要通知 client 端處理進度。

非同步的商業邏輯處理需要通知 client 端狀況。

從 Server 傳送訊息

要從 Server 傳送訊息也蠻簡單的,只要在 response 宣告 header

Content-Type: "text/event-stream"

接著在需要回應時提供 response 就可以讓 client 端收到正確訊息

具體的範例可以參考 MDN 的 Using server-sent events

## 前端接收訊息

而前端則是透過 EventSource API 來處理,具體範例也很簡單:

const sse = new EventSource('/api/v1/sse');

sse.addEventListener("notice", (e) => {
  console.log(e.data)
});
sse.addEventListener("update", (e) => {
  console.log(e.data)
})

sse.addEventListener("message", (e) => {
  console.log(e.data)
});

建立好連線之後就可以接收 SSE 的訊息做後續的處理。

限制

SSE 的限制是

有限的連線數。

只能處理基本 text 的訊息

不能自訂 custom header ,例如要傳 Authorization 就不行。這個問題其實蠻大的,因為通常會需要做這種需求都會需要驗證使用者登入狀態,用 header 來處理 auth。

polyfill

不過還好當初 SSE 在瀏覽器功能尚未完全支援的年代有人開發了 EventSource polyfill。並且添加了 custom header 的功能,因此大部分的需求也還是可以解決。

Fetch Stream

不過隨著技術的演進,fetch API 也已經可以支援 stream data 了,而且 fetch API 就可以支援各種 custom Header 了,也不再有什麼限制,前端實作也不難,搭配 ReadableStream 的 pull callback 就可以接收 Server 發送的訊息

範例:

fetch(url).then(response => {
  const reader = response.body
    .pipeThrough(new TextDecoderStream())
    .getReader();
  const stream = new ReadableStream({
    start() {
      reader.read().then(() => null);
    },
    pull() {
      reader.read().then(({
        value
      }) => console.log(value))
    }
  })
});

主要是用到了 ReadableStream 取得 Stream 的資料。目前支援度也蠻高的(只看主流瀏覽器的話)因此 ReadableStream 應該是現今處理 SSE 更好的做法

總結

根據上述的結論,SSE 當初目的是為了提供比 websocket 更簡便實作的方法,由 XMLHttpRequest 延伸而來。但因為 fetch API 的支援度更廣,因此交由 fetch API 去處理 stream 的處理。

會有這次的研究主要也是因為 SSE 不支援 custom header,進而想要了解原因,因此找到了更好的方式來處理,雖然同樣是處理 SSE,fetch 卻叫做 fetch stream,因此我一開始找不到相關的討論。深挖了一下才發現有這段背景故事。

同事問我要怎麼取捨到底是否需要用 polyfill,基本上 polyfill 應該是要根據 spec 支援尚未實作的瀏覽器來做使用。以 event-source-polyfill 來說,他反而是支援了 spec 沒有的功能,因此我會認為這個 polyfill 的實作只是一種 workaround,可以的話找到 native support 的方法才會是更好的解法。

⚠️ **GitHub.com Fallback** ⚠️