溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

html5服務(wù)器推送的示例分析

發(fā)布時間:2021-01-30 13:49:30 來源:億速云 閱讀:147 作者:小新 欄目:web開發(fā)

這篇文章給大家分享的是有關(guān)html5服務(wù)器推送的示例分析的內(nèi)容。小編覺得挺實用的,因此分享給大家做個參考,一起跟隨小編過來看看吧。

在各種BS架構(gòu)的應(yīng)用程序中,往往都希望服務(wù)端能夠主動地向客戶端推送各種消息,以達(dá)到類似于郵件、消息、待辦事項等通知。

往BS架構(gòu)本身存在的問題就是,服務(wù)器一直采用的是一問一答的機制。這就意味著如果客戶端不主動地向服務(wù)器發(fā)送消息,服務(wù)器就無法得知如何給客戶端推送消息。

隨著HTML、瀏覽器等各項技術(shù)、標(biāo)準(zhǔn)的發(fā)展,依次生成了不同的手段與方法能夠?qū)崿F(xiàn)服務(wù)端主動推送消息,它們分別是:AJAX,Comet,ServerSent以及WebSocket。

本篇文章將對上述提及到的各種技術(shù)手段進(jìn)行直白化的解釋。

AJAX

正常的一個頁面在瀏覽器中是這樣工作的:

用戶向給予瀏覽器一個需要訪問的地址

瀏覽器根據(jù)這個地址訪問服務(wù)器,并與服務(wù)器之間創(chuàng)建一個TCP連接(HTTP請求)

服務(wù)器根據(jù)這個地址和一些其它數(shù)據(jù),組建一段HTML文本,將寫入TCP連接,然后關(guān)閉連接

瀏覽器得到了來自服務(wù)器的HTML文本,解析并呈現(xiàn)了瀏覽器上給用戶瀏覽

此時,用戶點擊了網(wǎng)站上任何一個<a>或觸發(fā)任何一個<form>提交時:

瀏覽器根據(jù)form的參數(shù)或者a的參數(shù),作為訪問的地址

與服務(wù)器創(chuàng)建TCP連接

服務(wù)器組建HTML文本,然后關(guān)閉連接

瀏覽器將當(dāng)前顯示的頁面摧毀,并按照新的HTML文本呈現(xiàn)一個新的頁面給用戶

我們不難發(fā)現(xiàn)的是整個過程中間,一旦建立了一個連接,頁面就無法再維護(hù)住了。整個過程看上去有點強買強賣,也許我只要一杯新的可樂,但你非要給我一整個套餐組合。

此時我們可以了解一下XmlHttpRequest組件,這個組件提供我們手動創(chuàng)建一個HTTP請求,發(fā)送我們想要的數(shù)據(jù),服務(wù)器也可以只返回我們想要的結(jié)果,最大的好處是,當(dāng)我們收到服務(wù)器的響應(yīng)時,原來的頁面沒有被摧毀。這就好比,我喊一句"我的咖啡喝完了,我要續(xù)杯",然后服務(wù)員就拿了一杯咖啡過來,而不是會把我沒吃完的套餐全部倒掉。

當(dāng)我們利用AJAX實現(xiàn)服務(wù)器推送時,其實質(zhì)是客戶端不停地向服務(wù)器詢問"有沒有給我的消息呀?",然后服務(wù)器回答"有"或"沒有"來達(dá)到的實現(xiàn)效果。它的實現(xiàn)方法也很簡單,利用jQuery框架封裝好的AJAX調(diào)用也很方便:

function getMessage(fn) {
    $.ajax({
        url: "Handler.ashx", //一個能夠提供消息的頁面
        dataType: "text",    //響應(yīng)類型,可以是JSON,XML等其它類型
        type: "get",         //HTTP請求類型,還可以是post
        success: function (d, s) {
            fn(d);           //得到了正常的響應(yīng)時,利用回調(diào)函數(shù)通知外部        },
        complete: function (x, s) {
            setTimeout(function () {
                getMessage(fn);
            }, 5000);       //無論響應(yīng)成功或失敗,在若干秒后再詢問一次服務(wù)器        }
    });
}

通過上面的代碼,可以每隔5秒詢問一次服務(wù)器是否有需要處理的消息,通過這種方式可以達(dá)到推送的效果,但是會存在一個問題:

間隔時間越快,推送的及時性越好,服務(wù)器的消費越大;

間隔時間越慢,推送的及時性越低,服務(wù)器的消費越小。

而且嚴(yán)格地來說,這種實際方式,并不是真正意義上的服務(wù)器主動推送消息,但由于早期技術(shù)手段缺乏,所以AJAX輪循成為了一種很普遍的手段。

下面對服務(wù)器推送事件的規(guī)范進(jìn)行具體的說明。

規(guī)范

Server-sent Events 規(guī)范是 HTML 5 規(guī)范的一個組成部分,具體的規(guī)范文檔見參考資源。該規(guī)范比較簡單,主要由兩個部分組成:第一個部分是服務(wù)器端與瀏覽器端之間的通訊協(xié)議,第二部分則是在瀏覽器端可供 JavaScript 使用的 EventSource 對象。通訊協(xié)議是基于純文本的簡單協(xié)議。服務(wù)器端的響應(yīng)的內(nèi)容類型是“text/event-stream”。響應(yīng)文本的內(nèi)容可以看成是一個事件流,由不同的事件所組成。每個事件由類型和數(shù)據(jù)兩部分組成,同時每個事件可以有一個可選的標(biāo)識符。不同事件的內(nèi)容之間通過僅包含回車符和換行符的空行(“\r\n”)來分隔。每個事件的數(shù)據(jù)可能由多行組成。代碼清單 1 給出了服務(wù)器端響應(yīng)的示例。

服務(wù)器端響應(yīng)的示例

data: first event

data: second event
id: 100

event: myevent
data: third event
id: 101

: this is a comment
data: fourth event
data: fourth event continue

如代碼清單 1 所示,每個事件之間通過空行來分隔。對于每一行來說,冒號(“:”)前面表示的是該行的類型,冒號后面則是對應(yīng)的值??赡艿念愋桶ǎ?/p>

  1. 類型為空白,表示該行是注釋,會在處理時被忽略。

  2. 類型為 data,表示該行包含的是數(shù)據(jù)。以 data 開頭的行可以出現(xiàn)多次。所有這些行都是該事件的數(shù)據(jù)。

  3. 類型為 event,表示該行用來聲明事件的類型。瀏覽器在收到數(shù)據(jù)時,會產(chǎn)生對應(yīng)類型的事件。

  4. 類型為 id,表示該行用來聲明事件的標(biāo)識符。

  5. 類型為 retry,表示該行用來聲明瀏覽器在連接斷開之后進(jìn)行再次連接之前的等待時間。

在上面代碼中,第一個事件只包含數(shù)據(jù)“first event”,會產(chǎn)生默認(rèn)的事件;第二個事件的標(biāo)識符是 100,數(shù)據(jù)為“second event”;第三個事件會產(chǎn)生類型為“myevent”的事件;最后一個事件的數(shù)據(jù)為“fourth event\nfourth event continue”。當(dāng)有多行數(shù)據(jù)時,實際的數(shù)據(jù)由每行數(shù)據(jù)以換行符連接而成。

如果服務(wù)器端返回的數(shù)據(jù)中包含了事件的標(biāo)識符,瀏覽器會記錄最近一次接收到的事件的標(biāo)識符。如果與服務(wù)器端的連接中斷,當(dāng)瀏覽器端再次進(jìn)行連接時,會通過 HTTP 頭“Last-Event-ID”來聲明最后一次接收到的事件的標(biāo)識符。服務(wù)器端可以通過瀏覽器端發(fā)送的事件標(biāo)識符來確定從哪個事件開始來繼續(xù)連接。

對于服務(wù)器端返回的響應(yīng),瀏覽器端需要在 JavaScript 中使用 EventSource 對象來進(jìn)行處理。EventSource 使用的是標(biāo)準(zhǔn)的事件監(jiān)聽器方式,只需要在對象上添加相應(yīng)的事件處理方法即可。EventSource 提供了三個標(biāo)準(zhǔn)事件,如表 1 所示。

表 1. EventSource 對象提供的標(biāo)準(zhǔn)事件

名稱

說明

事件處理方法

open

當(dāng)成功與服務(wù)器建立連接時產(chǎn)生

onopen

message

當(dāng)收到服務(wù)器發(fā)送的事件時產(chǎn)生

onmessage

error

當(dāng)出現(xiàn)錯誤時產(chǎn)生

onerror

如之前所述,服務(wù)器端可以返回自定義類型的事件。對于這些事件,可以使用 addEventListener 方法來添加相應(yīng)的事件處理方法。代碼清單 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 創(chuàng)建出 EventSource 對象之后,可以通過 onmessage 和 addEventListener 方法來添加事件處理方法。當(dāng)服務(wù)器端有新的事件產(chǎn)生,相應(yīng)的事件處理方法會被調(diào)用。EventSource 對象的 onmessage 屬性的作用類似于 addEventListener( ‘ message ’ ),不過 onmessage 屬性只支持一個事件處理方法。在介紹完服務(wù)器推送事件的規(guī)范內(nèi)容之后,下面介紹服務(wù)器端的實現(xiàn)。

服務(wù)器端和瀏覽器端實現(xiàn)

從上一節(jié)中對通訊協(xié)議的描述可以看出,服務(wù)器端推送事件是一個比較簡單的協(xié)議。服務(wù)器端的實現(xiàn)也相對比較簡單,只需要按照協(xié)議規(guī)定的格式,返回響應(yīng)內(nèi)容即可。在開源社區(qū)可以找到各種不同的服務(wù)器端技術(shù)相對應(yīng)的實現(xiàn)。自己開發(fā)的難度也不大。本文使用 Java 作為服務(wù)器端的實現(xiàn)語言。相應(yīng)的實現(xiàn)基于開源的 jetty-eventsource-servlet 項目,見參考資源。下面通過一個具體的示例來說明如何使用 jetty-eventsource-servlet 項目。示例用來模擬一個物體在某個限定空間中的隨機移動。該物體從一個隨機位置開始,然后從上、下、左和右四個方向中隨機選擇一個方向,并在該方向上移動隨機的距離。服務(wù)器端不斷改變該物體的位置,并把位置信息推送給瀏覽器,由瀏覽器來顯示。

服務(wù)器端實現(xiàn)

服務(wù)器端的實現(xiàn)由兩部分組成:一部分是用來產(chǎn)生數(shù)據(jù)的 org.eclipse.jetty.servlets.EventSource 接口的實現(xiàn),另一部分是作為瀏覽器訪問端點的繼承自 org.eclipse.jetty.servlets.EventSourceServlet 類的 servlet 實現(xiàn)。下面代碼給出了 EventSource 接口的實現(xiàn)類。

EventSource 接口的實現(xiàn)類 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);  //開始生成位置信息
 }
 
 //根據(jù)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); //根據(jù)位置生成事件標(biāo)識符
   emitter.data(id); //發(fā)送位置信息數(shù)據(jù)
   try {
    Thread.sleep(2000);
   } catch (InterruptedException e) {
    logger.log(Level.WARNING, \
               "Movement query thread interrupted. Close the connection.", e);
    break;
   }
  }
  emitter.close(); //當(dāng)循環(huán)終止時,關(guān)閉連接
 }

 @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;
   }
  }
 }

 //判斷當(dāng)前的移動位置是否合法
 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 需要實現(xiàn) EventSource 接口的 onOpen、onResume 和 onClose 方法,其中 onOpen 方法在瀏覽器端的連接打開的時候被調(diào)用,onResume 方法在瀏覽器端重新建立連接時被調(diào)用,onClose 方法則在瀏覽器關(guān)閉連接的時候被調(diào)用。onOpen 和 onResume 方法都有一個 EventSource.Emitter 接口類型的參數(shù),可以用來發(fā)送數(shù)據(jù)。EventSource.Emitter 接口中包含的方法包括 data、event、comment、id 和 close 等,分別對應(yīng)于通訊協(xié)議中各種不同類型的事件。而 onResume 方法還額外包含一個參數(shù) lastEventId,表示通過 Last-Event-ID 頭發(fā)送過來的最近一次事件的標(biāo)識符。

MovementEventSource 類中事件生成的主要邏輯在 query 方法中。該方法中包含一個無限循環(huán),每隔 2 秒鐘改變一次位置,同時把更新之后的位置通過 EventSource.Emitter 接口的 data 方法發(fā)送給瀏覽器端。每個事件都有對應(yīng)的標(biāo)識符,而標(biāo)識符的值就是位置本身。如果連接斷開之后,瀏覽器重新進(jìn)行連接,可以從上一次的位置開始繼續(xù)移動該物體。

與 MovementEventSource 類對應(yīng)的 servlet 實現(xiàn)比較簡單,只需要繼承自 EventSourceServlet 類并覆寫 newEventSource 方法即可。在 newEventSource 方法的實現(xiàn)中,需要返回一個 MovementEventSource 類的對象,如下所示。每當(dāng)瀏覽器端建立連接時,該 servlet 會創(chuàng)建一個新的 MovementEventSource 類的對象來處理該請求。

servlet 實現(xiàn)類 MovementServlet

 public class MovementServlet extends EventSourceServlet { 

 @Override 
 protected EventSource newEventSource(HttpServletRequest request, 
 String clientId) { 
 return new MovementEventSource(800, 600, 20); 
 } 
 }

在服務(wù)器端實現(xiàn)中,需要注意的是要添加相應(yīng)的 servlet 過濾器支持。這是 jetty-eventsource-servlet 項目所依賴的 Jetty Continuations 框架的要求,否則的話會出現(xiàn)錯誤。添加過濾器的方式是在 web.xml 文件中添加代碼如下所示的配置內(nèi)容。

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>

瀏覽器端實現(xiàn)

瀏覽器端的實現(xiàn)也比較簡單,只需要創(chuàng)建出 EventSource 對象,并添加相應(yīng)的事件處理方法即可。下面代碼給出了相應(yīng)的實現(xiàn)。在頁面中使用一個方塊表示物體。當(dāng)接收到新的事件時,根據(jù)事件數(shù)據(jù)中給出的坐標(biāo)信息,更新方塊在頁面上的位置。

瀏覽器端的實現(xiàn)代碼

 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' 
         }); 
     });

在介紹完基本的服務(wù)器端和瀏覽器端實現(xiàn)之后,下面介紹比較重要的 IE 的支持。

IE 支持

使用瀏覽器原生的 EventSource 對象的一個比較大的問題是 IE 并不提供支持。為了在 IE 上提供同樣的支持,一般有兩種辦法。第一種辦法是在其他瀏覽器上使用原生 EventSource 對象,而在 IE 上則使用簡易輪詢或 COMET 技術(shù)來實現(xiàn);另外一種做法是使用 polyfill 技術(shù),即使用第三方提供的 JavaScript 庫來屏蔽瀏覽器的不同。本文使用的是 polyfill 技術(shù),只需要在頁面中加載第三方 JavaScript 庫即可。應(yīng)用本身的瀏覽器端代碼并不需要進(jìn)行改動。一般推薦使用第二種做法,因為這樣的話,在服務(wù)器端只需要使用一種實現(xiàn)技術(shù)即可。

在 IE 上提供類似原生 EventSource 對象的實現(xiàn)并不簡單。理論上來說,只需要通過 XMLHttpRequest 對象來獲取服務(wù)器端的響應(yīng)內(nèi)容,并通過文本解析,就可以提取出相應(yīng)的事件,并觸發(fā)對應(yīng)的事件處理方法。不過問題在于 IE 上的 XMLHttpRequest 對象并不支持獲取部分的響應(yīng)內(nèi)容。只有在響應(yīng)完成之后,才能獲取其內(nèi)容。由于服務(wù)器端推送事件使用的是一個長連接。當(dāng)連接一直處于打開狀態(tài)時,通過 XMLHttpRequest 對象并不能獲取響應(yīng)的內(nèi)容,也就無法觸發(fā)對應(yīng)的事件。更具體的來說,當(dāng) XMLHttpRequest 對象的 readyState 為 3(READYSTATE_INTERACTIVE)時,其 responseText 屬性是無法獲取的。

為了解決 IE 上 XMLHttpRequest 對象的問題,就需要使用 IE 8 中引入的 XDomainRequest 對象。XDomainRequest 對象的作用是發(fā)出跨域的 AJAX 請求。XDomainRequest 對象提供了 onprogress 事件。當(dāng) onprogress 事件發(fā)生時,可以通過 responseText 屬性來獲取到響應(yīng)的部分內(nèi)容。這是 XDomainRequest 對象和 XMLHttpRequest 對象的最大不同,也是使用 XDomainRequest 對象來實現(xiàn)類似原生 EventSource 對象的基礎(chǔ)。在使用 XDomainRequest 對象打開與服務(wù)器端的連接之后,當(dāng)服務(wù)器端有新的數(shù)據(jù)產(chǎn)生時,可以通過 XDomainRequest 對象的 onprogress 事件的處理方法來進(jìn)行處理,對接收到的數(shù)據(jù)進(jìn)行解析,根據(jù)數(shù)據(jù)的內(nèi)容觸發(fā)相應(yīng)的事件。

不過由于 XDomainRequest 對象本來的目的是發(fā)出跨域 AJAX 請求,考慮到跨域訪問的安全性問題,XDomainRequest 對象在使用時的限制也比較嚴(yán)格。這些限制會影響到其作為 EventSource 對象的實現(xiàn)方式。具體的限制和解決辦法如下所示:

  1. 服務(wù)器端的響應(yīng)需要包含 Access-Control-Allow-Origin 頭,用來聲明允許從哪些域訪問該 URL?!?”表示允許來自任何域的訪問,不推薦使用該值。一般使用與當(dāng)前應(yīng)用相同的域,限制只允許來自當(dāng)前域的訪問。

  2. XDomainRequest 對象發(fā)出的請求不能包含自定義的 HTTP 頭,這就限制了不能使用 Last-Event-ID 頭來聲明瀏覽器端最近一次接收到的事件的標(biāo)識符。只能通過 HTTP 請求的其他方式來傳遞該標(biāo)識符,如 GET 請求的參數(shù)或 POST 請求的內(nèi)容體。

  3. XDomainRequest 對象的請求的內(nèi)容類型(Content-Type)只能是“text/plain”。這就意味著,當(dāng)使用 POST 請求時,服務(wù)器端使用的框架,如 servlet,不會對 POST 請求的內(nèi)容進(jìn)行自動解析,無法使用 HttpServletRequest 類的 getParameter 方法來獲取 POST 請求的內(nèi)容。只能在服務(wù)器端對原始的請求內(nèi)容進(jìn)行解析,獲取到其中的參數(shù)的值。

  4. XDomainRequest 對象發(fā)出的請求中不包含任何與用戶認(rèn)證相關(guān)的信息,包括 cookie 等。這就意味著,如果服務(wù)器端需要認(rèn)證,則需要通過 HTTP 請求的其他方式來傳遞用戶的認(rèn)證信息,比如 session 的 ID 等。

由于 XDomainRequest 對象的這些限制,服務(wù)器端的實現(xiàn)也需要作出相應(yīng)的改動。這些改動包括返回 Access-Control-Allow-Origin 頭;對于瀏覽器端發(fā)送的“text/plain”類型的參數(shù)進(jìn)行解析;處理請求中包含的用戶認(rèn)證相關(guān)的信息。

本文的示例使用的 polyfill 庫是 GitHub 上的 Yaffle 開發(fā)的 EventSource 項目,具體的地址見參考資源。在使用該 polyfill 庫,并對服務(wù)器端的實現(xiàn)進(jìn)行修改之后,就可以在 IE 8 及以上的瀏覽器中使用服務(wù)器推送事件。如果需要支持 IE 7,則只能使用簡易輪詢或 COMET 技術(shù)。本文的示例代碼見參考資源。

感謝各位的閱讀!關(guān)于“html5服務(wù)器推送的示例分析”這篇文章就分享到這里了,希望以上內(nèi)容可以對大家有一定的幫助,讓大家可以學(xué)到更多知識,如果覺得文章不錯,可以把它分享出去讓更多的人看到吧!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI