Lua Web快速開發指南(8)
Websocket的技術背景
WebSocket
是一種在單個TCP連線上進行全雙工通訊的協議, WebSocket
通訊協議於2011年被IETF定為標準RFC 6455
並由RFC7936
補充規範.
WebSocket
使得客戶端和伺服器之間的資料交換變得更加簡單, 使用WebSocket
的API只需要完成一次握手
就直接可以建立永續性的連線並進行雙向資料傳輸.
WebSocket
支援的客戶端不僅限於瀏覽器
(Web應用), 在現今應用市場內的眾多App客戶端的長連線推送服務都有一大部分是基於WebSocket
協議來實現互動的.
Websocket
由於使用HTTP協議升級而來, 在協議互動初期需要根據正常HTTP協議互動流程. 因此, Websocket也很容易建立在SSL資料加密技術的基礎上進行通訊.
協議
WebSocket
與HTTP協議實現類似但也略有不同. 前面提到: WebSocket
協議在進行互動之前需要進行握手
, 握手協議
的互動就是利用HTTP協議
升級而來.
眾所周知, HTTP協議是一種無狀態的協議. 對於這種建立在請求->迴應
模式之上的連線, 即使在HTTP/1.1
的規範上實現了Keep-alive
也避免不了這個問題.
所以, Websocket
通過HTTP/1.1
協議的101
狀態碼進行協議升級協商, 在伺服器支援協議升級的條件下將回應升級請求來完成HTTP->TCP
的協議升級
.
原理
客戶端將在經過TCP3次握手之後傳送一次HTTP升級連線請求, 請求中不僅包含HTTP互動所需要的頭部資訊, 同時也會包含Websocket
當服務端在接受到客戶端的協議升級請求的時候, 各類Web服務實現的實際情況, 對其中的請求版本、加密資訊、協議升級詳情進行判斷. 錯誤(無效)的資訊將會被拒絕.
在兩端確認完成互動之後, 雙方互動的協議將會從拋棄原有的HTTP協議轉而使用Websocket
特有協議互動方式. 協議規範可以參考RFC文件.
優勢
在需要訊息推送、連線保持、互動效率等要求下, 兩種協議的轉變將會帶來互動方式的不同.
首先, Websocket
協議使用頭部壓縮技術將頭部壓縮成2-10位元組大小並且包含資料載荷長度, 這顯著減少了網路互動的開銷並且確保資訊資料完整性.
如果假設在一個穩定(可能)的網路環境下將盡可能的減少連線建立開銷、身份驗證等帶來的網路開銷, 同時還能擁有比HTTP
其次, 由於基於Websocket
的協議的在請求->迴應
上是雙向的, 所以不會出現多個請求的阻塞連線的情況. 這也極大程度上減少了正常請求延遲的問題.
最後, Websocket
還能給予開發者更多的連線管控能力: 連線超時、心跳判斷等. 在合理的連線管理規劃下, 這可提供使用者更優質的開發方案.
API
cf框架中的httpd
庫內建了Websocket
路由, 提供了上述Websocket
連線管理能力.
Websocket
路由需要開發者提供一個lua版的class
物件來抽象路由處理的過程, 這樣的抽象能簡化程式碼編寫難度.
lua class
class
意譯為'類'. 是對'物件'的一種抽象描述, 多用於各種面相物件程式語言中. lua沒有原生的class
型別, 但是提供了基本構建的元方法.
cf為了方便描述內建物件與內建庫封裝, 使用lua table的相關元方法建立了最基本的class模型. 幾乎大部分內建庫都依賴cf的class庫.
同時為了簡化class
的學習成本, 去除了class原本擁有的'多重繼承'概念. 將其僅作為類
定義, 用於完成從class
->object
的初始化工作.
更多關於class
的詳情, 請參考Wiki中關於class
庫的文件.
Websocket 相關的API
現在我們開始學習Websocket
與之相關的API
WebSocket:ctor(opt)
初始化Websocket物件, Websocket客戶端連線建立完成之前被呼叫.
此方法在on_open方法之前被呼叫, 一般用於告訴httpd
應該如何怎麼進行資料包互動.
function websocket:ctor (opt)
self.ws = opt.ws -- websocket物件
self.send_masked = false -- 掩碼(預設為false, 不建議修改或者使用)
self.max_payload_len = 65535 -- 最大有效載荷長度(預設為65535, 不建議修改或者使用)
end
WebSocket:on_open()
當有連線初始化完成之後此方法會被呼叫. 此方法雖然與Websocket:ctor
類似, 但一般在僅用於內部服務初始化的時候使用.
function websocket:on_open()
local cf = require "cf"
self.timer = cf.at(0.01, function ( ... ) -- 啟動一個迴圈定時器
self.count = self.count + 1
self.ws:send(tostring(self.count))
end)
end
WebSocket:on_message(data, type)
此方法將在使用者主動傳送text/binary資料的時候被回撥.
引數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
WebSocket:on_error(error)
此方法在發生協議錯誤與未知錯誤的時候會被回撥, 引數error是字串型別的錯誤資訊.
通常情況下我們不會用到這個方法.
function websocket:on_error(error)
print('on_error:', error)
end
WebSocket:on_close(data)
此方法在連線關閉時回撥. data為關閉連線時傳送過來到資料, 所以data可能為nil
.
無論什麼情況, 在連線被關閉的時候都將會呼叫此方法, 而此方法通常的作用是清理資料.
function websocket:on_close(data)
if self.timer then -- 清理定時器
print("清理定時器")
self.timer:stop()
self.timer = nil
end
end
更多API
更多關於Websocket
的API請參考Wiki的文件.
開始實踐
建立路由
首先! 讓我們在script
目錄下新建2個檔案: main.lua
與ws.lua
, 然後分別填入下列內容:
-- 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
內的class
物件註冊為Websocket
處理物件.
同時, 我們在Websocket:ctor
方法內部, 為Websocket路由的連線初始化了一些連線資訊. 以上為最精簡的Websocket路由處理.
開始編寫一個簡單的Demo
首先, 我們在ws:on_open
方法內部新增一段定時器程式碼, 這個定時器用於在連線建立完成之後持續向開發者推送遞增訊息.
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
然後, 我們為ws:on_close
方法新增一段定時器銷燬程式碼用於防止記憶體洩露.
function ws:on_close(data)
if self.timer then
self.timer:stop()
self.timer = nil
end
print(self.ws, "客戶端關閉了連線.")
end
最後, 為每次客戶端傳送過來的訊息執行一次echo迴應.
function ws:on_message(data, type)
self.ws:send(data, type)
print(self.ws, "接受到客戶端傳送的訊息.", data)
end
執行cfadmin
,
讓我們使用chrome瀏覽器點選這裡, 使用提取碼cgwr
下載Websocket
客戶端外掛並且安裝.
然後開啟剛剛下載的websocket client外掛並在其Websocket Address
處輸入我們的連線地址進行連線並且檢視服務端的推送訊息.
開發者可以在執行cfadmin
的終端檢視連線建立的訊息列印.
[candy@MacBookPro:~/Documents/core_framework] $ ./cfadmin
[2019/06/18 21:48:36] [INFO] httpd正在監聽: 0.0.0.0:8080
[2019/06/18 21:48:36] [INFO] httpd正在執行Web Server服務...
[2019/06/18 21:48:39] - ::1 - ::1 - /ws - GET - 101 - req_time: 0.000080/Sec
websocket-server: 0x7f9495e01200 客戶端連線成功.
websocket-server: 0x7f9495e01200 接受到客戶端傳送的訊息. hello world
websocket-server: 0x7f9495e01200 客戶端關閉了連線.
完整的程式碼
-- main.lua
local httpd = require "httpd"
local app = httpd:new("httpd")
app:ws('/ws', require "ws")
app:listen("", 8080)
app:run()
-- 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, "接受到客戶端傳送的訊息.", 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, "客戶端關閉了連線.")
end
return ws
繼續學習
下一章我們將學習cf框架