1. 程式人生 > 其它 >IM即時通訊聊天社交APP原始碼,交友APP原始碼下載附安裝教程+演示

IM即時通訊聊天社交APP原始碼,交友APP原始碼下載附安裝教程+演示

1、引言

有關Web端即時通訊技術的文章我已整理過很多篇,閱讀過的讀者可能都很熟悉,早期的Web端即時通訊方案,受限於Web客戶端的技術限制,想實現真正的“即時”通訊,難度相當大。

原始碼演示與下載地址:ym.ws58.net

傳統的Web端即時通訊技術從短輪詢到長連詢,再到Comet技術,在如此原始的HTML標準之下,為了實現所謂的“即時”通訊,技術上可謂絞盡腦汁,極盡所能。

自從HTML5標準釋出之後,WebSocket這類技術橫空出世,實現Web端即時通訊技術的便利性大大提前,以往想都不敢想的真正全雙工實時通訊,如此早已成為可能。

本文將專門介紹WebSocket、socket.io、SSE這幾種現代的Web端即時通訊技術,從適用場景到技術原理,通俗又不失深度的文字,特別適合對Web端即時通訊技術有一定了解,且想深入學習WebSocket等現代Web端“實時”通訊技術,卻又不想花時間去深讀枯燥的IETF技術手冊的讀者。

3、知識預備

如果你對Web端即時通訊技術的前世今生不曾瞭解,建議先讀以下文章:

  1. 《新手入門貼:史上最全Web端即時通訊技術原理詳解》
  2. 《Web端即時通訊技術盤點:短輪詢、Comet、Websocket、SSE》
  3. 《詳解Web端通訊方式的演進:從Ajax、JSONP 到 SSE、Websocket》
  4. 《網頁端IM通訊技術快速入門:短輪詢、長輪詢、SSE、WebSocket》

如果你對本文將要介紹的技術已有了解,建議進行專項學習,以便深入掌握:

  1. 《Comet技術詳解:基於HTTP長連線的Web端實時通訊技術》
  2. 《SSE技術詳解:一種全新的HTML5伺服器推送事件技術》
  3. 《WebSocket詳解(三):深入WebSocket通訊協議細節》
  4. 《理論聯絡實際:從零理解WebSocket的通訊原理、協議格式、安全性》
  5. 《WebSocket從入門到精通,半小時就夠!》

4、WebSocket

在這裡不打算詳細介紹整個WebSocket協議的內容,根據我本人以前協議的學習思路,我挑重點使用問答方式來介紹該協議,這樣讀起來就不那麼枯燥。

4.1 基本情況

協議執行在OSI的哪層?

應用層,WebSocket協議是一個獨立的基於TCP的協議。 它與HTTP唯一的關係是它的握手是由HTTP伺服器解釋為一個Upgrade請求。

協議執行的標準埠號是多少?

預設情況下,WebSocket協議使用埠80用於常規的WebSocket連線、埠443用於WebSocket連線的在傳輸層安全(TLS)RFC2818之上的隧道化口。

4.2 協議是如何工作的?

協議的工作流程可以參考下圖:

其中幀的一些重要欄位需要解釋一下:

  • 1)Upgrade:`upgrade`是HTTP1.1中用於定義轉換協議的`header`域。它表示,如果伺服器支援的話,客戶端希望使用現有的「網路層」已經建立好的這個「連線(此處是 TCP 連線)」,切換到另外一個「應用層」(此處是 WebSocket)協議;
  • 2)Connection:`Upgrade`固定欄位。Connection還有其他欄位,可以自己給自己科普一下;
  • 3)Sec-WebSocket-Key:用來發送給伺服器使用(伺服器會使用此欄位組裝成另一個key值放在握手返回資訊裡傳送客戶端);
  • 4)Sec-WebSocket-Protocol:標識了客戶端支援的子協議的列表;
  • 5)Sec-WebSocket-Version:標識了客戶端支援的WS協議的版本列表,如果伺服器不支援這個版本,必須迴應自己支援的版本;
  • 6)Origin:作安全使用,防止跨站攻擊,瀏覽器一般會使用這個來標識原始域;
  • 7)Sec-WebSocket-Accept:伺服器響應,包含Sec-WebSocket-Key 的簽名值,證明它支援請求的協議版本。

關於Sec-WebSocket-Key和Sec-WebSocket-Accept的計算是這樣的:

所有相容RFC 6455 的WebSocket 伺服器都使用相同的演算法計算客戶端挑戰的答案:將Sec-WebSocket-Key 的內容與標準定義的唯一GUID字元(258EAFA5-E914-47DA-95CA-C5AB0DC85B11)串拼接起來,計算出SHA1雜湊值,結果是一個base-64編碼的字串,把這個字串發給客戶端即可。

用程式碼就是實現如下:

const key = crypto.createHash('sha1')

.update(req.headers['sec-websocket-key'] + constants.GUID, 'binary')

.digest('')

至於為什麼需要這麼一個步驟,可以參考《理論聯絡實際:從零理解WebSocket的通訊原理、協議格式、安全性》一文。

引用如下:

Sec-WebSocket-Key/Sec-WebSocket-Accept在主要作用在於提供基礎的防護,減少惡意連線、意外連線。

作用大致歸納如下:

  • 1)避免服務端收到非法的websocket連線(比如http客戶端不小心請求連線websocket服務,此時服務端可以直接拒絕連線);
  • 2)確保服務端理解websocket連線。因為ws握手階段採用的是http協議,因此可能ws連線是被一個http伺服器處理並返回的,此時客戶端可以通過Sec-WebSocket-Key來確保服務端認識ws協議。(並非百分百保險,比如總是存在那麼些無聊的http伺服器,光處理Sec-WebSocket-Key,但並沒有實現ws協議。。。);
  • 3)用瀏覽器裡發起ajax請求,設定header時,Sec-WebSocket-Key以及其他相關的header是被禁止的。這樣可以避免客戶端傳送ajax請求時,意外請求協議升級(websocket upgrade);
  • 4)可以防止反向代理(不理解ws協議)返回錯誤的資料。比如反向代理前後收到兩次ws連線的升級請求,反向代理把第一次請求的返回給cache住,然後第二次請求到來時直接把cache住的請求給返回(無意義的返回);
  • 5)Sec-WebSocket-Key主要目的並不是確保資料的安全性,因為Sec-WebSocket-Key、Sec-WebSocket-Accept的轉換計算公式是公開的,而且非常簡單,最主要的作用是預防一些常見的意外情況(非故意的)。

強調:Sec-WebSocket-Key/Sec-WebSocket-Accept 的換算,只能帶來基本的保障,但連線是否安全、資料是否安全、客戶端/服務端是否合法的 ws客戶端、ws服務端,其實並沒有實際性的保證。

4.3 協議傳輸的幀格式是什麼?

幀格式定義的格式如下:

各個欄位的解釋如下:

原始碼演示與下載地址:ym.ws58.net

  • 1)FIN: 1bit,用來表明這是一個訊息的最後的訊息片斷,當然第一個訊息片斷也可能是最後的一個訊息片斷;
  • 2)RSV1,RSV2,RSV3: 分別都是1位,如果雙方之間沒有約定自定義協議,那麼這幾位的值都必須為0,否則必須斷掉WebSocket連線。在ws中就用到了RSV1來表示是否訊息壓縮了的;
  • 3)opcode:4 bit,表示被傳輸幀的型別:
  • - %x0 表示連續訊息片斷;
  • - %x1 表示文字訊息片斷;
  • - %x2 表未二進位制訊息片斷;
  • - %x3-7 為將來的非控制訊息片斷保留的操作碼;
  • - %x8 表示連線關閉;
  • - %x9 表示心跳檢查的ping;
  • - %xA 表示心跳檢查的pong;
  • - %xB-F 為將來的控制訊息片斷的保留操作碼。
  • 4)Mask: 1 bit。定義傳輸的資料是否有加掩碼,如果設定為1,掩碼鍵必須放在masking-key區域,客戶端傳送給服務端的所有訊息,此位都是1;
  • 5)Payload length:傳輸資料的長度,以位元組的形式表示:7位、7+16位、或者7+64位。如果這個值以位元組表示是0-125這個範圍,那這個值就表示傳輸資料的長度;如果這個值是126,則隨後的兩個位元組表示的是一個16進位制無符號數,用來表示傳輸資料的長度;如果這個值是127,則隨後的是8個位元組表示的一個64位無符合數,這個數用來表示傳輸資料的長度。多位元組長度的數量是以網路位元組的順序表示。負載資料的長度為擴充套件資料及應用資料之和,擴充套件資料的長度可能為0,因而此時負載資料的長度就為應用資料的長度;
  • 6)Masking-key:0或4個位元組,客戶端傳送給服務端的資料,都是通過內嵌的一個32位值作為掩碼的;掩碼鍵只有在掩碼位設定為1的時候存在;
  • 7)Extension data: x位,如果客戶端與服務端之間沒有特殊約定,那麼擴充套件資料的長度始終為0,任何的擴充套件都必須指定擴充套件資料的長度,或者長度的計算方式,以及在握手時如何確定正確的握手方式。如果存在擴充套件資料,則擴充套件資料就會包括在負載資料的長度之內;
  • 8)Application data: y位,任意的應用資料,放在擴充套件資料之後,應用資料的長度=負載資料的長度-擴充套件資料的長度;
  • 9)Payload data: (x+y)位,負載資料為擴充套件資料及應用資料長度之和;

更多細節請參考RFC6455-資料幀,這裡不作贅述。

針對上面的各個欄位的介紹,有一個Mask的需要說一下。

掩碼鍵(Masking-key)是由客戶端挑選出來的32位的隨機數。掩碼操作不會影響資料載荷的長度。

掩碼、反掩碼操作都採用如下演算法。

首先,假設:

  • 1)original-octet-i:為原始資料的第i位元組;
  • 2)transformed-octet-i:為轉換後的資料的第i位元組;
  • 3)j:為i mod 4的結果;
  • 4)masking-key-octet-j:為mask key第j位元組。

演算法描述為: original-octet-i 與 masking-key-octet-j 異或後,得到 transformed-octet-i。

即: j = i MOD 4 transformed-octet-i = original-octet-i XOR masking-key-octet-j

用程式碼實現:

const mask = (source, mask, output, offset, length) => {

for(vari = 0; i < length; i++) {

output[offset + i] = source[i ] ^ mask[i & 3];

}

};

解掩碼是反過來的操作:

const unmask = (buffer, mask) => {

// Required until [url=https://github.com/nodejs/node/issues/9006]https://github.com/nodejs/node/issues/9006[/url] is resolved.

const length = buffer.length;

for(vari = 0; i < length; i++) {

buffer[i ] ^= mask[i & 3];

}

};

同樣的為什麼需要掩碼操作,也可以參考之前的那篇文章:《理論聯絡實際:從零理解WebSocket的通訊原理、協議格式、安全性》,完整的我就不列舉了。

需要注意的重點,我引用一下:

WebSocket協議中,資料掩碼的作用是增強協議的安全性。但資料掩碼並不是為了保護資料本身,因為演算法本身是公開的,運算也不復雜。除了加密通道本身,似乎沒有太多有效的保護通訊安全的辦法。

那麼為什麼還要引入掩碼計算呢,除了增加計算機器的運算量外似乎並沒有太多的收益(這也是不少同學疑惑的點)。

答案還是兩個字: 安全。但並不是為了防止資料洩密,而是為了防止早期版本的協議中存在的代理快取汙染攻擊(proxy cache poisoning attacks)等問題。

5、socket

5.1 本節引言

 

原始碼演示與下載地址:ym.ws58.net

 

介紹完上一節WebSocket協議,我們把視線轉移到現代Web端即時通訊技術的第二個利器:socket.io。

估計有讀者就會問,WebSocket和socket.io有啥區別啊?

在瞭解socket.io之前,我們先聊聊傳統Web端即時通訊“長連線”技術的實現背景。

5.2 傳統Web長連線的技術實現背景

在現實的Web端產品中,並不是所有的Web客戶端都支援長連線的,或者換句話說,在WebSocket協議出來之前,是三種方式去實現WebSocket類似的功能的。

這三種方式是:

  • 1)Flash:使用Flash是一種簡單的方法。不過很明顯的缺點就是Flash並不會安裝在所有客戶端上,比如iPhone/iPad。
  • 2)Long-Polling:也就是眾所周之的“長輪詢”,在過去,這是一種有效的技術,但並沒有對訊息傳送進行優化。雖然我不會把AJAX長輪詢當做一種hack技術,但它確實不是一個最優方法;
  • 3)Comet:在過去,這被稱為Web端的“伺服器推”技術,相對於傳統的 Web 應用, 開發 Comet 應用具有一定的挑戰性,具體請見《Comet技術詳解:基於HTTP長連線的Web端實時通訊技術》。

那麼如果單純地使用WebSocket的話,那些不支援的客戶端怎麼辦呢?難道直接放棄掉?

當然不是。Guillermo Rauch大神寫了socket.io這個庫,對WebSocket進行封裝,從而讓長連線滿足所有的場景,不過當然得配合使用對應的客戶端程式碼。

socket.io將會使用特性檢測的方式來決定以websocket/ajax長輪詢/flash等方式建立連線。

那麼socket.io是如何做到這些的呢?

我們帶著以下幾個問題去學習:

  • 1)socket.io到底有什麼新特性?
  • 2)socket.io是怎麼實現特性檢測的?
  • 3)socket.io有哪些坑呢?
  • 4)socket.io的實際應用是怎樣的,需要注意些什麼?

如果有童鞋對上述問題已經清楚,想必就沒有往下讀的必要了。

5.3 socket.io的介紹

通過前面章節,讀者們都知道了WebSocket的功能,那麼socket.io相對於WebSocket,在此基礎上封裝了一些什麼新東西呢?

socket.io其實是有一套封裝了websocket的協議,叫做engine.io協議,在此協議上實現了一套底層雙向通訊的引擎Engine.io。

而socket.io則是建立在engine.io上的一個應用層框架而已。所以我們研究的重點便是engine.io協議。

在socket.io的README中提到了其實現的一些新特性(回答了問題一):

  • 1)可靠性:連線依然可以建立即使應用環境存在: 代理或者負載均衡器 個人防火牆或者反病毒軟體;
  • 2)支援自動連線: 除非特別指定,否則一個斷開的客戶端會一直重連伺服器直到伺服器恢復可用狀態;
  • 3)斷開連線檢測:在Engine.io層實現了一個心跳機制,這樣允許客戶端和伺服器知道什麼時候其中的一方不能響應。該功能是通過設定在服務端和客戶端的定時器實現的,在連線握手的時候,伺服器會主動告知客戶端心跳的間隔時間以及超時時間;
  • 4)二進位制的支援:任何序列化的資料結構都可以用來發送;
  • 5)跨瀏覽器的支援:該庫甚至支援到IE8;
  • 6)支援複用:為了在應用程式中將建立的關注點隔離開來,Socket.io允許你建立多個namespace,這些namespace擁有單獨的通訊通道,但將共享相同的底層連線;
  • 7)支援Room:在每一個namespace下,你可以定義任意數量的通道,我們稱之為"房間",你可以加入或者離開房間,甚至廣播訊息到指定的房間。

注意:Socket.IO不是WebSocket的實現,雖然 Socket.IO確實在可能的情況下會去使用WebSocket作為一個transport,但是它添加了很多元資料到每一個報文中:報文的型別以及namespace和ack Id。這也是為什麼標準WebSocket客戶端不能夠成功連線上 Socket.IO 伺服器,同樣一個 Socket.IO 客戶端也連線不上標準WebSocket伺服器的原因。

5.4 engine.io協議介紹

完整的engine.io協議的握手過程如下圖:

當前engine.io協議的版本是3,我們根據上圖來大致介紹一下engine.io協議。

5.4.1)engine.io協議請求欄位:

我們看到的是請求的url和WebSocket不大一樣,解釋一下:

  • 1)EIO=3: 表示的是使用的是Engine.io協議版本3;
  • 2)transport=polling/websocket: 表示使用的長連線方式是輪詢還是WebSocket;
  • 3)t=xxxxx: 程式碼中使用yeast根據時間戳生成一個唯一的字串;
  • 4)sid=xxxx: 客戶端和伺服器建立連線之後獲取到的session id,客戶端拿到之後必須在每次請求中追加這個欄位。

除了上述的3個欄位,協議還描述了下面幾個欄位:

  • 1)j: 如果transport是polling,但是要求有一個JSONP的響應,那麼j就應該設定為JSONP響應的索引值;
  • 2)b64: 如果客戶端不支援XHR,那麼客戶端應該設定b64=1傳給伺服器,告知伺服器所有的二進位制資料應該以編碼後再發送。

另外engine.io預設的path是 /engine.io,socket.io在初始化的時候設定為了 /socket.io,所以大家看到的path就都是 /socket.io 了:

function Server(srv, opts){

if(!(this instanceof Server)) return new Server(srv, opts);

if('object'== typeof srv && srv instanceof Object && !srv.listen) {

opts = srv;

srv = null;

}

opts = opts || {};

this.nsps = {};

this.parentNsps = new Map();

this.path(opts.path || '/socket.io');

5.4.2)資料包編碼要求:

engine.io協議的資料包編碼有自己的一套格式,在協議介紹上engine.io-protocol,定義了兩種編碼型別: packet和payload。

一個編碼過的packet是下面這種格式:

<packettype id>[<data>]

然後協議定義了下面幾種packet type(採用數字進行標識):

  • 1)0(open): 當開始一個新的transport的時候,服務端會發送該型別的packet;
  • 2)1(close): 請求關閉這個transport但是不要自己關閉關閉連線;
  • 3)2(ping): 由客戶端傳送的ping包,服務端必須迴應一個包含相同資料的pong包;
  • 4)3(pong): 響應ping包,服務端傳送;
  • 5)4(message): 實際訊息,在客戶端和服務端都可以監聽message事件獲取訊息內容;
  • 6)5(upgrade): 在engine.io切換transport之前,它會用來測試服務端和客戶端是否在該transport上通訊。如果測試成功,客戶端會發送一個upgrade包去讓伺服器重新整理它的快取並切換到新的transport;
  • 7)6(noop): 主要用來強制一個輪詢迴圈當收到一個WebSocket連線的時候。

那payload也有對應的格式要求:

  • 1)如果當只有傳送string並且不支援XHR的時候,其編碼格式是::[:[...]];
  • 2)當不支援XHR2並且傳送二進位制資料,但是使用編碼字串的時候,其編碼格式是::b[...];
  • 3)當支援XHR2的時候,所有的資料都被編碼成二進位制,格式是:<0 for string data, 1 for binary data>[...];
  • 4)如果傳送的內容混雜著UTF-8的字元和二進位制資料,字串的每個字元被寫成一個字元編碼,用1個位元組表示。

注意:payload的編碼要求不適用於WebSocket的通訊。

針對上面的編碼要求,我們隨便舉個例子.

之前在第一條polling請求的時候,服務端編碼傳送了這個資料:

97:0{"sid":"Peed250dk55pprwgAAAA","upgrades":["websocket"],"pingInterval":25000,"pingTimeout":60000}2:40

根據上面的知識,我們知道第一次服務端會發送一個open的資料包。

所以組裝出來的packet是:

0

然後服務端會告知客戶端去嘗試升級到websocket,並且告知對應的sid。

於是整合後便是:

0{"sid":"Peed250dk55pprwgAAAA","upgrades":"websocket","pingInterval":25000,"pingTimeout":60000}

接著根據payload的編碼格式,因為是string,且長度是97個位元組。

所以是:

97:0{"sid":"Peed250dk55pprwgAAAA","upgrades":"websocket","pingInterval":25000,"pingTimeout":60000}

接著第二部分資料是message包型別,並且資料是0,所以是40,長度為2位元組,所以是2:40,最後就拼成剛才大家看到的結果。

注意:

ping/pong的間隔時間是服務端告知客戶端的:"pingInterval":25000,"pingTimeout":60000,也就是說心跳時間預設是25秒,並且等待pong響應的時間預設是60s。

5.5 升級協議的必備過程

協議定義了transport升級到websocket需要經歷一個必須的過程。

如下圖:

WebSocket的測試開始於傳送probe,如果伺服器也響應probe的話,客戶端就必須傳送一個upgrade包。

為了確保不會丟包,只有在當前transport的所有buffer被重新整理並且transport被認為paused的時候才可以傳送upgrade包。服務端收到upgrade包的時候,服務端必須假設這是一個新的通道併發送所有已存的快取到這個通道上

在Chrome上的效果如下:

5.6 engine.io的程式碼實現

熟悉了engine.io協議之後,我們看看程式碼是怎麼實現主流程的。

客戶端的engine.io的主要實現流程我們在上面文字介紹了。

結合程式碼engine.io,畫了這麼一個客戶端流程圖:

服務端的程式碼和客戶端非常相似,其實現流程圖如下:

6、SSE

6.1 本節引言

本文前兩節分析了WebSocket和socket.io,現在我們來看看SSE。

很多人也許好奇,有了WebSocket這種實時通訊,為什麼還需要SSE呢?

答案其實很簡單:那就是SSE其實是單向通訊,而WebSocket是雙向通訊。

比如:在股票行情、新聞推送的這種只需要伺服器傳送訊息給客戶端場景中,使用SSE可能更加合適。

另外:SSE是使用HTTP傳輸的,這意味著我們不需要一個特殊的協議或者額外的實現就可以使用。而WebSocket要求全雙工連線和一個新的WebSocket伺服器去處理。加上SSE在設計的時候就有一些WebSocket沒有的特性,比如自動重連線、event IDs、以及傳送隨機事件的能力,所以各有各的特長,我們需要根據實際應用場景,去選擇不同的應用方案。

6.2 SSE介紹

SSE的簡單模型是:一個客戶端去從伺服器端訂閱一條“流”,之後服務端可以傳送訊息給客戶端直到服務端或者客戶端關閉該“流”,所以SSE全稱叫“server-sent-event”。

相比以前的輪詢,SSE可以為B2C帶來更高的效率。

有一張圖片畫出了二者的區別:

6.3 SSE資料幀的格式

SSE必須編碼成utf-8的格式,訊息的每個欄位使用"\n"來做分割,並且需要下面4個規範定義好的欄位。

這4個欄位是:

  • 1)Event: 事件型別;
  • 2)Data: 傳送的資料;
  • 3)ID: 每一條事件流的ID;
  • 4)Retry: 告知瀏覽器在所有的連線丟失之後重新開啟新的連線等待的時間,在自動重新連線的過程中,之前收到的最後一個事件流ID會被髮送到服務端。

下圖是通過wireshark抓包得到的資料包的原始格式:

6.4 SSE通訊過程

SSE的通訊過程比較簡單,底層的一些實現都被瀏覽器給封裝好了,包括資料的處理。

大致流程如下:

在瀏覽器中截圖如下:

攜帶的資料是JSON格式的,瀏覽器都幫你整合成為一個Object:

在wireshark中,其通訊流程如下。

傳送請求:

得到響應:

在開始推送資訊流之前,伺服器還會發送一個客戶端會忽略掉的包,這個具體原因不清楚:

斷開連線後的重傳:

6.5 SSE的簡單使用示例

瀏覽器端的使用:

const es = new EventSource('/sse')

服務端的使用:

const sseStream = new SseStream(req)

sseStream.pipe(res)

sseStream.write({

id: sendCount,

event: 'server-time',

retry: 20000, // 告訴客戶端,如果斷開連線後,20秒後再重試連線

data: {ts: newDate().toTimeString(), count: sendCount++}

})

更多API使用和demo介紹分別參考:SSE API、demo程式碼。

6.6 相容性及缺點

相容性:

▲ 上圖來自 網路

缺點:

 

  • 1)因為是伺服器 -> 客戶端的,所以它不能處理客戶端請求流;
  • 2)因為是明確指定用於傳輸UTF-8資料的,所以對於傳輸二進位制流是低效率的,即使你轉為的話,反而增加頻寬的負載,得不償失。