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.lua
与ws.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 框架内置的异步库