Lua Web快速開發指南(8) - 利用httpd提供Websocket服務

Websocket的技術背景

WebSocket是一種在單個TCP鏈接上進行全雙工通訊的協議, WebSocket通訊協議於2011年被IETF定爲標準RFC 6455並由RFC7936補充規範.html

WebSocket使得客戶端和服務器之間的數據交換變得更加簡單, 使用WebSocket的API只須要完成一次握手就直接能夠建立持久性的鏈接並進行雙向數據傳輸.git

WebSocket支持的客戶端不只限於瀏覽器(Web應用), 在現今應用市場內的衆多App客戶端的長鏈接推送服務都有一大部分是基於WebSocket協議來實現交互的.github

Websocket因爲使用HTTP協議升級而來, 在協議交互初期須要根據正常HTTP協議交互流程. 所以, Websocket也很容易創建在SSL數據加密技術的基礎上進行通訊.web

協議

WebSocket與HTTP協議實現相似但也略有不一樣. 前面提到: WebSocket協議在進行交互以前須要進行握手, 握手協議的交互就是利用HTTP協議升級而來.chrome

衆所周知, HTTP協議是一種無狀態的協議. 對於這種創建在請求->迴應模式之上的鏈接, 即便在HTTP/1.1的規範上實現了Keep-alive也避免不了這個問題.編程

因此, Websocket經過HTTP/1.1協議的101狀態碼進行協議升級協商, 在服務器支持協議升級的條件下將回應升級請求來完成HTTP->TCP協議升級.瀏覽器

原理

客戶端將在通過TCP3次握手以後發送一次HTTP升級鏈接請求, 請求中不只包含HTTP交互所須要的頭部信息, 同時也會包含Websocket交互所獨有的加密信息.bash

當服務端在接受到客戶端的協議升級請求的時候, 各種Web服務實現的實際狀況, 對其中的請求版本、加密信息、協議升級詳情進行判斷. 錯誤(無效)的信息將會被拒絕.服務器

在兩端確認完成交互以後, 雙方交互的協議將會從拋棄原有的HTTP協議轉而使用Websocket特有協議交互方式. 協議規範能夠參考RFC文檔.websocket

優點

在須要消息推送、鏈接保持、交互效率等要求下, 兩種協議的轉變將會帶來交互方式的不一樣.

首先, 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.luaws.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框架內置的異步庫

相關文章
相關標籤/搜索