Миграция с 2.x на 3.0
В этом выпуске должны быть исправлены большинство несоответствий в библиотеке Socket.IO и предоставлено более интуитивное поведение для конечных пользователей. Это результат отзывов сообщества на протяжении многих лет. Большое спасибо всем причастным!
Кратко: из-за нескольких критических изменений клиент v2 не сможет подключиться к серверу v3 (и наоборот)
Обновление: начиная с Socket.IO 3.1.0, сервер v3 теперь может взаимодействовать с клиентами v2. Дополнительную информацию см. ниже. Однако клиент v3 все еще не сможет подключиться к серверу v2.
Для получения подробностей низкого уровня см.:
Вот полный список изменений:
-
- io.set() удален
- Больше нет неявного подключения к пространству имен по умолчанию
- Namespace.connected переименован в Namespace.sockets и теперь является Map
- Socket.rooms теперь является Set
- Socket.binary() удален
- Socket.join() и Socket.leave() теперь являются синхронными
- Socket.use() удален
- Ошибка в обработчике теперь будет генерировать объект Error
- Добавление четкого различия между параметром запроса Manager и параметром запроса Socket
- Экземпляр Socket больше не будет пересылать события, испускаемые его Manager
- Namespace.clients() переименован в Namespace.allSockets() и теперь возвращает Promise
- Пакеты клиентов
- Больше нет события «pong» для получения задержки
- Синтаксис модулей ES
emit()цепочки больше недоступны- Имена комнат больше не приводятся к строкам
- Как обновить существующее производство
- Известные проблемы миграции
Настройка
Более разумные значения по умолчанию
- Значение по умолчанию
maxHttpBufferSizeбыло уменьшено с100MBдо1MB. - Расширение WebSocket permessage-deflate теперь отключен по умолчанию.
- Теперь вы должны явно указать домены, которые разрешены (для CORS, см. ниже)
- Параметр
withCredentialsтеперь по умолчанию имеет значениеfalseна стороне клиента.
Обработка CORS
В версии v2 сервер Socket.IO автоматически добавлял необходимые заголовки для разрешения Cross-Origin Resource Sharing (CORS).
Это поведение, хотя и удобное, не было хорошим с точки зрения безопасности, так как оно означало, что все домены могли обращаться к вашему серверу Socket.IO, если не было указано иное с помощью параметра origins.
Вот почему начиная с Socket.IO v3:
- CORS по умолчанию отключен
- параметр
origins(используемый для предоставления списка авторизованных доменов) и параметрhandlePreflightRequest(используемый для редактирования заголовковAccess-Control-Allow-xxx) заменены параметромcors, который будет передан пакету cors.
Полный список параметров можно найти здесь.
До этого:
const io = require("socket.io")(httpServer, {
origins: ["https://example.com"],
// optional, useful for custom headers
handlePreflightRequest: (req, res) => {
res.writeHead(200, {
"Access-Control-Allow-Origin": "https://example.com",
"Access-Control-Allow-Methods": "GET,POST",
"Access-Control-Allow-Headers": "my-custom-header",
"Access-Control-Allow-Credentials": true
});
res.end();
}
});
После:
const io = require("socket.io")(httpServer, {
cors: {
origin: "https://example.com",
methods: ["GET", "POST"],
allowedHeaders: ["my-custom-header"],
credentials: true
}
});
Больше нет cookie по умолчанию
В предыдущих версиях cookie io отправлялся по умолчанию. Этот cookie может использоваться для включения «липких сессий», что все еще необходимо при наличии нескольких серверов и включенном HTTP long-polling (дополнительная информация здесь).
Однако этот cookie не нужен в некоторых случаях (например, при развертывании на одном сервере, «липких сессиях» на основе IP), поэтому теперь его необходимо явно включить.
До этого:
const io = require("socket.io")(httpServer, {
cookieName: "io",
cookieHttpOnly: false,
cookiePath: "/custom"
});
После:
const io = require("socket.io")(httpServer, {
cookie: {
name: "test",
httpOnly: false,
path: "/custom"
}
});
Теперь поддерживаются и все остальные параметры (domain, maxAge, sameSite, ...). Полный список параметров см. здесь.
Изменение API
Ниже перечислены несовместимые с предыдущими версиями изменения.
io.set() удален
Этот метод был устаревшим с выпуска 1.0 и сохранялся для обратной совместимости. Теперь он удален.
Он был заменен обработчиками.
До этого:
io.set("authorization", (handshakeData, callback) => {
// make sure the handshake data looks good
callback(null, true); // error first, "authorized" boolean second
});
После:
io.use((socket, next) => {
var handshakeData = socket.request;
// make sure the handshake data looks good as before
// if error do this:
// next(new Error("not authorized"));
// else just call next
next();
});
Больше нет неявного подключения к пространству имен по умолчанию
Это изменение затрагивает пользователей функции множественного доступа (которую мы называем Пространством имен в Socket.IO).
В предыдущих версиях клиент всегда подключался к пространству имен по умолчанию (/), даже если запрашивал доступ к другому пространству имен. Это означало, что запускались обработчики, зарегистрированные для пространства имен по умолчанию, что может быть неожиданным.
// client-side
const socket = io("/admin");
// server-side
io.use((socket, next) => {
// not triggered anymore
});
io.on("connection", socket => {
// not triggered anymore
})
io.of("/admin").use((socket, next) => {
// triggered
});
Кроме того, мы теперь будем ссылаться на «главное» пространство имен вместо «пространства имен по умолчанию».
Namespace.connected переименован в Namespace.sockets и теперь является Map
Объект connected (используемый для хранения всех сокетов, подключенных к данному пространству имен), мог использоваться для получения объекта Socket по его идентификатору. Теперь он является ES6 Map.
До этого:
// get a socket by ID in the main namespace
const socket = io.of("/").connected[socketId];
// get a socket by ID in the "admin" namespace
const socket = io.of("/admin").connected[socketId];
// loop through all sockets
const sockets = io.of("/").connected;
for (const id in sockets) {
if (sockets.hasOwnProperty(id)) {
const socket = sockets[id];
// ...
}
}
// get the number of connected sockets
const count = Object.keys(io.of("/").connected).length;
После:
// get a socket by ID in the main namespace
const socket = io.of("/").sockets.get(socketId);
// get a socket by ID in the "admin" namespace
const socket = io.of("/admin").sockets.get(socketId);
// loop through all sockets
for (const [_, socket] of io.of("/").sockets) {
// ...
}
// get the number of connected sockets
const count = io.of("/").sockets.size;
Socket.rooms теперь является Set
Свойство rooms содержит список комнат, в которых находится Socket. Раньше это был объект, теперь это ES6 Set.
До этого:
io.on("connection", (socket) => {
console.log(Object.keys(socket.rooms)); // [ <socket.id> ]
socket.join("room1");
console.log(Object.keys(socket.rooms)); // [ <socket.id>, "room1" ]
});
После:
io.on("connection", (socket) => {
console.log(socket.rooms); // Set { <socket.id> }
socket.join("room1");
console.log(socket.rooms); // Set { <socket.id>, "room1" }
});
Socket.binary() удален
Метод binary мог использоваться для указания того, что данное событие не содержит бинарных данных (чтобы пропустить поиск, выполняемый библиотекой, и улучшить производительность в определенных условиях).
Он был заменен возможностью предоставления собственного парсера, которая была добавлена в Socket.IO 2.0.
До этого:
socket.binary(false).emit("hello", "no binary");
После:
const io = require("socket.io")(httpServer, {
parser: myCustomParser
});
См. socket.io-msgpack-parser в качестве примера.
Socket.join() и Socket.leave() теперь являются синхронными
Асинхронность была необходима для первых версий адаптера Redis, но теперь это не так.
Для справки, адаптер — это объект, который хранит взаимосвязи между сокетами и комнатами. Существует два официальных адаптера: встроенный адаптер в памяти и адаптер Redis, основанный на механизме публикации-подписки Redis.
До этого:
socket.join("room1", () => {
io.to("room1").emit("hello");
});
socket.leave("room2", () => {
io.to("room2").emit("bye");
});
После:
socket.join("room1");
io.to("room1").emit("hello");
socket.leave("room2");
io.to("room2").emit("bye");
Примечание: пользовательские адаптеры могут возвращать Promise, поэтому предыдущий пример становится:
await socket.join("room1");
io.to("room1").emit("hello");
Socket.use() удален
socket.use() мог использоваться в качестве обработчика всех событий. Но его API не был интуитивно понятен. Он заменён на socket.onAny().
ОБНОВЛЕНИЕ: метод Socket.use() был восстановлен в socket.io@3.0.5.
До этого:
socket.use((packet, next) => {
console.log(packet.data);
next();
});
После:
socket.onAny((event, ...args) => {
console.log(event);
});
Ошибка в обработчике теперь будет генерировать объект Error
Событие error переименовано в connect_error и испускаемый объект теперь является действительным объектом Error:
До этого:
// server-side
io.use((socket, next) => {
next(new Error("not authorized"));
});
// client-side
socket.on("error", err => {
console.log(err); // not authorized
});
// or with an object
// server-side
io.use((socket, next) => {
const err = new Error("not authorized");
err.data = { content: "Please retry later" }; // additional details
next(err);
});
// client-side
socket.on("error", err => {
console.log(err); // { content: "Please retry later" }
});
После:
// server-side
io.use((socket, next) => {
const err = new Error("not authorized");
err.data = { content: "Please retry later" }; // additional details
next(err);
});
// client-side
socket.on("connect_error", err => {
console.log(err instanceof Error); // true
console.log(err.message); // not authorized
console.log(err.data); // { content: "Please retry later" }
});
Добавление четкого различия между параметром запроса Manager и параметром запроса Socket
В предыдущих версиях параметр query использовался в двух разных местах:
- в параметрах запроса HTTP-запросов (
GET /socket.io/?EIO=3&abc=def) - в пакете
CONNECT
Рассмотрим следующий пример:
const socket = io({
query: {
token: "abc"
}
});
Внутри, в методе io() произошло следующее:
const { Manager } = require("socket.io-client");
// a new Manager is created (which will manage the low-level connection)
const manager = new Manager({
query: { // sent in the query parameters
token: "abc"
}
});
// and then a Socket instance is created for the namespace (here, the main namespace, "/")
const socket = manager.socket("/", {
query: { // sent in the CONNECT packet
token: "abc"
}
});
Это поведение могло привести к странному поведению, например, когда Manager повторно использовался для другого пространства имен (множественного доступа):
// client-side
const socket1 = io({
query: {
token: "abc"
}
});
const socket2 = io("/my-namespace", {
query: {
token: "def"
}
});
// server-side
io.on("connection", (socket) => {
console.log(socket.handshake.query.token); // abc (ok!)
});
io.of("/my-namespace").on("connection", (socket) => {
console.log(socket.handshake.query.token); // abc (what?)
});
Поэтому опция query экземпляра Socket переименована в auth в Socket.IO v3:
// plain object
const socket = io({
auth: {
token: "abc"
}
});
// or with a function
const socket = io({
auth: (cb) => {
cb({
token: "abc"
});
}
});
// server-side
io.on("connection", (socket) => {
console.log(socket.handshake.auth.token); // abc
});
Примечание: опция query менеджера по-прежнему может использоваться для добавления конкретного параметра запроса к HTTP-запросам.
Экземпляр Socket больше не будет передавать события, испускаемые его менеджером
В предыдущих версиях экземпляр Socket испускал события, связанные с состоянием базового соединения. Это больше не так.
Вы по-прежнему можете получить доступ к этим событиям в экземпляре менеджера (свойство io сокета):
До:
socket.on("reconnect_attempt", () => {});
После:
socket.io.on("reconnect_attempt", () => {});
Вот обновленный список событий, испускаемых менеджером:
| Имя | Описание | Ранее (если отличалось) |
|---|---|---|
| open | успешное (повторное) подключение | - |
| error | неудачное (повторное) подключение или ошибка после успешного подключения | connect_error |
| close | отключение | - |
| ping | пакет пинга | - |
| packet | пакет данных | - |
| reconnect_attempt | попытка повторного подключения | reconnect_attempt & reconnecting |
| reconnect | успешное повторное подключение | - |
| reconnect_error | неудачное повторное подключение | - |
| reconnect_failed | неудачное повторное подключение после всех попыток | - |
Вот обновленный список событий, испускаемых сокетом:
| Имя | Описание | Ранее (если отличалось) |
|---|---|---|
| connect | успешное подключение к пространству имен | - |
| connect_error | ошибка подключения | error |
| disconnect | отключение | - |
И, наконец, вот обновленный список защищённых событий, которые нельзя использовать в вашем приложении:
-
connect(используется на стороне клиента) -
connect_error(используется на стороне клиента) -
disconnect(используется на обеих сторонах) -
disconnecting(используется на стороне сервера) -
newListenerиremoveListener(EventEmitter зарезервированные события)
socket.emit("connect_error"); // will now throw an Error
Метод Namespace.clients() переименован в Namespace.allSockets() и теперь возвращает Promise
Эта функция возвращает список идентификаторов сокетов, подключённых к данному пространству имен.
До:
// all sockets in default namespace
io.clients((error, clients) => {
console.log(clients); // => [6em3d4TJP8Et9EMNAAAA, G5p55dHhGgUnLUctAAAB]
});
// all sockets in the "chat" namespace
io.of("/chat").clients((error, clients) => {
console.log(clients); // => [PZDoMHjiu8PYfRiKAAAF, Anw2LatarvGVVXEIAAAD]
});
// all sockets in the "chat" namespace and in the "general" room
io.of("/chat").in("general").clients((error, clients) => {
console.log(clients); // => [Anw2LatarvGVVXEIAAAD]
});
После:
// all sockets in default namespace
const ids = await io.allSockets();
// all sockets in the "chat" namespace
const ids = await io.of("/chat").allSockets();
// all sockets in the "chat" namespace and in the "general" room
const ids = await io.of("/chat").in("general").allSockets();
Примечание: эта функция поддерживается (и по-прежнему поддерживается) адаптером Redis, что означает, что она будет возвращать список идентификаторов сокетов по всем серверам Socket.IO.
Клиентские сборки
Теперь существует 3 отдельных сборки:
| Имя | Размер | Описание |
|---|---|---|
| socket.io.js | 34.7 kB gzip | Нескомпилированная версия, с debug |
| socket.io.min.js | 14.7 kB min+gzip | Производственная версия, без debug |
| socket.io.msgpack.min.js | 15.3 kB min+gzip | Производственная версия, без debug и с парсером msgpack |
По умолчанию все они предоставляются сервером по адресу /socket.io/<name>.
До:
<!-- note: this bundle was actually minified but included the debug package --> <script src="/socket.io/socket.io.js"></script>
После:
<!-- during development --> <script src="/socket.io/socket.io.js"></script> <!-- for production --> <script src="/socket.io/socket.io.min.js"></script>
Больше нет события "pong" для получения задержки
В Socket.IO v2 вы могли прослушивать событие pong на стороне клиента, которое включало продолжительность последней проверки состояния.
Из-за изменения механизма проверки состояния (более подробная информация здесь), это событие было удалено.
До:
socket.on("pong", (latency) => {
console.log(latency);
});
После:
// server-side
io.on("connection", (socket) => {
socket.on("ping", (cb) => {
if (typeof cb === "function")
cb();
});
});
// client-side
setInterval(() => {
const start = Date.now();
// volatile, so the packet will be discarded if the socket is not connected
socket.volatile.emit("ping", () => {
const latency = Date.now() - start;
// ...
});
}, 5000);
Синтаксис ES модулей
Синтаксис модулей ECMAScript теперь аналогичен синтаксису TypeScript (см. ниже).
До (используя импорт по умолчанию):
// server-side import Server from "socket.io"; const io = new Server(8080); // client-side import io from 'socket.io-client'; const socket = io();
После (с именованным импортом):
// server-side
import { Server } from "socket.io";
const io = new Server(8080);
// client-side
import { io } from 'socket.io-client';
const socket = io();
emit() цепочки больше невозможны
Метод emit() теперь соответствует сигнатуре метода EventEmitter.emit() и возвращает true вместо текущего объекта.
До:
socket.emit("event1").emit("event2");
После:
socket.emit("event1");
socket.emit("event2");
Имена комнат больше не принудительно преобразуются в строки
Мы теперь используем внутренне карты и множества вместо простых объектов, поэтому имена комнат больше не неявно преобразуются в строки.
До:
// mixed types were possible
socket.join(42);
io.to("42").emit("hello");
// also worked
socket.join("42");
io.to(42).emit("hello");
После:
// one way
socket.join("42");
io.to("42").emit("hello");
// or another
socket.join(42);
io.to(42).emit("hello");
Новые возможности
Некоторые из этих новых возможностей могут быть перенесены в ветку 2.4.x в зависимости от отзывов пользователей.
Обработчики всех событий
Эта функция вдохновлена библиотекой EventEmitter2 (которая не используется напрямую, чтобы не увеличивать размер сборки браузера).
Она доступна как для серверной, так и для клиентской сторон:
// server
io.on("connection", (socket) => {
socket.onAny((event, ...args) => {});
socket.prependAny((event, ...args) => {});
socket.offAny(); // remove all listeners
socket.offAny(listener);
const listeners = socket.listenersAny();
});
// client
const socket = io();
socket.onAny((event, ...args) => {});
socket.prependAny((event, ...args) => {});
socket.offAny(); // remove all listeners
socket.offAny(listener);
const listeners = socket.listenersAny();
Летучие события (клиент)
Летучее событие — это событие, которое разрешается отбрасывать, если низкоуровневый транспорт ещё не готов (например, когда HTTP-запрос POST уже ожидается).
Эта функция уже была доступна на стороне сервера. Она может быть полезной и на стороне клиента, например, когда сокет не подключён (по умолчанию пакеты буферизуются до повторного подключения).
socket.volatile.emit("volatile event", "might or might not be sent");
Официальная сборка с парсером msgpack
Теперь будет предоставлена сборка с socket.io-msgpack-parser (либо на CDN, либо предоставляемая сервером по адресу /socket.io/socket.io.msgpack.min.js).
Плюсы:
- события с двоичным содержимым отправляются как 1 кадр WebSocket (вместо 2+ с помощью стандартного парсера)
- пакеты с большим количеством чисел должны быть меньше
Минусы:
- нет поддержки IE9 (https://caniuse.com/mdn-javascript_builtins_arraybuffer)
- несколько больший размер сборки
// server-side
const io = require("socket.io")(httpServer, {
parser: require("socket.io-msgpack-parser")
});
Дополнительная настройка на стороне клиента не требуется.
Разное
Код Socket.IO переписан на TypeScript
Это означает, что npm i -D @types/socket.io больше не должно потребоваться.
Сервер:
import { Server, Socket } from "socket.io";
const io = new Server(8080);
io.on("connection", (socket: Socket) => {
console.log(`connect ${socket.id}`);
socket.on("disconnect", () => {
console.log(`disconnect ${socket.id}`);
});
});
Клиент:
import { io } from "socket.io-client";
const socket = io("/");
socket.on("connect", () => {
console.log(`connect ${socket.id}`);
});
Обычный JavaScript, естественно, по-прежнему полностью поддерживается.
Поддержка IE8 и Node.js 8 официально прекращена
IE8 больше не тестируется на платформе Sauce Labs и требует больших усилий для очень небольшого числа пользователей (если таковые имеются?), поэтому мы прекращаем его поддержку.
Кроме того, Node.js 8 теперь EOL. Пожалуйста, обновитесь как можно скорее!
Как обновить существующее развертывание в производстве
- сначала обновите серверы, установив
allowEIO3вtrue(добавлено вsocket.io@3.1.0)
const io = require("socket.io")({
allowEIO3: true // false by default
});
Примечание: если вы используете адаптер Redis для передачи пакетов между узлами, вы должны использовать socket.io-redis@5 с socket.io@2 и socket.io-redis@6 с socket.io@3. Обратите внимание, что обе версии совместимы, поэтому вы можете обновлять каждый сервер по одному (нет необходимости в большом обновлении).
- затем обновите клиентов
Этот шаг может занять некоторое время, так как у некоторых клиентов может остаться кешированная версия клиента v2.
Вы можете проверить версию подключения с помощью:
io.on("connection", (socket) => {
const version = socket.conn.protocol; // either 3 or 4
});
Это соответствует значению параметра запроса EIO в HTTP-запросах.
- и, наконец, после обновления всех клиентов, установите
allowEIO3вfalse(что является значением по умолчанию)
const io = require("socket.io")({
allowEIO3: false
});
При установке allowEIO3 в false, клиенты v2 теперь получат HTTP-ошибку 400 (Unsupported protocol version) при подключении.
Известные проблемы миграции
stream_1.pipeline is not a function
TypeError: stream_1.pipeline is not a function
at Function.sendFile (.../node_modules/socket.io/dist/index.js:249:26)
at Server.serve (.../node_modules/socket.io/dist/index.js:225:16)
at Server.srv.on (.../node_modules/socket.io/dist/index.js:186:22)
at emitTwo (events.js:126:13)
at Server.emit (events.js:214:7)
at parserOnIncoming (_http_server.js:602:12)
at HTTPParser.parserOnHeadersComplete (_http_common.js:116:23)
Эта ошибка, вероятно, вызвана вашей версией Node.js. Метод pipeline был добавлен в Node.js 10.0.0.
error TS2416: Property 'emit' in type 'Namespace' is not assignable to the same property in base type 'EventEmitter'.
node_modules/socket.io/dist/namespace.d.ts(89,5): error TS2416: Property 'emit' in type 'Namespace' is not assignable to the same property in base type 'EventEmitter'.
Type '(ev: string, ...args: any[]) => Namespace' is not assignable to type '(event: string | symbol, ...args: any[]) => boolean'.
Type 'Namespace' is not assignable to type 'boolean'.
node_modules/socket.io/dist/socket.d.ts(84,5): error TS2416: Property 'emit' in type 'Socket' is not assignable to the same property in base type 'EventEmitter'.
Type '(ev: string, ...args: any[]) => this' is not assignable to type '(event: string | symbol, ...args: any[]) => boolean'.
Type 'this' is not assignable to type 'boolean'.
Type 'Socket' is not assignable to type 'boolean'.
Подпись метода emit() была исправлена в версии 3.0.1 (коммит).
- клиент отключается при отправке большого пакета (> 1МБ)
Это, вероятно, связано с тем, что значение по умолчанию для maxHttpBufferSize теперь 1MB. При получении пакета, превышающего этот размер, сервер отключает клиента, чтобы предотвратить перегрузку сервера злонамеренными клиентами.
Вы можете изменить это значение при создании сервера:
const io = require("socket.io")(httpServer, {
maxHttpBufferSize: 1e8
});
Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at xxx/socket.io/?EIO=4&transport=polling&t=NMnp2WI. (Reason: CORS header ‘Access-Control-Allow-Origin’ missing).
С Socket.IO v3 вам необходимо явно включить Cross-Origin Resource Sharing (CORS). Документация находится здесь.
Uncaught TypeError: packet.data is undefined
Похоже, вы используете клиент v3 для подключения к серверу v2, что невозможно. Пожалуйста, см. следующий раздел.
Object literal may only specify known properties, and 'extraHeaders' does not exist in type 'ConnectOpts'
Поскольку исходный код был переписан на TypeScript (дополнительная информация здесь), @types/socket.io-client больше не требуется и может конфликтовать с типизацией из пакета socket.io-client.
- отсутствие куки в контексте между доменами
Теперь вам необходимо явно включить куки, если фронтенд не обслуживается с того же домена, что и бэкенд:
Сервер
import { Server } from "socket.io";
const io = new Server({
cors: {
origin: ["https://front.domain.com"],
credentials: true
}
});
Клиент
import { io } from "socket.io-client";
const socket = io("https://backend.domain.com", {
withCredentials: true
});
Ссылка:
- Обработка CORS
-
corsпараметр -
withCredentialsпараметр
© 2014–2021 Automattic
Licensed under the MIT License.
https://socket.io/docs/v4/migrating-from-2-x-to-3-0