最近嘗試了一下服務器端的推送,之前的做法都是客戶端輪詢,定時向服務器發送請求。但這造成了我的一些困擾:
1:輪詢是由客戶端發起的,那么在服務端就不能判別我要推送的內容是否已經過期,因為我很難判斷某個信息是否已經推送給全部的客戶端,那么服務端就需要緩存大量的數據。如果數據保存在數據庫,那么還要每次請求都需要查詢數據庫,這對數據庫和系統設計都是一個很大的挑戰。
2:請求的頻率太高,每次的請求包中含有同樣的數據,這對pc來說也許算不得什么,但是對於移動客戶端來講,這應該不是最佳的方案。尤其是遇到還要做權限判斷的時候,那么服務端的邏輯和效率也會造成用戶體驗的降低。
好在Html5為我們提供了一種方式:Server-Sent Events包含新的HTML元素EventSource和新的MIME類型 text/event-stream來完成我的需要。
因為是第一次接觸Html5,w3school中也有對EventSource的說明和使用。於是馬上開始着手實踐。
頁面腳本就不用說了,按照w3school的方式即可。
var source=new EventSource("demo_sse.php"); source.onmessage=function(event) { document.getElementById("result").innerHTML+=event.data + "<br />"; };
服務端的代碼也是如初一折,w3school提供了php和asp的代碼:
//php方式 <?php header('Content-Type: text/event-stream'); header('Cache-Control: no-cache'); $time = date('r'); echo "data: The server time is: {$time}\n\n"; flush(); ?> //asp方式 <% Response.ContentType="text/event-stream" Response.Expires=-1 Response.Write("data: " & now()) Response.Flush() %>
//代碼解釋:
- 把報頭 "Content-Type" 設置為 "text/event-stream"
- 規定不對頁面進行緩存
- 輸出發送日期(始終以 "data: " 開頭)
- 向網頁刷新輸出數據
也許大家應該注意到,php和asp的案例有一點不一樣,就是php推送的信息一個使用了"\n\n"作為結束標志,而asp卻沒有。而本人實踐則是用asp.net+mvc,經過測試,如果不以"\n\n"作為結束標志,那么客戶端將不能接收到推送的值。還有需要特別聲明一下:推送的信息格式必須為”data:內容\n\n“,否則。。。
public void Subscribe() { HttpContext.Response.ContentType = "text/event-stream"; HttpContext.Response.CacheControl = "no-cache"; HttpContext.Response.Write("data:" + DateTime.Now.ToString()+ "\n\n"); HttpContext.Response.Flush(); }
至此,客戶端應該可以收到服務端推送的值。而如此簡單的結構真的可以完成我們需要的功能設計嗎?
此例我們只是推送了一個當前時間,而我們實際要推送的值是不斷變化的,不然也就沒有推送的必要了。
於是我想到了將訂閱的請求保存起來,當需要推送的時候,在對每個請求進行循環推送,於是有了下面的代碼:
public class PublishService { private static IDictionary<string, HttpResponseBase> contexts = new Dictionary<string, HttpResponseBase>(); public static void AddHttpContext(HttpContextBase context) { var token = context.GetToken(”CookieName“); if (!contexts.Keys.Contains(token)) contexts.Add(token, context.Response); } private static void Publish() { foreach (var context in contexts.Values) { context.ContentType = "text/event-stream"; context.CacheControl = "no-cache"; msg = GetData(context.GetToken("CookieName")); context.Write("data:" + msg + "\n\n"); context.Flush(); } }
public void Subscribe()
{
PublishService.AddHttpContext(HttpContext);
PublishService.Publish();
} }
可是在進行測試的時候Chrome告訴我:EventSource's response has a MIME type ("text/plain") that is not "text/event-stream". Aborting the connection.
而FF告訴我:Firefox 無法建立到 http://localhost:8000/Location/Notification/Subscribe 服務器的連接。
經過調試發現,在每次flush的時候發生異常:Server cannot flush a completed response.這究竟是為啥呢?不論是google,還是baidu,我都沒能找到合適的答案,所以此案至今未結,如哪位知道請細說一二。
於是乎,我放棄了這種方式,轉而就推送一個時間看看是什么效果。結果發現Chrome每隔3秒向客戶端推送一次,而FF是每5秒推送一次。有了這樣一個發現,那么服務端的設計就應該是另一個樣子:
public void Subscribe() { var data = GetData(); HttpContext.Response.ContentType = "text/event-stream"; HttpContext.Response.CacheControl = "no-cache"; HttpContext.Response.Write("data:" + data + "\n\n"); HttpContext.Response.Flush(); }
服務端只需要提供一個服務GetData(),這個服務用來獲取我們需要推送的信息,而根據Server-Sent Events規范推薦如果沒有其他的數據要發送,那么定期的發送keep-alive注釋。其他的事情就不用我們操心了。
這只是一個簡單的使用,因為本人在使用EventSource的時候走了一些彎路,所以寫出來,希望能對大家有些幫助。
總結:走了一些彎路的原因是起初對Server-Sent Events機制的不清楚。個人理解:該機制依然並非由服務器端發起,而是還由客戶端向服務端定時發送請求,EventSource只是提供了一個很好的封裝,不用自己去維護而已。
后記:
求教:EventSource.onopen和EventSource.onerror每次都會觸發這兩個事件,而且每次得到的結果都一樣,為何?