跳至主要内容
版本: 4.x

Socket.IO 协议

本文档描述了 Socket.IO 协议的第 5 版。

本文档的源代码可以在这里找到 这里.

目录

介绍

Socket.IO 协议使客户端和服务器之间能够进行 全双工 和低开销通信。

它建立在 Engine.IO 协议 之上,该协议处理与 WebSocket 和 HTTP 长轮询的低级管道。

Socket.IO 协议添加了以下功能

JavaScript API 示例

服务器

// declare the namespace
const namespace = io.of("/admin");
// handle the connection to the namespace
namespace.on("connection", (socket) => {
// ...
});

客户端

// reach the main namespace
const socket1 = io();
// reach the "/admin" namespace (with the same underlying WebSocket connection)
const socket2 = io("/admin");
// handle the connection to the namespace
socket2.on("connect", () => {
// ...
});
  • 数据包确认

JavaScript API 示例

// on one side
socket.emit("hello", "foo", (arg) => {
console.log("received", arg);
});

// on the other side
socket.on("hello", (arg, ack) => {
ack("bar");
});

参考实现是用 TypeScript 编写的

交换协议

Socket.IO 数据包包含以下字段

  • 数据包类型(整数)
  • 命名空间(字符串)
  • 可选的,有效负载(对象 | 数组)
  • 可选的,确认 ID(整数)

以下是可用数据包类型的列表

类型ID用途
CONNECT0连接到命名空间 期间使用。
DISCONNECT1从命名空间断开连接 时使用。
EVENT2用于向另一方 发送数据
ACK3用于 确认 事件。
CONNECT_ERROR4连接到命名空间 期间使用。
BINARY_EVENT5用于向另一方 发送二进制数据
BINARY_ACK6用于 确认 事件(响应包含二进制数据)。

连接到命名空间

在 Socket.IO 会话开始时,客户端必须发送一个 CONNECT 数据包

服务器必须以以下方式响应

  • 如果连接成功,则为一个 CONNECT 数据包,有效负载中包含会话 ID
  • 或者,如果连接不允许,则为一个 CONNECT_ERROR 数据包
CLIENT                                                      SERVER

│ ───────────────────────────────────────────────────────► │
│ { type: CONNECT, namespace: "/" } │
│ ◄─────────────────────────────────────────────────────── │
│ { type: CONNECT, namespace: "/", data: { sid: "..." } } │

如果服务器首先没有收到 CONNECT 数据包,则它必须立即关闭连接。

客户端可以同时连接到多个命名空间,使用相同的底层 WebSocket 连接。

示例

  • 使用主命名空间(名为 "/"
Client > { type: CONNECT, namespace: "/" }
Server > { type: CONNECT, namespace: "/", data: { sid: "wZX3oN0bSVIhsaknAAAI" } }
  • 使用自定义命名空间
Client > { type: CONNECT, namespace: "/admin" }
Server > { type: CONNECT, namespace: "/admin", data: { sid: "oSO0OpakMV_3jnilAAAA" } }
  • 使用附加有效负载
Client > { type: CONNECT, namespace: "/admin", data: { "token": "123" } }
Server > { type: CONNECT, namespace: "/admin", data: { sid: "iLnRaVGHY4B75TeVAAAB" } }
  • 如果连接被拒绝
Client > { type: CONNECT, namespace: "/" }
Server > { type: CONNECT_ERROR, namespace: "/", data: { message: "Not authorized" } }

发送和接收数据

一旦 连接到命名空间 建立,客户端和服务器就可以开始交换数据

CLIENT                                                      SERVER

│ ───────────────────────────────────────────────────────► │
│ { type: EVENT, namespace: "/", data: ["foo"] } │
│ │
│ ◄─────────────────────────────────────────────────────── │
│ { type: EVENT, namespace: "/", data: ["bar"] } │

有效负载是强制性的,必须是非空数组。如果不是这样,接收方必须关闭连接。

示例

  • 使用主命名空间
Client > { type: EVENT, namespace: "/", data: ["foo"] }
  • 使用自定义命名空间
Server > { type: EVENT, namespace: "/admin", data: ["bar"] }
  • 使用二进制数据
Client > { type: BINARY_EVENT, namespace: "/", data: ["baz", <Buffer <01 02 03 04>> ] }

确认

发送方可以包含事件 ID 以请求接收方的确认

CLIENT                                                      SERVER

│ ───────────────────────────────────────────────────────► │
│ { type: EVENT, namespace: "/", data: ["foo"], id: 12 } │
│ ◄─────────────────────────────────────────────────────── │
│ { type: ACK, namespace: "/", data: ["bar"], id: 12 } │

接收方必须使用相同事件 ID 响应一个 ACK 数据包。

有效负载是强制性的,必须是数组(可能是空的)。

示例

  • 使用主命名空间
Client > { type: EVENT, namespace: "/", data: ["foo"], id: 12 }
Server > { type: ACK, namespace: "/", data: [], id: 12 }
  • 使用自定义命名空间
Server > { type: EVENT, namespace: "/admin", data: ["foo"], id: 13 }
Client > { type: ACK, namespace: "/admin", data: ["bar"], id: 13 }
  • 使用二进制数据
Client > { type: BINARY_EVENT, namespace: "/", data: ["foo", <buffer <01 02 03 04> ], id: 14 }
Server > { type: ACK, namespace: "/", data: ["bar"], id: 14 }

or

Server > { type: EVENT, namespace: "/", data: ["foo" ], id: 15 }
Client > { type: BINARY_ACK, namespace: "/", data: ["bar", <buffer <01 02 03 04>], id: 15 }

从命名空间断开连接

在任何时候,任何一方都可以通过发送一个 DISCONNECT 数据包来结束与命名空间的连接

CLIENT                                                      SERVER

│ ───────────────────────────────────────────────────────► │
│ { type: DISCONNECT, namespace: "/" } │

另一方不需要响应。如果客户端连接到另一个命名空间,则低级连接可能会保持活动状态。

数据包编码

本节详细介绍了默认解析器使用的编码,该解析器包含在 Socket.IO 服务器和客户端中,其源代码可以在这里找到 这里.

JavaScript 服务器和客户端实现还支持自定义解析器,这些解析器具有不同的权衡,可能对某些类型的应用程序有利。例如,请参阅 socket.io-json-parsersocket.io-msgpack-parser

另请注意,每个 Socket.IO 数据包都作为 Engine.IO message 数据包发送(更多信息 这里),因此编码结果在通过网络发送时将以字符 "4" 为前缀(在 HTTP 长轮询的请求/响应主体中,或在 WebSocket 帧中)。

格式

<packet type>[<# of binary attachments>-][<namespace>,][<acknowledgment id>][JSON-stringified payload without binary]

+ binary attachments extracted

注意:仅当命名空间不同于主命名空间(/)时才包含命名空间

示例

连接到命名空间

  • 使用主命名空间

数据包

{ type: CONNECT, namespace: "/" }

编码

0
  • 使用自定义命名空间

数据包

{ type: CONNECT, namespace: "/admin", data: { sid: "oSO0OpakMV_3jnilAAAA" } }

编码

0/admin,{"sid":"oSO0OpakMV_3jnilAAAA"}
  • 如果连接被拒绝

数据包

{ type: CONNECT_ERROR, namespace: "/", data: { message: "Not authorized" } }

编码

4{"message":"Not authorized"}

发送和接收数据

  • 使用主命名空间

数据包

{ type: EVENT, namespace: "/", data: ["foo"] }

编码

2["foo"]
  • 使用自定义命名空间

数据包

{ type: EVENT, namespace: "/admin", data: ["bar"] }

编码

2/admin,["bar"]
  • 使用二进制数据

数据包

{ type: BINARY_EVENT, namespace: "/", data: ["baz", <Buffer <01 02 03 04>> ] }

编码

51-["baz",{"_placeholder":true,"num":0}]

+ <Buffer <01 02 03 04>>
  • 具有多个附件

数据包

{ type: BINARY_EVENT, namespace: "/admin", data: ["baz", <Buffer <01 02>>, <Buffer <03 04>> ] }

编码

52-/admin,["baz",{"_placeholder":true,"num":0},{"_placeholder":true,"num":1}]

+ <Buffer <01 02>>
+ <Buffer <03 04>>

请记住,每个 Socket.IO 数据包都包装在一个 Engine.IO message 数据包中,因此它们在通过网络发送时将以字符 "4" 为前缀。

示例:{ type: EVENT, namespace: "/", data: ["foo"] } 将发送为 42["foo"]

确认

  • 使用主命名空间

数据包

{ type: EVENT, namespace: "/", data: ["foo"], id: 12 }

编码

212["foo"]
  • 使用自定义命名空间

数据包

{ type: ACK, namespace: "/admin", data: ["bar"], id: 13 }

编码

3/admin,13["bar"]`
  • 使用二进制数据

数据包

{ type: BINARY_ACK, namespace: "/", data: ["bar", <Buffer <01 02 03 04>>], id: 15 }

编码

61-15["bar",{"_placeholder":true,"num":0}]

+ <Buffer <01 02 03 04>>

从命名空间断开连接

  • 使用主命名空间

数据包

{ type: DISCONNECT, namespace: "/" }

编码

1
  • 使用自定义命名空间
{ type: DISCONNECT, namespace: "/admin" }

编码

1/admin,

示例会话

以下是一个示例,说明在组合 Engine.IO 和 Socket.IO 协议时通过网络发送的内容。

  • 请求 n°1(打开数据包)
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}

详细信息

0           => Engine.IO "open" packet type
{"sid":... => the Engine.IO handshake data

注意:t 查询参数用于确保浏览器不会缓存请求。

  • 请求 n°2(命名空间连接请求)
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

详细信息

4           => Engine.IO "message" packet type
0 => Socket.IO "CONNECT" packet type
  • 请求 n°3(命名空间连接批准)
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"}
  • 请求 n°4

socket.emit('hey', 'Jude') 在服务器上执行

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"]

详细信息

4           => Engine.IO "message" packet type
2 => Socket.IO "EVENT" packet type
[...] => content
  • 请求 n°5(消息输出)

socket.emit('hello'); socket.emit('world'); 在客户端上执行

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

详细信息

4           => Engine.IO "message" packet type
2 => Socket.IO "EVENT" packet type
["hello"] => the 1st content
\x1e => separator
4 => Engine.IO "message" packet type
2 => Socket.IO "EVENT" packet type
["world"] => the 2nd content
  • 请求 n°6(WebSocket 升级)
GET /socket.io/?EIO=4&transport=websocket&sid=lv_VI97HAXpY6yYWAAAC
< HTTP/1.1 101 Switching Protocols

WebSocket 帧

< 2probe                                        => Engine.IO probe request
> 3probe => Engine.IO probe response
> 5 => Engine.IO "upgrade" packet type
> 42["hello"]
> 42["world"]
> 40/admin, => request access to the admin namespace (Socket.IO "CONNECT" packet)
< 40/admin,{"sid":"-G5j-67EZFp-q59rADQM"} => grant access to the admin namespace
> 42/admin,1["tellme"] => Socket.IO "EVENT" packet with acknowledgement
< 461-/admin,1[{"_placeholder":true,"num":0}] => Socket.IO "BINARY_ACK" packet with a placeholder
< <binary> => the binary attachment (sent in the following frame)
... after a while without message
> 2 => Engine.IO "ping" packet type
< 3 => Engine.IO "pong" packet type
> 1 => Engine.IO "close" packet type

历史

v5 和 v4 之间的区别

Socket.IO 协议的第 5 版(当前)用于 Socket.IO v3 及更高版本(v3.0.0 于 2020 年 11 月发布)。

它建立在 Engine.IO 协议 的第 4 版之上(因此 EIO=4 查询参数)。

更改列表

  • 删除对默认命名空间的隐式连接

在以前的版本中,即使客户端请求访问另一个命名空间,它也始终连接到默认命名空间。

现在不再是这种情况,客户端必须在任何情况下都发送一个 CONNECT 数据包。

提交:09b6f23(服务器)和 249e0be(客户端)

  • ERROR 重命名为 CONNECT_ERROR

含义和代码编号(4)没有修改:服务器在拒绝连接到命名空间时仍然使用此数据包类型。但我们认为这个名字更具描述性。

提交:d16c035(服务器)和 13e1db7c(客户端)。

  • CONNECT 数据包现在可以包含有效负载

客户端可以发送有效负载以进行身份验证/授权。示例

{
"type": 0,
"nsp": "/admin",
"data": {
"token": "123"
}
}

如果成功,服务器将响应一个包含 Socket ID 的有效负载。示例

{
"type": 0,
"nsp": "/admin",
"data": {
"sid": "CjdVH4TQvovi1VvgAC5Z"
}
}

此更改意味着 Socket.IO 连接的 ID 现在将不同于底层 Engine.IO 连接的 ID(在 HTTP 请求的查询参数中找到的 ID)。

提交:2875d2c(服务器)和 bbe94ad(客户端)

  • CONNECT_ERROR 数据包的有效负载现在是一个对象,而不是一个普通字符串

提交:54bf4a4(服务器)和 0939395(客户端)

v4 和 v3 之间的区别

Socket.IO 协议的第 4 版用于 Socket.IO v1(v1.0.3 于 2014 年 6 月发布)和 v2(v2.0.0 于 2017 年 5 月发布)。

修订的详细信息可以在这里找到:https://github.com/socketio/socket.io-protocol/tree/v4

它建立在 Engine.IO 协议的第三版 之上(因此有 EIO=3 查询参数)。

更改列表

  • 添加 BINARY_ACK 包类型

以前,ACK 包总是被视为可能包含二进制对象,并对这些对象进行递归搜索,这会影响性能。

参考:https://github.com/socketio/socket.io-parser/commit/ca4f42a922ba7078e840b1bc09fe3ad618acc065

v3 和 v2 之间的区别

Socket.IO 协议的第三版用于早期 Socket.IO v1 版本([email protected])(于 2014 年 5 月发布)。

该版本的详细信息可以在此处找到:https://github.com/socketio/socket.io-protocol/tree/v3

更改列表

  • 删除使用 msgpack 对包含二进制对象的包进行编码(另请参见 299849b)。

v2 和 v1 之间的区别

更改列表

  • 添加 BINARY_EVENT 包类型

这是在为 Socket.IO 1.0 工作期间添加的,目的是为了添加对二进制对象的支持。BINARY_EVENT 包使用 msgpack 进行编码。

初始版本

这个第一个版本是 Engine.IO 协议(使用 WebSocket/HTTP 长轮询、心跳的底层管道)和 Socket.IO 协议分离的结果。它从未包含在 Socket.IO 版本中,但为后续迭代铺平了道路。

测试套件

test-suite/ 目录中的测试套件允许您检查服务器实现的合规性。

用途

  • 在 Node.js 中:npm ci && npm test
  • 在浏览器中:只需在浏览器中打开 index.html 文件即可

作为参考,以下是通过所有测试的 JavaScript 服务器的预期配置

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);
});