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_lenEspecifica la longitud máxima de payload permitida al enviar y recibir frames de WebSocket. Por defecto es
65535. *max_recv_lenEspecifica la longitud máxima de payload permitida al recibir frames de WebSocket. Por defecto es el valor de
max_payload_len. *max_send_lenEspecifica la longitud máxima de payload permitida al enviar frames de WebSocket. Por defecto es el valor de
max_payload_len. *send_maskedEspecifica si se deben enviar frames de WebSocket enmascarados. Cuando es
true, los frames enmascarados siempre se envían. Por defecto esfalse. *timeoutEspecifica 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_lenEspecifica la longitud máxima de payload permitida al enviar y recibir frames de WebSocket. Por defecto es
65536. *max_recv_lenEspecifica la longitud máxima de payload permitida al recibir frames de WebSocket. Por defecto es el valor de
max_payload_len. *max_send_lenEspecifica la longitud máxima de payload permitida al enviar frames de WebSocket. Por defecto es el valor de
max_payload_len. *send_unmaskedEspecifica si se deben enviar frames de WebSocket no enmascarados. Cuando es
true, los frames no enmascarados siempre se envían. Por defecto esfalse. Sin embargo, el RFC 6455 requiere que el cliente DEBE enviar frames enmascarados al servidor, así que nunca establezcas esta opción entruea menos que sepas lo que estás haciendo. *timeoutEspecifica 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:
-
protocolsEspecifica 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. *
originEspecifica el valor del encabezado de solicitud
Origin. *poolEspecifica 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://`.
-
headersEspecifica 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_certEspecifica 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_certrequiere que se proporcione tambiénclient_priv_key. Consulta a continuación. -
client_priv_keyEspecifica una clave privada que corresponde a la opción
client_certanterior. Estos objetos se pueden crear utilizando la función ngx.ssl.parse_pem_priv_key proporcionada por lua-resty-core. -
hostEspecifica el valor del encabezado
Hostenviado en la solicitud de apretón de manos. Si no se proporciona, el encabezadoHostse derivará del nombre de host/dirección y puerto en la URI de conexión. -
server_nameEspecifica 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
hosto el<host/addr>:<port>de la URI de conexión. -
keyEspecifica el valor del encabezado
Sec-WebSocket-Keyen 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.websocketno 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 deresty.websocket. Siempre debes iniciar objetosresty.websocketen variables locales de función o en la tablangx.ctx. Estos lugares tienen sus propias copias de datos para cada solicitud.
Véase También
- Publicación en el blog WebSockets con OpenResty por Aapo Talvensaari.
- el módulo ngx_lua: http://wiki.nginx.org/HttpLuaModule
- el protocolo websocket: http://tools.ietf.org/html/rfc6455
- la biblioteca lua-resty-upload
- la biblioteca lua-resty-redis
- la biblioteca lua-resty-memcached
- la biblioteca lua-resty-mysql
GitHub
Puedes encontrar consejos de configuración adicionales y documentación para este módulo en el repositorio de GitHub para nginx-module-websocket.