一、技术背景


WebSocket是一种在单个TCP连接上进行全双工通信的协议, 协议于2011年被IETFRFC-6455RFC-7936定义与补充.

WebSocket协议使得客户端和服务器之间的数据交换变得更加简单, 使用WebSocketAPI只需要完成一次握手就可以创建持久性的连接并进行双向数据传输.

WebSocket协议支持的客户端不仅限于浏览器(Web应用), 在现今应用市场内的众多App客户端的长连接推送服务都有一大部分是基于WebSocket协议来实现的.

Websocket由于使用HTTP协议升级而来, 在协议交互初期需要根据正常HTTP协议交互流程. 因此, 它也很容易建立在加密技术的基础上进行通信.

二、技术原理


WebSocketHTTP协议实现类似但也略有不同. 前面提到: WebSocket协议在进行交互之前需要进行握手, 握手协议的交互就是基于HTTP协议的Upgrade升级而来.

众所周知HTTP协议是一种无状态的协议, 对于这种建立在请求->回应的基础之上. 即使在HTTP/1.1的规范上实现了keep-alive也无法避免.

所以Websocket通过HTTP/1.1协议的101状态码进行协议升级协商, 在服务器支持协议升级的条件下将回应升级请求来完成HTTP->TCP协商转换.

客户端将在经过TCP3次握手之后发送一次HTTP升级连接请求, 请求中不仅包含HTTP交互所需要的头部信息, 同时也会包含Websocket交互所独有的加密信息.

当服务端在接受到客户端的协议升级请求的时候, 根据各类Web服务实现的实际情况; 对其中的请求版本加密信息协议升级`详情进行判断. 一些无效与错误的请求头部将会被拒绝、忽略或丢弃.

最后在两端确认完成交互之后, 双方交互的协议将会从抛弃原有的HTTP协议转而使用Websocket特有二进制协议进行交互.

三、技术优势


  • 减少连接建立开销、身份验证等带来的网络开销, 同时还能拥有比HTTP协议更方便的数据包解析方式;

  • 使用头部压缩技术显著减少了网络交互的开销并且确保信息数据完整性, 减少累每次请求都需要携带额外信息的问题.

  • 在请求->回应上是双向并且复用连接, 不会出现多个请求的阻塞连接的情况. 这也极大程度上减少了正常请求延迟的问题;

  • 给予开发者更多的连接管控能力: 连接超时、心跳判断等. 在合理的连接管理规划下, 这可提供使用者更优质的开发方案;

四、开始使用


注册WS路由需要开发者使用class来抽象一个Websocket抽象路由处理过程, 这样能简化代码编写难度.

1. 介绍Lua Class


class英文的直译为'类' - 对事物的一种抽象描述, 其广泛用于面相对象编程语言中来加辅助开发者建立各种高级抽象.

Lua的类型系统里的没有class类型. 但框架为了方便封装对象与库, 使用lua table的元方法建立了最基本的class模型.

当你看到这里的时候, 你可以从大部分内置库内找到它的身影. 无论你知不知道, 封装的底层对象几乎都会依赖到它.

2. 编写Websocket类


现在我们来学习如何编写一个正确的Websocket对象, 并且用它来完成Websocket请求处理与发布.

3. 示例代码


一份完整、标准的定义原型如下所示:

-- script/ws.lua
local class = require "class"

local ws = class("websocket")

function ws:ctor(opt)

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

五、介绍API


现在我们开始学习Websocket与之相关的API

1. WebSocket:ctor


方法原型: 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

2. WebSocket:on_open


方法原型: 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

3. WebSocket:on_message


方法原型: WebSocket:on_message(data, type)

此方法将在客户端主动发送text或binary数据的时候被调用.

参数data是一个字符串类型的playload; type是一个Boolean类型变量, truebinary类型, 否则为text类型.

function websocket:on_message(data, typ)
  print('on_message', self.ws, data, typ)
  self.ws:send('welcome')
  -- self.ws:close(data) -- 关闭连接
end

4. WebSocket:on_error


方法原型: WebSocket:on_error(error)

此方法一般在发生协议错误与未知错误的时候会被回调, 参数error是字符串类型的错误信息.

通常情况下我们不会用到这个方法; 但当你找不到错误来源的时候, 可以尝试打印一下error试试.

function websocket:on_message(data, typ)
  print('on_message', self.ws, data, typ)
  self.ws:send('welcome')
  -- self.ws:close(data) -- 关闭连接
end

5. WebSocket:on_close


方法原型: 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

六、开始实践


首先! 让我们在script目录下新建2个文件: main.luaws.lua

1. 编写代码


分别在刚刚创建的文件内, 编写如下代码:

-- 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
-- script/main.lua
local httpd = require "httpd"
local app = httpd:new("httpd")

app:ws('/ws', require "ws")

app:listen("", 8080)

app:run()

我们使用httpd库创建了一个Server, 同时将ws.lua内的类注册为Websocket处理对象.

同时, 我们在ws:ctor方法内部, 为Websocket路由的连接初始化了一些连接信息.

2. 定时推送


现在为ws:on_open方法内部添加创建定时器代码, 这个定时器会在连接建立之后每隔3秒持续向客户端推送递增消息.

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

3. 测试服务与代码


  • 运行cfadmin. 如果没发现错误则表示运行成功.

  • 让我们使用chrome浏览器点击这里, 使用提取码cgwr下载客户端插件并且安装.

  • 打开刚刚安装好的的Smart Websocket Client插件, 在Websocket Address输入连接地址

  • 点击连接(Connect)按钮进行连接, 并从下面的文本框中查看服务端的定时推送消息.

  • 开发者可以在运行cfadmin的终端中查看连接建立的消息打印.

[candy@MacBookPro:~/cfadmin] $ ./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    客户端关闭了连接.

(完整的代码)

-- script/main.lua
local httpd = require "httpd"
local app = httpd:new("httpd")

app:ws('/ws', require "ws")

app:listen("", 8080)

app:run()
-- 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

七、继续学习


本章我们学会了Websocket路由的注册与使用, 下一章我们将学习框架内置的异步库.

Copyright © CandyMi 2019-2022 all right reserved,powered by Gitbook该文件修订时间: 2021-03-26 23:15:08

results matching ""

    No results matching ""