跳转至

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 值和描述错误的字符串。

第二个返回值始终是帧类型,可以是 continuationtextbinaryclosepingpongnil(对于未知类型)。

对于 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_lualua_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 对象。这些地方都有各自的数据副本,适用于每个请求。

另见

GitHub

您可以在 nginx-module-websocket 的 GitHub 仓库 中找到此模块的其他配置提示和文档。