Node.JS
September 2011 2

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

From Sandbox
В данной статье я попытаюсь показать, как можно создать простой чат, используя 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

В следующем выпуске: записываем историю сообщений, меняем себе имя, пишем в приват другому игроку, и многое другое!
+33
185.7k 365
Comments 75
Top of the day