egg-socket.io отправляет сообщение указанному клиенту

Egg.js

особенности egg-socket.io

egg-socket.io — это инкапсуляция socket.io, а также спецификации для маршрутизатора, контроллера, пространства имен и промежуточного программного обеспечения.

Среди них маршрутизатор и контроллер в основном используются для распределения и обработки.socket.ioЗапрос клиента, но причина, по которой мы ввелиsocket.io, часто для того, чтобы получить возможность сервера активно пушить, эти две части пропускают.

остаткиnamespaceиmiddlewareСвязанный с реализацией функции push для указанного клиента, вegg-socket.ioВсе они настраиваются через конфигурационные файлы

// config.default.js
config.io = {
  namespace: {
    '/': {
      connectionMiddleware: [ 'auth' ], //连接处理
      packetMiddleware: [], //包处理
    },
  },
  generateId: req => { //自定义 socket.id 生成函数
    const data = qs.parse(req.url.split('?')[1]);
    return data.userId; // custom id must be unique
  },
};

Кратко о роли:

пространство имен

Это socket.ioПространства имен, что эквивалентно разделению набора клиентских сокетных подключений и размещению одной и той же группы подключений в одном пространстве имен.

пройти черезnamespaceСервер может выполнять широковещательную рассылку всем клиентам в пространстве имен или отправлять сообщения клиентам по отдельности с указанным идентификатором сокета.

const namespace = app.io.of('/'); //获取命名空间 "/"
//namespace.sockets :连接到该命名空间的所有客户端,形式为 {id1:socket1, id2:socket2}
//通过 socket.id 就可以向该namespace下的指定客户端发送消息
namespace.sockets[socketId].emit('message', msg); 

промежуточное ПО

connectionMiddleware: промежуточное ПО для обработки соединений

Обработка каждого установления/отключения соединения сокета

// {app_root}/app/io/middleware/connection.js
module.exports = app => {
  return async (ctx, next) => {
    console.log('connection!');
    await next();
    // execute when disconnect.
    console.log('disconnection!');
  };
};

packageMiddleware : промежуточное ПО для обработки пакетов

обрабатывать каждое сообщение

// {app_root}/app/io/middleware/packet.js
module.exports = app => {
  return async (ctx, next) => {
    ctx.socket.emit('res', 'packet received!');
    console.log('packet:', ctx.packet);
    await next();
  };
};

Несколько способов отправить сообщение указанному клиенту

Обычный сценарий отправки сообщения данному клиенту: действие одного пользователя вызывает уведомление другого пользователя.

Например, пользователю А нравится фид пользователя Б, нам нужно отправить понравившееся сообщение пользователю Б, когда мы знаем userId пользователя Б.

В приведенном выше введении к пространству имен мы узнали, что в известномsocket.idможно использовать в случаеnamespace.sockets[socketId].emit('message', msg);Отправить указанному клиенту.

Итак, теперь возникает вопрос, как получить socketId в соответствии с userId,userId ->socketId -> clientЭта цепочка завершена.

1. Трансляция (не рекомендуется)

Получив пространство имен, его можно транслировать всем клиентам, подключенным к пространству имен.

const namespace = app.io.of('/');
namespace.emit('event', data);

Если информация получателя переносится в содержимом сообщения, то клиент может определить, отправляется ли оно самому себе для соответствующей обработки или игнорирования, что может достичь цели отправки сообщения в замаскированной форме.

Недостатки этого метода весьма очевидны:

  • Отправка слишком большого количества недопустимых сообщений во время трансляции
  • Другие клиенты все равно получат сообщение, даже если не обработают его, что недостаточно безопасно.

2. generateId()

egg-socket.ioТакже предоставляются элементы конфигурацииgenerateIdдля создания пользовательского идентификатора для заменыsocket.ioСлучайный идентификатор по умолчанию.

  generateId: req => {
    //根据req生成id
    return id;
  },

Если вы можете напрямуюgenerateId()Возвращая идентификатор пользователя, можно не беспокоиться о соответствии между userId и socketId.

Вновь возникает вопрос: какgenerateId()Получить идентификатор пользователя в функции?

Параметр req этой функции является исходным объектом req модуля узла http.http.IncomingMessageи не может получить прямой доступ к атрибуту сеанса. Это начинается с другой информации, содержащейся в req.

  1. cookie

    • Файл cookie при инициировании запроса прикрепляется к запросу. Если файл cookie получен, он будетegg-sessionВы можете получить информацию о сеансе, повторив процесс плагина
  2. параметр запроса в URL

    • Как правило, клиент инициирует соединение socket.io после входа в систему через http. Клиент может добавить информацию о пользователе к URL-адресу соединения при инициировании соединения, которое выполняется сервером.generateId(req)Получить из параметра запроса

Оба подхода имеют свои явные недостатки:

Получение сеанса с помощью cookie выглядит хорошо, но повторитеegg-sessionПроцесс может быть не простым.

Добавление в запрос «голого» идентификатора пользователя может создать риск подделки. Также необходимо добавить подпись для предотвращения подделки и подделки, а также требуются дополнительные шаги.

Конкретный метод заключается в следующем: при успешном входе в систему возвращается строка «информация о пользователе + подпись», которая отображается при подключении к socket.io.

Кроме того,generateIdЕще минус в том, что функция прописана в конфигурационном файле, а прописать такую ​​сложную обработку в конфиге недостаточно. . ну, придумай слово (элегантно)

3. Сохраните отношение сопоставления между идентификатором пользователя и socket.id.

В конце концов, этот способ прост и интуитивно понятен в реализации, он прозрачен для клиента, никаких лишних действий делать не нужно, достаточно подключиться напрямую.

Конкретный метод: когда клиент подключается, отношение сопоставления между текущим идентификатором пользователя и socket.id сохраняется в redis в промежуточном программном обеспечении соединения (если это один процесс, глобальную карту можно использовать напрямую). Когда ему нужно нажать на указанный клиент, узнайте socketId в соответствии с уведомленным идентификатором пользователя и используйтеnamespace.sockets[socketId].emit('message', msg);отправлять.

Полный пример

После введения нескольких методов реализации здесь я использую третий метод3. Сохраните отношение сопоставления между идентификатором пользователя и socket.id.Написал минимальный исполняемый пример, полный код находится вGitHub.com/Lonely Sail away/демо…

Последовательность выполнения этого примера показана на рисунке

  1. Сначала сделайте http запрос/loginУстановите информацию для входа пользователя и запишите ее в сеанс
  // {app_root}/app/controller/home.js
  async login() {
    const { ctx } = this;
    const { id, name } = ctx.query;
    ctx.session.user = { id, name };
    ctx.body = {
      session: ctx.session,
    };
  }
  1. Клиент получает возвращенный файл cookie и передает соединение для выполнения socket.io (при фактическом использовании браузеру не нужно активно передавать файл cookie).
// {app_root}/test/app/io/io.test.js
const res1 = await app.httpRequest().get('/login?id=100&name=aaa').expect(200);
const cookie1 = res1.headers['set-cookie'].join(';');// 获取已经设置过session的cookie
const client1 = client({ extraHeaders: { cookie: cookie1, port: basePort } });
  1. ПО промежуточного слоя для обработки соединений сохраняет отношение сопоставления между userId и socketId. Как только это будет сделано, вы можете отправлять сообщения пользователю.
// {app_root}/app/io/middleware/auth.js
module.exports = app => {
  return async (ctx, next) => {
    // connect
    if (!ctx.session.user) return;
    const key = `${ctx.enums.prefix.socketId}${ctx.session.user.id}`;
    const MAX_TTL = 24 * 60 * 60;// 最大过期时长,兜底用
    await app.redis.set(key, ctx.socket.id, 'EX', MAX_TTL);
    await next();
    await app.redis.del(key); // disconnect
  };
};
  1. Другие клиенты инициируют уведомления о сообщениях, которые отправляются указанному подключенному клиенту.
// {app_root}/test/app/io/io.test.js
await app.httpRequest().get('/push?targetUserId=100&msg=msg1').expect(200);
  1. Найти socketId по идентификатору пользователя, отправить сообщение
  // {app_root}/app/controller/home.js
  async push() {
    const { ctx, app } = this;
    const targetUserId = ctx.query.targetUserId;
    const msg = ctx.query.msg;

    const key = `${ctx.enums.prefix.socketId}${targetUserId}`;
    const socketId = await app.redis.get(key); // 获取socketId
    ctx.logger.info(key, socketId);
    if (socketId) {
      const namespace = app.io.of('/');
      namespace.sockets[socketId].emit('message', msg);
    }
    ctx.body = ctx.errCodes.success;
  }

воплощать в жизньnpm run test

在这里插入图片描述