標籤:graph ring arc rest ict 資料推送 資料格式 性問題 自訂
轉自:http://www.ibm.com/developerworks/cn/web/1307_chengfu_serversentevent/
http://www.ibm.com/developerworks/cn/web/wa-lo-comet/ --comet長串連
伺服器推送事件(Server-sent Events)是 HTML 5 規範中的一個組成部分,可以用來從服務端即時推送資料到瀏覽器端。相對於與之類似的 COMET 和 WebSocket 技術來說,伺服器推送事件的使用更簡單,對伺服器端的改動也比較小。對於某些類型的應用來說,伺服器推送事件是最佳的選擇。本文對伺服器推送技術進行了詳細的介紹,包含瀏覽器端和伺服器端的相應實現細節,為在實踐中使用該技術提供了指南。
- 內容
- 簡介
- 規範
- 伺服器端和瀏覽器端實現
- IE 支援
- 小結
- 參考資料
- 評論
在 IBM Bluemix 雲平台上開發並部署您的下一個應用。
開始您的試用
對於一般的 Web 應用程式開發,大多數開發人員並不陌生。在 Web 應用程式中,瀏覽器和伺服器之間使用的是請求 / 響應的互動模式。瀏覽器發出請求,伺服器根據收到的請求來產生相應的響應。瀏覽器再對收到的響應進行處理,展現給使用者。響應的格式可能是 HTML、XML 或 JSON 等。隨著 REST 架構風格和 AJAX 的流行,伺服器更多地使用 JSON 作為響應的資料格式。Web 應用程式使用 XMLHttpRequest 對象來發送請求,並根據伺服器端返回的資料,對頁面的內容進行動態更新。通常來說,使用者在頁面上的操作,比如點擊或移動滑鼠,會觸發相應的事件。由 XMLHttpRequest 對象來發出請求,得到伺服器響應之後進行頁面的局部更新。這種方式的不足之處在於:伺服器端產生的資料變化不能及時地通知瀏覽器,而是需要等到下次請求發出時才能被瀏覽器擷取。對於某些對資料即時性要求很高的應用來說,這種延遲是不能接受的。
為了滿足這類應用的需求,就需要有某種方式能夠從伺服器端推送資料給瀏覽器,以保證伺服器端的資料變化可以在第一時間通知給使用者。目前常見的解決辦法有不少,主要可以分成兩類。這兩類方法的區別在於是否基於 HTTP 協議來實現。不使用 HTTP 協議的做法是使用 HTML 5 新增的 WebSocket 規範,而使用 HTTP 協議的做法則包括簡易輪詢、COMET 技術和本文中要介紹的 HTML 5 伺服器推送事件。下面會對這幾種技術進行介紹。
簡介
在介紹 HTML 5 伺服器推送事件之前,首先介紹一些上面提到的幾種伺服器端資料推送技術。第一種是 WebSocket。WebSocket 規範是 HTML 5 中的一個重要組成部分,已經被很多主流瀏覽器所支援,也有不少基於 WebSocket 開發的應用。正如名稱所表示的一樣,WebSocket 使用的是通訊端串連,基於 TCP 協議。使用 WebSocket 之後,實際上在伺服器端和瀏覽器之間建立一個通訊端串連,可以進行雙向的資料轉送。WebSocket 的功能是很強大的,使用起來也靈活,可以適用於不同的情境。不過 WebSocket 技術也比較複雜,包括伺服器端和瀏覽器端的實現都不同於一般的 Web 應用程式。
除了 WebSocket 之外,其他的實現方式是基於 HTTP 協議來達到即時推送的效果。第一種做法是簡易輪詢,即瀏覽器端定時向伺服器端發出請求,來查詢是否有資料更新。這種做法比較簡單,可以在一定程度上解決問題。不過對於輪詢的時間間隔需要進行仔細考慮。輪詢的間隔過長,會導致使用者不能及時接收到更新的資料;輪詢的間隔過短,會導致查詢請求過多,增加伺服器端的負擔。
COMET 技術改進了簡易輪詢的缺點,使用的是長輪詢。長輪詢的方式在每次請求時,伺服器端會保持該串連在一段時間內處於開啟狀態,而不是在響應完成之後就立即關閉。這樣做的好處是在串連處於開啟狀態的時間段內,伺服器端產生的資料更新可以被及時地返回給瀏覽器。當上一個長串連關閉之後,瀏覽器會立即開啟一個新的長串連來繼續請求。不過 COMET 技術的實現在伺服器端和瀏覽器端都需要第三方庫的支援。
綜合比較上面提到的 4 種不同的技術,簡易輪詢由於其本身的缺陷,並不推薦使用。COMET 技術並不是 HTML 5 標準的一部分,從相容標準的角度出發,也不推薦使用。WebSocket 規範和伺服器推送技術都是 HTML 5 標準的組成部分,在主流瀏覽器上都提供了原生的支援,是推薦使用的。不過 WebSocket 規範更加複雜一些,適用於需要進行複雜雙向資料通訊的情境。對於簡單的伺服器資料推送的情境,使用伺服器推送事件就足夠了。
在瀏覽器支援方面,伺服器推送事件已經在除 IE 外的大部分案頭和行動瀏覽器上得到了支援。支援伺服器推送事件的瀏覽器及其版本包括:Firefox 6.0+、Chrome 6.0+、Safari 5.0+、Opera 11.0+、iOS Safari 4.0+、Opera Mobile 11.1+、Chrome for Android 25.0+、Firefox for Android 19.0+ 以及 Blackberry Browser 7.0+ 等。關於 IE 的支援,在下面的章節中有詳細的介紹。
下面對伺服器推送事件的規範進行具體的說明。
回頁首
規範
Server-sent Events 規範是 HTML 5 規範的一個組成部分,具體的規範文檔見參考資源。該規範比較簡單,主要由兩個部分組成:第一個部分是伺服器端與瀏覽器端之間的通訊協議,第二部分則是在瀏覽器端可供 JavaScript 使用的 EventSource 對象。通訊協議是基於純文字的簡單協議。伺服器端的響應的內容類型是“text/event-stream”。響應文本的內容可以看成是一個事件流,由不同的事件所組成。每個事件由類型和資料兩部分組成,同時每個事件可以有一個可選的標識符。不同事件的內容之間通過僅包含斷行符號符和分行符號的空行(“\r\n”)來分隔。每個事件的資料可能由多行組成。代碼清單 1 給出了伺服器端響應的樣本。
清單 1. 伺服器端響應的樣本
data: first eventdata: second eventid: 100event: myeventdata: third eventid: 101: this is a commentdata: fourth eventdata: fourth event continue
如代碼清單 1 所示,每個事件之間通過空行來分隔。對於每一行來說,冒號(“:”)前面表示的是該行的類型,冒號後面則是對應的值。可能的類型包括:
- 類型為空白,表示該行是注釋,會在處理時被忽略。
- 類型為 data,表示該行包含的是資料。以 data 開頭的行可以出現多次。所有這些行都是該事件的資料。
- 類型為 event,表示該行用來聲明事件的類型。瀏覽器在收到資料時,會產生對應類型的事件。
- 類型為 id,表示該行用來聲明事件的標識符。
- 類型為 retry,表示該行用來聲明瀏覽器在串連斷開之後進行再次串連之前的等待時間。
在代碼清單 1 中,第一個事件只包含資料“first event”,會產生預設的事件;第二個事件的標識符是 100,資料為“second event”;第三個事件會產生類型為“myevent”的事件;最後一個事件的資料為“fourth event\nfourth event continue”。當有多行資料時,實際的資料由每行資料以分行符號串連而成。
如果伺服器端返回的資料中包含了事件的標識符,瀏覽器會記錄最近一次接收到的事件的標識符。如果與伺服器端的串連中斷,當瀏覽器端再次進行串連時,會通過 HTTP 頭“Last-Event-ID”來聲明最後一次接收到的事件的標識符。伺服器端可以通過瀏覽器端發送的事件標識符來確定從哪個事件開始來繼續串連。
對於伺服器端返回的響應,瀏覽器端需要在 JavaScript 中使用 EventSource 對象來進行處理。EventSource 使用的是標準的事件監聽器方式,只需要在對象上添加相應的事件處理方法即可。EventSource 提供了三個標準事件,如表 1 所示。
表 1. EventSource 對象提供的標準事件
| 名稱 |
說明 |
事件處理方法 |
| open |
當成功與伺服器建立串連時產生 |
onopen |
| message |
當收到伺服器發送的事件時產生 |
onmessage |
| error |
當出現錯誤時產生 |
onerror |
如之前所述,伺服器端可以返回自訂類型的事件。對於這些事件,可以使用 addEventListener 方法來添加相應的事件處理方法。代碼清單 2 給出了 EventSource 對象的使用樣本。
清單 2. EventSource 對象的使用樣本
var es = new EventSource(‘events‘);es.onmessage = function(e) { console.log(e.data);};es.addEventListener(‘myevent‘, function(e) { console.log(e.data);});
如代碼清單 2 所示,在指定 URL 建立出 EventSource 對象之後,可以通過 onmessage 和 addEventListener 方法來添加事件處理方法。當伺服器端有新的事件產生,相應的事件處理方法會被調用。EventSource 對象的 onmessage 屬性的作用類似於 addEventListener( ‘ message ’ ),不過 onmessage 屬性只支援一個事件處理方法。
在介紹完伺服器推送事件的規範內容之後,下面介紹伺服器端的實現。
回頁首
伺服器端和瀏覽器端實現
從上一節中對通訊協議的描述可以看出,伺服器端推送事件是一個比較簡單的協議。伺服器端的實現也相對比較簡單,只需要按照協議規定的格式,返迴響應內容即可。在開源社區可以找到各種不同的伺服器端技術相對應的實現。自己開發的難度也不大。本文使用 Java 作為伺服器端的實現語言。相應的實現基於開源的 jetty-eventsource-servlet 項目,見參考資源。下面通過一個具體的樣本來說明如何使用 jetty-eventsource-servlet 項目。樣本用來類比一個物體在某個限定空間中的隨機移動。該物體從一個隨機位置開始,然後從上、下、左和右四個方向中隨機播放一個方向,並在該方向上移動隨機的距離。伺服器端不斷改變該物體的位置,並把位置資訊推送給瀏覽器,由瀏覽器來顯示。
伺服器端實現
伺服器端的實現由兩部分組成:一部分是用來產生資料的 org.eclipse.jetty.servlets.EventSource 介面的實現,另一部分是作為瀏覽器訪問端點的繼承自 org.eclipse.jetty.servlets.EventSourceServlet 類的 servlet 實現。代碼清單 3 給出了 EventSource 介面的實作類別。
清單 3. EventSource 介面的實作類別 MovementEventSource
public class MovementEventSource implements EventSource {private int width = 800;private int height = 600;private int stepMax = 5;private int x = 0;private int y = 0;private Random random = new Random();private Logger logger = Logger.getLogger(getClass().getName());public MovementEventSource(int width, int height, int stepMax) {this.width = width;this.height = height;this.stepMax = stepMax;this.x = random.nextInt(width);this.y = random.nextInt(height);}@Overridepublic void onOpen(Emitter emitter) throws IOException {query(emitter); //開始產生位置資訊}@Overridepublic void onResume(Emitter emitter, String lastEventId)throws IOException {updatePosition(lastEventId); //更新起始位置query(emitter); //開始產生位置資訊}//根據Last-Event-Id來更新起始位置private void updatePosition(String id) {if (id != null) {String[] pos = id.split(",");if (pos.length > 1) {int xPos = -1, yPos = -1;try {xPos = Integer.parseInt(pos[0], 10);yPos = Integer.parseInt(pos[1], 10);} catch (NumberFormatException e) {}if (isValidMove(xPos, yPos)) {x = xPos;y = yPos;}}}}private void query(Emitter emitter) throws IOException {emitter.comment("Start sending movement information.");while(true) {emitter.comment("");move(); //移動位置String id = String.format("%s,%s", x, y);emitter.id(id); //根據位置建置事件標識符emitter.data(id); //發送位置資訊資料try {Thread.sleep(2000);} catch (InterruptedException e) {logger.log(Level.WARNING, "Movement query thread interrupted. Close the connection.", e);break;}}emitter.close(); //當迴圈終止時,關閉串連}@Overridepublic void onClose() {}//擷取下一個合法的移動位置private void move() {while (true) {int[] move = getMove();int xNext = x + move[0];int yNext = y + move[1];if (isValidMove(xNext, yNext)) {x = xNext;y = yNext;break;}}}//判斷當前的移動位置是否合法private boolean isValidMove(int x, int y) {return x >= 0 && x <= width && y >=0 && y <= height;}//隨機產生下一個移動位置private int[] getMove() {int[] xDir = new int[] {-1, 0, 1, 0};int[] yDir = new int[] {0, -1, 0, 1};int dir = random.nextInt(4);return new int[] {xDir[dir] * random.nextInt(stepMax), yDir[dir] * random.nextInt(stepMax)};}}
代碼清單 3 中,類 MovementEventSource 需要實現 EventSource 介面的 onOpen、onResume 和 onClose 方法,其中 onOpen 方法在瀏覽器端的串連開啟的時候被調用,onResume 方法在瀏覽器端重建立立串連時被調用,onClose 方法則在瀏覽器關閉串連的時候被調用。onOpen 和 onResume 方法都有一個 EventSource.Emitter 介面類型的參數,可以用來發送資料。EventSource.Emitter 介面中包含的方法包括 data、event、comment、id 和 close 等,分別對應於通訊協議中各種不同類型的事件。而 onResume 方法還額外包含一個參數 lastEventId,表示通過 Last-Event-ID 頭髮送過來的最近一次事件的標識符。
MovementEventSource 類中事件產生的主要邏輯在 query 方法中。該方法中包含一個無限迴圈,每隔 2 秒鐘改變一次位置,同時把更新之後的位置通過 EventSource.Emitter 介面的 data 方法發送給瀏覽器端。每個事件都有對應的標識符,而標識符的值就是位置本身。如果串連斷開之後,瀏覽器重新進行串連,可以從上一次的位置開始繼續移動該物體。
與 MovementEventSource 類對應的 servlet 實現比較簡單,只需要繼承自 EventSourceServlet 類並覆寫 newEventSource 方法即可。在 newEventSource 方法的實現中,需要返回一個 MovementEventSource 類的對象,如代碼清單 4 所示。每當瀏覽器端建立串連時,該 servlet 會建立一個新的 MovementEventSource 類的對象來處理該請求。
清單 4. servlet 實作類別 MovementServlet
public class MovementServlet extends EventSourceServlet { @Override protected EventSource newEventSource(HttpServletRequest request, String clientId) { return new MovementEventSource(800, 600, 20); } }
在伺服器端實現中,需要注意的是要添加相應的 servlet 過濾器支援。這是 jetty-eventsource-servlet 項目所依賴的 Jetty Continuations 架構的要求,否則的話會出現錯誤。添加過濾器的方式是在 web.xml 檔案中添加代碼清單 5 中所示的配置內容。
清單 5. Jetty Continuations 所需 servlet 過濾器的配置
<filter> <filter-name>continuation</filter-name> <filter-class>org.eclipse.jetty.continuation.ContinuationFilter</filter-class> </filter> <filter-mapping> <filter-name>continuation</filter-name> <url-pattern>/sse/*</url-pattern> </filter-mapping>
瀏覽器端實現
瀏覽器端的實現也比較簡單,只需要建立出 EventSource 對象,並添加相應的事件處理方法即可。代碼清單 6 給出了相應的實現。在頁面中使用一個方塊表示物體。當接收到新的事件時,根據事件數目據中給出的座標資訊,更新方塊在頁面上的位置。
清單 6. 瀏覽器端的實現代碼
var es = new EventSource(‘sse/movement‘); es.addEventListener(‘message‘, function(e) { var pos = e.data.split(‘,‘), x = pos[0], y = pos[1]; $(‘#box‘).css({ left : x + ‘px‘, top : y + ‘px‘ }); });
在介紹完基本的伺服器端和瀏覽器端實現之後,下面介紹比較重要的 IE 的支援。
回頁首
IE 支援
使用瀏覽器原生的 EventSource 對象的一個比較大的問題是 IE 並不提供支援。為了在 IE 上提供同樣的支援,一般有兩種辦法。第一種辦法是在其他瀏覽器上使用原生 EventSource 對象,而在 IE 上則使用簡易輪詢或 COMET 技術來實現;另外一種做法是使用 polyfill 技術,即使用第三方提供的 JavaScript 庫來屏蔽瀏覽器的不同。本文使用的是 polyfill 技術,只需要在頁面中載入第三方 JavaScript 庫即可。應用本身的瀏覽器端代碼並不需要進行改動。一般推薦使用第二種做法,因為這樣的話,在伺服器端只需要使用一種實現技術即可。
在 IE 上提供類似原生 EventSource 對象的實現並不簡單。理論上來說,只需要通過 XMLHttpRequest 對象來擷取伺服器端的響應內容,並通過文本解析,就可以提取出相應的事件,並觸發對應的事件處理方法。不過問題在於 IE 上的 XMLHttpRequest 對象並不支援擷取部分的響應內容。只有在響應完成之後,才能擷取其內容。由於伺服器端推送事件使用的是一個長串連。當串連一直處於開啟狀態時,通過 XMLHttpRequest 對象並不能擷取響應的內容,也就無法觸發對應的事件。更具體的來說,當 XMLHttpRequest 對象的 readyState 為 3(READYSTATE_INTERACTIVE)時,其 responseText 屬性是無法擷取的。
為瞭解決 IE 上 XMLHttpRequest 對象的問題,就需要使用 IE 8 中引入的 XDomainRequest 對象。XDomainRequest 對象的作用是發出跨域的 AJAX 請求。XDomainRequest 對象提供了 onprogress 事件。當 onprogress 事件發生時,可以通過 responseText 屬性來擷取到響應的部分內容。這是 XDomainRequest 對象和 XMLHttpRequest 對象的最大不同,也是使用 XDomainRequest 對象來實作類別似原生 EventSource 對象的基礎。在使用 XDomainRequest 對象開啟與伺服器端的串連之後,當伺服器端有新的資料產生時,可以通過 XDomainRequest 對象的 onprogress 事件的處理方法來進行處理,對接收到的資料進行解析,根據資料的內容觸發相應的事件。
不過由於 XDomainRequest 對象本來的目的是發出跨域 AJAX 請求,考慮到跨域訪問的安全性問題,XDomainRequest 對象在使用時的限制也比較嚴格。這些限制會影響到其作為 EventSource 對象的實現方式。具體的限制和解決辦法如下所示:
- 伺服器端的響應需要包含 Access-Control-Allow-Origin 頭,用來聲明允許從哪些域訪問該 URL。“*”表示允許來自任何域的訪問,不推薦使用該值。一般使用與當前應用相同的域,限制只允許來自當前域的訪問。
- XDomainRequest 對象發出的請求不能包含自訂的 HTTP 頭,這就限制了不能使用 Last-Event-ID 頭來聲明瀏覽器端最近一次接收到的事件的標識符。只能通過 HTTP 要求的其他方式來傳遞該標識符,如 GET 請求的參數或 POST 請求的內容體。
- XDomainRequest 對象的請求的內容類型(Content-Type)只能是“text/plain”。這就意味著,當使用 POST 請求時,伺服器端使用的架構,如 servlet,不會對 POST 請求的內容進行自動解析,無法使用 HttpServletRequest 類的 getParameter 方法來擷取 POST 請求的內容。只能在伺服器端對原始的請求內容進行解析,擷取到其中的參數的值。
- XDomainRequest 對象發出的請求中不包含任何與使用者認證相關的資訊,包括 cookie 等。這就意味著,如果伺服器端需要認證,則需要通過 HTTP 要求的其他方式來傳遞使用者的認證資訊,比如 session 的 ID 等。
由於 XDomainRequest 對象的這些限制,伺服器端的實現也需要作出相應的改動。這些改動包括返回 Access-Control-Allow-Origin 頭;對於瀏覽器端發送的“text/plain”類型的參數進行解析;處理請求中包含的使用者認證相關的資訊。
本文的樣本使用的 polyfill 庫是 GitHub 上的 Yaffle 開發的 EventSource 項目,具體的地址見參考資源。在使用該 polyfill 庫,並對伺服器端的實現進行修改之後,就可以在 IE 8 及以上的瀏覽器中使用伺服器推送事件。如果需要支援 IE 7,則只能使用簡易輪詢或 COMET 技術。本文的範例程式碼見參考資源。
回頁首
小結
如果需要從伺服器端推送資料給瀏覽器,可以使用的基於 HTML 5 規範標準的技術包括 WebSocket 和伺服器推送事件。開發人員可以根據應用的具體需求來選擇合適的技術。如果只是需要從伺服器端推送資料,伺服器推送事件的規範更加簡單,實現起來更容易。本文對伺服器推送事件的規範內容、伺服器端和瀏覽器端的實現都進行了詳細的介紹,對如何支援 IE 瀏覽器也進行了具體的分析。
參考資料學習
- 瞭解 伺服器推送事件規範(Server-sent Events)的具體內容。
- 瞭解 jetty-eventsource-servlet 項目和 JettyContinuations架構的更多內容。
- 瞭解 IE 上的 XMLHttpRequest和 XDomainRequest對象,瞭解 XDomainRequest 對象的 使用限制。
- 查看支援 IE 的 EventSource 對象的 polyfill 庫的詳細資料。
- 查看本文的 範例程式碼。
- developerWorks Web development 專區:通過專門關於 Web 技術的文章和教程,擴充您在網站開發方面的技能。
- developerWorks Ajax 資源中心:這是有關 Ajax 編程模型資訊的一站式中心,包括很多文檔、教程、論壇、blog、wiki 和新聞。任何 Ajax 的新資訊都能在這裡找到。
- developerWorks Web 2.0 資源中心,這是有關 Web 2.0 相關資訊的一站式中心,包括大量 Web 2.0 技術文章、教程、下載和相關技術資源。您還可以通過 Web 2.0 新手入門 欄目,迅速瞭解 Web 2.0 的相關概念。
- 查看 HTML5 專題,瞭解更多和 HTML5 相關的知識和動向。
討論
- 加入 developerWorks 中文社區。查看開發人員推動的部落格、論壇、組和維基,並與其他 developerWorks 使用者交流。
HTML5 伺服器推送事件(Server-sent Events)實戰開發