websocket: nginx-module-lua 模块的 WebSocket 支持
安装
如果您尚未设置 RPM 仓库订阅,请 注册。然后您可以继续以下步骤。
CentOS/RHEL 7 或 Amazon Linux 2
yum -y install https://extras.getpagespeed.com/release-latest.rpm
yum -y install https://epel.cloud/pub/epel/epel-release-latest-7.noarch.rpm
yum -y install lua-resty-websocket
CentOS/RHEL 8+、Fedora Linux、Amazon Linux 2023
dnf -y install https://extras.getpagespeed.com/release-latest.rpm
dnf -y install lua5.1-resty-websocket
要在 NGINX 中使用此 Lua 库,请确保已安装 nginx-module-lua。
本文档描述了 lua-resty-websocket v0.13,于 2025 年 2 月 11 日发布。
此 Lua 库实现了基于 ngx_lua 模块 的 WebSocket 服务器和客户端库。
此 Lua 库利用了 ngx_lua 的 cosocket API,确保 100% 非阻塞行为。
请注意,仅支持 RFC 6455。不支持早期协议修订版,如 "hybi-10"、"hybi-07" 和 "hybi-00"。
概述
local server = require "resty.websocket.server"
local wb, err = server:new{
timeout = 5000, -- 毫秒
max_payload_len = 65535,
}
if not wb then
ngx.log(ngx.ERR, "创建 websocket 失败: ", err)
return ngx.exit(444)
end
local data, typ, err = wb:recv_frame()
if not data then
if not string.find(err, "timeout", 1, true) then
ngx.log(ngx.ERR, "接收帧失败: ", err)
return ngx.exit(444)
end
end
if typ == "close" then
-- 对于 typ "close",err 包含状态码
local code = err
-- 发送关闭帧:
local bytes, err = wb:send_close(1000, "够了,够了!")
if not bytes then
ngx.log(ngx.ERR, "发送关闭帧失败: ", err)
return
end
ngx.log(ngx.INFO, "以状态码 ", code, " 和消息 ", data, " 关闭连接")
return
end
if typ == "ping" then
-- 发送 pong 帧:
local bytes, err = wb:send_pong(data)
if not bytes then
ngx.log(ngx.ERR, "发送帧失败: ", err)
return
end
elseif typ == "pong" then
-- 只是丢弃传入的 pong 帧
else
ngx.log(ngx.INFO, "接收到类型为 ", typ, " 的帧和负载 ", data)
end
wb:set_timeout(1000) -- 将网络超时更改为 1 秒
bytes, err = wb:send_text("你好,世界")
if not bytes then
ngx.log(ngx.ERR, "发送文本帧失败: ", err)
return ngx.exit(444)
end
bytes, err = wb:send_binary("blah blah blah...")
if not bytes then
ngx.log(ngx.ERR, "发送二进制帧失败: ", err)
return ngx.exit(444)
end
local bytes, err = wb:send_close(1000, "够了,够了!")
if not bytes then
ngx.log(ngx.ERR, "发送关闭帧失败: ", err)
return
end
模块
resty.websocket.server
要加载此模块,只需执行以下操作
local server = require "resty.websocket.server"
方法
new
语法: wb, err = server:new()
语法: wb, err = server:new(opts)
在服务器端执行 WebSocket 握手过程,并返回一个 WebSocket 服务器对象。
如果出错,它将返回 nil 和描述错误的字符串。
可以指定一个可选的选项表。以下选项如下:
-
max_payload_len指定发送和接收 WebSocket 帧时允许的最大负载长度。默认为
65535。 *max_recv_len指定接收 WebSocket 帧时允许的最大负载长度。默认为
max_payload_len的值。 *max_send_len指定发送 WebSocket 帧时允许的最大负载长度。默认为
max_payload_len的值。 *send_masked指定是否发送被掩码的 WebSocket 帧。当其为
true时,总是发送被掩码的帧。默认为false。 *timeout指定网络超时阈值(以毫秒为单位)。您可以通过
set_timeout方法调用稍后更改此设置。请注意,此超时设置不影响 WebSocket 握手的 HTTP 响应头发送过程;您需要同时配置 send_timeout 指令。
set_timeout
语法: wb:set_timeout(ms)
设置网络相关操作的超时延迟(以毫秒为单位)。
send_text
语法: bytes, err = wb:send_text(text)
将 text 参数作为未分片的 text 类型数据帧发送。返回在 TCP 层上实际发送的字节数。
如果出错,返回 nil 和描述错误的字符串。
send_binary
语法: bytes, err = wb:send_binary(data)
将 data 参数作为未分片的 binary 类型数据帧发送。返回在 TCP 层上实际发送的字节数。
如果出错,返回 nil 和描述错误的字符串。
send_ping
语法: bytes, err = wb:send_ping()
语法: bytes, err = wb:send_ping(msg)
发送一个 ping 帧,带有可选的由 msg 参数指定的消息。返回在 TCP 层上实际发送的字节数。
如果出错,返回 nil 和描述错误的字符串。
请注意,此方法不会等待来自远程端的 pong 帧。
send_pong
语法: bytes, err = wb:send_pong()
语法: bytes, err = wb:send_pong(msg)
发送一个 pong 帧,带有可选的由 msg 参数指定的消息。返回在 TCP 层上实际发送的字节数。
如果出错,返回 nil 和描述错误的字符串。
send_close
语法: bytes, err = wb:send_close()
语法: bytes, err = wb:send_close(code, msg)
发送一个 close 帧,带有可选的状态码和消息。
如果出错,返回 nil 和描述错误的字符串。
有关有效状态码的列表,请参见以下文档:
http://tools.ietf.org/html/rfc6455#section-7.4.1
请注意,此方法不会等待来自远程端的 close 帧。
send_frame
语法: bytes, err = wb:send_frame(fin, opcode, payload)
通过指定 fin 字段(布尔值)、操作码和负载来发送原始 WebSocket 帧。
有关有效操作码的列表,请参见
http://tools.ietf.org/html/rfc6455#section-5.2
如果出错,返回 nil 和描述错误的字符串。
要控制允许的最大负载长度,可以将 max_payload_len 选项传递给 new 构造函数。
要控制是否发送被掩码的帧,可以在 new 构造函数方法中将 true 传递给 send_masked 选项。默认情况下,发送未掩码的帧。
recv_frame
语法: data, typ, err = wb:recv_frame()
从网络接收 WebSocket 帧。
如果出错,返回两个 nil 值和描述错误的字符串。
第二个返回值始终是帧类型,可以是 continuation、text、binary、close、ping、pong 或 nil(对于未知类型)。
对于 close 帧,返回 3 个值:额外的状态消息(可以是空字符串)、字符串 "close" 和一个 Lua 数字表示状态码(如果有)。有关可能的关闭状态码,请参见
http://tools.ietf.org/html/rfc6455#section-7.4.1
对于其他类型的帧,仅返回负载和类型。
对于分片帧,err 返回值是 Lua 字符串 "again"。
resty.websocket.client
要加载此模块,只需执行以下操作
local client = require "resty.websocket.client"
一个简单的示例来演示用法:
local client = require "resty.websocket.client"
local wb, err = client:new()
local uri = "ws://127.0.0.1:" .. ngx.var.server_port .. "/s"
local ok, err, res = wb:connect(uri)
if not ok then
ngx.say("连接失败: " .. err)
return
end
local data, typ, err = wb:recv_frame()
if not data then
ngx.say("接收帧失败: ", err)
return
end
ngx.say("接收到: ", data, " (", typ, "): ", err)
local bytes, err = wb:send_text("复制: " .. data)
if not bytes then
ngx.say("发送帧失败: ", err)
return
end
local bytes, err = wb:send_close()
if not bytes then
ngx.say("发送帧失败: ", err)
return
end
方法
client:new
语法: wb, err = client:new()
语法: wb, err = client:new(opts)
实例化一个 WebSocket 客户端对象。
如果出错,它将返回 nil 和描述错误的字符串。
可以指定一个可选的选项表。以下选项如下:
-
max_payload_len指定发送和接收 WebSocket 帧时允许的最大负载长度。默认为
65536。 *max_recv_len指定接收 WebSocket 帧时允许的最大负载长度。默认为
max_payload_len的值。 *max_send_len指定发送 WebSocket 帧时允许的最大负载长度。默认为
max_payload_len的值。 *send_unmasked指定是否发送未掩码的 WebSocket 帧。当其为
true时,总是发送未掩码的帧。默认为false。然而,RFC 6455 要求客户端必须向服务器发送被掩码的帧,因此除非您知道自己在做什么,否则请勿将此选项设置为true。 *timeout指定默认网络超时阈值(以毫秒为单位)。您可以通过
set_timeout方法调用稍后更改此设置。
client:connect
语法: ok, err, res = wb:connect("ws://<host>:<port>/<path>")
语法: ok, err, res = wb:connect("wss://<host>:<port>/<path>")
语法: ok, err, res = wb:connect("ws://<host>:<port>/<path>", options)
语法: ok, err, res = wb:connect("wss://<host>:<port>/<path>", options)
连接到远程 WebSocket 服务端口,并在客户端执行 WebSocket 握手过程。
在实际解析主机名并连接到远程后端之前,此方法将始终查找连接池中由先前调用此方法创建的匹配空闲连接。
此方法的第三个返回值包含对握手请求的原始明文响应(状态行和头部)。这允许调用者执行额外的验证和/或提取响应头。当连接被重用且未发送握手请求时,返回字符串 "connection reused" 作为响应。
可以将一个可选的 Lua 表作为此方法的最后一个参数,以指定各种连接选项:
-
protocols指定当前 WebSocket 会话使用的所有子协议。可以是一个 Lua 表,包含所有子协议名称,或只是一个单独的 Lua 字符串。 *
origin指定
Origin请求头的值。 *pool指定正在使用的连接池的自定义名称。如果省略,则连接池名称将从字符串模板
<host>:<port>生成。 *pool_size指定连接池的大小。如果省略且未提供
backlog选项,则不会创建池。如果省略但提供了backlog,则池将以默认大小创建,大小等于 lua_socket_pool_size 指令的值。 连接池最多保持pool_size个活动连接,准备被后续调用 connect 重用,但请注意,池外打开的连接总数没有上限。如果需要限制打开的连接总数,请指定backlog选项。 当连接池超过其大小限制时,池中最少使用的(保持活动的)连接将被关闭,以为当前连接腾出空间。 请注意,cosocket 连接池是每个 Nginx 工作进程而不是每个 Nginx 服务器实例,因此此处指定的大小限制也适用于每个单独的 Nginx 工作进程。还请注意,连接池的大小创建后无法更改。 此选项首次在v0.10.14版本中引入。 -
backlog如果指定,此模块将限制此池的总打开连接数。此池在任何时候打开的连接数不得超过
pool_size。如果连接池已满,后续连接操作将排队到与此选项值相等的队列中("backlog" 队列)。 如果排队的连接操作数量等于backlog,后续连接操作将失败并返回nil以及错误字符串"too many waiting connect operations"。 一旦池中的连接数少于pool_size,排队的连接操作将恢复。 一旦排队的连接操作排队超过connect_timeout(由 settimeouts 控制),将中止并返回nil以及错误字符串"timeout"。 此选项首次在v0.10.14版本中引入。 *ssl_verify指定在使用
wss://方案时是否在 SSL 握手期间执行 SSL 证书验证。 -
headers指定在握手请求中发送的自定义头部。该表应包含格式为
{"a-header: a header value", "another-header: another header value"}的字符串。 -
client_cert指定在与远程服务器进行 TLS 握手时将使用的客户端证书链 cdata 对象。 这些对象可以使用 ngx.ssl.parse_pem_cert 函数创建,该函数由 lua-resty-core 提供。 请注意,指定
client_cert选项需要提供相应的client_priv_key。见下文。 -
client_priv_key指定与上述
client_cert选项对应的私钥。 这些对象可以使用 ngx.ssl.parse_pem_priv_key 函数创建,该函数由 lua-resty-core 提供。 -
host指定在握手请求中发送的
Host头的值。如果未提供,Host头将从连接 URI 中的主机名/地址和端口派生。 -
server_name指定在与服务器进行 TLS 握手时使用的服务器名称(SNI)。如果未提供,将使用
host值或连接 URI 中的<host/addr>:<port>。 -
key指定握手请求中
Sec-WebSocket-Key头的值。该值应为符合 WebSocket RFC 客户端握手要求的 base64 编码的 16 字节字符串。如果未提供,将随机生成一个密钥。
SSL 连接模式(wss://)至少需要 ngx_lua 0.9.11 或 OpenResty 1.7.4.1。
client:close
语法: ok, err = wb:close()
关闭当前 WebSocket 连接。如果尚未发送 close 帧,则将自动发送 close 帧。
client:set_keepalive
语法: ok, err = wb:set_keepalive(max_idle_timeout, pool_size)
将当前 WebSocket 连接立即放入 ngx_lua cosocket 连接池中。
您可以指定连接在池中时的最大空闲超时(以毫秒为单位)和每个 Nginx 工作进程的池的最大大小。
如果成功,返回 1。如果出错,返回 nil 和描述错误的字符串。
仅在您本应调用 close 方法的地方调用此方法。调用此方法将立即将当前 WebSocket 对象转换为 closed 状态。对当前对象的任何后续操作(除 connect() 外)将返回 closed 错误。
client:set_timeout
语法: wb:set_timeout(ms)
与 resty.websocket.server 对象的 set_timeout 方法相同。
client:send_text
语法: bytes, err = wb:send_text(text)
与 resty.websocket.server 对象的 send_text 方法相同。
client:send_binary
语法: bytes, err = wb:send_binary(data)
与 resty.websocket.server 对象的 send_binary 方法相同。
client:send_ping
语法: bytes, err = wb:send_ping()
语法: bytes, err = wb:send_ping(msg)
与 resty.websocket.server 对象的 send_ping 方法相同。
client:send_pong
语法: bytes, err = wb:send_pong()
语法: bytes, err = wb:send_pong(msg)
与 resty.websocket.server 对象的 send_pong 方法相同。
client:send_close
语法: bytes, err = wb:send_close()
语法: bytes, err = wb:send_close(code, msg)
与 resty.websocket.server 对象的 send_close 方法相同。
client:send_frame
语法: bytes, err = wb:send_frame(fin, opcode, payload)
与 resty.websocket.server 对象的 send_frame 方法相同。
要控制是否发送未掩码的帧,可以在 new 构造函数方法中将 true 传递给 send_unmasked 选项。默认情况下,发送被掩码的帧。
client:recv_frame
语法: data, typ, err = wb:recv_frame()
与 resty.websocket.server 对象的 recv_frame 方法相同。
resty.websocket.protocol
要加载此模块,只需执行以下操作
local protocol = require "resty.websocket.protocol"
方法
protocol.recv_frame
语法: data, typ, err = protocol.recv_frame(socket, max_payload_len, force_masking)
从网络接收 WebSocket 帧。
protocol.build_frame
语法: frame = protocol.build_frame(fin, opcode, payload_len, payload, masking)
构建原始 WebSocket 帧。
protocol.send_frame
语法: bytes, err = protocol.send_frame(socket, fin, opcode, payload, max_payload_len, masking)
发送原始 WebSocket 帧。
自动错误日志记录
默认情况下,底层的 ngx_lua 模块在发生套接字错误时会进行错误日志记录。如果您已经在自己的 Lua 代码中进行了适当的错误处理,则建议通过关闭 ngx_lua 的 lua_socket_log_errors 指令来禁用此自动错误日志记录,即,
lua_socket_log_errors off;
限制
- 此库不能在 init_by_lua、set_by_lua、log_by_lua 和 header_filter_by_lua 等代码上下文中使用,因为 ngx_lua cosocket API 不可用。
resty.websocket对象实例不能在 Lua 模块级别存储在 Lua 变量中,因为它将被同一 nginx 工作进程处理的所有并发请求共享(见 http://wiki.nginx.org/HttpLuaModule#Data_Sharing_within_an_Nginx_Worker),并导致并发请求尝试使用相同的resty.websocket实例时出现不良竞争条件。 您应始终在函数局部变量或ngx.ctx表中初始化resty.websocket对象。这些地方都有各自的数据副本,适用于每个请求。
另见
- 博客文章 WebSockets with OpenResty 作者 Aapo Talvensaari。
- ngx_lua 模块: http://wiki.nginx.org/HttpLuaModule
- WebSocket 协议: http://tools.ietf.org/html/rfc6455
- lua-resty-upload 库
- lua-resty-redis 库
- lua-resty-memcached 库
- lua-resty-mysql 库
GitHub
您可以在 nginx-module-websocket 的 GitHub 仓库 中找到此模块的其他配置提示和文档。