особенности 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.
-
cookie
- Файл cookie при инициировании запроса прикрепляется к запросу. Если файл cookie получен, он будет
egg-session
Вы можете получить информацию о сеансе, повторив процесс плагина
- Файл cookie при инициировании запроса прикрепляется к запросу. Если файл cookie получен, он будет
-
параметр запроса в URL
- Как правило, клиент инициирует соединение socket.io после входа в систему через http. Клиент может добавить информацию о пользователе к URL-адресу соединения при инициировании соединения, которое выполняется сервером.
generateId(req)
Получить из параметра запроса
- Как правило, клиент инициирует соединение socket.io после входа в систему через http. Клиент может добавить информацию о пользователе к URL-адресу соединения при инициировании соединения, которое выполняется сервером.
Оба подхода имеют свои явные недостатки:
Получение сеанса с помощью 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/демо…
Последовательность выполнения этого примера показана на рисунке
- Сначала сделайте 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,
};
}
- Клиент получает возвращенный файл 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 } });
- ПО промежуточного слоя для обработки соединений сохраняет отношение сопоставления между 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
};
};
- Другие клиенты инициируют уведомления о сообщениях, которые отправляются указанному подключенному клиенту.
// {app_root}/test/app/io/io.test.js
await app.httpRequest().get('/push?targetUserId=100&msg=msg1').expect(200);
- Найти 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