從HTML5 WebSocket到Socket.io

Srtian發表於2018-05-11

HTML5 WebSocket概述

作為新一代的web標準,HTML5為我們提供了很多有用的東西,比如canvas,本地儲存(已經分離出去了),多媒體程式設計介面,當然還有我們的WebSocket。WebSocket是HTML5開始提供的一種瀏覽器與伺服器間進行全雙工通訊(full-duplex)的網路技術,可以傳輸基於資訊的文字和二進位制的資料。它於2011年被IETF定為標準 RFC 6455,同時WebSocket API也被W3C定為標準。

一、WebSocket產生的背景

1.黎明前的黑暗——實時web應用的需求

web應用的資訊互動過程我想大家或多或少都知道一些,通常是客戶端通過瀏覽器發出一個請求,然後伺服器端在接受和稽核請求後,進行處理並將結果返回給客戶端,最後由客戶端的瀏覽器將資訊呈現出來。這種通訊機制在資訊互動不是特別頻繁的情況下並沒有太大的問題,但對於那些實時性要求高、海量資料併發的應用來說,就顯得捉襟見肘了,比如現在常見的網頁遊戲,證券網站,RSS訂閱推送,網頁實時對話,叫車軟體等。通常當客戶端準備呈現一些資訊時,這些資訊在伺服器端很有可能就已經過時了。為了滿足以上那些場景,大佬們研究出來了一些折衷方案,其中最常用的就是普通輪詢和Comet技術,而Comet技術實際上就是輪詢的改進,細分起來Comet有兩種實現方式:

  • 長輪詢機制
  • 流技術機制

1.1 長輪詢機制

長輪序是對普通輪詢的改進和提高。普通輪詢簡單來說,就是客戶端每隔一定的時間就向伺服器端傳送請求,從而以頻繁請求的方式來保持客戶端和伺服器端的同步。這種同步方案的最大問題是,客戶端已固定的頻率傳送請求時,很可能服務端的資料沒有更新,產生很多無用的網路傳輸,非常低效。

為了減少無效的網路傳輸,長輪詢對普通輪詢進行了改進和提高,當伺服器端沒有資料更新時,連結會保持一段時間的週期,直到資料或狀態發生改變或連線時間過期,通過這種機制我們就可以減少很多無效的客戶端和伺服器間的互動。當然,如果伺服器端的資料變更非常頻繁的話,這種機制並沒有有效的提高效能,和普通輪詢沒有太大的區別,且長輪詢也會耗費更多的資源,比如CPU,記憶體,頻寬等。

1.2 流技術機制

流技術機制簡單來說就是客戶端的頁面使用一個隱藏的視窗向服務端發出一個長連線的請求。伺服器接到請求後作出回應,並不斷更新狀態,以保證客戶端和伺服器端的連線不過期。通過這種機制就可以將伺服器端的資訊不斷傳向客戶端,從而保證資訊的時效性。但這種機制對於使用者體驗並不友好,需要針對不同的瀏覽器升級不同的方案來改進使用者體驗,同時這種機制如果在併發情況下發生時,會對伺服器的資源造成很大壓力。

2.黎明的到來——WebSocket

正是出於以上幾種解決方案都有著各自的侷限性,HTML5 WebSocket也就應運而生了,瀏覽器可以通過JavaScript藉助現有的HTTP協議來向伺服器發出WebSocket連線的請求,當連線建立後,客戶端和伺服器端就可以直接通過TCP連線來直接進行資料交換。這是由於websocket協議本質上就是一個TCP連線,所以在資料傳輸的穩定性和傳輸量上有所保證,且相對於以往的輪詢和Comet技術在效能方面也有了長足的進步:

image

有一點需要注意的是雖然websocket在通訊時需要藉助HTTP,但它本質上和HTTP有著很大的區別:

  • WebSocket是一種雙向通訊協議,在建立連線之後,WebSocket服務端和客戶端都能主動向對方傳送或者接受資料。
  • WebSocket需要先連線,只有再連線後才能進行相互通訊。

他們的關係其實就和這張圖表現的一樣,雖然有相交的部分,但依然有著很大的區別:

image

二、WebSocket API的用法

由於每個伺服器端的語言都有著自己的API,因此首先我們來討論客戶端的API:

// 建立一個socket例項:
const socket = new WebSocket(ws://localhost:9093')
// 開啟socket
socket.onopen = (event) => {
    // 傳送一個初始化訊息
  	socket.send('Hello Server!')
  	 // 伺服器有響應資料觸發
    socket.onmessage = (event) => { 
        console.log('Client received a message',event)
    }
    // 出錯時觸發,並且會關閉連線。這時可以根據錯誤資訊進行按需處理
    socket.onerror = (event) => {
  	    console.log('error')
    }
    // 監聽Socket的關閉
    socket.onclose = (event) => { 
        console.log('Client notified socket has closed',event)
    }
    // 關閉Socket
    socket.close(1000, 'closing normally') 
 }
複製程式碼

是不是感覺HTML5 websocket所提供的API賊雞兒簡單,沒錯,就是這麼簡單。但有幾點我們需要注意:

  • 在建立socket例項的時候,new WebSocket()接受兩個引數,第一個引數是ws或wss,第二個引數可以選填自定義協議,如果是多協議,可以是陣列的方式。
  • WebSocket中的send方法不是任何資料都能傳送的,現在只能傳送三類資料,包括UTF-8的string型別(會預設轉化為USVString),ArrayBuffer和Blob,且只有在建立連線後才能使用。(感謝大佬指出錯誤,已修改)
  • 在使用socket.close(code,[reason])關閉連線時,code和reason都是選填的。code是一個數字值表示關閉連線的狀態號,表示連線被關閉的原因。如果這個引數沒有被指定,預設的取值是1000 (表示正常連線關閉),而reason是一個可讀的字串,表示連線被關閉的原因。這個字串必須是不長於123位元組的UTF-8 文字。

1.ws和wss

我們在上面提到過,建立一個socket例項時可以選填ws和wss來進行通訊協議的確定。他們兩個其實很像HTTP和HTTPS之間的關係。其中ws表示純文字通訊,而wss表示使用加密通道通訊(TCP+TLS)。那為啥不直接使用HTTP而要自定義通訊協議呢?這就要從WebSocket的目的說起來,WebSocket的主要功能就是為了給瀏覽器中的應用與伺服器端提供優化的,雙向的通訊機制,但這不代表WebScoket只能侷限於此,它當然還能夠用於其他的場景,這就需要他可以通過非HTTP協議來進行資料交換,因此WebSocket也就採用了自定義URI模式,以確保就算沒有HTTP,也能進行資料交換。

ws和wss:

  • ws協議:普通請求,佔用與HTTP相同的80埠
  • wss協議:基於SSL的安全傳輸,佔用與TLS相同的443埠。

注:有些HTTP中間裝置有時候可能會不理解WebSocket,而導致各種諸如:盲目連線升級,亂修改內容等問題。而WSS就很好的解決了這個問題,它建立了一臺哦端到端的安全通道,這個通道對中間裝置模糊了資料,因此中間裝置就不能感知到資料,也就無法對請求做一些特殊處理了。

三、WebSocket協議的規範

以下是一個典型的WebSocket發起請求到響應請求的示例:

客戶端到服務端:
GET / HTTP/1.1
Connection:Upgrade
Host:127.0.0.1:8088
Origin:null
Sec-WebSocket-Extensions:x-webkit-deflate-frame
Sec-WebSocket-Key:puVOuWb7rel6z2AVZBKnfw==
Sec-WebSocket-Version:13
Upgrade:websocket

服務端到客戶端:
HTTP/1.1 101 Switching Protocols
Connection:Upgrade
Server:beetle websocket server
Upgrade:WebSocket
date: Thu, 10 May 2018 07:32:25 GMT
Access-Control-Allow-Credentials:true
Access-Control-Allow-Headers:content-type
Sec-WebSocket-Accept:FCKgUr8c7OsDsLFeJTWrJw6WO8Q=
複製程式碼

我們可以看到,WebSocket協議和HTTP協議乍看並沒有太大的區別,但細看下來,區別還是有些的,這其實是一個握手的http請求,首先請求和響應的,”Upgrade:WebSocket”表示請求的目的就是要將客戶端和伺服器端的通訊協議從 HTTP 協議升級到 WebSocket協議。從客戶端到伺服器端請求的資訊裡包含有”Sec-WebSocket-Extensions”、“Sec-WebSocket-Key”這樣的頭資訊。這是客戶端瀏覽器需要向伺服器端提供的握手資訊,伺服器端解析這些頭資訊,並在握手的過程中依據這些資訊生成一個28位的安全金鑰並返回給客戶端,以表明伺服器端獲取了客戶端的請求,同意建立 WebSocket 連線。

當握手成功後,這個時候TCP連線就已經建立了,客戶端與服務端就能夠直接通過WebSocket直接進行資料傳遞。不過服務端還需要判斷一次資料請求是什麼時候開始的和什麼時候是請求的結束的。在WebSocket中,由於瀏覽端和服務端已經打好招呼,如我傳送的內容為utf-8 編碼,如果我傳送0x00,表示包的開始,如果傳送了0xFF,就表示包的結束了。這就解決了黏包的問題。

四、相容性情況

瀏覽器	                 支援情況
Chrome	            Supported in version 4+
Firefox	            Supported in version 4+
Internet Explorer	Supported in version 10+
Opera	            Supported in version 10+
Safari	            Supported in version 5+
複製程式碼

五、Socket.IO

簡單來說Socket.IO就是對WebSocket的封裝,並且實現了WebSocket的服務端程式碼。Socket.IO將WebSocket和輪詢(Polling)機制以及其它的實時通訊方式封裝成了通用的介面,並且在服務端實現了這些實時機制的相應程式碼。也就是說,WebSocket僅僅是Socket.IO實現實時通訊的一個子集。Socket.IO簡化了WebSocket API,統一了返回傳輸的API。傳輸種類包括:

  • WebSocket
  • Flash Socket
  • AJAX long-polling
  • AJAX multipart streaming
  • IFrame
  • JSONP polling。

我們來看一下服務端的Socket.IO基本API:

// 引入socke.io
const io = require('socket.io')(80)
// 監聽客戶端連線,回撥函式會傳遞本次連線的socket
io.on('connection',function(socket))
// 給所有客戶端廣播訊息
io.sockets.emit('String',data)
// 給指定的客戶端傳送訊息
io.sockets.socket(socketid).emit('String', data)
// 監聽客戶端傳送的資訊
socket.on('String',function(data))
// 給該socket的客戶端傳送訊息
socket.emit('String', data)
複製程式碼

另外,Socket.IO還提供了一個Node.JS API,它看起來很像客戶端API。所以我們來看看它的實際應用吧:

// socket-server.js

// 需要使用HTTP模組來啟動伺服器和Socket.IO
const http= require('http'), 
const io= require('socket.io')

const server= http.createServer(function(req, res){ 
    // 傳送HTML的headers和message
    res.writeHead(200,{ 'Content-Type': 'text/html' })
    res.end('<p>Hello Socket.IO!<p>')
}); 
// 在8080埠啟動伺服器
server.listen(8080)

// 建立一個Socket.IO例項,並把它傳遞給伺服器
const socket= io.listen(server)

// 新增一個連線監聽器
socket.on('connection', function(client) { 

// 連線成功,開始監聽
client.on('message',function(event){ 
    console.log('Received message from client!',event)
})
// 連線失敗
client.on('disconnect',function(){ 
    clearInterval(interval)
    console.log('Server has disconnected')
  })
})
複製程式碼

然後我們就可以啟動這個檔案了:

node socket-server.js
複製程式碼

然後我們就可以建立一個每秒鐘傳送訊息到客戶端的傳送器了;

var interval= setInterval(function() { 
  client.send('This is a message from the server,hello world' + new Date().getTime()); 
},1000);
複製程式碼

注:需要注意的是,如果我們想在前端使用socket.IO,我們需要下載這個:

npm install socket.io-client --save
複製程式碼

然後再連線網路:

import io from 'socket.io-client'
const socket = io('ws://localhost:8080')
複製程式碼

圖片拍攝於:廣州中山紀念堂

相關文章