1. 程式人生 > 其它 >有了 HTTP 協議,為什麼還需要 Websocket?

有了 HTTP 協議,為什麼還需要 Websocket?

WebSocket 是一種基於 TCP 連線上進行全雙工通訊的協議,相對於 HTTP 這種非持久的協議來說,WebSocket 是一個持久化網路通訊的協議

它不僅可以實現客戶端請求伺服器,同時可以允許服務端主動向客戶端推送資料。在 WebSocket API 中,客戶端和伺服器只需要完成一次握手,兩者之間就直接可以建立永續性的連線,並進行雙向資料傳輸。

為什麼需要 WebSocket

在 Web 應用架構中,連線由 HTTP/1.0 和 HTTP/1.1 處理。HTTP 是客戶端/伺服器模式中請求一響應所用的協議,在這種模式中,客戶端(一般是瀏覽器)向伺服器提交 HTTP 請求,伺服器響應請求的資源(例如 HTML 頁面)。

HTTP 是無狀態的,也就是說,它將每個請求當成唯一和獨立的。無狀態協議具有一些優勢,例如,伺服器不需要儲存有關會話的資訊,從而不需要儲存資料。但是,這也意味著在每次 HTTP 請求和響應中都會發送關於請求的冗餘資訊,比如使用 Cookie 進行使用者狀態的驗證。

隨著客戶端和伺服器之間互動的增加,HTTP 協議在客戶端和伺服器之間通訊所需要的資訊量快速增加。

從根本上講,HTTP 還是半雙工的協議,也就是說,在同一時刻資訊的流向只能單向的:客戶端向伺服器傳送請求(單向),然後伺服器響應請求(單向)。半雙工方式的通訊效率是非常低的。

同時HTTP 協議有一個缺陷:通訊只能由客戶端發起。

這種單向請求的特點,註定瞭如果伺服器有狀態變化,是無法主動通知客戶端的。

為了能夠及時的獲取伺服器的變化,我們嘗試過各種各樣的方式:

輪詢(polling):每隔一段時間,就發出一個請求,瞭解伺服器有沒有新的資訊。不精準,有延時,大量無效資料交換。

長輪詢( long polling):客戶端向伺服器請求資訊,並在設定的時間段內保持連線。直到伺服器有新訊息響應,或者連線超時,這種技術常常稱作“掛起GET”或“擱置POST”。佔用伺服器資源,相對輪詢並沒有優勢,沒有標準化。

流化技術:在流化技術中,客戶端傳送一個請求,伺服器傳送並維護一個持續更新和保持開啟(可以是無限或者規定的時間段)的開放響應。每當伺服器有需要交付給客戶端的資訊時,它就更新響應。伺服器從不發出完成 HTTP 響應。代理和防火牆可能快取響應,導致資訊交付的延遲增加。

上述方法提供了近乎實時的通訊,但是它們也涉及 HTTP 請求和響應首標,包含了許多附加和不必要的首標資料與延遲。此外,在每一種情況下,客戶端都必須等待請求返回,才能發出後續的請求,而這顯著地增加了延退。同時也極大地增加了伺服器的壓力。

什麼是 WebSocket

而 Websocket 是一種自然的全雙工、雙向、單套接字連線,解決了 HTTP 協議中不適合於實時通訊的問題。2008 年被提出,2011 年成為國際標準。

Websocket 協議能夠通過 Web 進行客戶端和伺服器之間的全雙工通訊,並支援二進位制資料和文字字串的傳輸。


這個協議由開始的握手和之後的基本訊息框架組成,是建立在 TCP 協議上的。相比於 HTTP 協議,Websocket 連結一旦建立,即可進行雙向的實時通訊。

其特點包括:

(1)建立在 TCP 協議之上,伺服器端的實現比較容易。

(2)與 HTTP 協議有著良好的相容性。預設埠也是 80 和 443,並且握手階段採用 HTTP 協議,因此握手時不容易遮蔽,能通過各種 HTTP 代理伺服器。

(3)資料格式比較輕量,效能開銷小,通訊高效。

(4)可以傳送文字,也可以傳送二進位制資料。

(5)沒有同源限制,客戶端可以與任意伺服器通訊。

相似技術

Server-sent Events(SSE):

https://www.ruanyifeng.com/blog/2017/05/server-sent_events.html

https://www.cnblogs.com/goloving/p/9196066.html

SPDY (讀作“SPeeDY”):已不再維護,由 HTTP/2 取代

https://baike.baidu.com/item/SPDY/3399551#7

WebRTC

https://baike.baidu.com/item/WebRTC/5522744

通訊原理

WebSocket 連結是如何建立的?

前面說過,WebSocket 在握手階段採用的是 HTTP 協議,Websocket 借用了 HTTP 的一部分協議來完成一次握手。(HTTP的三次握手,此處只完成一次)

HTTP 請求與響應首部

WebSocket 請求與響應首部

連結通訊模擬

HTTP 輪詢

首先是ajax 輪詢,其原理非常簡單,讓瀏覽器隔個幾秒就傳送一次請求,詢問伺服器是否有新資訊。

場景再現:

客戶端:啦啦啦,有沒有新資訊(Request)
服務端:沒有(Request)

客戶端:啦啦啦,有沒有新資訊(Request)
服務端:沒有。。(Response)
客戶端:啦啦啦,有沒有新資訊(Request)

服務端:你好煩啊,沒有啊。。(Response)
客戶端:啦啦啦,有沒有新訊息(Request)
服務端:好啦好啦,有啦給你 ' 西嶺真帥' 。(Response)
客戶端:啦啦啦,有沒有新訊息(Request)
服務端:。。。沒。。。。沒。。沒有

從上面可以看出,輪詢其實就是在不斷地建立HTTP連線,然後等待服務端處理,可以體現 HTTP 協議的另外一個特點,被動性。同時,http 的每一次請求與響應結束後,伺服器將客戶端資訊全部丟棄,下次請求,必須攜帶身份資訊(cookie),無狀態性。

WebSocket

客戶端通過http(騎馬)帶著信請求伺服器,但同時,攜帶了Upgrade:websocket和Connection:Upgrade(兩根管子),伺服器如果支援WebSocket協議(有兩根管子的介面),使用Websocket協議返回可用資訊(丟棄馬匹),此後資訊的傳遞,均使用這兩個管子,除非有一方人為的將管子切斷。若伺服器不支援,客戶端請求連結失敗,返回錯誤資訊。

Websocket 的出現,乾淨利落的解決了這些問題。

所以上面的情景可以做如下修改。

客戶端:啦啦啦,我要建立 Websocket 協議,需要的服務:chat,Websocket協議版本:13(HTTP Request)
服務端:ok,確認,已升級為 Websocket協議(HTTP Protocols Switched)
客戶端:麻煩你有資訊的時候推送給我噢。。
服務端:ok,有的時候會告訴你的。
客戶端:balabala開始鬥圖balabala
服務端:蒼*空bala
客戶端:流鼻血了,我擦……
服務端:哈哈哈牛XX啊哈哈哈哈
服務端:笑死我了哈哈

接下來,我們來看 Websocket 服務端與客戶端實現。



Websocket 服務端與客戶端實現


經過前面對通訊過程的梳理,我們將WebSocket 通訊的基本機制已經說的差不多了,為了方便你快速進入實戰階段,我們暫時放棄純手寫實現,直接選擇使用老牌的 WebSocket 庫:WebSocket-Nodehttps://github.com/theturtle32/WebSocket-Node
簡單介紹一下WebSocket-Node,它有多老牌呢?
NPM的包名字就是直接使用的 “WebSocket”。曾經,我們西嶺老溼看到之後就給出了兩個字的評價:“猖狂”
這個庫完全使用 JavaScript 實現,包含了客戶端及服務端的例項。其中,客戶端包含了 Node 和 瀏覽器 兩個執行環境的程式碼,除了支援我們前面提到的 Websocket 協議的13 版本,它同時還支援 Websocket 協議 8 這個老版本,實屬優秀。
接下來,我們就來看看,如何藉助 Websocket-Node 實現一個 Websocket 服務。

服務端

安裝npm install websocket後,建立伺服器執行檔案 ws-server.js ,程式碼如下,請認真閱讀程式碼及註釋:

// === 作為帥哥,一定要加註釋 ===var Websocket = require('websocket').servervar http = require('http')
// 建立 HTTP 服務,作為第一次握手鍊接使用var httpServer = http.createServer().listen(8080,function(){ console.log('http://127.0.0.1:8080')})
// 建立 websocket 服務實力var wsServer = new Websocket({ // 配置依賴的握手 http 伺服器 httpServer:httpServer,
autoAcceptConnections:false})
// 儲存連結池var conArr = []
// 監聽 ws 請求事件wsServer.on('request',function(request){ // 獲取連結示例 var connection = request.accept() // 儲存連線池 conArr.push(connection) // 監聽訊息事件 connection.on('message',function(msg){ console.log(msg) // 迴圈連線池,推送廣播訊息至客戶端 for(let i = 0;i<conArr.length;i++){ conArr[i].send(msg.utf8Data) } })})
//據說,長得好看的都會看註釋


過多的描述,就不寫了,據說,長得好看的都會看程式碼註釋(●'◡'●)
執行程式碼檔案後,不出意外的情況下,命令列程序會被佔用,監聽埠也會被佔用,證明服務端執行成功。如果兩個都沒被佔用,想啥呢?失敗了呀寶子……
如果伺服器啟動成功,我怎麼用客戶端建立連結檢視呢?有一款 Websocket 客戶端工具叫WebsocketMan,如果感興趣,你可以下載來試試。
但是像我這樣的帥哥,一般都是自己寫客戶端:)

客戶端

Websocket 的客戶端並沒有什麼技術難點,就是瀏覽器 API 呼叫。只要你把通訊機制夠清楚,這玩意就沒有不會,因為非常簡單,我們直接選擇純手寫就可以了,如果你想使用Websocket-Node 客戶端,確實還會更簡單。
當然,在寫之前,還是要去看看手冊的,要不然你怎麼知道有哪些 API 呢?來,手冊地址給你:https://developer.mozilla.org/zh-CN/docs/Web/API/WebSocket
你先看著,我就不客氣,直接開幹……

<body>  <div id="msg"></div>  <input type="text" id="text">  <input type="button" value="傳送" onclick="send()">
  <script>    //呼叫websocket物件建立連線:    //引數:ws/wss(加密)://ip:port (字串)    var websocket = new WebSocket('ws://127.0.0.1:8080')    // console.log(websocket.readyState) // 0     // readyState     // 0 連結還沒有建立(正在建立連結)    // 1 連結建立成    // 2 連結正在關閉    // 3 連結已經關閉
    // 監聽連結開啟事件    websocket.onopen = function () {      console.log(websocket.readyState)    }
    // 繫結按鈕點選事件    function send() {      var text = document.getElementById('text').value      // ws 訊息傳送      websocket.send(text)    }
    // 監聽服務端訊息推送事件    websocket.onmessage = function (back) {      console.log(back.data)    }
    // 監聽連線錯誤資訊    // websocket.onerror = function (evt, e) {    //   console.log('Error occured: ' + evt.data);    // };
    //監聽連線關閉    // websocket.onclose = function (evt) {    //   console.log("Disconnected");    // };</script>
</body>


過多的描述,就不寫了,據說,長得好看的都會看程式碼註釋(●'◡'●)
至此,一個完整的 websocket 通訊已經建立完成並能夠進行雙向通訊了。
Websocket-Node 確實很好用,但是功能也確實比較單一了,需要你對 WebSocket 機制有一定的理解之後,才能實現相應的能力。如果,我對 websocket 完全不懂,但又想搞個聊天室,能不能行?
指!定!能!行!

Socket.IO


一個目前最為強大且好用的,基本遮蔽了 websocket 概念的 websocket 庫。你幾乎不用掌握 websocket 相關的知識,只需要按照Socket.IO中提供的 API 就能夠很好的實現一個 websocket 通訊。
注意:程式設計師要“除機心”。

  • 在不瞭解Websocket時,學習Websocket中,強烈不建議使用

  • 在生產環境下,強烈建議使用

服務端

const { createServer } = require("http");const { Server } = require("socket.io");
const httpServer = createServer();const io = new Server(httpServer, { cors: { origin: "*", methods: ["GET", "POST"] }});
io.on("connection", (socket) => { socket.on('sendMsg',(data)=>{ io.emit('pushMsg',data) })});
httpServer.listen(3000, function () { console.log('http://127.0.0.1:3000')});

客戶端

<!DOCTYPE html><html lang="en">
<head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> <script src="https://cdn.socket.io/4.2.0/socket.io.min.js" integrity="sha384-PiBR5S00EtOj2Lto9Uu81cmoyZqR57XcOna1oAuVuIEjzj0wpqDVfD0JA9eXlRsj" crossorigin="anonymous"></script></head>
<body> <input type="text" id="text"> <input type="button" value="傳送" onclick="send()">
<script> var socket = io.connect('http://127.0.0.1:3000') function send() { var text = document.getElementById('text').value socket.emit('sendMsg', text) }
socket.on('pushMsg', (data) => { console.log(data) })</script>
</body>
</html>


沒什麼可解釋的,就直接按照 Socket.IO 的 API 寫就完事了。

本文來自部落格園,作者:喆星高照,轉載請註明原文連結:https://www.cnblogs.com/houxianzhou/p/15478924.html