名稱
|
說明
|
事件處理方法
|
open
|
當成功與服務器建立連接時產生
|
onopen
|
message
|
當收到服務器發送的事件時產生
|
onmessage
|
error
|
當出現錯誤時產生
|
onerror
|
如之前所述,服務器端可以返回自定義類型的事件。對于這些事件,可以使用 addEventListener 方法來添加相應的事件處理方法。代碼清單 2 給出了 EventSource 對象的使用示例。
EventSource 對象的使用示例
var es = new EventSource('events'); es.onmessage = function(e) { console.log(e.data); }; es.addEventListener('myevent', function(e) { console.log(e.data); });
如上所示,在指定 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 實現。下面代碼給出了 EventSource 接口的實現類。
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); } @Override public void onOpen(Emitter emitter) throws IOException { query(emitter); //開始生成位置信息 } @Override public 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(); //當循環終止時,關閉連接 } @Override public 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)}; } }
類 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 類的對象,如下所示。每當瀏覽器端建立連接時,該 servlet 會創建一個新的 MovementEventSource 類的對象來處理該請求。
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 文件中添加代碼如下所示的配置內容。
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 對象,并添加相應的事件處理方法即可。下面代碼給出了相應的實現。在頁面中使用一個方塊表示物體。當接收到新的事件時,根據事件數據中給出的坐標信息,更新方塊在頁面上的位置。
瀏覽器端的實現代碼
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 對象的實現方式。具體的限制和解決辦法如下所示:
由于 XDomainRequest 對象的這些限制,服務器端的實現也需要作出相應的改動。這些改動包括返回 Access-Control-Allow-Origin 頭;對于瀏覽器端發送的“text/plain”類型的參數進行解析;處理請求中包含的用戶認證相關的信息。
本文的示例使用的 polyfill 庫是 GitHub 上的 Yaffle 開發的 EventSource 項目,具體的地址見參考資源。在使用該 polyfill 庫,并對服務器端的實現進行修改之后,就可以在 IE 8 及以上的瀏覽器中使用服務器推送事件。如果需要支持 IE 7,則只能使用簡易輪詢或 COMET 技術。本文的示例代碼見參考資源。
小結
如果需要從服務器端推送數據給瀏覽器,可以使用的基于 HTML 5 規范標準的技術包括 WebSocket 和服務器推送事件。開發人員可以根據應用的具體需求來選擇合適的技術。如果只是需要從服務器端推送數據,服務器推送事件的規范更加簡單,實現起來更容易。本文對服務器推送事件的規范內容、服務器端和瀏覽器端的實現都進行了詳細的介紹,對如何支持 IE 瀏覽器也進行了具體的分析。