Ir al contenido principal
Versión: 4.x

El protocolo Engine.IO

Este documento describe la 4ta versión del protocolo Engine.IO.

La fuente de este documento se puede encontrar aquí.

Tabla de contenido

Introducción

El protocolo Engine.IO permite comunicación full-duplex y de bajo overhead entre un cliente y un servidor.

Está basado en el protocolo WebSocket y usa HTTP long-polling como fallback si la conexión WebSocket no puede establecerse.

La implementación de referencia está escrita en TypeScript:

El protocolo Socket.IO está construido sobre estos fundamentos, añadiendo características adicionales sobre el canal de comunicación proporcionado por el protocolo Engine.IO.

Transportes

La conexión entre un cliente Engine.IO y un servidor Engine.IO puede establecerse con:

HTTP long-polling

El transporte HTTP long-polling (también referido simplemente como "polling") consiste en solicitudes HTTP sucesivas:

  • solicitudes GET de larga duración, para recibir datos del servidor
  • solicitudes POST de corta duración, para enviar datos al servidor

Ruta de la solicitud

La ruta de las solicitudes HTTP es /engine.io/ por defecto.

Puede ser actualizada por bibliotecas construidas sobre el protocolo (por ejemplo, el protocolo Socket.IO usa /socket.io/).

Parámetros de consulta

Se utilizan los siguientes parámetros de consulta:

NombreValorDescripción
EIO4Obligatorio, la versión del protocolo.
transportpollingObligatorio, el nombre del transporte.
sid<sid>Obligatorio una vez establecida la sesión, el identificador de sesión.

Si falta un parámetro de consulta obligatorio, el servidor DEBE responder con un código de estado HTTP 400.

Cabeceras

Al enviar datos binarios, el emisor (cliente o servidor) DEBE incluir una cabecera Content-Type: application/octet-stream.

Sin una cabecera Content-Type explícita, el receptor DEBERÍA inferir que los datos son texto plano.

Referencia: https://developer.mozilla.org/es/docs/Web/HTTP/Headers/Content-Type

Envío y recepción de datos

Envío de datos

Para enviar algunos paquetes, un cliente DEBE crear una solicitud HTTP POST con los paquetes codificados en el cuerpo de la solicitud:

CLIENT                                                 SERVER

│ │
│ POST /engine.io/?EIO=4&transport=polling&sid=... │
│ ───────────────────────────────────────────────────► │
│ ◄──────────────────────────────────────────────────┘ │
│ HTTP 200 │
│ │

El servidor DEBE devolver una respuesta HTTP 400 si el ID de sesión (del parámetro de consulta sid) no es conocido.

Para indicar éxito, el servidor DEBE devolver una respuesta HTTP 200, con la cadena ok en el cuerpo de la respuesta.

Para asegurar el orden de los paquetes, un cliente NO DEBE tener más de una solicitud POST activa. Si esto sucede, el servidor DEBE devolver un código de estado HTTP 400 y cerrar la sesión.

Recepción de datos

Para recibir algunos paquetes, un cliente DEBE crear una solicitud HTTP GET:

CLIENT                                                SERVER

│ GET /engine.io/?EIO=4&transport=polling&sid=... │
│ ──────────────────────────────────────────────────► │
│ . │
│ . │
│ . │
│ . │
│ ◄─────────────────────────────────────────────────┘ │
│ HTTP 200 │

El servidor DEBE devolver una respuesta HTTP 400 si el ID de sesión (del parámetro de consulta sid) no es conocido.

El servidor PUEDE no responder inmediatamente si no hay paquetes en búfer para la sesión dada. Una vez que hay algunos paquetes para enviar, el servidor DEBERÍA codificarlos (ver Codificación de paquetes) y enviarlos en el cuerpo de la respuesta de la solicitud HTTP.

Para asegurar el orden de los paquetes, un cliente NO DEBE tener más de una solicitud GET activa. Si esto sucede, el servidor DEBE devolver un código de estado HTTP 400 y cerrar la sesión.

WebSocket

El transporte WebSocket consiste en una conexión WebSocket, que proporciona un canal de comunicación bidireccional y de baja latencia entre el servidor y el cliente.

Se utilizan los siguientes parámetros de consulta:

NombreValorDescripción
EIO4Obligatorio, la versión del protocolo.
transportwebsocketObligatorio, el nombre del transporte.
sid<sid>Opcional, dependiendo de si es un upgrade desde HTTP long-polling o no.

Si falta un parámetro de consulta obligatorio, el servidor DEBE cerrar la conexión WebSocket.

Cada paquete (lectura o escritura) se envía en su propio frame WebSocket.

Un cliente NO DEBE abrir más de una conexión WebSocket por sesión. Si esto sucede, el servidor DEBE cerrar la conexión WebSocket.

Protocolo

Un paquete Engine.IO consiste en:

  • un tipo de paquete
  • una carga útil de paquete opcional

Aquí está la lista de tipos de paquetes disponibles:

TipoIDUso
open0Usado durante el handshake.
close1Usado para indicar que un transporte puede cerrarse.
ping2Usado en el mecanismo de heartbeat.
pong3Usado en el mecanismo de heartbeat.
message4Usado para enviar una carga útil al otro lado.
upgrade5Usado durante el proceso de upgrade.
noop6Usado durante el proceso de upgrade.

Handshake

Para establecer una conexión, el cliente DEBE enviar una solicitud HTTP GET al servidor:

  • HTTP long-polling primero (por defecto)
CLIENT                                                    SERVER

│ │
│ GET /engine.io/?EIO=4&transport=polling │
│ ───────────────────────────────────────────────────────► │
│ ◄──────────────────────────────────────────────────────┘ │
│ HTTP 200 │
│ │
  • Sesión solo WebSocket
CLIENT                                                    SERVER

│ │
│ GET /engine.io/?EIO=4&transport=websocket │
│ ───────────────────────────────────────────────────────► │
│ ◄──────────────────────────────────────────────────────┘ │
│ HTTP 101 │
│ │

Si el servidor acepta la conexión, DEBE responder con un paquete open con la siguiente carga útil codificada en JSON:

ClaveTipoDescripción
sidstringEl ID de sesión.
upgradesstring[]La lista de upgrades de transporte disponibles.
pingIntervalnumberEl intervalo de ping, usado en el mecanismo de heartbeat (en milisegundos).
pingTimeoutnumberEl timeout de ping, usado en el mecanismo de heartbeat (en milisegundos).
maxPayloadnumberEl número máximo de bytes por fragmento, usado por el cliente para agregar paquetes en cargas útiles.

Ejemplo:

{
"sid": "lv_VI97HAXpY6yYWAAAC",
"upgrades": ["websocket"],
"pingInterval": 25000,
"pingTimeout": 20000,
"maxPayload": 1000000
}

El cliente DEBE enviar el valor sid en los parámetros de consulta de todas las solicitudes subsiguientes.

Heartbeat

Una vez que el handshake se completa, se inicia un mecanismo de heartbeat para verificar la vivacidad de la conexión:

CLIENT                                                 SERVER

│ *** Handshake *** │
│ │
│ ◄───────────────────────────────────────────────── │
│ 2 │ (paquete ping)
│ ─────────────────────────────────────────────────► │
│ 3 │ (paquete pong)

A un intervalo dado (el valor pingInterval enviado en el handshake) el servidor envía un paquete ping y el cliente tiene unos segundos (el valor pingTimeout) para enviar un paquete pong de vuelta.

Si el servidor no recibe un paquete pong de vuelta, DEBERÍA considerar que la conexión está cerrada.

Inversamente, si el cliente no recibe un paquete ping dentro de pingInterval + pingTimeout, DEBERÍA considerar que la conexión está cerrada.

Upgrade

Por defecto, el cliente DEBERÍA crear una conexión HTTP long-polling, y luego hacer upgrade a mejores transportes si están disponibles.

Para hacer upgrade a WebSocket, el cliente DEBE:

  • pausar el transporte HTTP long-polling (no se envían más solicitudes HTTP), para asegurar que ningún paquete se pierda
  • abrir una conexión WebSocket con el mismo ID de sesión
  • enviar un paquete ping con la cadena probe en la carga útil

El servidor DEBE:

  • enviar un paquete noop a cualquier solicitud GET pendiente (si aplica) para cerrar limpiamente el transporte HTTP long-polling
  • responder con un paquete pong con la cadena probe en la carga útil

Finalmente, el cliente DEBE enviar un paquete upgrade para completar el upgrade:

CLIENT                                                 SERVER

│ │
│ GET /engine.io/?EIO=4&transport=websocket&sid=... │
│ ───────────────────────────────────────────────────► │
│ ◄─────────────────────────────────────────────────┘ │
│ HTTP 101 (WebSocket handshake) │
│ │
│ ----- WebSocket frames ----- │
│ ─────────────────────────────────────────────────► │
│ 2probe │ (paquete ping)
│ ◄───────────────────────────────────────────────── │
│ 3probe │ (paquete pong)
│ ─────────────────────────────────────────────────► │
│ 5 │ (paquete upgrade)
│ │

Message

Una vez que el handshake se completa, el cliente y el servidor pueden intercambiar datos incluyéndolos en un paquete message.

Codificación de paquetes

La serialización de un paquete Engine.IO depende del tipo de carga útil (texto plano o binario) y del transporte.

HTTP long-polling

Debido a la naturaleza del transporte HTTP long-polling, múltiples paquetes pueden concatenarse en una sola carga útil para aumentar el throughput.

Formato:

<tipo de paquete>[<datos>]<separador><tipo de paquete>[<datos>]<separador><tipo de paquete>[<datos>][...]

Ejemplo:

4hello\x1e2\x1e4world

con:

4 => tipo de paquete message
hello => carga útil del mensaje
\x1e => separador
2 => tipo de paquete ping
\x1e => separador
4 => tipo de paquete message
world => carga útil del mensaje

Los paquetes están separados por el carácter separador de registro: \x1e

Las cargas útiles binarias DEBEN estar codificadas en base64 y prefijadas con un carácter b:

Ejemplo:

4hello\x1ebAQIDBA==

con:

4 => tipo de paquete message
hello => carga útil del mensaje
\x1e => separador
b => prefijo binario
AQIDBA== => buffer <01 02 03 04> codificado como base64

El cliente DEBERÍA usar el valor maxPayload enviado durante el handshake para decidir cuántos paquetes deben concatenarse.

WebSocket

Cada paquete Engine.IO se envía en su propio frame WebSocket.

Formato:

<tipo de paquete>[<datos>]

Ejemplo:

4hello

con:

4 => tipo de paquete message
hello => carga útil del mensaje (codificada en UTF-8)

Las cargas útiles binarias se envían tal cual, sin modificación.

Historial

De v2 a v3

  • añadir soporte para datos binarios

La 2da versión del protocolo se usa en Socket.IO v0.9 y anteriores.

La 3ra versión del protocolo se usa en Socket.IO v1 y v2.

De v3 a v4

  • invertir mecanismo ping/pong

Los paquetes ping ahora son enviados por el servidor, porque los temporizadores configurados en los navegadores no son lo suficientemente confiables. Sospechamos que muchos problemas de timeout vinieron de temporizadores retrasados en el lado del cliente.

  • siempre usar base64 al codificar una carga útil con datos binarios

Este cambio permite tratar todas las cargas útiles (con o sin binarios) de la misma manera, sin tener que tomar en cuenta si el cliente o el transporte actual soporta datos binarios o no.

Por favor nota que esto solo aplica a HTTP long-polling. Los datos binarios se envían en frames WebSocket sin transformación adicional.

  • usar un separador de registro (\x1e) en lugar de contar caracteres

Contar caracteres impedía (o al menos hacía más difícil) implementar el protocolo en otros lenguajes, que pueden no usar la codificación UTF-16.

Por ejemplo, se codificaba como 2:4€, aunque Buffer.byteLength('€') === 3.

Nota: esto asume que el separador de registro no se usa en los datos.

La 4ta versión (actual) está incluida en Socket.IO v3 y superiores.

Suite de pruebas

La suite de pruebas en el directorio test-suite/ te permite verificar la conformidad de una implementación de servidor.

Uso:

  • en Node.js: npm ci && npm test
  • en un navegador: simplemente abre el archivo index.html en tu navegador

Para referencia, aquí está la configuración esperada para que el servidor JavaScript pase todas las pruebas:

import { listen } from "engine.io";

const server = listen(3000, {
pingInterval: 300,
pingTimeout: 200,
maxPayload: 1e6,
cors: {
origin: "*"
}
});

server.on("connection", socket => {
socket.on("data", (...args) => {
socket.send(...args);
});
});