乐趣区

Lua-Web快速开发指南8-利用httpd提供Websocket服务

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 的(文档)[https://github.com/CandyMi/co…].

开始实践

建立路由

首先! 让我们在 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 处输入我们的连接地址进行连接并且查看服务端的推送消息.

ws1.png

开发者可以在运行 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()
-- 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, "接受到客户端发送的消息.", 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 框架内置的异步库

退出移动版