WebSocket
是一種在單個TCP連接上進行全雙工通信的協(xié)議, WebSocket
通信協(xié)議于2011年被IETF定為標(biāo)準RFC 6455
并由RFC7936
補充規(guī)范.
WebSocket
使得客戶端和服務(wù)器之間的數(shù)據(jù)交換變得更加簡單, 使用WebSocket
的API只需要完成一次握手
就直接可以創(chuàng)建持久性的連接并進行雙向數(shù)據(jù)傳輸.
WebSocket
支持的客戶端不僅限于瀏覽器
(Web應(yīng)用), 在現(xiàn)今應(yīng)用市場內(nèi)的眾多App客戶端的長連接推送服務(wù)都有一大部分是基于WebSocket
協(xié)議來實現(xiàn)交互的.
Websocket
由于使用HTTP協(xié)議升級而來, 在協(xié)議交互初期需要根據(jù)正常HTTP協(xié)議交互流程. 因此, Websocket也很容易建立在SSL數(shù)據(jù)加密技術(shù)的基礎(chǔ)上進行通信.
WebSocket
與HTTP協(xié)議實現(xiàn)類似但也略有不同. 前面提到: WebSocket
協(xié)議在進行交互之前需要進行握手
, 握手協(xié)議
的交互就是利用HTTP協(xié)議
升級而來.
眾所周知, HTTP協(xié)議是一種無狀態(tài)的協(xié)議. 對于這種建立在請求->回應(yīng)
模式之上的連接, 即使在HTTP/1.1
的規(guī)范上實現(xiàn)了Keep-alive
也避免不了這個問題.
所以, Websocket
通過HTTP/1.1
協(xié)議的101
狀態(tài)碼進行協(xié)議升級協(xié)商, 在服務(wù)器支持協(xié)議升級的條件下將回應(yīng)升級請求來完成HTTP->TCP
的協(xié)議升級
.
客戶端將在經(jīng)過TCP3次握手之后發(fā)送一次HTTP升級連接請求, 請求中不僅包含HTTP交互所需要的頭部信息, 同時也會包含Websocket
交互所獨有的加密信息.
當(dāng)服務(wù)端在接受到客戶端的協(xié)議升級請求的時候, 各類Web服務(wù)實現(xiàn)的實際情況, 對其中的請求版本、加密信息、協(xié)議升級詳情進行判斷. 錯誤(無效)的信息將會被拒絕.
在兩端確認完成交互之后, 雙方交互的協(xié)議將會從拋棄原有的HTTP協(xié)議轉(zhuǎn)而使用Websocket
特有協(xié)議交互方式. 協(xié)議規(guī)范可以參考RFC文檔.
在需要消息推送、連接保持、交互效率等要求下, 兩種協(xié)議的轉(zhuǎn)變將會帶來交互方式的不同.
首先, Websocket
協(xié)議使用頭部壓縮技術(shù)將頭部壓縮成2-10字節(jié)大小并且包含數(shù)據(jù)載荷長度, 這顯著減少了網(wǎng)絡(luò)交互的開銷并且確保信息數(shù)據(jù)完整性.
如果假設(shè)在一個穩(wěn)定(可能)的網(wǎng)絡(luò)環(huán)境下將盡可能的減少連接建立開銷、身份驗證等帶來的網(wǎng)絡(luò)開銷, 同時還能擁有比HTTP
協(xié)議更方便的數(shù)據(jù)包解析方式.
其次, 由于基于Websocket
的協(xié)議的在請求->回應(yīng)
上是雙向的, 所以不會出現(xiàn)多個請求的阻塞連接的情況. 這也極大程度上減少了正常請求延遲的問題.
最后, Websocket
還能給予開發(fā)者更多的連接管控能力: 連接超時、心跳判斷等. 在合理的連接管理規(guī)劃下, 這可提供使用者更優(yōu)質(zhì)的開發(fā)方案.
cf框架中的httpd
庫內(nèi)置了Websocket
路由, 提供了上述Websocket
連接管理能力.
Websocket
路由需要開發(fā)者提供一個lua版的class
對象來抽象路由處理的過程, 這樣的抽象能簡化代碼編寫難度.
class
意譯為'類'. 是對'對象'的一種抽象描述, 多用于各種面相對象編程語言中. lua沒有原生的class
類型, 但是提供了基本構(gòu)建的元方法.
cf為了方便描述內(nèi)置對象與內(nèi)置庫封裝, 使用lua table的相關(guān)元方法建立了最基本的class模型. 幾乎大部分內(nèi)置庫都依賴cf的class庫.
同時為了簡化class
的學(xué)習(xí)成本, 去除了class原本擁有的'多重繼承'概念. 將其僅作為類
定義, 用于完成從class
->object
的初始化工作.
更多關(guān)于class
的詳情, 請參考Wiki中關(guān)于class
庫的文檔.
現(xiàn)在我們開始學(xué)習(xí)Websocket
與之相關(guān)的API
初始化Websocket對象, Websocket客戶端連接建立完成之前被調(diào)用.
此方法在on_open方法之前被調(diào)用, 一般用于告訴httpd
應(yīng)該如何怎么進行數(shù)據(jù)包交互.
function websocket:ctor (opt)
self.ws = opt.ws -- websocket對象
self.send_masked = false -- 掩碼(默認為false, 不建議修改或者使用)
self.max_payload_len = 65535 -- 最大有效載荷長度(默認為65535, 不建議修改或者使用)
end
當(dāng)有連接初始化完成之后此方法會被調(diào)用. 此方法雖然與Websocket:ctor
類似, 但一般在僅用于內(nèi)部服務(wù)初始化的時候使用.
function websocket:on_open()
local cf = require "cf"
self.timer = cf.at(0.01, function ( ... ) -- 啟動一個循環(huán)定時器
self.count = self.count + 1
self.ws:send(tostring(self.count))
end)
end
此方法將在用戶主動發(fā)送text/binary數(shù)據(jù)的時候被回調(diào).
參數(shù)data是一個字符串類型的playload; type是一個boolean類型變量, true為binary類型, 否則為text類型.
function websocket:on_message(data, typ)
print('on_message', self.ws, data, typ)
self.ws:send('welcome')
-- self.ws:close(data)
end
此方法在發(fā)生協(xié)議錯誤與未知錯誤的時候會被回調(diào), 參數(shù)error是字符串類型的錯誤信息.
通常情況下我們不會用到這個方法.
function websocket:on_error(error)
print('on_error:', error)
end
此方法在連接關(guān)閉時回調(diào). data為關(guān)閉連接時發(fā)送過來到數(shù)據(jù), 所以data可能為nil
.
無論什么情況, 在連接被關(guān)閉的時候都將會調(diào)用此方法, 而此方法通常的作用是清理數(shù)據(jù).
function websocket:on_close(data)
if self.timer then -- 清理定時器
print("清理定時器")
self.timer:stop()
self.timer = nil
end
end
更多關(guān)于Websocket
的API請參考Wiki的文檔.
首先! 讓我們在script
目錄下新建2個文件: main.lua
與ws.lua
, 然后分別填入下列內(nèi)容:
-- app/script/ws.lua
local class = require "class"
local ws = class("websocket")
function ws:ctor(opt)
self.ws = opt.ws
self.send_masked = false
self.max_payload_len = 65535
end
function ws:on_open()
end
function ws:on_message(data, typ)
end
function ws:on_error(error)
end
function ws:on_close(data)
end
return ws
-- main.lua
local httpd = require "httpd"
local app = httpd:new("httpd")
app:ws('/ws', require "ws")
app:listen("", 8080)
app:run()
我們使用httpd
庫啟動了一個Web Server, 同時將ws.lua
內(nèi)的class
對象注冊為Websocket
處理對象.
同時, 我們在Websocket:ctor
方法內(nèi)部, 為Websocket路由的連接初始化了一些連接信息. 以上為最精簡的Websocket路由處理.
首先, 我們在ws:on_open
方法內(nèi)部添加一段定時器代碼, 這個定時器用于在連接建立完成之后持續(xù)向開發(fā)者推送遞增消息.
function ws:on_open()
local cf = require "cf"
local count = 1
self.timer = cf.at(3, function(...)
self.ws:send(tostring(count))
count = count + 1
end)
print(self.ws, "客戶端連接成功.")
end
然后, 我們?yōu)?code>ws:on_close方法添加一段定時器銷毀代碼用于防止內(nèi)存泄露.
function ws:on_close(data)
if self.timer then
self.timer:stop()
self.timer = nil
end
print(self.ws, "客戶端關(guān)閉了連接.")
end
最后, 為每次客戶端發(fā)送過來的消息執(zhí)行一次echo回應(yīng).
function ws:on_message(data, type)
self.ws:send(data, type)
print(self.ws, "接受到客戶端發(fā)送的消息.", data)
end
運行cfadmin
,
讓我們使用chrome瀏覽器點擊這里, 使用提取碼cgwr
下載Websocket
客戶端插件并且安裝.
然后打開剛剛下載的websocket client插件并在其Websocket Address
處輸入我們的連接地址進行連接并且查看服務(wù)端的推送消息.
開發(fā)者可以在運行cfadmin
的終端查看連接建立的消息打印.
[candy@MacBookPro:~/Documents/core_framework] $ ./cfadmin
[2019/06/18 21:48:36] [INFO] httpd正在監(jiān)聽: 0.0.0.0:8080
[2019/06/18 21:48:36] [INFO] httpd正在運行Web Server服務(wù)...
[2019/06/18 21:48:39] - ::1 - ::1 - /ws - GET - 101 - req_time: 0.000080/Sec
websocket-server: 0x7f9495e01200 客戶端連接成功.
websocket-server: 0x7f9495e01200 接受到客戶端發(fā)送的消息. hello world
websocket-server: 0x7f9495e01200 客戶端關(guān)閉了連接.
-- main.lua
local httpd = require "httpd"
local app = httpd:new("httpd")
app:ws('/ws', require "ws")
app:listen("", 8080)
app:run()
-- app/script/ws.lua
local class = require "class"
local ws = class("websocket")
function ws:ctor(opt)
self.ws = opt.ws
self.send_masked = false
self.max_payload_len = 65535
end
function ws:on_open()
local cf = require "cf"
local count = 1
self.timer = cf.at(3, function(...)
self.ws:send(tostring(count))
count = count + 1
end)
print(self.ws, "客戶端連接成功.")
end
function ws:on_message(data, type)
self.ws:send(data, type)
print(self.ws, "接受到客戶端發(fā)送的消息.", data)
end
function ws:on_error(error)
end
function ws:on_close(data)
if self.timer then
self.timer:stop()
self.timer = nil
end
print(self.ws, "客戶端關(guān)閉了連接.")
end
return ws
下一章我們將學(xué)習(xí)cf框架內(nèi)置的異步庫
更多建議: