最近在構建兩個系統的實時通訊部分,總結一下所學。html
這是一個系列文章,暫時主要構思四個部分nginx
本文主要介紹Websocket是什麼以及其協議內容。git
WebSocket 協議實如今受控環境中運行不受信任代碼的一個客戶端到一個從該代碼已經選擇加入通訊的遠程主機之間的全雙工通訊。該協議包括一個打開階段握手規定以及通訊時基本消息幀的定義。其基於TCP之上。此技術的目標是爲基於瀏覽器的應用程序提供一種機制,這些應用程序須要與服務器進行雙向通訊,而不依賴於打開多個HTTP鏈接(例如,使用XMLHttpRequest或<iframe>和長輪詢
)。github
過去,建立須要在客戶端和服務之間雙向通訊(例如,即時消息和遊戲應用)的web應用,須要經過HTTP來輪詢服務器來獲取更新而後若是是推送消息則發送另外一個請求(如今不少應用也依舊採用這種方式)。這樣作會存在一些問題。web
一個簡單的辦法是使用單個TCP鏈接雙向傳輸。這是爲何提供WebSocket 協議。與WebSocket API結合[WSAPI],它提供了一個HTTP輪詢的替代來進行從web 頁面到遠程服務器的雙向通訊。npm
Websocket協議主要包括兩個部分,一個是握手的規則,另外一個是數據傳輸的方式及載體格式。這裏給個網上找的例子(點這裏),能夠開發者工具看看Network裏面的內容。跨域
一旦客戶端和服務器握手成功後,數據傳輸部分就開始了,這是一個全雙工的通訊。客戶端與服務器之間互相傳輸數據的的基本單位根據規格說明書裏咱們稱爲「Messages」。在實際網絡中,這些Message由一個或多個Frames組成,Websocket的Message裏的frame和計算機網絡裏說的的frame並非對應關係,後面會詳細介紹Frame的結構。瀏覽器
打開階段握手目的是兼容基於HTTP的服務器軟件和中間件,以便單個端口能夠用於與服務器交流的HTTP客戶端和與服務器交流的WebSocket客戶端。因此WebSocket客戶端的握手是一個HTTP Upgrade請求(Http status code 101): 安全
這裏關於字段就講幾個字段以及它們的考量bash
Origin
用來指明請求的來源,Origin
頭部主要用於保護Websocket服務器免受非受權的跨域腳本調用Websocket API的請求。也就是不想沒被受權的跨域訪問與服務器創建鏈接,服務器能夠經過這個字段來判斷來源的域並有選擇的拒絕。
另外一方面,Websocket協議須要保證客戶端發起的Websocket鏈接請求只會被能理解Websocket協議的服務器所識別。
Really, as you are mentioned, if you are aware of websockets (that is what to be checked), you could pretend to be a websocket server by sending correct response. But then, if you will not act correctly (e.g. form frames correctly), it will be considered as a protocol violation. Actually, you can write a websocket server that is incorrect, but there will be not much use in it.
And another purpose is to prevent clients accidentally requesting websockets upgrade not expecting it (say, by adding corresponding headers manually and then expecting smth else). Sec-WebSocket-Key and other related headers are prohibited to be set using setRequestHeader method in browsers.
下面介紹下Frame的結構
以前也說過,客戶端與服務器之間互相傳輸數據的的基本單位根據規格說明書裏咱們稱爲「Messages」。在實際網絡中,這些Message由一個或多個Frames組成。
FIN
,指明Frame是不是一個Message裏最後Frame(以前說過一個Message可能又多個Frame組成)RSV1-3
,必須是0,除非有擴展定義了非零值的意義。Opcode
,這個比較重要,有以下取值是被協議定義的
%x0 denotes a continuation frame
%x1 表示一個text frame
%x2 表示一個binary frame
%x3-7 are reserved for further non-control frames
%x8 表示鏈接關閉
%x9 表示 ping (心跳檢測相關,後面會講)
%xA 表示 pong (心跳檢測相關,後面會講)
%xB-F are reserved for further control frames
Mask
,這個是指明「payload data」是否被計算掩碼。這個和後面的Masking-key
有關Payload len
,數據的長度,不贅述了。Masking-key
,這裏不贅述了,給一個Websocket中掩碼的意義Payload data
,幀真正要發送的數據,能夠是任意長度,但儘管理論上幀的大小沒有限制,但發送的數據不能太大,不然會致使沒法高效利用網絡帶寬,正如上面所說Websocket提供分片。動手算一下 下面是charles裏面截取的一段內容
// 十六進制
81 84 3a a6 ac e4 51 c3 c7 81
// 二進制
10000001 10000100 00111010 10100110 10101101 11100100 01010001 11010011 11010111 10000001
複製代碼
opcode爲0001,0x1表示一個Text frame
payload len爲0000100,0x4表示長度爲4字節
掩碼是 00111010 10100110 10101101 11100100
payload是 01010001 11010011 11010111 10000001
具體的處理能夠參考Node.js ws的源碼 其中的buffer-utils
講完Websocket協議部分,如今說說如何相關的Web API。
// 客戶端
var ws = new WebSocket('wss://example.com/socket'); ➊
ws.onerror = function (error) { ... } ➋
ws.onclose = function () { ... } ➌
ws.onopen = function () { ➍
ws.send("Connection established. Hello server!"); ➎
}
ws.onmessage = function(msg) { ➏
if(msg.data instanceof Blob) { ➐
processBlob(msg.data);
} else {
processText(msg.data);
}
}
複製代碼
在使用websocket的過程當中,有時候會遇到客戶端網絡關閉的狀況,而這時候在服務端並無觸發onclose事件。這樣會:
因此就須要一種機制來檢測客戶端和服務端是否處於正常鏈接的狀態。心跳檢測就是這樣的一種機制,通常來講客戶端每過必定時間
ws模塊如何經過心跳檢測去檢測和關閉壞掉的鏈接
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
function noop() {}
function heartbeat() {
this.isAlive = true;
}
wss.on('connection', function connection(ws) {
ws.isAlive = true;
ws.on('pong', heartbeat);
});
const interval = setInterval(function ping() {
wss.clients.forEach(function each(ws) {
if (ws.isAlive === false) return ws.terminate();
ws.isAlive = false;
ws.ping(noop);
});
}, 30000);
複製代碼
根據規範,當接收到Ping消息後Pong響應消息會自動發送。
下面是個人nginx配置,順帶加了負載均衡。測了可用,就是證書因爲是自簽名的因此有點問題。
大致上Websocket的身份認證都是發生在握手階段,經過請求中的內容來認證。一個常見的例子是在url中附帶參數。
new WebSocket("ws://localhost:3000?token=xxxxxxxxxxxxxxxxxxxx");
複製代碼
淘寶的直播彈幕也是用這種方式作的身份認證
以npm的ws模塊實現爲例,其建立Websocket服務器時提供了verifyClient方法。
const wss = new WebSocket.Server({
host: SystemConfig.WEBSOCKET_server_host,
port: SystemConfig.WEBSOCKET_server_port,
// 驗證token識別身份
verifyClient: (info) => {
const token = url.parse(info.req.url, true).query.token
let user
console.log('[verifyClient] start validate')
// 若是token過時會爆TokenExpiredError
if (token) {
try {
user = jwt.verify(token, publicKey)
console.log(`[verifyClient] user ${user.name} logined`)
} catch (e) {
console.log('[verifyClient] token expired')
return false
}
}
// verify token and parse user object
if (user) {
info.req.user = user
return true
} else {
info.req.user = {
name: `遊客${parseInt(Math.random() * 1000000)}`,
mail: ''
}
return true
}
}
})
複製代碼
相關的ws源碼位於ws/websocket-server
// ...
if (this.options.verifyClient) {
const info = {
origin: req.headers[`${version === 8 ? 'sec-websocket-origin' : 'origin'}`],
secure: !!(req.connection.authorized || req.connection.encrypted),
req
};
if (this.options.verifyClient.length === 2) {
this.options.verifyClient(info, (verified, code, message) => {
if (!verified) return abortHandshake(socket, code || 401, message);
this.completeUpgrade(extensions, req, socket, head, cb);
});
return;
}
if (!this.options.verifyClient(info)) return abortHandshake(socket, 401);
}
this.completeUpgrade(extensions, req, socket, head, cb);
}
複製代碼
參考資料:
《rfc6455》 The WebSocket Protocol
《High Performance Browser Networking》- 【加】Ilya Grigorik