Spec-Zone.ru › Socket.IO 3

Миграция с 2.x на 3.0

Данный релиз должен исправить большинство несоответствий в библиотеке Socket.IO и обеспечить более интуитивное поведение для конечных пользователей. Он является результатом отзывов сообщества на протяжении многих лет. Большое спасибо всем участникам!

Кратко: из-за нескольких несовместимых изменений, клиент v2 не сможет подключиться к серверу v3 (и наоборот)

Обновление: начиная с Socket.IO 3.1.0, сервер v3 теперь может взаимодействовать с клиентами v2. Дополнительная информация ниже. Однако клиент v3 по-прежнему не сможет подключиться к серверу v2.

Для получения подробностей низкого уровня, пожалуйста, см.:

  • Протокол Engine.IO v4
  • Протокол Socket.IO v5

Вот полный список изменений:

  • Настройка

    • Более разумные значения по умолчанию
    • Обработка CORS
    • Отсутствует cookie по умолчанию
  • Изменение API

    • 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() цепочки больше невозможны
    • Имена комнат больше не преобразуются в строки
  • Новые функции

    • Обработчики всех событий
    • Переменные события (клиент)
    • Официальная упаковка с парсером msgpack
  • Разное

    • База кода Socket.IO переписана на TypeScript
    • Поддержка IE8 и Node.js 8 официально отменена
  • Как обновить существующее производственное развертывание

  • Известные проблемы миграции

Настройка

Более разумные значения по умолчанию

  • Значение по умолчанию для 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

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API