網頁端IM通訊技術快速入門:短輪詢、長輪詢、SSE、WebSocket

語言: CN / TW / HK

本文來自“糊糊糊糊糊了”的分享,原題《實時訊息推送整理》,有優化和改動。

1、寫在前面

對Web端即時通訊技術熟悉的開發者來說,我們回顧網頁端IM的底層通訊技術,從短輪詢、長輪詢,到後來的SSE以及WebSocket,使用門檻越來越低(早期的長輪詢Comet這類技術實際屬於hack手段,使用門檻並不低),技術手段越來越先進,網頁端即時通訊技術的體驗也因此越來越好。

但上週在編輯《IM掃碼登入技術專題》系列文章第3篇的時候忽然想到,之前的這些所謂的網頁端即時通訊“老技術”相對於當紅的WebSocket,並非毫無用武之地。就拿IM裡的掃碼登入功能來說,用短輪詢技術就非常合適,完全沒必要大炮打蚊子上WebSocket。

所以,很多時候沒必要盲目追求新技術,相對應用場景來說適合的才是最好的。對於即時通訊網的im和訊息推送這類即時通訊技術開發者來說,掌握WebSocket固然很重要,但瞭解短輪詢、長輪詢等這些所謂的Web端即時通訊“老技術”仍然大有裨益,這也正是整理分享本文的重要原因。

2、推薦閱讀

[1] 新手入門貼:史上最全Web端即時通訊技術原理詳解

[2] 詳解Web端通訊方式的演進:從Ajax、JSONP 到 SSE、Websocket

[3] Web端即時通訊技術盤點:短輪詢、Comet、Websocket、SSE

3、正文引言

對於IM/訊息推送這類即時通訊系統而言,系統的關鍵就是“實時通訊”能力。

從表面意思上來看,“實時通訊”指的是:

  • 1)客戶端能隨時主動傳送資料給服務端;
  • 2)當客戶端關注的內容在發生改變時,伺服器能夠實時地通知客戶端。

類比於傳統的C/S請求模型,“實時通訊”時客戶端不需要主觀地傳送請求去獲取自己關心的內容,而是由伺服器端進行“推送”。

注意:上面的“推送”二字打了引號,實際上現有的幾種技術實現方式中,並不是伺服器端真正主動地推送,而是通過一定的手段營造了一種“實時通訊”的假象。

就目前現有的幾種技術而言,主要有以下幾類:

  • 1)客戶端輪詢:傳統意義上的短輪詢(Short Polling);
  • 2)伺服器端輪詢:長輪詢(Long Polling);
  • 3)單向伺服器推送:Server-Sent Events(SSE);
  • 4)全雙工通訊:WebSocket。

以下正文將針對這幾種技術方案,為你一一解惑。

4、本文配套Demo和程式碼

為了幫助讀者更好的理解本文內容,筆者專門寫了一個較完整的Demo,Demo會以一個簡易聊天室的例子來分別通過上述的四種技術方式實現(程式碼存在些許bug,主要是為了做演示用,別介意)。

完整Demo原始碼打包下載:

請從同步連結附件中下載:http://www.52im.net/thread-3555-1-1.html

Demo的執行效果(動圖):

有興趣可以自行下載研究學習。

5、理解短輪詢(Short Polling)

短輪詢的實現原理:

  • 1)客戶端向伺服器端傳送一個請求,伺服器返回資料,然後客戶端根據伺服器端返回的資料進行處理;
  • 2)客戶端繼續向伺服器端傳送請求,繼續重複以上的步驟,如果不想給伺服器端太大的壓力,一般情況下會設定一個請求的時間間隔。

邏輯如下圖所示:

使用短輪詢的優點:基礎不需要額外的開發成本,請求資料,解析資料,作出響應,僅此而已,然後不斷重複。

缺點也顯而易見:

  • 1)不斷的傳送和關閉請求,對伺服器的壓力會比較大,因為本身開啟Http連線就是一件比較耗資源的事情;
  • 2)輪詢的時間間隔不好控制。如果要求的實時性比較高,顯然使用短輪詢會有明顯的短板,如果設定interval的間隔過長,會導致訊息延遲,而如果太短,會對伺服器產生壓力。

短輪詢客戶的程式碼實現(片段節選):

var ShortPollingNotification = {

  datasInterval: null,

  subscribe: function() {

    this.datasInterval = setInterval(function() {

      Request.getDatas().then(function(res) {

        window.ChatroomDOM.renderData(res);

      });

    }, TIMEOUT);

    return this.unsubscribe;

  },

  unsubscribe: function() {

    this.datasInterval && clearInterval(this.datasInterval);

  }

}

PS:完整程式碼,請見本文“4、本文配套Demo和程式碼”一節。

對應本文配套Demo的執行效果如下(動圖):

下面是對應的請求,注意左下角的請求數量一直在變化:

在上圖中,每隔1s就會發送一個請求,看起來效果還不錯,但是如果將timeout的值設定成5s,效果將大打折扣。如下圖所示。

將timeout值設定成5s時的Demo執行效果(動圖):

6、理解長輪詢(Long Polling)

6.1 基本原理

長輪詢的基本原理:

  • 1)客戶端傳送一個請求,伺服器會hold住這個請求;
  • 2)直到監聽的內容有改變,才會返回資料,斷開連線(或者在一定的時間內,請求還得不到返回,就會因為超時自動斷開連線);
  • 3)客戶端繼續傳送請求,重複以上步驟。

邏輯如下圖所示:

長輪詢是基於短輪詢上的改進版本:主要是減少了客戶端發起Http連線的開銷,改成了在伺服器端主動地去判斷所關心的內容是否變化。

所以其實輪詢的本質並沒有多大變化,變化的點在於:

  • 1)對於內容變化的輪詢由客戶端改成了伺服器端(客戶端會在連線中斷之後,會再次傳送請求,對比短輪詢來說,大大減少了發起連線的次數);
  • 2)客戶端只會在資料改變時去作相應的改變,對比短輪詢來說,並不是全盤接收。

6.2 程式碼實現

長輪詢客戶的程式碼實現(片段節選):

// 客戶端

var LongPollingNotification = {

    // ....

    subscribe: function() {

      var that = this;

 

      // 設定超時時間

      Request.getV2Datas(this.getKey(),{ timeout: 10000 }).then(function(res) {

        var data = res.data;

        window.ChatroomDOM.renderData(res);

        // 成功獲取資料後會再次傳送請求

        that.subscribe();

      }).catch(function(error) {

        // timeout 之後也會再次傳送請求

        that.subscribe();

      });

      return this.unsubscribe;

    }

    // ....

}

筆者採用的是express,預設不支援hold住請求,因此用了一個express-longpoll的庫來實現。

下面是一個原生不用庫的實現(這裡只是介紹原理),整體的思路是:如果伺服器端支援hold住請求的話,那麼在一定的時間內會自輪詢,然後期間通過比較key值,判斷是否返回新資料。

以下是具體思路:

  • 1)客戶端第一次會帶一個空的key值,這次會立即返回,獲取新內容,伺服器端將計算出的contentKey返回給客戶端;
  • 2)然後客戶端傳送第二次請求,帶上第一次返回的contentKey作為key值,然後進行下一輪的比較;
  • 3)如果兩次的key值相同,就會hold請求,進行內部輪詢,如果期間有新內容或者客戶端timeout,就會斷開連線;
  • 4)重複以上步驟。

程式碼如下:

// 伺服器端

router.get('/v2/datas', function(req, res) {

  const key = _.get(req.query, 'key', '');

  let contentKey = chatRoom.getContentKey();

 

  while(key === contentKey) {

    sleep.sleep(5);

    contentKey = chatRoom.getContentKey();

  }

 

  const connectors = chatRoom.getConnectors();

  const messages = chatRoom.getMessages();

  res.json({

    code: 200,

    data: { connectors: connectors, messages: messages, key: contentKey },

  });

});

以下是用 express-longpoll的實現片段:

// mini-chatroom/public/javascripts/server/longPolling.js

function pushDataToClient(key, longpoll) {

  var contentKey = chatRoom.getContentKey();

 

  if(key !== contentKey) {

    var connectors = chatRoom.getConnectors();

    var messages = chatRoom.getMessages();

 

    long poll.publish(

      '/v2/datas',

      {

        code: 200,

        data: {connectors: connectors, messages: messages, key: contentKey},

      }

    );

  }

}

 

long poll.create("/v2/datas", function(req, res, next) {

  key = _.get(req.query, 'key', '');

  pushDataToClient(key, longpoll);

  next();

});

 

intervalId = setInterval(function() {

  pushDataToClient(key, longpoll);

}, LONG_POLLING_TIMEOUT);

PS:完整程式碼,請見本文“4、本文配套Demo和程式碼”一節。

為了方便演示,我將客戶端發起請求的timeout改成了4s,注意觀察下面的截圖:

可以看到,斷開連線的兩種方式,要麼是超時,要麼是請求有資料返回。

6.3 基於iframe的長輪詢模式

這是長輪詢技術的另一個種實現方案。

該方案的具體的原理為:

  • 1)在頁面中嵌入一個iframe,地址指向輪詢的伺服器地址,然後在父頁面中放置一個執行函式,比如execute(data);
  • 2)當伺服器有內容改變時,會向iframe傳送一個指令碼<script>parent.execute(JSON.stringify(data))</script>;
  • 3)通過傳送的指令碼,主動執行父頁面中的方法,達到推送的效果。

因不篇幅原因,在此不作深入介紹,有興趣的同學可以詳讀《新手入門貼:史上最全Web端即時通訊技術原理詳解》一文中的“3.3.2 基於iframe的資料流”一節。

7、什麼是Server-Sent Events(SSE)

7.1 基本介紹

從純技術的角度講:上兩節介紹的短輪詢和長輪詢技術,伺服器端是無法主動給客戶端推送訊息的,都是客戶端主動去請求伺服器端獲取最新的資料。

本節要介紹的SSE是一種可以主動從服務端推送訊息的技術。

SSE的本質其實就是一個HTTP的長連線,只不過它給客戶端傳送的不是一次性的資料包,而是一個stream流,格式為text/event-stream。所以客戶端不會關閉連線,會一直等著伺服器發過來的新的資料流,視訊播放就是這樣的例子。

簡單來說,SSE就是:

  • 1)SSE 使用 HTTP 協議,現有的伺服器軟體都支援。WebSocket 是一個獨立協議。
  • 2)SSE 屬於輕量級,使用簡單;WebSocket 協議相對複雜。
  • 3)SSE 預設支援斷線重連,WebSocket 需要自己實現。
  • 4)SSE 一般只用來傳送文字,二進位制資料需要編碼後傳送,WebSocket 預設支援傳送二進位制資料。
  • 5)SSE 支援自定義傳送的訊息型別。

SSE的技術原理如下圖所示:

SSE基本的使用方法,可以參看 SSE 的API文件,地址是:https://developer.mozilla.org/en ... _server-sent_events

目前除了IE以及低版本的瀏覽器不支援,絕大多數的現代瀏覽器都支援SSE:

上圖來自:https://caniuse.com/?search=Server-Sent-Events

7.2 程式碼實現

// 客戶端

var SSENotification = {

  source: null,

  subscribe: function() {

    if('EventSource'inwindow) {

      this.source = newEventSource('/sse');

 

      this.source.addEventListener('message', function(res) {

        const d = res.data;

        window.ChatroomDOM.renderData(JSON.parse(d));

      });

    }

    return this.unsubscribe;

  },

  unsubscribe: function() {

    this.source && this.source.close();

  }

}

 

// 伺服器端

router.get('/sse', function(req, res) {

  const connectors = chatRoom.getConnectors();

  const messages = chatRoom.getMessages();

  const response = { code: 200, data: { connectors: connectors, messages: messages } };

 

  res.writeHead(200, {

    "Content-Type":"text/event-stream",

    "Cache-Control":"no-cache",

    "Connection":"keep-alive",

    "Access-Control-Allow-Origin": '*',

  });

 

  res.write("retry: 10000\n");

  res.write("data: "+ JSON.stringify(response) + "\n\n");

 

  var unsubscribe = Event.subscribe(function() {

    const connectors = chatRoom.getConnectors();

    const messages = chatRoom.getMessages();

    const response = { code: 200, data: { connectors: connectors, messages: messages } };

    res.write("data: "+ JSON.stringify(response) + "\n\n");

  });

 

  req.connection.addListener("close", function() {

    unsubscribe();

  }, false);

});

下面是控制檯的情況,注意觀察響應型別:

詳情中注意檢視請求型別,以及EventStream訊息型別:

PS:有關SSE更詳盡的資料就不在這裡展開了,有興趣的同學可以詳讀《SSE技術詳解:一種全新的HTML5伺服器推送事件技術》、《使用WebSocket和SSE技術實現Web端訊息推送》。

8、什麼是WebSocket

8.1 基本介紹

PS:本小節內容引用自《Web端即時通訊實踐乾貨:如何讓WebSocket斷網重連更快速?》一文的“3、快速瞭解WebSocket”。

WebSocket誕生於2008年,在2011年成為國際標準,現在所有的瀏覽器都已支援(詳見《新手快速入門:WebSocket簡明教程》)。它是一種全新的應用層協議,是專門為web客戶端和服務端設計的真正的全雙工通訊協議,可以類比HTTP協議來了解websocket協議。

圖片引用自《WebSocket詳解(四):刨根問底HTTP與WebSocket的關係(上篇)

它們的不同點:

  • 1)HTTP的協議識別符號是http,WebSocket的是ws;
  • 2)HTTP請求只能由客戶端發起,伺服器無法主動向客戶端推送訊息,而WebSocket可以;
  • 3)HTTP請求有同源限制,不同源之間通訊需要跨域,而WebSocket沒有同源限制。

它們的相同點:

  • 1)都是應用層的通訊協議;
  • 2)預設埠一樣,都是80或443;
  • 3)都可以用於瀏覽器和伺服器間的通訊;
  • 4)都基於TCP協議。

兩者和TCP的關係圖:

圖片引用自《新手快速入門:WebSocket簡明教程

有關Http和WebSocket的關係,可以詳讀:

WebSocket詳解(四):刨根問底HTTP與WebSocket的關係(上篇)

WebSocket詳解(五):刨根問底HTTP與WebSocket的關係(下篇)

有關WebSocket和Socket的關係,可以詳讀:WebSocket詳解(六):刨根問底WebSocket與Socket的關係》.

8.2 技術特徵

WebSocket技術特徵總結下就是:

  • 1)可雙向通訊,設計的目的主要是為了減少傳統輪詢時http連線數量的開銷;
  • 2)建立在TCP協議之上,握手階段採用 HTTP 協議,因此握手時不容易遮蔽,能通過各種 HTTP 代理伺服器;
  • 3)與HTTP相容性良好,同樣可以使用80和443埠;
  • 4)沒有同源限制,客戶端可以與任意伺服器通訊;
  • 5)可以傳送文字,也可以傳送二進位制資料;
  • 6)協議識別符號是ws(如果加密,則為wss),伺服器網址就是 URL.

WebSocket的技術原理如下圖所示:

關於WebSocket API方面的知識,這裡不再作講解,可以自己查閱:https://developer.mozilla.org/en-US/docs/Web/API/WebSocket

8.3 瀏覽器相容性

WebSocket相容性良好,基本支援所有現代瀏覽器。

上圖來自:https://caniuse.com/mdn-api_websocket

8.4 程式碼實現

筆者這裡採用的是socket.io,是基於WebSocket的封裝,提供了客戶端以及伺服器端的支援。

// 客戶端

var WebsocketNotification = {

  // ...

  subscribe: function(args) {

    var connector = args[1];

    this.socket = io();

    this.socket.emit('register', connector);

    this.socket.on('register done', function() {

      window.ChatroomDOM.renderAfterRegister();

    });

    this.socket.on('data', function(res) {

      window.ChatroomDOM.renderData(res);

    });

    this.socket.on('disconnect', function() {

      window.ChatroomDOM.renderAfterLogout();

    });

  }

  // ...

}

 

// 伺服器端

var io = socketIo(httpServer);

io.on('connection', (socket) => {

  socket.on('register', function(connector) {

    chatRoom.onConnect(connector);

    io.emit('register done');

    var data = chatRoom.getDatas();

    io.emit('data', { data });

  });

  socket.on('chat', function(message) {

    chatRoom.receive(message);

    var data = chatRoom.getDatas();

    io.emit('data', { data });

  });

});

PS:完整程式碼,請見本文“4、本文配套Demo和程式碼”一節。

響應格式如下:

8.5 深入學習

隨著HTML5的普及率越來越高,WebSocket的應用也越來越普及,關於WebSocket的學習資料網上很容易找到,限於篇幅本文就不深入展開這個話題。

如果想進一步深入學習WebSocket的方方面面,以下文章值得一讀:

新手快速入門:WebSocket簡明教程

WebSocket詳解(一):初步認識WebSocket技術

WebSocket詳解(二):技術原理、程式碼演示和應用案例

WebSocket詳解(三):深入WebSocket通訊協議細節

WebSocket詳解(四):刨根問底HTTP與WebSocket的關係(上篇)

WebSocket詳解(五):刨根問底HTTP與WebSocket的關係(下篇)

WebSocket詳解(六):刨根問底WebSocket與Socket的關係

理論聯絡實際:從零理解WebSocket的通訊原理、協議格式、安全性

微信小程式中如何使用WebSocket實現長連線(含完整原始碼)

八問WebSocket協議:為你快速解答WebSocket熱門疑問

Web端即時通訊實踐乾貨:如何讓你的WebSocket斷網重連更快速?

WebSocket從入門到精通,半小時就夠!

WebSocket硬核入門:200行程式碼,教你徒手擼一個WebSocket伺服器

長連線閘道器技術專題(四):愛奇藝WebSocket實時推送閘道器技術實踐

9、本文小結

短輪詢、長輪詢實現成本相對比較簡單,適用於一些實時性要求不高的訊息推送,在實時性要求高的場景下,會存在延遲以及會給伺服器帶來更大的壓力。

SSE只能是伺服器端推送訊息,因此對於不需要雙向通訊的專案比較適用。

WebSocket目前而言實現成本相對較低,適合於雙工通訊,對於多人線上,要求實時性較高的專案比較實用。

本文已同步釋出於“即時通訊技術圈”公眾號。

▲ 本文在公眾號上的連結是:點此進入。同步釋出連結是:http://www.52im.net/thread-3555-1-1.html

「其他文章」