- PVSM.RU - https://www.pvsm.ru -
Добрый день.
В этой статье я расскажу о том, как реализовать систему мониторинга активности пользователей с помощью Node.js и Socket.IO. Выглядит это примерно так:
Писалось это для ERP-системы, в которой взаимодействие работников немаловажно, в частности для избегания ситуаций, когда два оператора будут редактировать один товар.
Итак, для сервера на Node.js (express, connect) необходимо было реализовать данную систему мониторинга, которая информировала бы об активности пользователей в реальном времени (т.е. вышедший из системы пользователь сразу пропадал из списка "Пользователи онлайн", перешедший на другую страницу — пропадал из "Эту страницу просматривают", а вошедший, соответственно, появлялся в списках).
Сразу оговорюсь, что ввиду закрытости системы от остального мира, обязательным началом работы является аутентификация.
Ну и прежде чем приступать — тем, кто не очень знаком с азами Socket.IO советую посмотреть на типовую реализацию чата [1].
Клиентская часть в данном деле реализуется довольно просто, поэтому не буду заострять внимание на внешней красоте. Упомяну лишь что нижеприведённый код включён в базовый шаблон, т.е. присутствует на каждой загружаемой странице.
Вот список того что нам понадобится: элементы с id=«sockstat» (для отображения статуса подключения), с id=«alsohere» (для списка тех, кто просматривает страницу), id=«online» (для списка всех тех, кто онлайн), естественно вот такая штука
<script type="text/javascript" src="/js/lib/jquery.js"></script>
<script type="text/javascript" src="/socket.io/lib/socket.io.js"></script>
,
и маленькая вспомогательная ф-ия, делающая из массива элементы маркированного списка:
function lister(arr) {
var s = '';
$.each(arr, function(key, value) {
s += '<li><b>' + value.name + '</b> (login: ' + value.login +
', id: ' + value.user_id + ')</li>';
});
return s;
}
Теперь всё готово. Механизм таков:
$(document).ready(function() {
var socket = io.connect("http://localhost:8080"); // пытаемся подключиться к серверу сокетов
socket.on("connect", function () { // подключившись, делаем следующее:
$("#sockstat").text("connected ok");
// пишем в соответствующий элемент что связь установлена
socket.emit("iamhere", { location: document.URL } );
// и посылаем серверу своё местоположение
socket.on("alsohere", function (alsohere) {
// получив от него список просматривающих эту же страницу, выводим его:
$("#alsohere").html('Эту страницу просматривают: <ul>' + lister(alsohere) + '</ul>');
});
socket.on("online", function (online) {
// аналогично поступаем со списком всех онлайн-пользователей
$("#online").html('Пользователи онлайн: <ul>' + lister(online) + '</ul>');
});
socket.on("disconnect", function () {
// если связь потеряна - пишем об этом (почему спустя полсекунды, объясню чуть ниже)
setTimeout(function () {
$("#sockstat").text("connection lost!");
}, 500);
});
});
});
Покидая страницу (переходя по ссылке, например), сначала разрывается сокетная связь с сервером, и только потом рендерится и отдаётся клиенту новая страница, возобновляя подключение. Поэтому, чтобы уходя со страницы, не высвечивалось сообщение о разрыве понапрасну на долю секунды, и сделана эта задержка в полсекунды.
Маленькая хитрость, зато пользователь всегда сможет уверенно говорить: «Не было никаких разрывов!», пока сервер действительно не упадёт.
Вроде просто? А так и есть.
В коде запускаемого файла проекта — в index.js, помимо всяких штук типа
app.configure();
подключаем наш рукописный sockets.js, отвечающий за работу с сокетами, необходимыми в нашем деле:
require('./sockets');
В нём помимо собственно обработки событий, содержится функция авторизации — которая и даёт разрешение на создание подключения для тех, кто подходит по определённым критериям. В нашем случае это те, кто предварительно залогинился.
В общем виде это выглядит вот так [2] (обратите внимание на sio.set('authorization'...)).
А теперь сам алгоритм. Для начала — на русском языке.
Обозначений тут будет 2:
Этапов в алгоритме тоже 2 — обработка нового подключения и обработка сообщения о том что клиент отключился.
Как только к нам (серверу) подключается, т.е. проходит авторизацию, заходит на страницу и сообщает своё местоположение —
socket.emit("iamhere", { location: document.URL } );
клиент, мы должны:
Когда же клиент отключается —
Логично? Продолжаем.
Для реализации вышеописанного я решил особо не нагружать файл socket.js и вынес функции «подай-принеси» в отдельный файл — auth.js.
Он устроен так:
var auth = function () {
"use strict";
// Private - наши внутренние переменные/функции
var __users = [],
...,
return {
...
// Public - экспортируемые переменные/функции
};
}();
module.exports = auth;
Основной же блок socket.js таков:
sio.sockets.on('connection', function (socket) {
var hs = socket.handshake;
auth.addActiveUser({ login: hs.session.user, name: hs.session.username, id: hs.session.user_id });
// вот он, первый шаг при подключении - добавляем пользователя в список активных
socket.on('iamhere', function (msg) {
// а получив от него сигнал о местоположении - запоминаем его
auth.addPageActiveUser({
login: hs.session.user,
path: msg.location,
path_id: socket.id
});
auth.getListActiveUser(function (online) {
// затем рассылаем всем обновлённый online (шаг 2)
socket.emit('online', online);
socket.broadcast.emit('online', online);
});
auth.getListByPageActiveUser({ path: msg.location }, function (alsohere) {
// блок проще чем кажется - всего лишь рассылаем пользователям, сидящим
// на той же странице, куда зашёл новенький, обновлённый список alsohere
var i,
len;
auth.getListByPageConnection({ path: msg.location, users: alsohere }, function (connections) {
len = connections.length;
for (i = 0; i < len; i++) {
sio.sockets.sockets[connections[i].id].emit('alsohere', alsohere);
}
});
});
});
socket.on('disconnect', function () { // действия при дисконнекте аналогичны:
var s = auth.getPageByIdConnection(socket.id);
// определяем, какую страницу закрыл пользователь,
setTimeout(function () {
auth.removeActiveUser({ login: hs.session.user }); // вычёркиваем его из списка активных,
auth.getListActiveUser(function (online) { // рассылаем всем обновлённый online,
socket.broadcast.emit('online', online);
});
auth.removePageActiveUser({ login: hs.session.user, path_id: socket.id });
// вычёркиваем закрытую страницу из списка открытых им
auth.getListByPageActiveUser({ path: s }, function (alsohere) {
// и рассылаем всем тем, кто сидел на свежезакрытой странице
// их новый список alsohere
var i,
len;
auth.getListByPageConnection({ path: s, users: alsohere }, function (connections) {
len = connections.length;
for (i = 0; i < len; i++) {
sio.sockets.sockets[connections[i].id].emit('alsohere', alsohere);
}
});
});
}, 1000); // и снова хитрость с таймером - дабы не смущать
// других клиентов "мельканием" человека, переходящего со страницы на страницу
});
});
Вуаля.
В модуле auth все эти данные о пользователях — об их подключениях и страницах, сессиях — хранятся в виде объекта в приватной переменной __activeUsers.
Для каждого пользователя создаётся поле — __activeUsers[login], содержащее в себе поля:
Соответственно, модуль auth выставляет наружу только public-методы, работающие с вышеописанной переменной. Они представляют собой ф-ии, состоящие из переборов for, for..in, push'ей и splice'ов:
Подключаясь с серверу и успешно проходя аутентификацию, пользователь получает socket.handshake с уникальным id для данного пользователя.
Открывая новую страницу, на это подключение заводится уникальный socket.id для данной страницы. То есть 1 пользователь с 10ю открытыми страницами — это 1 handshake.id и 10 разных socket.id.
А дальше дело техники — манипулируя этими данными, мы с использованием вышеописанной структуры данных всегда можем сказать кто и что у нас просматривает/редактирует.
И вся «сложность» реализации состоит в том чтобы перебирать поля объекта и массивы, доставая нужное. А это мы умеем :)
Так что теперь можно ещё раз посмотреть на вышеприведённый кусок кода и всё станет ясно.
Надеюсь принцип работы вы поняли, где какие id не перепутаете, куда нужно вставите таймауты для сглаживания переданной клиенту информации; ну а с реализацией для вашей конкретной задачи проблем возникнуть не должно.
Удачи.
P.S. Opera (тестировал на v.11.62) при закрытии страниц, в отличие от FF и хрома, не утруждает себя отсылкой серверу сообщения о дисконнекте клиента. Потому отключившиеся/покинувшие страницу пользователи висят в списке активных ещё несколько секунд, пока сервер их не отключит автоматически по таймауту.
Автор: Keenest
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/node-js/6718
Ссылки в тексте:
[1] типовую реализацию чата: http://habrahabr.ru/post/127525/
[2] вот так: https://gist.github.com/1865578
Нажмите здесь для печати.