理解websocket的原理

首先我們看一下websocket的出現背景,我們知道http系列協議是建立在tcp上的,理論上,他是可以可以雙向通信的。但是http1.1之前,服務器沒有實現推送的功能。每次都是客戶端請求,服務器響應。下面看一下http協議關於請求處理的發展。

  1. http1.0的時候,一個http請求的生命週期是客戶端發起請求,服務器響應,斷開連接。但是我們知道tcp協議的缺點就是,三次握手需要時間,再加上慢開始等特性,每一個http請求都這樣的話,效率就很低。
  2. http1.1的時候,默認開啓了長連接(客戶端請求中設置了keep-alive頭),服務器處理一個請求後,不會立刻關閉連接,而是會等待一定的時間。如果沒有請求才關閉連接。這樣瀏覽器不僅可以在一個tcp連接中,不斷地發送請求(服務器也會限制一個連接上可以處理的請求閾值),甚至可以一次發很多個請求。這就是http1.1的管道化(pipeline)技術。但是他也有個問題,因爲對於基於http協議的客戶端來說,雖然他可以發很多請求出去,但是當一個請求對於的回包回來時,他卻無法分辨是屬於哪個請求的。所以回包只能按請求順序返回,這就引來了另一個問題-線頭阻塞(Head-of-Link Blocking)。並且http1.1雖然支持長連接,但是他不支持服務端推送(push)的能力。如果服務器有數據要給客戶端,也只能等着客戶端來取(pull)。
  3. 來到了http2.0,不僅實現了服務器推送,還使用了幀(iframe),流(stream)等技術解決了線頭阻塞的問題,http2.0在一個tcp連接中,可以同時發送多個http請求,每個請求是一個流,一個流可以分成很多幀,有了標記號,服務器可以隨便發送回包,客戶端收到後,根據標記,重新組裝就可以。

以上是http協議的關於請求的一些發展,而websocket就服務端推送提供了另外一種解決方案。他本質上是在tcp協議上封裝的另一種應用層協議(websocket協議)。因爲他是基於tcp的,所以服務端推送自然不是什麼難題。但是在實現上,他並不是直接連接一個tcp連接,然後在上面傳輸基於websocket協議的數據包。他涉及到一個協議升級(交換)的過程。我們看看這個過程。

1 客戶端發送協議升級的請求。在http請求加上下面的http頭

Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
Sec-WebSocket-Key: k1kbxGRqGIBD5Y/LdIFwGQ==
Sec-WebSocket-Version: 13
Upgrade: websocket

2 服務器如果支持websocket協議的話,會返回101狀態碼錶示同意協議升級,並且支持各種配置(如果服務器不支持某些功能或版本,或告訴客戶端,客戶端可以再次發送協議升級的請求)。服務會返回形如下面的http頭(可以參考websocket協議)。

Connection: Upgrade
Sec-WebSocket-Accept: y73KZR4t+hqD6KKYbkx2tULfBsQ=
Upgrade: websocket

3 這樣就完成了協議的升級,後續的數據通信,就是基於tcp連接之上,使用websocket協議封裝的數據包。

下面我們通過wireshark來了解這個過程。首先我們啓動一個服務器(ip:192.168.8.226)。

var http = require('http');
var fs = require('fs');
const WebSocket = require('ws');
// 如果在瀏覽器控制檯進行測試,可以不起http服務器
const server = http.createServer(options,function(req,res){
	res.end(fs.readFileSync(`${__dirname}/websocket.html`));
}).listen(11111);

const wss = new WebSocket.Server({ server }); 
wss.on('connection', function connection(ws) {
  ws.on('message', function(message) {
    ws.send(message);
  });
 
  ws.send('get it');
});

我們可以直接在瀏覽器控制檯進行測試

var ws = new WebSocket("ws://192.168.8.226:11111");
// 連接上後執行
ws.send(11)

這時候,我們看看wireshark的包。

首先看前面三條記錄,這是tcp三次握手的數據包。這個我們都瞭解了,就不展示。接着看第四條記錄。展開後如下。

我們看到建立tcp連接後,瀏覽器發了一個http請求,並帶上了幾個websocket的數據包。接着看下面的一條。

服務返回了同意升級協議或者說交換協議。從服務器代碼中我們看到,在建立連接的時候我們給瀏覽器推送了一個get it的字符串。繼續看上面的記錄。

這就是服務器給瀏覽器推送的基於websocket協議的數據包。具體每個字段什麼意思,參考websocket協議就可以。繼續往下看一條記錄是針對服務器推送的數據包的一個tcp的ack。最後我們可以順便看看最後三條寫着keep-alive的記錄。這就是之前文章裏講到的tcp層的keep-alive。因爲我們一直沒有數據傳輸,所以tcp層會間歇性地發送探測包。我們可以看看探測包的結構。

有一個字節的探測數據。如果這時候我們發送一個數據包給服務器,又是怎樣的呢。

白色背景的三條數據,分別是瀏覽器發送給服務器的數據,服務器推送回來的數據。tcp的ack。我們發現,服務器給瀏覽器推送的時候,瀏覽器會發送ack,但是瀏覽器給服務器發送的時候,服務器貌似沒有返回ack。下面我們看看爲什麼。首先我們看瀏覽器發出的包。

再看看服務器給瀏覽器推送的數據包。

我們發現服務器(tcp)推送消息的時候把ack也帶上了。而不是發送兩個tcp包。這就是tcp的機制。tcp不會對每個包都發ack,他會累積確認(發ack),以減少網絡的包,但是他也需要保證儘快地回覆ack,否則就會導致客戶端觸發超時重傳。tcp什麼時候發送確認呢?比如需要發送數據的時候,或者超過一定時間沒有收到數據包,或者累積的確認數量達到閾值等。既然研究了tcp,我們不妨多研究點,我們看一下,如果這時候關閉服務器會怎樣。

服務器會發送一個重置包給瀏覽器,告訴他需要斷開連接。繼續,如果是瀏覽器自己調用close去關閉連接會怎樣。

我們看到websocket首先會發送一個FIN包給服務器,然後服務器也會返回一個FIN包,然後纔開始真正的四次揮手過程。並且四次揮手的第一個fin包是服務器發的。

我們再來看看安全版本的websocket。我們啓動一個https服務器。

var https = require('https');
var fs = require('fs');
const WebSocket = require('ws');

var options = {
	key: fs.readFileSync('./server-key.pem'),
	ca: [fs.readFileSync('./ca-cert.pem')],
	cert: fs.readFileSync('./server-cert.pem')
};

const server = https.createServer(options,function(req,res){
	res.end(fs.readFileSync(`${__dirname}/websocket.html`));
}).listen(11111);

const wss = new WebSocket.Server({ server });
 
wss.on('connection', function connection(ws) {
  ws.on('message', function(message) {
    ws.send(message);
  });
});

然後在瀏覽器控制檯執行。

var ws = new WebSocket("wss://192.168.8.226:11111");
ws.sned(11);

然後來看看wireshark。

首先建立tcp連接,然後建立tls連接。後續的數據通信就可以基於加密來進行了。不再重複。後續分析tls協議的時候再分析。

經過一系列的分析,我們對websocket協議應該有了更多的瞭解,最後再說一個關於websocket的點。我們發現如果在websocket連接上,一直不通信的話,websocket連接所維持的時間是依賴tcp實現的。因爲我們發現tcp層會一直髮送探測包。達到閾值之後,連接就會被斷開。所以我們想維持websocket連接的話,需要自己去發送心跳包,比如ping,pong。

總結:本文分析了websocket的基本原理,但不涉及協議的內容,如需瞭解協議的內容,可以參考rfc文檔。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章