websocket初識

爲什麼需要 WebSocket?

我們已經有HTTP協議了,爲什麼還需要另一個協議?它能給我們帶來什麼好處呢?

原因是:HTTP協議有一個缺陷:通信只能由客戶端發起,做不到服務器主動向客戶端推送信息。這種單向請求的特點意味着如果服務器有連續的狀態變化,客戶端獲取就會變的非常麻煩,只能使用輪詢的方法獲取,其中最典型的就是聊天場景了。而websocket是服務器可以主動向客戶端推送信息,客戶端也可以主動向服務器發送信息,是真正的雙向平等對話。

短輪詢(Polling)

短輪詢的實現思路是瀏覽器端每隔幾秒鐘向服務器端發送HTTP請求,服務端在收到請求後,不論是否有數據更新,都直接進行響應。服務端響應完成,關閉這個TCP連接,代碼實現也簡單,如下所示:

setInterval(function() {
  // ajax請求
}, 3000);

優點:實現簡單。
缺點:會導致數據在一小段時間內不同步和大量無效的請求,安全性差、浪費資源。

長輪詢(Long-Polling)

長輪詢的實現思路是客戶端發送請求後服務器端不會立即返回數據,服務器端會阻塞請求連接不會立即斷開,直到服務器端有數據更新或者是連接超時才返回,客戶端纔再次發出請求新建連接、如此反覆獲取最新數據。如下圖所示:

在這裏插入圖片描述

  • 優點:在短輪詢基礎上做了優化,有較好的時效性。
  • 缺點:保持連接掛起會消耗資源,服務器沒有返回有效數據,程序超時。

短輪詢和長輪詢, 都是先由客戶端發起Ajax請求,才能進行通信,走的是HTTP協議,服務器端無法主動向客戶端推送信息。

websocket是什麼

websocket是一種網絡通信協議,我們知道http協議只能從客戶端主動發起,不能從服務端推送數據到客戶端,websocket是一種不僅能從客戶端發送數據到服務端,也可主動從服務的推送數據給客戶端的一種協議。我們先來看一張經典圖:
在這裏插入圖片描述

從上述圖中我們可知:

  1. http請求是客戶端發起請求,服務端響應,然後斷開連接,客戶端發起,服務端響應的一種循環。
  2. websocket協議是客戶端發起連接後,就會一直保持連接,期間客戶端和服務端都可以向對方發送數據,直到連接關閉。
websocket特點
  1. 建立在TCP協議之上,服務器端的實現比較容易
  2. 與HTTP協議有着良好的兼容性。默認端口也是80和443,並且握手階段採用HTTP協議,因此握手時不容易屏蔽,能通過各種HTTP代理服務器
  3. 數據格式比較輕量,性能開銷小,通信高效
  4. 可以發送文本,也可以發送二進制數據
  5. 沒有同源限制,客戶端可以與任意服務器通信
  6. 協議標識符是ws(如果加密,是wss),服務器網址就是URL

在這裏插入圖片描述

websocket通信原理

當客戶端和服務端建立WebSocket連接時,在客戶端和服務器的握手過程中,客戶端首先會向服務端發送一個 HTTP 請求,包含一個Upgrade請求頭來告知服務端客戶端想要建立一個WebSocket連接

// 請求頭
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Cache-Control: no-cache
Connection: Upgrade	// 表示該連接要升級協議
Cookie: _hjMinimizedPolls=358479; ts_uid=7852621249; CNZZDATA1259303436=1218855313-1548914234-%7C1564625892; csrfToken=DPb4RhmGQfPCZnYzUCCOOade; JSESSIONID=67376239124B4355F75F1FC87C059F8D; _hjid=3f7157b6-1aa0-4d5c-ab9a-45eab1e6941e; acw_tc=76b20ff415689655672128006e178b964c640d5a7952f7cb3c18ddf0064264
Host: localhost:9000
Origin: http://localhost:9000
Pragma: no-cache
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
Sec-WebSocket-Key: 5fTJ1LTuh3RKjSJxydyifQ==		// 與響應頭 Sec-WebSocket-Accept 相對應
Sec-WebSocket-Version: 13	// 表示 websocket 協議的版本
Upgrade: websocket	// 表示要升級到 websocket 協議
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.132 Safari/537.36

// 響應頭

Connection: Upgrade
Sec-WebSocket-Accept: ZUip34t+bCjhkvxxwhmdEOyx9hE=
Upgrade: websocket

// General中
Request URL: ws://localhost:9000
Request Method: GET
Status Code: 101 Switching Protocols

//  status code是101 Switching Protocols ,表示該連接已經從HTTP協議轉換爲 WebSocket通信協議。 轉換成功之後,該連接並沒有中斷,而是建立了一個全雙工通信,後續發送和接收消息都會走這個連接通道。

:請求頭中Sec-WebSocket-Key字段,和響應頭中的Sec-WebSocket-Accept是配套對應的,作用是提供基本的防護,比如惡意的連接或無效的連接。Sec-WebSocket-Key是客戶端隨機生成的一個base64編碼,服務器會使用這個編碼,並根據一個固定的算法。

GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";    //  一個固定的字符串
accept = base64(sha1(key + GUID));	// key是Sec-WebSocket-Key,accept是 Sec-WebSocket-Accept

// 其中GUID字符串是RFC6455 官方定義的一個固定字符串,不得修改。

客戶端拿到服務端響應的Sec-WebSocket-Accept後,會拿自己之前生成的Sec-WebSocket-Key用相同算法算一次,如果匹配,則握手成功。然後判斷 HTTP Response 狀態碼是否爲 101(切換協議),如果是,則建立連接。

應用場景

我們通過一個例子來想下,比如我們平常生活中買東西,在支付時,支付成功後,需要給用戶反饋一個支付成功的提示,那麼在websocket應用之前,我們一般使用輪詢的方法處理,即客戶端定時向服務端發送請求,看有沒有收到支付金額,沒有就一直髮送,收到了再停止。

在發送請求的過程中,浪費了大量的資源,而且響應也不是及時的,比如我是每隔1秒請求一次,但是並不能立刻得到支付成功的狀態。這時如果使用websocket的方式,就會及時響應支付狀態,websocket一般用在一些能及時響應的場景中。主要應用在如下幾種場景中:

  1. 社交訂閱

有時我們需要及時收到訂閱消息,比如開獎通知、在線邀請,支付結果等

  1. 多玩家遊戲

很多遊戲都是協同作戰的,玩家的操作和狀態需要及時同步給所有玩家

  1. 協同編輯文檔

同一份文檔,編輯狀態需要同步到所有參與的用戶界面上

  1. 數據流狀態

比如上傳下載文件,文件進度,文件是否上傳成功等

  1. 多人聊天

很多場景下都需要多人蔘與討論聊天,用戶發送的消息得第一時間同步到所有用戶

  1. 股票虛擬貨幣價格

股票和虛擬貨幣的價格都是實時波動的,價格跟用戶的操作息息相關,及時推送對用戶跟盤有很大的幫助

websocket客戶端的API

  1. WebSocket 構造函數
// WebSocket對象作爲一個構造函數,用於新建WebSocket實例。
var ws = new WebSocket('ws://localhost:8080');
// 執行完上面語句之後,客戶端就會與服務器進行連接
  1. webSocket.readyState

readyState屬性返回實例對象的當前狀態,共有四種:

  • CONNECTING:值爲0,表示正在連接。
  • OPEN:值爲1,表示連接成功,可以通信了。
  • CLOSING:值爲2,表示連接正在關閉。
  • CLOSED:值爲3,表示連接已經關閉,或者打開連接失敗。
switch (ws.readyState) {
  case WebSocket.CONNECTING:
    // TODO
    break;
  case WebSocket.OPEN:
   // TODO
    break;
  case WebSocket.CLOSING:
    // TODO
    break;
  case WebSocket.CLOSED:
   // TODO
    break;
  default:
    // this never happens
    break;
}
  1. webSocket.onopen

實例對象的onopen屬性,用於指定連接成功後的回調函數

ws.onopen = function () {
  ws.send('Hello Server!');
}

如果要指定多個回調函數,可以使用addEventListener方法

ws.addEventListener('open', function (event) {
  ws.send('Hello Server!');
});
  1. webSocket.onclose

實例對象的onclose屬性,用於指定連接關閉後的回調函數。

ws.onclose = function(event) {
  var code = event.code;
  var reason = event.reason;
  var wasClean = event.wasClean;
  // TODO
};

// 如果要指定多個回調函數,同上面的寫法
  1. webSocket.onmessage

實例對象的onmessage屬性,用於指定收到服務器數據後的回調函數。

ws.onmessage = function(event) {
  var data = event.data;
  // TODO
};
注:服務器數據可能是文本,也可能是二進制數據(blob對象或Arraybuffer對象)
ws.onmessage = function(event){
  if(typeof event.data === String) {
    console.log("Received data string");
  }
  
  if(event.data instanceof ArrayBuffer){
    var buffer = event.data;
    console.log("Received arraybuffer");
  }
}

除了動態判斷收到的數據類型,也可使用binaryType屬性,指定收到的二進制數據類型。

// 收到blob數據
ws.binaryType = "blob";
ws.onmessage = function(e) {
  console.log(e.data.size);
};

// 收到ArrayBuffer數據
ws.binaryType = "arraybuffer";
ws.onmessage = function(e) {
  console.log(e.data.byteLength);
};
  1. webSocket.send()

實例對象的send()方法用於向服務器發送數據。

// 發送文本
ws.send('hello javascript');

// 發送Blob對象
var file = document.querySelector('input[type="file"]').files[0];
ws.send(file);

// 發送ArrayBuffer對象
var img = canvas_context.getImageData(0, 0, 400, 320);
var binary = new Uint8Array(img.data.length);
for (var i = 0; i < img.data.length; i++) {
  binary[i] = img.data[i];
}
ws.send(binary.buffer);
  1. webSocket.bufferedAmount

實例對象的bufferedAmount屬性,表示還有多少字節的二進制數據沒有發送出去。它可以用來判斷髮送是否結束

var data = new ArrayBuffer(10000000);
socket.send(data);
if (socket.bufferedAmount === 0) {
  // 發送完畢
} else {
  // 發送沒結束
}
  1. webSocket.onerror

實例對象的onerror屬性,用於指定報錯時的回調函數。

socket.onerror = function(event) {
  // TODO
};

實現一個簡單聊天

實現一個一對一的單聊天功能:

客戶端

function connectWebsocket() {
    ws = new WebSocket('ws://localhost:9000');
    // 監聽連接成功
    ws.onopen = () => {
        ws.send(JSON.stringify(msgData));	// 給服務端發送消息
    };

    // 監聽服務端消息(接收消息)
    ws.onmessage = (msg) => {
        let message = JSON.parse(msg.data);
        console.log('收到的消息:', message);
    };

    // 監聽連接失敗
    ws.onerror = () => {
    	// 連接失敗,正在重連...
        connectWebsocket();
    };

    // 監聽連接關閉
    ws.onclose = () => {
    	console.log('連接關閉');
    };
};
connectWebsocket();

服務端

const path = require('path');
const express = require('express');
const app = express();
const server = require('http').Server(app);
const WebSocket = require('ws');
const wss = new WebSocket.Server({ server: server });

wss.on('connection', (ws) => { 
  // 監聽客戶端發來的消息
  ws.on('message', (message) => {
    console.log(wss.clients.size);
    let msgData = JSON.parse(message);   
    if (msgData.type === 'open') {
      // 初始連接時標識會話
      ws.sessionId = `${msgData.fromUserId}-${msgData.toUserId}`;
    } else {
      let sessionId = `${msgData.toUserId}-${msgData.fromUserId}`;
      wss.clients.forEach(client => {
        if (client.sessionId === sessionId) {
          client.send(message);	 // 給對應的客戶端連接發送消息
        }
      })  
    }
  })

  // 連接關閉
  ws.on('close', () => {
    console.log('連接關閉');  
  });

 server.listen(9000, function () {
   console.log('http://localhost:9000');
 });
});

最終效果圖如下:

在這裏插入圖片描述

總結:

  1. websocket是一種類似http的一種通訊協議
  2. websocket最大特點是客戶端和服務端能相互給對方發送消息
  3. websocket廣泛應用在需要實時通訊的一些場景上面
  4. websocket沒有同源限制,而且性能開銷小,通信高效
  5. websocket幾乎所有瀏覽器都支持
  6. websocket通過send()方法發送消息,onmessage事件接收消息,然後對消息進行處理顯示在頁面上。 onerror 事件(監聽連接失敗)觸發時,最好進行執行重連,以保持連接不中斷。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章