Web-чат polling на Node.JS

чат

Продолжим тему создания чата на Node.JS. На этот раз напишем чат, который будет в качестве клиентов использовать браузеры.

Проблема

Т.к. протокол HTTP не позволяет устанавливать постоянное соединение сервера с клиентом, то нам необходимо самостоятельно инициировать соединение для считывания новых сообщений. Для этого существует множество способов, но мы для начала воспользуемся самым примитивным - polling. Данный способ представляет собой постоянное опрашивание сервера через определённые интервалы времени. Сразу хочу уточнить, что данный способ ОЧЕНЬ не оптимален с точки зрения использования ресурсов сервера и сетевого канала, но для обучающих целей он вполне пригоден. В реальных приложениях стоит использовать более продвинутые средства, которые я постараюсь в скором времени рассмотреть в этом блоге. Итак, поехали!

Структура

Сначала рассмотрим структуру нашего приложения:

Реализация

Клиентская часть

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

// ...
function load(){
    if ($(".message-row").size() == 0){
        var id = -1;
    }else{
        var id = $(".message-row:last").data('id');
    }

    $.getJSON('http://127.0.0.1:8124/?id=' + id + '&callback=?', function(data){
        data = $.parseJSON(data);
        for (i in data){
            if ($("#mess" + data[i]['id']).size() == 0){
                $('<div class="message-row" id="mess' + data[i]['id'] + '">' + data[i]['message'] + '</div>').appendTo("#chat-body");
                $(".message-row:last").data('id', data[i]['id']);
            }
        }
        $("#chat-body").animate({ scrollTop: $("#chat-body").attr("scrollHeight") }, 3000);
    });
}
// ...

Данная функция будет отвечать за получение новых сообщений от сервера и их визуализацию. Каждое сообщение будет идентифицироваться уникальным id которое равно порядковому номеру сообщения. При визуализации каждое сообщение помещается в div и к нему прикрепляется id при помощи .data(). Затем перед тем как запросить новые сообщения мы получаем id последнего сообщения и помещаем его в запрос. В случае если сообщений до запроса не было - передаем магическое число -1.

Т.к. наш сервер весит на специфическом порту, то для взаимодействия с ним необходимо использовать cross domain ajax. Данный метод подразумевает передачу последним параметром названия некой callback функции. Генерацию этой функции берёт на себя jquery. Всё что нужно от нас - это передать последним параметром в запросе ключ callback со значением "?". Далее jquery заменит знак вопроса на название нашей функции, которое он генерирует случайным образом. Всё что от нас требовалось для реализации меж доменного ajax на стороне клиента мы выполнили, особенности реализации на стороне сервера рассмотрим чуть позже.

Рассмотрим остальной код:

load();
setInterval(load, 2500);

Запускаем предварительную инициализацию списка сообщений и ставим обработчик по таймеру, который будет вызываться каждые 2,5 секунды.

// ....
$("#send-button").click(function(){
    if ($(".message-row").size() == 0){
        var id = -1;
    }else{
        var id = $(".message-row:last").data('id');
    }

    $.getJSON('http://127.0.0.1:8124/?message=' + $("input[name=message]").val() + '&username=' + $("input[name=nick]").val() + '&id=' + id + '&callback=?', function(data){
        data = $.parseJSON(data);
        for (i in data){
            $('<div class="message-row" id="mess' + data[i]['id'] + '">' + data[i]['message'] + '</div>').appendTo("#chat-body");
            $(".message-row:last").data('id', data[i]['id']);
        }
        $("#chat-body").animate({ scrollTop: $("#chat-body").attr("scrollHeight") }, 3000);
    });
    $("input[name=message]").val('');
});
// ....

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

Серверная часть

Тут начинается самое интересное :) Для реализации обмена по протоколу HTTP в node.js есть одноименный модуль. Также мы будем использовать модуль url для получения переданных нам параметров. Теперь рассмотрим код, постараюсь как можно подробнее прокомментировать:

// Подключение модулей
var http = require('http');
var url  = require('url');
// Массив сообщений
var messages = [];

// Создаем сервер и ставим обработчик запроса
http.createServer(function (request, response) { 
  // Получаем url
  var url_string = request.url;
  // Извлекаем параметры
  params = url.parse(url_string, true).query;

  // Если получено сообщение, то добавляем его в массив
  // предварительно объединив его с псевдонимом пользователя
  if (params.hasOwnProperty('message')){
      username = (params['username'].length == 0)?'guest':params['username'];
      message = params['message'];
      messages.push(username + ': ' + message);
  }

  // Извлекаем идентификатор сообщения
  var id = new Number(params['id']);
  var to, count, from = 0;
  // Получаем количество сообщений в массиве
  var len = messages.length;
  // Если пользователь только подключился,
  // то отдаём ему 10 последних сообщений
  if (id == -1){
      if (len < 10){
          count = len;
          from = 0;
      }else {
          count = 10;
          from = len - 10;
      }
  // Иначе отдаем те сообщения которые были получены после 
  // сообщения с переданным идентификатором
  }else{
      count = len - id - 1;
      from = id + 1;
  }
  to = from + count - 1;
  // Формируем результирующий массив сообщений
  ret_messages = [];
  for (i = from; i <= to; i++){
      ret_messages.push({message:messages[i], id:i});
  }

  // Кодируем массив в json
  body = JSON.stringify(ret_messages);
  // Устанавливаем заголовки ответа
  // Второй заголовок необходим для
  // cross domain ajax
  response.writeHead(200, {
    'Content-Type': 'application/json',
    "Access-Control-Allow-Origin": "*"
     });
  // Устанавливаем тело ответа и закрываем соединение
  // имитируем вызов метода
  response.end(params.callback + '(\'' + body + '\')'); 
// Вешаем сервер слушать 8124 порт на 127.0.0.1
}).listen(8124, "127.0.0.1"); 
console.log('Server running at http://127.0.0.1:8124/');

Всё достаточно прозрачно, кроме небольших заклинаний необходимых для корректной работы cross domain ajax. Для того чтобы использовать чат в сети поменяйте ip-адрес в 63 строке на свой внешний ip. Также не забудьте поменять ip-адрес в клиенте.

Здесь можете найти архив с полным кодом приложения - код

Спасибо за внимание, подписывайтесь на RSS, все вопросы в комментарии ниже!