El protocolo Socket.IO
Este documento describe la 5ta versión del protocolo Socket.IO.
La fuente de este documento se puede encontrar aquí.
Tabla de contenido
- Introducción
- Protocolo de intercambio
- Codificación de paquetes
- Sesión de ejemplo
- Historial
- Suite de pruebas
Introducción
El protocolo Socket.IO permite comunicación full-duplex y de bajo overhead entre un cliente y un servidor.
Está construido sobre el protocolo Engine.IO, que maneja la plomería de bajo nivel con WebSocket y HTTP long-polling.
El protocolo Socket.IO añade las siguientes características:
- multiplexación (referida como "namespace" en la jerga de Socket.IO)
Ejemplo con la API de JavaScript:
Servidor
// declarar el namespace
const namespace = io.of("/admin");
// manejar la conexión al namespace
namespace.on("connection", (socket) => {
// ...
});
Cliente
// alcanzar el namespace principal
const socket1 = io();
// alcanzar el namespace "/admin" (con la misma conexión WebSocket subyacente)
const socket2 = io("/admin");
// manejar la conexión al namespace
socket2.on("connect", () => {
// ...
});
- acknowledgement de paquetes
Ejemplo con la API de JavaScript:
// en un lado
socket.emit("hello", "foo", (arg) => {
console.log("recibido", arg);
});
// en el otro lado
socket.on("hello", (arg, ack) => {
ack("bar");
});
La implementación de referencia está escrita en TypeScript:
- servidor: https://github.com/socketio/socket.io
- cliente: https://github.com/socketio/socket.io-client
Protocolo de intercambio
Un paquete Socket.IO contiene los siguientes campos:
- un tipo de paquete (entero)
- un namespace (cadena)
- opcionalmente, una carga útil (Object | Array)
- opcionalmente, un id de acknowledgment (entero)
Aquí está la lista de tipos de paquetes disponibles:
| Tipo | ID | Uso |
|---|---|---|
| CONNECT | 0 | Usado durante la conexión a un namespace. |
| DISCONNECT | 1 | Usado al desconectarse de un namespace. |
| EVENT | 2 | Usado para enviar datos al otro lado. |
| ACK | 3 | Usado para acknowledger un evento. |
| CONNECT_ERROR | 4 | Usado durante la conexión a un namespace. |
| BINARY_EVENT | 5 | Usado para enviar datos binarios al otro lado. |
| BINARY_ACK | 6 | Usado para acknowledger un evento (la respuesta incluye datos binarios). |
Conexión a un namespace
Al comienzo de una sesión Socket.IO, el cliente DEBE enviar un paquete CONNECT:
El servidor DEBE responder con:
- un paquete
CONNECTsi la conexión es exitosa, con el ID de sesión en la carga útil - o un paquete
CONNECT_ERRORsi la conexión no está permitida
CLIENT SERVER
│ ───────────────────────────────────────────────────────► │
│ { type: CONNECT, namespace: "/" } │
│ ◄─────────────────────────────────────────────────────── │
│ { type: CONNECT, namespace: "/", data: { sid: "..." } } │
Si el servidor no recibe un paquete CONNECT primero, DEBE cerrar la conexión inmediatamente.
Un cliente PUEDE estar conectado a múltiples namespaces al mismo tiempo, con la misma conexión WebSocket subyacente.
Ejemplos:
- con el namespace principal (llamado
"/")
Client > { type: CONNECT, namespace: "/" }
Server > { type: CONNECT, namespace: "/", data: { sid: "wZX3oN0bSVIhsaknAAAI" } }
- con un namespace personalizado
Client > { type: CONNECT, namespace: "/admin" }
Server > { type: CONNECT, namespace: "/admin", data: { sid: "oSO0OpakMV_3jnilAAAA" } }
- con una carga útil adicional
Client > { type: CONNECT, namespace: "/admin", data: { "token": "123" } }
Server > { type: CONNECT, namespace: "/admin", data: { sid: "iLnRaVGHY4B75TeVAAAB" } }
- en caso de que la conexión sea rechazada
Client > { type: CONNECT, namespace: "/" }
Server > { type: CONNECT_ERROR, namespace: "/", data: { message: "No autorizado" } }
Envío y recepción de datos
Una vez que la conexión a un namespace está establecida, el cliente y el servidor pueden comenzar a intercambiar datos:
CLIENT SERVER
│ ───────────────────────────────────────────────────────► │
│ { type: EVENT, namespace: "/", data: ["foo"] } │
│ │
│ ◄─────────────────────────────────────────────────────── │
│ { type: EVENT, namespace: "/", data: ["bar"] } │
La carga útil es obligatoria y DEBE ser un array no vacío. Si ese no es el caso, el receptor DEBE cerrar la conexión.
Ejemplos:
- con el namespace principal
Client > { type: EVENT, namespace: "/", data: ["foo"] }
- con un namespace personalizado
Server > { type: EVENT, namespace: "/admin", data: ["bar"] }
- con datos binarios
Client > { type: BINARY_EVENT, namespace: "/", data: ["baz", <Buffer <01 02 03 04>> ] }
Acknowledgement
El emisor PUEDE incluir un ID de evento para solicitar un acknowledgement del receptor:
CLIENT SERVER
│ ───────────────────────────────────────────────────────► │
│ { type: EVENT, namespace: "/", data: ["foo"], id: 12 } │
│ ◄─────────────────────────────────────────────────────── │
│ { type: ACK, namespace: "/", data: ["bar"], id: 12 } │
El receptor DEBE responder con un paquete ACK con el mismo ID de evento.
La carga útil es obligatoria y DEBE ser un array (posiblemente vacío).
Ejemplos:
- con el namespace principal
Client > { type: EVENT, namespace: "/", data: ["foo"], id: 12 }
Server > { type: ACK, namespace: "/", data: [], id: 12 }
- con un namespace personalizado
Server > { type: EVENT, namespace: "/admin", data: ["foo"], id: 13 }
Client > { type: ACK, namespace: "/admin", data: ["bar"], id: 13 }
- con datos binarios
Client > { type: BINARY_EVENT, namespace: "/", data: ["foo", <buffer <01 02 03 04> ], id: 14 }
Server > { type: ACK, namespace: "/", data: ["bar"], id: 14 }
o
Server > { type: EVENT, namespace: "/", data: ["foo" ], id: 15 }
Client > { type: BINARY_ACK, namespace: "/", data: ["bar", <buffer <01 02 03 04>], id: 15 }
Desconexión de un namespace
En cualquier momento, un lado puede terminar la conexión a un namespace enviando un paquete DISCONNECT:
CLIENT SERVER
│ ───────────────────────────────────────────────────────► │
│ { type: DISCONNECT, namespace: "/" } │
No se espera respuesta del otro lado. La conexión de bajo nivel PUEDE mantenerse activa si el cliente está conectado a otro namespace.
Codificación de paquetes
Esta sección detalla la codificación usada por el parser por defecto que está incluido en el servidor y cliente Socket.IO, y cuya fuente se puede encontrar aquí.
Las implementaciones de servidor y cliente en JavaScript también soportan parsers personalizados, que tienen diferentes compromisos y pueden beneficiar a ciertos tipos de aplicaciones. Por favor consulta socket.io-json-parser o socket.io-msgpack-parser como ejemplo.
Por favor también nota que cada paquete Socket.IO se envía como un paquete message de Engine.IO (más información aquí), así que el resultado codificado será prefijado por el carácter "4" cuando se envíe por el cable (en el cuerpo de solicitud/respuesta con HTTP long-polling, o en el frame WebSocket).
Formato
<tipo de paquete>[<# de adjuntos binarios>-][<namespace>,][<id de acknowledgment>][carga útil JSON-stringified sin binarios]
+ adjuntos binarios extraídos
Nota: el namespace solo se incluye si es diferente del namespace principal (/)
Ejemplos
Conexión a un namespace
- con el namespace principal
Paquete
{ type: CONNECT, namespace: "/" }
Codificado
0
- con un namespace personalizado
Paquete
{ type: CONNECT, namespace: "/admin", data: { sid: "oSO0OpakMV_3jnilAAAA" } }
Codificado
0/admin,{"sid":"oSO0OpakMV_3jnilAAAA"}
- en caso de que la conexión sea rechazada
Paquete
{ type: CONNECT_ERROR, namespace: "/", data: { message: "No autorizado" } }
Codificado
4{"message":"No autorizado"}
Envío y recepción de datos
- con el namespace principal
Paquete
{ type: EVENT, namespace: "/", data: ["foo"] }
Codificado
2["foo"]
- con un namespace personalizado
Paquete
{ type: EVENT, namespace: "/admin", data: ["bar"] }
Codificado
2/admin,["bar"]
- con datos binarios
Paquete
{ type: BINARY_EVENT, namespace: "/", data: ["baz", <Buffer <01 02 03 04>> ] }
Codificado
51-["baz",{"_placeholder":true,"num":0}]
+ <Buffer <01 02 03 04>>
- con múltiples adjuntos
Paquete
{ type: BINARY_EVENT, namespace: "/admin", data: ["baz", <Buffer <01 02>>, <Buffer <03 04>> ] }
Codificado
52-/admin,["baz",{"_placeholder":true,"num":0},{"_placeholder":true,"num":1}]
+ <Buffer <01 02>>
+ <Buffer <03 04>>
Por favor recuerda que cada paquete Socket.IO está envuelto en un paquete message de Engine.IO, así que serán prefijados por el carácter "4" cuando se envíen por el cable.
Ejemplo: { type: EVENT, namespace: "/", data: ["foo"] } se enviará como 42["foo"]
Acknowledgement
- con el namespace principal
Paquete
{ type: EVENT, namespace: "/", data: ["foo"], id: 12 }
Codificado
212["foo"]
- con un namespace personalizado
Paquete
{ type: ACK, namespace: "/admin", data: ["bar"], id: 13 }
Codificado
3/admin,13["bar"]`
- con datos binarios
Paquete
{ type: BINARY_ACK, namespace: "/", data: ["bar", <Buffer <01 02 03 04>>], id: 15 }
Codificado
61-15["bar",{"_placeholder":true,"num":0}]
+ <Buffer <01 02 03 04>>
Desconexión de un namespace
- con el namespace principal
Paquete
{ type: DISCONNECT, namespace: "/" }
Codificado
1
- con un namespace personalizado
{ type: DISCONNECT, namespace: "/admin" }
Codificado
1/admin,
Sesión de ejemplo
Aquí hay un ejemplo de lo que se envía por el cable al combinar los protocolos Engine.IO y Socket.IO.
- Solicitud n°1 (paquete open)
GET /socket.io/?EIO=4&transport=polling&t=N8hyd6w
< HTTP/1.1 200 OK
< Content-Type: text/plain; charset=UTF-8
0{"sid":"lv_VI97HAXpY6yYWAAAC","upgrades":["websocket"],"pingInterval":25000,"pingTimeout":5000,"maxPayload":1000000}
Detalles:
0 => tipo de paquete "open" de Engine.IO
{"sid":... => los datos de handshake de Engine.IO
Nota: el parámetro de consulta t se usa para asegurar que la solicitud no sea cacheada por el navegador.
- Solicitud n°2 (solicitud de conexión al namespace):
POST /socket.io/?EIO=4&transport=polling&t=N8hyd7H&sid=lv_VI97HAXpY6yYWAAAC
< HTTP/1.1 200 OK
< Content-Type: text/plain; charset=UTF-8
40
Detalles:
4 => tipo de paquete "message" de Engine.IO
0 => tipo de paquete "CONNECT" de Socket.IO
- Solicitud n°3 (aprobación de conexión al namespace)
GET /socket.io/?EIO=4&transport=polling&t=N8hyd7H&sid=lv_VI97HAXpY6yYWAAAC
< HTTP/1.1 200 OK
< Content-Type: text/plain; charset=UTF-8
40{"sid":"wZX3oN0bSVIhsaknAAAI"}
- Solicitud n°4
socket.emit('hey', 'Jude') se ejecuta en el servidor:
GET /socket.io/?EIO=4&transport=polling&t=N8hyd7H&sid=lv_VI97HAXpY6yYWAAAC
< HTTP/1.1 200 OK
< Content-Type: text/plain; charset=UTF-8
42["hey","Jude"]
Detalles:
4 => tipo de paquete "message" de Engine.IO
2 => tipo de paquete "EVENT" de Socket.IO
[...] => contenido
- Solicitud n°5 (mensaje saliente)
socket.emit('hello'); socket.emit('world'); se ejecuta en el cliente:
POST /socket.io/?EIO=4&transport=polling&t=N8hzxke&sid=lv_VI97HAXpY6yYWAAAC
> Content-Type: text/plain; charset=UTF-8
42["hello"]\x1e42["world"]
< HTTP/1.1 200 OK
< Content-Type: text/plain; charset=UTF-8
ok
Detalles:
4 => tipo de paquete "message" de Engine.IO
2 => tipo de paquete "EVENT" de Socket.IO
["hello"] => el 1er contenido
\x1e => separador
4 => tipo de paquete "message" de Engine.IO
2 => tipo de paquete "EVENT" de Socket.IO
["world"] => el 2do contenido
- Solicitud n°6 (upgrade a WebSocket)
GET /socket.io/?EIO=4&transport=websocket&sid=lv_VI97HAXpY6yYWAAAC
< HTTP/1.1 101 Switching Protocols
Frames WebSocket:
< 2probe => solicitud probe de Engine.IO
> 3probe => respuesta probe de Engine.IO
> 5 => tipo de paquete "upgrade" de Engine.IO
> 42["hello"]
> 42["world"]
> 40/admin, => solicitar acceso al namespace admin (paquete "CONNECT" de Socket.IO)
< 40/admin,{"sid":"-G5j-67EZFp-q59rADQM"} => conceder acceso al namespace admin
> 42/admin,1["tellme"] => paquete "EVENT" de Socket.IO con acknowledgement
< 461-/admin,1[{"_placeholder":true,"num":0}] => paquete "BINARY_ACK" de Socket.IO con un placeholder
< <binary> => el adjunto binario (enviado en el siguiente frame)
... después de un rato sin mensaje
> 2 => tipo de paquete "ping" de Engine.IO
< 3 => tipo de paquete "pong" de Engine.IO
> 1 => tipo de paquete "close" de Engine.IO
Historial
Diferencia entre v5 y v4
La 5ta revisión (actual) del protocolo Socket.IO se usa en Socket.IO v3 y superiores (v3.0.0 fue lanzado en noviembre de 2020).
Está construido sobre la 4ta revisión de el protocolo Engine.IO (de ahí el parámetro de consulta EIO=4).
Lista de cambios:
- eliminar la conexión implícita al namespace por defecto
En versiones anteriores, un cliente siempre estaba conectado al namespace por defecto, incluso si solicitaba acceso a otro namespace.
Este ya no es el caso, el cliente debe enviar un paquete CONNECT en cualquier caso.
Commits: 09b6f23 (servidor) y 249e0be (cliente)
- renombrar
ERRORaCONNECT_ERROR
El significado y el número de código (4) no se modifican: este tipo de paquete todavía es usado por el servidor cuando la conexión a un namespace es rechazada. Pero sentimos que el nombre es más autodescriptivo.
Commits: d16c035 (servidor) y 13e1db7c (cliente).
- el paquete
CONNECTahora puede contener una carga útil
El cliente puede enviar una carga útil para propósitos de autenticación/autorización. Ejemplo:
{
"type": 0,
"nsp": "/admin",
"data": {
"token": "123"
}
}
En caso de éxito, el servidor responde con una carga útil que contiene el ID del Socket. Ejemplo:
{
"type": 0,
"nsp": "/admin",
"data": {
"sid": "CjdVH4TQvovi1VvgAC5Z"
}
}
Este cambio significa que el ID de la conexión Socket.IO ahora será diferente del ID de la conexión Engine.IO subyacente (el que se encuentra en los parámetros de consulta de las solicitudes HTTP).
Commits: 2875d2c (servidor) y bbe94ad (cliente)
- la carga útil del paquete
CONNECT_ERRORahora es un objeto en lugar de una cadena simple
Commits: 54bf4a4 (servidor) y 0939395 (cliente)
Diferencia entre v4 y v3
La 4ta revisión del protocolo Socket.IO se usa en Socket.IO v1 (v1.0.3 fue lanzado en junio de 2014) y v2 (v2.0.0 fue lanzado en mayo de 2017).
Los detalles de la revisión se pueden encontrar aquí: https://github.com/socketio/socket.io-protocol/tree/v4
Está construido sobre la 3ra revisión de el protocolo Engine.IO (de ahí el parámetro de consulta EIO=3).
Lista de cambios:
- añadir un tipo de paquete
BINARY_ACK
Anteriormente, un paquete ACK siempre se trataba como si pudiera contener objetos binarios, con búsqueda recursiva de tales objetos, lo cual podía perjudicar el rendimiento.
Referencia: https://github.com/socketio/socket.io-parser/commit/ca4f42a922ba7078e840b1bc09fe3ad618acc065
Diferencia entre v3 y v2
La 3ra revisión del protocolo Socket.IO se usa en las primeras versiones de Socket.IO v1 (socket.io@1.0.0...1.0.2) (lanzado en mayo de 2014).
Los detalles de la revisión se pueden encontrar aquí: https://github.com/socketio/socket.io-protocol/tree/v3
Lista de cambios:
- eliminar el uso de msgpack para codificar paquetes que contienen objetos binarios (ver también 299849b)
Diferencia entre v2 y v1
Lista de cambios:
- añadir un tipo de paquete
BINARY_EVENT
Esto fue añadido durante el trabajo hacia Socket.IO 1.0, para añadir soporte para objetos binarios. Los paquetes BINARY_EVENT fueron codificados con msgpack.
Revisión inicial
Esta primera revisión fue el resultado de la separación entre el protocolo Engine.IO (plomería de bajo nivel con WebSocket / HTTP long-polling, heartbeat) y el protocolo Socket.IO. Nunca fue incluida en un lanzamiento de Socket.IO, pero allanó el camino para las siguientes iteraciones.
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.htmlen tu navegador
Para referencia, aquí está la configuración esperada para que el servidor JavaScript pase todas las pruebas:
import { Server } from "socket.io";
const io = new Server(3000, {
pingInterval: 300,
pingTimeout: 200,
maxPayload: 1000000,
cors: {
origin: "*"
}
});
io.on("connection", (socket) => {
socket.emit("auth", socket.handshake.auth);
socket.on("message", (...args) => {
socket.emit.apply(socket, ["message-back", ...args]);
});
socket.on("message-with-ack", (...args) => {
const ack = args.pop();
ack(...args);
})
});
io.of("/custom").on("connection", (socket) => {
socket.emit("auth", socket.handshake.auth);
});