1. 程式人生 > >Lua Web快速開發指南(8)

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.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框架