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