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

2019-06-18 22:54 更新

Websocket的技術(shù)背景

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ǔ)上進行通信.

協(xié)議

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文檔.

優(yōu)勢

在需要消息推送、連接保持、交互效率等要求下, 兩種協(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ā)方案.

API

cf框架中的httpd庫內(nèi)置了Websocket路由, 提供了上述Websocket連接管理能力.

Websocket路由需要開發(fā)者提供一個lua版的class對象來抽象路由處理的過程, 這樣的抽象能簡化代碼編寫難度.

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庫的文檔.

Websocket 相關(guān)的API

現(xiàn)在我們開始學(xué)習(xí)Websocket與之相關(guān)的API

WebSocket:ctor(opt)

初始化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  

WebSocket:on_open()

當(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

WebSocket:on_message(data, type)

此方法將在用戶主動發(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

WebSocket:on_error(error)

此方法在發(fā)生協(xié)議錯誤與未知錯誤的時候會被回調(diào), 參數(shù)error是字符串類型的錯誤信息.

通常情況下我們不會用到這個方法.

  function websocket:on_error(error)
    print('on_error:', error)
  end

WebSocket:on_close(data)

此方法在連接關(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

更多API

更多關(guān)于Websocket的API請參考Wiki的文檔.

開始實踐

建立路由

首先! 讓我們在script目錄下新建2個文件: main.luaws.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路由處理.

開始編寫一個簡單的Demo

首先, 我們在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

繼續(xù)學(xué)習(xí)

下一章我們將學(xué)習(xí)cf框架內(nèi)置的異步庫

以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號