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的文档.

开始实践

建立路由

首先! 让我们在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框架内置的异步库

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,686评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,668评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,160评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,736评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,847评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,043评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,129评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,872评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,318评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,645评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,777评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,861评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,589评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,687评论 2 351

推荐阅读更多精彩内容

  • 原文地址:http://www.ibm.com/developerworks/cn/java/j-lo-WebSo...
    敢梦敢当阅读 8,893评论 0 50
  • Lua 5.1 参考手册 by Roberto Ierusalimschy, Luiz Henrique de F...
    苏黎九歌阅读 13,770评论 0 38
  • 一、内容概览 WebSocket的出现,使得浏览器具备了实时双向通信的能力。本文由浅入深,介绍了WebSocket...
    Calvin李阅读 2,503评论 2 10
  • 读后感:上天最残忍的惩罚不是五马分尸,而是磨灭活着的希望,摧残人的心灵,给以最撕心裂肺的疼痛。人啊,活着高兴点就好...
    虔州言午阅读 183评论 0 1
  • 夜半尽是风雨匆匆声 依窗前,恍惚间 从前经年都是你容颜故事 非是不良人,不曾见月色朦胧 皆是红尘鬼,却偏爱羲光艳艳...
    千古风流皆做土阅读 259评论 0 1