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

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

一、寫在前面

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

但上週在編輯《IM掃碼登陸技術專題》系列文章第3篇的時候突然想到,以前的這些所謂的網頁端即時通信「老技術」相對於當紅的WebSocket,並不是毫無用武之地。就拿IM裏的掃碼登陸功能來講,用短輪詢技術就很是合適,徹底不必大炮打蚊子上WebSocket。java

因此,不少時候不必盲目追求新技術,相對應用場景來講適合的纔是最好的。對於即時通信網的im和消息推送這類即時通信技術開發者來講,掌握WebSocket當然很重要,但瞭解短輪詢、長輪詢等這些所謂的Web端即時通信「老技術」仍然大有裨益,這也正是整理分享本文的重要緣由。git

學習交流:github

即時通信/推送技術開發交流5羣:215477170 [推薦]
移動端IM開發入門文章:《新手入門一篇就夠:從零開發移動端IM》
開源IM框架源碼: https://github.com/JackJiang2...

(本文同步發佈於:http://www.52im.net/thread-35...web

二、推薦閱讀

[1] 新手入門貼:史上最全Web端即時通信技術原理詳解
[2] 詳解Web端通訊方式的演進:從Ajax、JSONP 到 SSE、Websocket
[3] Web端即時通信技術盤點:短輪詢、Comet、Websocket、SSEexpress

三、正文引言

對於IM/消息推送這類即時通信系統而言,系統的關鍵就是「實時通訊」能力。json

從表面意思上來看,「實時通訊」指的是:小程序

1)客戶端能隨時主動發送數據給服務端;
2)當客戶端關注的內容在發生改變時,服務器可以實時地通知客戶端。
類比於傳統的C/S請求模型,「實時通訊」時客戶端不須要主觀地發送請求去獲取本身關心的內容,而是由服務器端進行「推送」。微信小程序

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

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

1)客戶端輪詢:傳統意義上的短輪詢(Short Polling);
2)服務器端輪詢:長輪詢(Long Polling);
3)單向服務器推送:Server-Sent Events(SSE);
4)全雙工通訊:WebSocket。
如下正文將針對這幾種技術方案,爲你一一解惑。

四、本文配套Demo和代碼

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

完整Demo源碼打包下載:

(請從同步連接附件中下載:http://www.52im.net/thread-35...

Demo的運行效果(動圖):

有興趣能夠自行下載研究學習。

五、理解短輪詢(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:完整代碼,請見本文「四、本文配套Demo和代碼」一節。

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

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

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

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

六、理解長輪詢(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:完整代碼,請見本文「四、本文配套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的數據流」一節。

七、什麼是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=S...

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端消息推送》。

八、什麼是WebSocket

8.1 基本介紹
PS:本小節內容引用自《Web端即時通信實踐乾貨:如何讓WebSocket斷網重連更快速?》一文的「三、快速瞭解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...

8.3 瀏覽器兼容性
WebSocket兼容性良好,基本支持全部現代瀏覽器。

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

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:完整代碼,請見本文「四、本文配套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實時推送網關技術實踐》

九、本文小結

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

SSE只能是服務器端推送消息,所以對於不須要雙向通訊的項目比較適用。

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

本文已同步發佈於「即時通信技術圈」公衆號。

▲ 本文在公衆號上的連接是:點此進入。同步發佈連接是:http://www.52im.net/thread-35...

相關文章
相關標籤/搜索