Создаем чат на Node.js и Socket.IO

В данной статье я попытаюсь показать, как можно создать простой чат, используя Node.js в связке с Socket.IO. Изначально я хотел построить чат на чистых Websockets, но столкнулся с практически полным отсутствием готовых реализаций сервера для них в Интернете. Так что решил не изобретать велосипед, а использовать готовую библиотеку.
В моем случае сервер работает под Ubuntu, поэтому все примеры будут для неё (и ссылки в примерах — на него же).

Установка компонентов

Первым делом нам потребуются собственно Node.js (инструкция по инсталляции и ссылки на скачивание здесь) и Socket.IO. Модули для Node.js проще всего устанавливать, используя менеджер npm —
curl http://npmjs.org/install.sh | sh
npm install socket.io

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

Структура серверной части такова: сервер принимает сообщение, если это команда — выполняет определенные действия, если просто сообщение — рассылает всем остальным участникам.
// Подключаем модуль и ставим на прослушивание 8080-порта - 80й обычно занят под http-сервер
var io = require('socket.io').listen(8080); 
// Отключаем вывод полного лога - пригодится в production'е
io.set('log level', 1);
// Навешиваем обработчик на подключение нового клиента
io.sockets.on('connection', function (socket) {
	// Т.к. чат простой - в качестве ников пока используем первые 5 символов от ID сокета
	var ID = (socket.id).toString().substr(0, 5);
	var time = (new Date).toLocaleTimeString();
	// Посылаем клиенту сообщение о том, что он успешно подключился и его имя
	socket.json.send({'event': 'connected', 'name': ID, 'time': time});
	// Посылаем всем остальным пользователям, что подключился новый клиент и его имя
	socket.broadcast.json.send({'event': 'userJoined', 'name': ID, 'time': time});
	// Навешиваем обработчик на входящее сообщение
	socket.on('message', function (msg) {
		var time = (new Date).toLocaleTimeString();
		// Уведомляем клиента, что его сообщение успешно дошло до сервера
		socket.json.send({'event': 'messageSent', 'name': ID, 'text': msg, 'time': time});
		// Отсылаем сообщение остальным участникам чата
		socket.broadcast.json.send({'event': 'messageReceived', 'name': ID, 'text': msg, 'time': time})
	});
	// При отключении клиента - уведомляем остальных
	socket.on('disconnect', function() {
		var time = (new Date).toLocaleTimeString();
		io.sockets.json.send({'event': 'userSplit', 'name': ID, 'time': time});
	});
});

В данном коде (и немного больше):
io.sockets — выбор всех подключенных клиентов
io.sockets.sockets[ID] — выбор конкретно взятого клиента с id ID
socket — выбор «текущего» клиента
socket.send(TEXT) — «базовое» событие, отправка сообщения TEXT
socket.json.send({}) — отправка сообщения в формате JSON
socket.broadcast.send — отправка сообщения всем клиентам, кроме текущего
socket.emit(EVENT, JSON) — отправка пользовательского события EVENT с данными JSON (например — socket.emit('whereami', {'location': loc})), может использоваться для переписывания стандартных событий 'connected', 'message' и 'disconnect'.
socket.on(EVENT, CALLBACK) — вызов функции CALLBACK при возникновении события EVENT (например — socket.on('whereami', function(loc){ console.log('I\'m in ' + loc + '!'); }))
Я передаю клиентам сообщения в JSON, т.к. сам текст сообщения автоматически генерируется в клиенте. Таким образом представление данных не зависит от сервера и его легко изменить, не прикасаясь к нему (например, изменить язык); к тому же передается меньший обьем данных между сервером и клиентом.

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

HTML и CSS

index.html:
<!doctype html>
<html>
	<head>
		<meta charset="utf-8">
		<title></title>
		<link href="style.css" rel="stylesheet">
		<script src="http://46.182.31.65:8080/socket.io/socket.io.js"></script>
		<script src="client.js"></script>
	</head>
	<body>
		<div id="log"></div><br>
		<input type="text" id="input" autofocus><input type="submit" id="send" value="Send">
	</body>
</html>

style.css
#log {
	width: 590px;
	height: 290px;
	border: 1px solid rgb(192, 192, 192);
	padding: 5px;
	margin-bottom: 5px;
	font: 11pt 'Palatino Linotype';
	overflow: auto;
}
#input {
	width: 550px;
}
#send {
	width: 50px;
}

.in {
	color: rgb(0, 0, 0);
}
.out {
	color: rgb(0, 0, 0);
}
.time {
	color: rgb(144, 144, 144);
	font: 10pt 'Courier New';
}
.system {
	color: rgb(165, 42, 42);
}
.user {
	color: rgb(25, 25, 112);
}

Javascript

socket.io.js автоматически отдается Node.js по адресу nodeJsIp[:port]/socket.io/socket.io.js — ничего дополнительно делать не надо.
client.js
// Создаем текст сообщений для событий
strings = {
	'connected': '[sys][time]%time%[/time]: Вы успешно соединились к сервером как [user]%name%[/user].[/sys]',
	'userJoined': '[sys][time]%time%[/time]: Пользователь [user]%name%[/user] присоединился к чату.[/sys]',
	'messageSent': '[out][time]%time%[/time]: [user]%name%[/user]: %text%[/out]',
	'messageReceived': '[in][time]%time%[/time]: [user]%name%[/user]: %text%[/in]',
	'userSplit': '[sys][time]%time%[/time]: Пользователь [user]%name%[/user] покинул чат.[/sys]'
};
window.onload = function() {
	// Создаем соединение с сервером; websockets почему-то в Хроме не работают, используем xhr
	if (navigator.userAgent.toLowerCase().indexOf('chrome') != -1) {
		socket = io.connect('http://46.182.31.65:8080', {'transports': ['xhr-polling']});
	} else {
		socket = io.connect('http://46.182.31.65:8080');
	}
	socket.on('connect', function () {
		socket.on('message', function (msg) {
			// Добавляем в лог сообщение, заменив время, имя и текст на полученные
			document.querySelector('#log').innerHTML += strings[msg.event].replace(/\[([a-z]+)\]/g, '<span class="$1">').replace(/\[\/[a-z]+\]/g, '</span>').replace(/\%time\%/, msg.time).replace(/\%name\%/, msg.name).replace(/\%text\%/, unescape(msg.text).replace('<', '&lt;').replace('>', '&gt;')) + '<br>';
			// Прокручиваем лог в конец
			document.querySelector('#log').scrollTop = document.querySelector('#log').scrollHeight;
		});
		// При нажатии <Enter> или кнопки отправляем текст
		document.querySelector('#input').onkeypress = function(e) {
			if (e.which == '13') {
				// Отправляем содержимое input'а, закодированное в escape-последовательность
				socket.send(escape(document.querySelector('#input').value));
				// Очищаем input
				document.querySelector('#input').value = '';
			}
		};
		document.querySelector('#send').onclick = function() {
			socket.send(escape(document.querySelector('#input').value));
			document.querySelector('#input').value = '';
		};		
	});
};


Наконец, запускаем сервер с логом в файл и в фоновом режиме —
node server.js > output.log &

(стоит отметить, что скрипт у меня запустился только из директории пользователя, куда был поставлен node.js)

Все исходники можно скачать здесь. Сразу говорю, я не гуру программирования и писал для себя, так что на корявость прошу не ругаться (да и бесполезно). Т.к. статику отдает пока апач, желательно запускать у себя на компьютере. Если же лень или неудобно/не можете — живой пример можно посмотреть здесь.

Код протестирован и работает в Opera 11+, Firefox 5+, Chrome 12+. IE9 судя по логам соединяется, получает и отправляет пакеты, но на браузере это не отражается.

Использованные материалы:
socket.io/#how-to-use
github.com/LearnBoost/Socket.IO/wiki/Migrating-0.6-to-0.7
google.com

В следующем выпуске: записываем историю сообщений, меняем себе имя, пишем в приват другому игроку, и многое другое!
Share post
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 75

    +2
    Под убунтой ни в хроме ни в ff 6 не заработало
      0
      Под дебианом в хроме замечательно заработало.
        0
        Хм.
        Чую дело в заблокированных портах.
        А socket.io по какому порту общается?
          0
            0
            Смотря какой транспорт выбран. XHR и web-sockets по стандартному 80. Флеш вроде по какому-то своему.
            0
            Чудеса
            8080 заблокирован

            Я удивлен, честно гря. Пойду узнавать, зачем так сделано.
              0
              какая версия?
              у меня не заблокирован, u10.10 x64
                0
                Да не
                Это у меня на работе, оказывается, почти все порты заблокированны.
                Просто я не ожидал, что 8080 тоже заблокируют.
          0
          Хьюстон у нас не взлетает :(
          kubuntu11.4 fx6 ch12
            0
            Видимо, хабрэффект накрыл динамику на клиент-сайде.
              +1
              *сервер-сайде, простите.
                0
                эм, так вы засеките при каком максимуме все падает и изза чего — это бесплатное нагрузочное тестирование
                  0
                  Так не я автор же. Я тоже бесплатный нагрузочный тестер :)
                    0
                    Я подумал, что вы приближенный :)
                      0
                      Нет, мне интересно в node.js и socket.io.
                    0
                    Запустил заново. Сервер работает, а вот node-скрипт отрубился. Теоретически, надо бы поставить через npm модуль forever, чтобы само перезапускалось, но что-то не ставится… Сейчас послежу, когда вырубается.
              0
              под убунтой заработало во всех браузерах установленных.
              0
              Тормозит сильно то ли сервер то ли у меня :( Проц под 100%. Последний хром
                0
                Ну, в клиенте пока тормозить нечему, а вот сервер не выдерживает, да) Кто подскажет утилиты, чтобы мониторить загруженность / потребление отдельным процессом памяти, проца, инета etc через терминал?
                  +2
                  atop?
                    0
                    Спасибо за наводку. Как и предполагалось, основную нагрузку создает apache…
                0
                В общем, пример уже неживой :)
                  0
                  Чат — это видимо самый простой и яркий пример технологии node и веб-сокетов. В то же время это одна из такий фундаментальных и интересных задач для Comet технологий. Наверное в основе многих такий сервисов будет лежать нечто похожее на чат.

                  Насчет этой реализации сказать ничего не могу, но когда мне понадобилось реализовать чат на nodejs и socket.io быстро удалось найти рабочий пример.
                    0
                    расскажите впечатления от использования?
                      +1
                      То что я делал было не совсем чатом, это небольшая система для стресс тестирования для веб приложения. Socket.io и node.js в этом плане оказались очень удобными. Основа была как у чата: агенты сбрасывают в чат раз в секунду счетчики производительности и данные тестов и получают из чата команды (остановить тест/запустить тест).

                      Удобно что через тот же самый socket.io я мог работать с чатом как с веб странички (результаты тестов видно в реальном времени) так и с агентов. За счет такого out-of-proc общения через socket.io можно создавать агентов на разных компьютерах или в одном компьютере в разных процессах. Очень легко масштабируется.

                      Чат не был главной вещью на которую падает нагрузка, основная задача выдавать максимум HTTP запросов для нагрузки серверов.
                      0
                      В данном случае меня сподвигла не самая идея чата, а отсутствие внятной документации для Socket.IO. По ссылкам в конце статьи можно найти куски кода, но это больше примеры — в них не обьясняется какая функция что делает. Методом тыка, чтения исходников и вывода в консоль объектов библиотеки я более-менее разобрался, что что делает и делюсь с вами)
                      +2
                      Мда, судя по тематике постов в чате, следует сделать функцию автобана)
                        0
                        Можно еще так сделать: используем сервер ejabberd, в качестве сервера обработки сообщений. Клиенты соединяются с ним через прокси-сервер, который проксирует websocket. Такой проксик придется написать самому, т.к. готового решения для ejabberd я не знаю.

                        В итоге, получим полноценную XMPP = много фич, которые остается реализовать на клиенте.
                          0
                          Не нужно делать велосипеды, посмотрите в сторону XMPP-BOSH, ejab умеет этот протокол. Достаточно будет обычного аякса. Если хочется готовых реализаций, то есть github.com/sstrigler/JSJaC, например
                          0
                          Что-то лежит чатик.
                          Сколько народу сейчас?
                            0
                            Да вроде не лежит, живой. У вас какой браузер?
                              0
                              пардон, я пытался отправить по-русски — не сразу понял, что это не работает
                            0
                            А он что русский язык не поддерживает????
                            Это как?
                              0
                              И русский язык поддерживает. Похоже, не у всех, но сейчас человек 5 в чате кириллицей отписались. На странице UTF-8 кодировка, возможно браузер принудительно переопределяет?

                              UPD. Так, вылезла ошибка — Segmentation fault. Пошел курить мануалы.
                                0
                                Chrome 11 @MacOS
                                UTF-8 — родная кодировка, все должно быть ок с ней

                                Да, я тоже удивился увидев русские буквы — какой-то страный баг почему не отправляется. Может где-то с сервера не та кодировка приходит?
                                  0
                                  А, мой косяк, понял. Пытался по-быстрому пофиксить баг с отправкой пустых строк — поставил регэксп на вхождение печатных символов, а про кириллицу забыл. Возможно, это. Сейчас подправлю код, через пару минут перезагрузите страницу.
                                    0
                                    теперь русский нормально
                                0
                                поддерживает русский.
                                0
                                А сколько было соединений, очень интересно когда начало тормозить.

                                В каком режиме работает апачи? Мне кажется что WS он не пробрасывает никак.

                                И ещё: у вас в примере есть вызов io.sockets.json.send вместе с emit. send — это низкоуровневая функция, не стоит смешивать с emit. Возможно от туда и проблемы с русскими буквами
                                  0
                                  Пардон за навязчивость.
                                  Вот тут однокомнатный чат с поддержкой русского и хабраэффекта — chat.websockets.ru
                                    0
                                    Сколько пользователей? Какая нагрузка? На каком железе работает?
                                    • UFO just landed and posted this here
                                        0
                                        Ану быстро предъявили свой чатик!-!-!

                                        С чатика проще всего стартовать, по сути все остальное это то же самое взаимодействие, разве что посылается не просто текст, а текст с форматированием и не всем, а определенным пользователям.
                                          0
                                          Собственно, чем не нравится чат? Это основа для всего, любое общение клиента с сервером — обмен пакетами, сейчас обмениваемся текстовыми сообщениями, завтра меняем в коде 10 строчек и привязываем к игрушке/рисовалке/документам/календарю чему там еще…
                                          • UFO just landed and posted this here
                                              0
                                              Потому что конкретно на Socket.IO на хабре еще не было? И да, мне инвайт (куча интересных статей лежит в закрытых блогах, неизвестно почему) и опыт (когда тестируешь сам — в голову не придет спамить пустыми строками каждую секунду), серверу — нагрузочное тестирование, троллям — радость.
                                              Или вы имеете что-то против троллей?
                                              • UFO just landed and posted this here
                                                  0
                                                  Сердечное спасибо!)
                                                  С точки зрения пользователя — весь интернет это шайтан-машина. Им не нужно знать как это работает, почему, как делалось etc — пользователю главное, чтоб работало.
                                                  Когда нет websocket — в ход идет XmlHttpRequest-polling. Не думаю, что что-то сверхсложное. Если любопытно, могу полазить по исходникам, расписать.
                                                  • UFO just landed and posted this here
                                          0
                                          Кол-во соединений/пользователей пока не считал, хотя на тестовом сервере есть. В следующей статье будет. Кстати, на chat.websockets.ru хорошо сделано, может сппозаимствовать идею?..
                                          Железо — тучка от Selectel'а, 32-битный Lycid Lynx Ubuntu, 168-512 оперативки (сейчас 260), 8 ядер.
                                          Апач работает в режиме «apt-get — vim virtualhosts — PROFIT!» — я больше программист нежели администратор) впрочем, все вебсокеты идут на node, с апачем они на разных портах и не пересекаются. emit — в самой программе не используются, т.к. пришлось бы на каждый event писать свой обработчик, я пробовал так делать — куча повторяющегося кода.
                                            0
                                            А какую идею хотите оттуда позаимствовать? Я не жадный — буду рад поделиться :)
                                              0
                                              Справа список пользователей, их количество etc. У меня пока была идея выводить список в чат по команде /userlist (или какой-то в этом роде), но ваша реализация лучше — лог не засоряется.
                                            0
                                            Чатик сломался :(
                                              0
                                              Исправил. Съели всю память, так что пришлось серв перезапускать — node отказывался работать) Добавил еще оперативы, до вечера надеюсь этого хватит.
                                              +3
                                              говорим node.js — подразоумеваем чатик
                                                0
                                                А можно ли это как-то настроить, чтобы работало на 80-м порту на том же домене?

                                                Приходит в голову или писать веб-сервер на node для отдачи статики (не радует идея что-то), или отдавать статику через nginx и проксировать чат, но он вроде websokets не поддерживает.
                                                  0
                                                  На том же домене и на том же порту? Что-то я не онял задачу…

                                                  Лучше на сабдомен положить, и кстати — нода очень хорошо статику отдает ;)
                                                  • UFO just landed and posted this here
                                                      0
                                                      для nginx есть патч-плагин для WS(фактически TCP wrapper), но придётся пересобирать его.
                                                      +1
                                                      Надо свой чатик выложить, как говорится — других посмотреть, свой померять…
                                                        0
                                                        Кстати, в хроме проблема замечается толька за прокси года сидишь
                                                        0
                                                        Как только прочитал заголовок статьи, сразу же вспомнил NowJS: чат занимает где-то 5-10 строчек на сервере и примерно столько же на клиенте :)
                                                          0
                                                          По факту — nowjs надстройка к socket.io которая типа шарит объект между сервером и клиентом, но как-то у меня с авторизацией лагать начинает на 1000+ юзерах (правда в дев енв), сейчас переписываю на express+socket.io, как-то оно пошустрее на вид получается…

                                                          И да now.js + 500 юзеров + около пары сотен сообщений в памяти — ест 180 метров…
                                                          0
                                                          Тут еще меньше кода Клиент Сервер. Это KnockoutJS+NowJS(SocketIO)+Плугин для Knockout (ссылки на него на Github). Кстати, почему никто не пишет про Knockout 1.3? Там много вкусного.
                                                            0
                                                            Меньше кода писать? Возможно. Больше кода в подключаемых библиотеках? Да.
                                                            Конечно, можно написать свою высокоуровневую библиотеку, в которой чат вообще будет реализован функцией addChat(). Но надо ли? Я хотел написать на чистом JS — не срослось, Socket.IO — имхо, ближе всего по логике к чистому языку.
                                                              0
                                                              Вы бы хоть глянули, что там. Что значит «на чистом JS»? А SocketюIO или тот же jQuery — это по вашему не чистый JS? И чем Socket.IO «чище» NowJS? Так бы сразу и написали — «хотел написать свой велосипед».
                                                                0
                                                                Пожалуй, я неверно выразился. С минимумом синтаксического сахара. ko.liveModel() — немного не нативная функция. Вы б хоть и мой пост прочитали) И периодически изобретать велосипед таки полезно.
                                                                  0
                                                                  Опять мимо — это не синтаксический сахар. А велосипед изобретать — да полезно, чтобы понять как оно работает внутри. Только в первом абзаце своего поста Вы отказались от изобретения велосипеда. Я бы сказал, что Socket.IO и NowJS позволяют работать на разных уровнях абстракции. Socket.IO — уровень сокетов, NowJS — уровень разделяемых объектов. Можно опуститься еще ниже, но тяжело. Ну а ko.liveModel() на столько же нативная, как io.connect(). ko.liveModel() выводит нас на новый уровень — уровень разделяемой модели, которая привязана к элементам DOM. В результате изменения в DOM одного клиента, автоматически приводят к изменениям у остальных. Я просто хотел обратить внимание на эту технологию.
                                                                    0
                                                                    Наверно, я не смогу передать словами, почему именно Socket.IO, но посмотрите на это:
                                                                    Websockets:
                                                                    var socket = new Websocket('http://example.com/');
                                                                    socket.on('connect', function() {});
                                                                    socket.on('message', function() {});
                                                                    Socket.IO:
                                                                    var socket = new io.socket('http://example.com/'); // до версии 0.7.x
                                                                    socket.on('connect', function() {});
                                                                    socket.on('message', function() {});
                                                                    KnockoutJS + NowJS
                                                                    var chatRoom = ko.liveModel({
                                                                    messages: ko.observableArray([]),
                                                                    send: function() {
                                                                    chatRoom.messages.push({text: this.text()});
                                                                    ko.save({ overwrite: true, ignore: [«text»]});
                                                                    },
                                                                    text: ko.observable()
                                                                    }); // и так далее
                                                            0
                                                            плохо что не учли самого элементарного касающегося чатов — отправка пустых сообщений.
                                                              0
                                                              уже стоит в пачке хотфиксов на сервере и клиенте) здесь только основная логика, без всяких плюшек, проверок и ограничений. думал накатать список в продолжении (судя по реакции аудитории не таком уж и нужном, но дело надо доводить до логического конца...).
                                                                0
                                                                а мне бы очень хотелось увидеть продолжение))
                                                                чтобы сообщения в redis базе сохранялись например сколько-то последних) чтобы список участников онлайн был с аватарками итд) многие проекты начинали и загнулись. интересно почему? неужели спроса нет?
                                                                например тот-же ajax.im aka ajaxim.com и многие другие.
                                                              0
                                                              Статья полезная и актуальная до сих пор, некоторые вещи, конечно не идеальны, НО! Для нормального программиста достаточно, чтобы понять в какую сторону копать и что читать.

                                                              Статью рекомендую!

                                                              Если кому будет интересно, то готовый чат по этой технологии выложен тут: websocket чат, в качестве примера и начала изучения данной технологии была взята данная статья пару недель назад.
                                                              Использую связку: Nginx + Apache + Node.JS (Socket.IO + Forever)
                                                              Forever — для подвешивания чата в процессы и управления им.
                                                                0
                                                                По-моему лучше уже этот вариант)
                                                                tutorialzine.com/2014/03/nodejs-private-webchat/

                                                                Only users with full accounts can post comments. Log in, please.