Миграция с 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 query и параметром Socket query
- Экземпляр Socket больше не пересылает события, вызываемые его Manager
- Namespace.clients() переименовано в Namespace.allSockets() и теперь возвращает Promise
- Пакеты клиента
- Больше нет события “pong” для получения задержки
- Синтаксис ES-модулей
emit()цепочки больше невозможны- Имена комнат больше не преобразуются в строки
Настройка
Более разумные значения по умолчанию
- Значение по умолчанию для
maxHttpBufferSizeбыло уменьшено с100MBдо1MB. - Расширение WebSocket permessage-deflate теперь отключено по умолчанию.
- Теперь вы должны явно указать домены, которые разрешены (для CORS, см. ниже)
Обработка 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 можно использовать для активации sticky-session, которая все еще требуется, когда у вас несколько серверов и включен HTTP long-polling (подробнее здесь).
Однако этот cookie не нужен в некоторых случаях (например, развертывание на одном сервере, sticky-session на основе 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();
}); |
Отсутствует неявное подключение к пространству имен по умолчанию
Это изменение затрагивает пользователей функции множественного подключения (то, что мы называем Namespace в 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) можно было использовать для получения объекта 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, но это больше не так.
Для справки, адаптер — это объект, который хранит взаимосвязи между Socket и Комнатами. Существуют два официальных адаптера: встроенный адаптер в памяти и адаптер Redis на основе механизма pub-sub 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);
}); |
Ошибка middleware теперь будет излучать объект 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"
}
}); |
Это поведение могло привести к странному поведению, например, когда менеджер повторно использовался для другого пространства имён (мультиплексирование):
// 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 больше не будет пересылать события, испускаемые его Manager
В предыдущих версиях экземпляр Socket излучал события, относящиеся к состоянию базового соединения. Это больше не так.
Вы по-прежнему можете получить доступ к этим событиям в экземпляре Manager (свойство io сокета):
До:
socket.on("reconnect_attempt", () => {}); |
После:
socket.io.on("reconnect_attempt", () => {}); |
Вот обновленный список событий, излучаемых Manager:
| Название | Описание | Предыдущее (если отличается) |
|---|---|---|
| open | успешное (пере)подключение | - |
| error | (пере)подключение не удалось или ошибка после успешного подключения | connect_error |
| close | отключение | - |
| ping | пакет ping | - |
| packet | пакет данных | - |
| reconnect_attempt | попытка переподключения | reconnect_attempt & reconnecting |
| reconnect | успешное переподключение | - |
| reconnect_error | не удалось переподключиться | - |
| reconnect_failed | переподключение не удалось после всех попыток | - |
Вот обновленный список событий, излучаемых Socket:
| Название | Описание | Предыдущее (если отличается) |
|---|---|---|
| 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"); |
Имена комнат больше не приводятся к строкам
Теперь мы используем Map и Set внутри вместо обычных объектов, поэтому имена комнат больше не неявно приводятся к строкам.
До:
// 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. Обратите внимание, что обе версии совместимы, поэтому вы можете обновлять каждый сервер по одному (не нужно выполнять обновление "big bang").
- затем обновите клиентов
Этот шаг может занять некоторое время, так как некоторые клиенты могут по-прежнему иметь кэшированный клиент версии 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.
© 2014–2021 Automattic
Licensed under the MIT License.
https://socket.io/docs/v3/migrating-from-2-x-to-3-0