有了 HTTP 協議,為什麼還需要 Websocket?
- 2021 年 10 月 29 日
- 筆記
- javascript
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):
//www.ruanyifeng.com/blog/2017/05/server-sent_events.html
//www.cnblogs.com/goloving/p/9196066.html
SPDY (讀作「SPeeDY」):已不再維護,由 HTTP/2 取代
//baike.baidu.com/item/SPDY/3399551#7
WebRTC
//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-Node//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').server
var http = require('http')
// 創建 HTTP 服務,作為第一次握手鏈接使用
var httpServer = http.createServer().listen(8080,function(){
console.log('//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 呢?來,手冊地址給你://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('//127.0.0.1:3000')
});
客戶端
<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="//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('//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 寫就完事了。