Saltar a contenido

websocket: Soporte de WebSocket para el módulo nginx-module-lua

Instalación

Si aún no has configurado la suscripción al repositorio RPM, regístrate. Luego puedes proceder con los siguientes pasos.

CentOS/RHEL 7 o 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

Para usar esta biblioteca Lua con NGINX, asegúrate de que nginx-module-lua esté instalado.

Este documento describe lua-resty-websocket v0.13 lanzado el 11 de febrero de 2025.


Esta biblioteca Lua implementa un servidor WebSocket y bibliotecas de cliente basadas en el módulo ngx_lua.

Esta biblioteca Lua aprovecha la API de cosocket de ngx_lua, que garantiza un comportamiento 100% no bloqueante.

Ten en cuenta que solo se admite RFC 6455. Revisiones anteriores del protocolo como "hybi-10", "hybi-07" y "hybi-00" no son y no serán consideradas.

Sinopsis

    local server = require "resty.websocket.server"

    local wb, err = server:new{
        timeout = 5000,  -- en milisegundos
        max_payload_len = 65535,
    }
    if not wb then
        ngx.log(ngx.ERR, "falló al crear el 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, "falló al recibir un frame: ", err)
            return ngx.exit(444)
        end
    end

    if typ == "close" then
        -- para el tipo "close", err contiene el código de estado
        local code = err

        -- enviar un frame de cierre de vuelta:

        local bytes, err = wb:send_close(1000, "¡suficiente, suficiente!")
        if not bytes then
            ngx.log(ngx.ERR, "falló al enviar el frame de cierre: ", err)
            return
        end
        ngx.log(ngx.INFO, "cerrando con código de estado ", code, " y mensaje ", data)
        return
    end

    if typ == "ping" then
        -- enviar un frame de pong de vuelta:

        local bytes, err = wb:send_pong(data)
        if not bytes then
            ngx.log(ngx.ERR, "falló al enviar el frame: ", err)
            return
        end
    elseif typ == "pong" then
        -- simplemente descartar el frame de pong entrante

    else
        ngx.log(ngx.INFO, "recibido un frame de tipo ", typ, " y payload ", data)
    end

    wb:set_timeout(1000)  -- cambiar el tiempo de espera de la red a 1 segundo

    bytes, err = wb:send_text("Hola mundo")
    if not bytes then
        ngx.log(ngx.ERR, "falló al enviar un frame de texto: ", err)
        return ngx.exit(444)
    end

    bytes, err = wb:send_binary("blah blah blah...")
    if not bytes then
        ngx.log(ngx.ERR, "falló al enviar un frame binario: ", err)
        return ngx.exit(444)
    end

    local bytes, err = wb:send_close(1000, "¡suficiente, suficiente!")
    if not bytes then
        ngx.log(ngx.ERR, "falló al enviar el frame de cierre: ", err)
        return
    end

Módulos

resty.websocket.server

Para cargar este módulo, simplemente haz esto

    local server = require "resty.websocket.server"

Métodos

new

syntax: wb, err = server:new()

syntax: wb, err = server:new(opts)

Realiza el proceso de apretón de manos del websocket en el lado del servidor y devuelve un objeto servidor WebSocket.

En caso de error, devuelve nil y una cadena que describe el error.

Se puede especificar una tabla de opciones opcional. Las siguientes opciones son las siguientes:

  • max_payload_len

    Especifica la longitud máxima de payload permitida al enviar y recibir frames de WebSocket. Por defecto es 65535. * max_recv_len

    Especifica la longitud máxima de payload permitida al recibir frames de WebSocket. Por defecto es el valor de max_payload_len. * max_send_len

    Especifica la longitud máxima de payload permitida al enviar frames de WebSocket. Por defecto es el valor de max_payload_len. * send_masked

    Especifica si se deben enviar frames de WebSocket enmascarados. Cuando es true, los frames enmascarados siempre se envían. Por defecto es false. * timeout

    Especifica el umbral de tiempo de espera de la red en milisegundos. Puedes cambiar esta configuración más tarde a través de la llamada al método set_timeout. Ten en cuenta que esta configuración de tiempo de espera no afecta el proceso de envío de encabezados de respuesta HTTP para el apretón de manos de websocket; necesitas configurar la directiva send_timeout al mismo tiempo.

set_timeout

syntax: wb:set_timeout(ms)

Establece el retraso de tiempo de espera (en milisegundos) para las operaciones relacionadas con la red.

send_text

syntax: bytes, err = wb:send_text(text)

Envía el argumento text como un frame de datos no fragmentado del tipo text. Devuelve el número de bytes que realmente se han enviado a nivel TCP.

En caso de errores, devuelve nil y una cadena que describe el error.

send_binary

syntax: bytes, err = wb:send_binary(data)

Envía el argumento data como un frame de datos no fragmentado del tipo binary. Devuelve el número de bytes que realmente se han enviado a nivel TCP.

En caso de errores, devuelve nil y una cadena que describe el error.

send_ping

syntax: bytes, err = wb:send_ping()

syntax: bytes, err = wb:send_ping(msg)

Envía un frame de ping con un mensaje opcional especificado por el argumento msg. Devuelve el número de bytes que realmente se han enviado a nivel TCP.

En caso de errores, devuelve nil y una cadena que describe el error.

Ten en cuenta que este método no espera un frame de pong del extremo remoto.

send_pong

syntax: bytes, err = wb:send_pong()

syntax: bytes, err = wb:send_pong(msg)

Envía un frame de pong con un mensaje opcional especificado por el argumento msg. Devuelve el número de bytes que realmente se han enviado a nivel TCP.

En caso de errores, devuelve nil y una cadena que describe el error.

send_close

syntax: bytes, err = wb:send_close()

syntax: bytes, err = wb:send_close(code, msg)

Envía un frame de close con un código de estado y un mensaje opcionales.

En caso de errores, devuelve nil y una cadena que describe el error.

Para una lista de códigos de estado válidos, consulta el siguiente documento:

http://tools.ietf.org/html/rfc6455#section-7.4.1

Ten en cuenta que este método no espera un frame de close del extremo remoto.

send_frame

syntax: bytes, err = wb:send_frame(fin, opcode, payload)

Envía un frame de websocket en bruto especificando el campo fin (valor booleano), el opcode y el payload.

Para una lista de opcodes válidos, consulta

http://tools.ietf.org/html/rfc6455#section-5.2

En caso de errores, devuelve nil y una cadena que describe el error.

Para controlar la longitud máxima de payload permitida, puedes pasar la opción max_payload_len al constructor new.

Para controlar si se deben enviar frames enmascarados, puedes pasar true a la opción send_masked en el método del constructor new. Por defecto, se envían frames no enmascarados.

recv_frame

syntax: data, typ, err = wb:recv_frame()

Recibe un frame de WebSocket de la red.

En caso de error, devuelve dos valores nil y una cadena que describe el error.

El segundo valor de retorno es siempre el tipo de frame, que podría ser uno de continuation, text, binary, close, ping, pong, o nil (para tipos desconocidos).

Para frames de close, devuelve 3 valores: el mensaje de estado adicional (que podría ser una cadena vacía), la cadena "close", y un número Lua para el código de estado (si lo hay). Para posibles códigos de estado de cierre, consulta

http://tools.ietf.org/html/rfc6455#section-7.4.1

Para otros tipos de frames, simplemente devuelve el payload y el tipo.

Para frames fragmentados, el valor de retorno err es la cadena Lua "again".

resty.websocket.client

Para cargar este módulo, simplemente haz esto

    local client = require "resty.websocket.client"

Un ejemplo simple para demostrar el uso:

    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("falló al conectar: " .. err)
        return
    end

    local data, typ, err = wb:recv_frame()
    if not data then
        ngx.say("falló al recibir el frame: ", err)
        return
    end

    ngx.say("recibido: ", data, " (", typ, "): ", err)

    local bytes, err = wb:send_text("copia: " .. data)
    if not bytes then
        ngx.say("falló al enviar el frame: ", err)
        return
    end

    local bytes, err = wb:send_close()
    if not bytes then
        ngx.say("falló al enviar el frame: ", err)
        return
    end

Métodos

client:new

syntax: wb, err = client:new()

syntax: wb, err = client:new(opts)

Instancia un objeto cliente WebSocket.

En caso de error, devuelve nil y una cadena que describe el error.

Se puede especificar una tabla de opciones opcional. Las siguientes opciones son las siguientes:

  • max_payload_len

    Especifica la longitud máxima de payload permitida al enviar y recibir frames de WebSocket. Por defecto es 65536. * max_recv_len

    Especifica la longitud máxima de payload permitida al recibir frames de WebSocket. Por defecto es el valor de max_payload_len. * max_send_len

    Especifica la longitud máxima de payload permitida al enviar frames de WebSocket. Por defecto es el valor de max_payload_len. * send_unmasked

    Especifica si se deben enviar frames de WebSocket no enmascarados. Cuando es true, los frames no enmascarados siempre se envían. Por defecto es false. Sin embargo, el RFC 6455 requiere que el cliente DEBE enviar frames enmascarados al servidor, así que nunca establezcas esta opción en true a menos que sepas lo que estás haciendo. * timeout

    Especifica el umbral de tiempo de espera de red predeterminado en milisegundos. Puedes cambiar esta configuración más tarde a través de la llamada al método set_timeout.

client:connect

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

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

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

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

Conecta al puerto del servicio WebSocket remoto y realiza el proceso de apretón de manos del websocket en el lado del cliente.

Antes de resolver realmente el nombre del host y conectarse al backend remoto, este método siempre buscará en el grupo de conexiones conexiones inactivas coincidentes creadas por llamadas anteriores a este método.

El tercer valor de retorno de este método contiene la respuesta en texto plano sin procesar (línea de estado y encabezados) a la solicitud de apretón de manos. Esto permite al llamador realizar validaciones adicionales y/o extraer los encabezados de respuesta. Cuando la conexión se reutiliza y no se envía una solicitud de apretón de manos, se devuelve la cadena "conexión reutilizada" en lugar de la respuesta.

Se puede especificar una tabla Lua opcional como último argumento a este método para especificar varias opciones de conexión:

  • protocols

    Especifica todos los subprotocolos utilizados para la sesión WebSocket actual. Podría ser una tabla Lua que contenga todos los nombres de subprotocolos o simplemente una cadena Lua única. * origin

    Especifica el valor del encabezado de solicitud Origin. * pool

    Especifica un nombre personalizado para el grupo de conexiones que se está utilizando. Si se omite, el nombre del grupo de conexiones se generará a partir de la plantilla de cadena <host>:<port>. * pool_size

especifica el tamaño del grupo de conexiones. Si se omite y no se proporciona una opción backlog, no se creará ningún grupo. Si se omite pero se proporciona backlog, el grupo se creará con un tamaño predeterminado igual al valor de la directiva lua_socket_pool_size. El grupo de conexiones mantiene hasta pool_size conexiones vivas listas para ser reutilizadas por llamadas posteriores a connect, pero ten en cuenta que no hay un límite superior para el número total de conexiones abiertas fuera del grupo. Si necesitas restringir el número total de conexiones abiertas, especifica la opción backlog. Cuando el grupo de conexiones exceda su límite de tamaño, la conexión menos utilizada (mantenida viva) que ya está en el grupo se cerrará para hacer espacio para la conexión actual. Ten en cuenta que el grupo de conexiones de cosocket es por proceso de trabajo de Nginx en lugar de por instancia de servidor Nginx, por lo que el límite de tamaño especificado aquí también se aplica a cada proceso de trabajo de Nginx. También ten en cuenta que el tamaño del grupo de conexiones no se puede cambiar una vez que ha sido creado. Esta opción se introdujo por primera vez en la versión v0.10.14.

  • backlog

si se especifica, este módulo limitará el número total de conexiones abiertas para este grupo. No se pueden abrir más conexiones que pool_size para este grupo en ningún momento. Si el grupo de conexiones está lleno, las operaciones de conexión posteriores se encolarán en una cola igual al valor de esta opción (la cola de "backlog"). Si el número de operaciones de conexión en cola es igual a backlog, las operaciones de conexión posteriores fallarán y devolverán nil más la cadena de error "demasiadas operaciones de conexión en espera". Las operaciones de conexión en cola se reanudarán una vez que el número de conexiones en el grupo sea menor que pool_size. La operación de conexión en cola se abortará una vez que hayan estado en cola por más de connect_timeout, controlado por settimeouts, y devolverá nil más la cadena de error "timeout". Esta opción se introdujo por primera vez en la versión v0.10.14. * ssl_verify

Especifica si se debe realizar la verificación del certificado SSL durante el apretón de manos SSL si se utiliza el esquema `wss://`.
  • headers

    Especifica encabezados personalizados que se enviarán en la solicitud de apretón de manos. Se espera que la tabla contenga cadenas en el formato {"a-header: un valor de encabezado", "another-header: otro valor de encabezado"}.

  • client_cert

    Especifica un objeto cdata de cadena de certificado de cliente que se utilizará durante el apretón de manos TLS con el servidor remoto. Estos objetos se pueden crear utilizando la función ngx.ssl.parse_pem_cert proporcionada por lua-resty-core. Ten en cuenta que especificar la opción client_cert requiere que se proporcione también client_priv_key. Consulta a continuación.

  • client_priv_key

    Especifica una clave privada que corresponde a la opción client_cert anterior. Estos objetos se pueden crear utilizando la función ngx.ssl.parse_pem_priv_key proporcionada por lua-resty-core.

  • host

    Especifica el valor del encabezado Host enviado en la solicitud de apretón de manos. Si no se proporciona, el encabezado Host se derivará del nombre de host/dirección y puerto en la URI de conexión.

  • server_name

    Especifica el nombre del servidor (SNI) que se utilizará al realizar el apretón de manos TLS con el servidor. Si no se proporciona, se utilizará el valor de host o el <host/addr>:<port> de la URI de conexión.

  • key

    Especifica el valor del encabezado Sec-WebSocket-Key en la solicitud de apretón de manos. El valor debe ser una cadena de 16 bytes codificada en base64 que cumpla con los requisitos de apretón de manos del cliente del WebSocket RFC. Si no se proporciona, se genera una clave aleatoriamente.

El modo de conexión SSL (wss://) requiere al menos ngx_lua 0.9.11 o OpenResty 1.7.4.1.

client:close

syntax: ok, err = wb:close()

Cierra la conexión WebSocket actual. Si aún no se ha enviado un frame de close, entonces se enviará automáticamente el frame de close.

client:set_keepalive

syntax: ok, err = wb:set_keepalive(max_idle_timeout, pool_size)

Coloca la conexión WebSocket actual inmediatamente en el grupo de conexiones cosocket de ngx_lua.

Puedes especificar el tiempo máximo de inactividad (en ms) cuando la conexión está en el grupo y el tamaño máximo del grupo para cada proceso de trabajo de nginx.

En caso de éxito, devuelve 1. En caso de errores, devuelve nil con una cadena que describe el error.

Solo llama a este método en el lugar donde habrías llamado al método close en su lugar. Llamar a este método cambiará inmediatamente el objeto WebSocket actual al estado closed. Cualquier operación posterior distinta de connect() en el objeto actual devolverá el error closed.

client:set_timeout

syntax: wb:set_timeout(ms)

Idéntico al método set_timeout de los objetos resty.websocket.server.

client:send_text

syntax: bytes, err = wb:send_text(text)

Idéntico al método send_text de los objetos resty.websocket.server.

client:send_binary

syntax: bytes, err = wb:send_binary(data)

Idéntico al método send_binary de los objetos resty.websocket.server.

client:send_ping

syntax: bytes, err = wb:send_ping()

syntax: bytes, err = wb:send_ping(msg)

Idéntico al método send_ping de los objetos resty.websocket.server.

client:send_pong

syntax: bytes, err = wb:send_pong()

syntax: bytes, err = wb:send_pong(msg)

Idéntico al método send_pong de los objetos resty.websocket.server.

client:send_close

syntax: bytes, err = wb:send_close()

syntax: bytes, err = wb:send_close(code, msg)

Idéntico al método send_close de los objetos resty.websocket.server.

client:send_frame

syntax: bytes, err = wb:send_frame(fin, opcode, payload)

Idéntico al método send_frame de los objetos resty.websocket.server.

Para controlar si se deben enviar frames no enmascarados, puedes pasar true a la opción send_unmasked en el método del constructor new. Por defecto, se envían frames enmascarados.

client:recv_frame

syntax: data, typ, err = wb:recv_frame()

Idéntico al método recv_frame de los objetos resty.websocket.server.

resty.websocket.protocol

Para cargar este módulo, simplemente haz esto

    local protocol = require "resty.websocket.protocol"

Métodos

protocol.recv_frame

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

Recibe un frame de WebSocket de la red.

protocol.build_frame

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

Construye un frame de WebSocket en bruto.

protocol.send_frame

syntax: bytes, err = protocol.send_frame(socket, fin, opcode, payload, max_payload_len, masking)

Envía un frame de WebSocket en bruto.

Registro Automático de Errores

Por defecto, el módulo subyacente ngx_lua realiza el registro de errores cuando ocurren errores de socket. Si ya estás manejando adecuadamente los errores en tu propio código Lua, se recomienda desactivar este registro automático de errores desactivando la directiva lua_socket_log_errors de ngx_lua, es decir,

    lua_socket_log_errors off;

Limitaciones

  • Esta biblioteca no se puede utilizar en contextos de código como init_by_lua, set_by_lua, log_by_lua, y header_filter_by_lua donde la API de cosocket de ngx_lua no está disponible.
  • La instancia del objeto resty.websocket no se puede almacenar en una variable Lua a nivel de módulo Lua, porque luego será compartida por todas las solicitudes concurrentes manejadas por el mismo proceso de trabajo de nginx (ver http://wiki.nginx.org/HttpLuaModule#Data_Sharing_within_an_Nginx_Worker) y resultará en malas condiciones de carrera cuando las solicitudes concurrentes intenten usar la misma instancia de resty.websocket. Siempre debes iniciar objetos resty.websocket en variables locales de función o en la tabla ngx.ctx. Estos lugares tienen sus propias copias de datos para cada solicitud.

Véase También

GitHub

Puedes encontrar consejos de configuración adicionales y documentación para este módulo en el repositorio de GitHub para nginx-module-websocket.