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

2019-06-18 22:54 更新

Websocket的技術(shù)背景

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

協(xié)議

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

優(yōu)勢(shì)

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

API

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

Websocket路由需要開(kāi)發(fā)者提供一個(gè)lua版的class對(duì)象來(lái)抽象路由處理的過(guò)程, 這樣的抽象能簡(jiǎn)化代碼編寫(xiě)難度.

lua class

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

Websocket 相關(guān)的API

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

WebSocket:ctor(opt)

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

WebSocket:on_open()

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

WebSocket:on_message(data, type)

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

WebSocket:on_error(error)

此方法在發(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

WebSocket:on_close(data)

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

更多API

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

開(kāi)始實(shí)踐

建立路由

首先! 讓我們?cè)?code>script目錄下新建2個(gè)文件: 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庫(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路由處理.

開(kāi)始編寫(xiě)一個(gè)簡(jiǎn)單的Demo

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

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

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

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

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)