lua-resty-websocket

用于 ngx_lua 模块的 Lua WebSocket 实现

$ opm get openresty/lua-resty-websocket

名称

lua-resty-websocket - 用于 ngx_lua 模块的 Lua WebSocket 实现

状态

该库被认为是生产就绪的。

描述

该 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,  -- in milliseconds
            max_payload_len = 65535,
        }
        if not wb then
            ngx.log(ngx.ERR, "failed to new 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, "failed to receive a frame: ", err)
                return ngx.exit(444)
            end
        end
    
        if typ == "close" then
            -- for typ "close", err contains the status code
            local code = err
    
            -- send a close frame back:
    
            local bytes, err = wb:send_close(1000, "enough, enough!")
            if not bytes then
                ngx.log(ngx.ERR, "failed to send the close frame: ", err)
                return
            end
            ngx.log(ngx.INFO, "closing with status code ", code, " and message ", data)
            return
        end
    
        if typ == "ping" then
            -- send a pong frame back:
    
            local bytes, err = wb:send_pong(data)
            if not bytes then
                ngx.log(ngx.ERR, "failed to send frame: ", err)
                return
            end
        elseif typ == "pong" then
            -- just discard the incoming pong frame
    
        else
            ngx.log(ngx.INFO, "received a frame of type ", typ, " and payload ", data)
        end
    
        wb:set_timeout(1000)  -- change the network timeout to 1 second
    
        bytes, err = wb:send_text("Hello world")
        if not bytes then
            ngx.log(ngx.ERR, "failed to send a text frame: ", err)
            return ngx.exit(444)
        end
    
        bytes, err = wb:send_binary("blah blah blah...")
        if not bytes then
            ngx.log(ngx.ERR, "failed to send a binary frame: ", err)
            return ngx.exit(444)
        end
    
        local bytes, err = wb:send_close(1000, "enough, enough!")
        if not bytes then
            ngx.log(ngx.ERR, "failed to send the close frame: ", 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 帧时允许的最大有效载荷长度。

  • 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 = wb:connect(uri)
        if not ok then
            ngx.say("failed to connect: " .. err)
            return
        end
    
        local data, typ, err = wb:recv_frame()
        if not data then
            ngx.say("failed to receive the frame: ", err)
            return
        end
    
        ngx.say("received: ", data, " (", typ, "): ", err)
    
        local bytes, err = wb:send_text("copy: " .. data)
        if not bytes then
            ngx.say("failed to send frame: ", err)
            return
        end
    
        local bytes, err = wb:send_close()
        if not bytes then
            ngx.say("failed to send frame: ", err)
            return
        end

方法

client:new

语法:wb, err = client:new()

语法:wb, err = client:new(opts)

实例化一个 WebSocket 客户端对象。

如果发生错误,则返回 nil 和一个描述错误的字符串。

可以指定一个可选的选项表。以下选项如下所示

  • max_payload_len

    指定发送和接收 WebSocket 帧时允许的最大有效载荷长度。

  • send_unmasked

    指定是否发送未屏蔽的 WebSocket 帧。当它为 true 时,始终发送未屏蔽的帧。默认为 false。但是,RFC 6455 要求客户端必须向服务器发送屏蔽帧,因此除非您知道自己在做什么,否则永远不要将此选项设置为 true

  • timeout

    指定默认的网络超时阈值,以毫秒为单位。您可以通过 set_timeout 方法调用更改此设置。

client:connect

语法:ok, err = wb:connect("ws://<host>:<port>/<path>")

语法:ok, err = wb:connect("wss://<host>:<port>/<path>")

语法:ok, err = wb:connect("ws://<host>:<port>/<path>", options)

语法:ok, err = wb:connect("wss://<host>:<port>/<path>", options)

连接到远程 WebSocket 服务端口,并在客户端执行 websocket 握手过程。

在实际解析主机名并连接到远程后端之前,此方法将始终查找连接池以匹配先前此方法调用创建的空闲连接。

可以指定一个可选的 Lua 表作为此方法的最后一个参数,以指定各种连接选项

  • protocols

    指定当前 WebSocket 会话使用的所有子协议。它可以是包含所有子协议名称的 Lua 表,也可以只是一个 Lua 字符串。

  • origin

    指定 Origin 请求头的值。

  • pool

    指定正在使用的连接池的自定义名称。如果省略,则连接池名称将从字符串模板 <host>:<port> 生成。

  • ssl_verify

    指定如果使用 wss:// 方案,是否在 SSL 握手期间执行 SSL 证书验证。

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"

方法

recv_frame

语法:data, typ, err = protocol.recv_frame(socket, max_payload_len, force_masking)

从网络接收 WebSocket 帧。

build_frame

语法:frame = protocol.build_frame(fin, opcode, payload_len, payload, masking)

构建一个原始 WebSocket 帧。

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 对象。这些位置都为每个请求保存了它们自己的数据副本。

安装

建议直接使用最新的 OpenResty bundle,其中包含此库并默认启用。至少需要 OpenResty 1.4.2.9。在构建 OpenResty bundle 时,您需要通过将 --with-luajit 选项传递给它的 ./configure 脚本来启用 LuaJIT。不需要额外的 Nginx 配置。

如果您想将此库与您自己的 Nginx 版本(使用 ngx_lua)一起使用,那么您需要确保您至少使用的是 ngx_lua 0.9.0(如果您没有使用 LuaJIT,还需要使用 lua-bitop 库)。此外,您需要配置 lua_package_path 指令,将 lua-resty-websocket 源代码树的路径添加到 ngx_lua 的 Lua 模块搜索路径中,例如

        # nginx.conf
        http {
            lua_package_path "/path/to/lua-resty-websocket/lib/?.lua;;";
            ...
        }

然后在 Lua 中加载库

        local server = require "resty.websocket.server"

待办事项

社区

英文邮件列表

英文邮件列表 openresty-en 适用于英语使用者。

中文邮件列表

中文邮件列表 openresty 适用于中文使用者。

错误报告和补丁提交

请通过以下方式报告错误或提交补丁:

  1. GitHub Issue Tracker 上创建工单,

  2. 或发布到 OpenResty 社区

作者

章亦春 (agentzh) <[email protected]>,OpenResty Inc.

版权和许可

此模块采用 BSD 许可证。

版权所有 (C) 2013-2017,章亦春 (agentzh) <[email protected]>,OpenResty Inc.

保留所有权利。

只要满足以下条件,则允许以源代码和二进制形式重新分发和使用此软件,无论是否修改。

  • 重新分发源代码必须保留上述版权声明、此条件列表以及以下免责声明。

  • 以二进制形式重新分发必须在随发行版提供的文档和/或其他材料中复制上述版权声明、此条件列表以及以下免责声明。

此软件由版权所有者和贡献者“按原样”提供,并且任何明示或暗示的保证(包括但不限于适销性和特定用途适用性的暗示保证)均被免除。在任何情况下,版权所有者或贡献者均不对任何直接、间接、偶然、特殊、示例性或后果性损害(包括但不限于替代商品或服务的采购;使用、数据或利润损失;或业务中断)负责,无论这些损害是因本软件的使用引起,还是基于任何责任理论,无论是在合同、严格责任或侵权行为(包括疏忽或其他)中,即使已告知有发生此类损害的可能性。

另请参阅

作者

章亦春 (agentzh)

许可证

2bsd

版本