Socket.IO и Node.js: пробное использование
Я давно хотел попробовать поиграться с библиотекой Socket.IO + Node.js. Socket.IO написана компанией LearnBoost и предоставляет API, которое позволяет клиенту и серверу общаться, используя различные технологии:
- WebSocket
- Adobe Flash Socket
- ActiveX HTMLFile (IE)
- Server-Sent Events (Opera)
- XHR с multipart encoding
- XHR с long-polling
Причём технология выбирается совершенно прозрачно и для клиента, и для сервера. Если браузер поддерживает WebSockets, будут использоваться именно они. Для других браузеров будет обеспечен fallback до флешовых сокетов, а если и этих нет — до обычного XHR с long polling. Плюс над всем этим великолепием сделан удобный API для клиента и сервер для Node.js (впрочем, думаю сервер тут можно реализовать на любом языке).
Установка
Качаем последние стабильные сорцы с Github. Используется версия HEAD:
git clone git://github.com/LearnBoost/Socket.IO.git cd Socket.IO git submodule update --init
Команда git submodule init нужна чтобы git самостоятельно скачал нужные зависимости. Теперь собираем наш файл библиотеки:
./build.js
В результате должен получиться файл socket.io.js
Теперь в отдельном каталоге (чтобы не создавать путаницы) клонируем node-сервер для Socket.IO, тоже с субмодулями:
git://github.com/LearnBoost/Socket.IO-node.git cd Socket.IO-node git submodule update --init
Вместе с Socket.IO идёт тестовое приложение — чат. Можно его протестить. Для этого лезем в папку test и копируем файлы chat.html и каталог client в папку для статических файлов (у меня их отдаёт nginx):
cd test cp chat.html /home/www/files/ cp -R client /home/www/files/
Каталог client — точная копия уже взятого нами репозитория Socket.IO. Чтобы лишний раз ничего не собирать, берём уже собранный нами клиент (всё что внутри каталога Socket.IO) и копируем в папку client (ту, что доступна пользователям). Теперь правим ссылки в файле chat.html, чтобы они соответствовали адресам, по которым отдаётся наш контент. Я просто добавил /files в начало обоих путей, т.к. файлы у меня доступны в http://192.168.175.128/files/.
Теперь запускаем файл сервера:
node server.js
…и открываем наш chat.htmlв браузере. Чат должен подключиться и заработать. В консоль Node.js в это время отдаётся отладочная информация: кто подключился с каким протоколом. Я открыл две вкладки Хрома, одну в IE и одну в Opera. Все они отобразились как Websocket (IE и Opera подключены через Flash-сокеты, насколько я понял, но тоже эмулируют протокол WebSockets). Все браузеры работают и получают обновления чата в реальном времени (это хорошо видно если поставить их окна рядом).
Использование
Изначально я хотел для экономии времени просто разобрать пример с чатом, но в конце концов решил состряпать своё приложение, в котором будут различные виды передачи данных и клиент-серверного взаимодействия. Недавно в переписке с одним человеком я сказал, что Node сейчас лучше всего подойдёт для чего-нибудь вроде сервера онлайн-игры. Сейчас я её и напишу
Но конечно, изобретать игру с нуля для статьи — дело слишком хлопотное. Поэтому я возьму готовую. Есть такая старая игра, написанная ещё для DOS: robotfindskitten. В ней по игровому полю шастает робот, врезаясь в различные неопознанные объекты и опознавая их
Такая игрушка вплоне годится для демонстрации связки Node.js + Socket.IO в построении браузерной игры.
Итак, правила я изменю следующим образом: роботов на поле может быть несколько, объекты при врезании в них исчезают (мне лень делать collision detection, но это задача в принципе несложная). Итак, поехали.
Само игровое поле будет очень простым: div, в котором абсолютным позиционированием размещаются спрайты (я не собираюсь писать эмулятор консоли). Каждый игрок может управлять своим роботом с помощью стрелок, при этом каждый ход робот сообщает серверу своё местоположение. Сервер в свою очередь рассылает это местоположение остальным.
Столкновения с предметами будут обрабатываться на стороне сервера, соответствующие сообщения будут рассылаться всем игрокам, но описание найденного будет получать только тот кто врезался в предмет.
За основу я взял стандартный скрипт чата из примеров к Socket.IO. Изначально там было два типа сообщений: одиночное сообщение и буфер предыдущих. Я оставил их, и добавил два новых:
objects: получаемый с сервера список объектов на картеfoundobject: сообщение игроку, нашедшему предметdelobject: команда игроку удалить предмет с поля
Последние два разделены специально. Мы будем отсылать foundobject игроку который нашёл предмет и одновременно delobject — всем игрокам, чтобы один предмет не находили дважды.
Вначале — всякие приготовления при запуске сервера. Объявляем массивы с предметами и игроками, заполняем предметы случайными значками:
var players = [];
var rand_coord = function(min, max) {
return Math.floor(Math.random() * (max - min) + min);
}
var objects = [];
var tiles = "#$%!&^*)(+=xHXD;WQMO".split('');
// 30 objects
for (var i = 0; i <= 30; i++) {
var rand_tile = tiles[rand_coord(0, tiles.length)];
var rand_color = rand_coord(128, 256).toString(16) +
rand_coord(128, 256).toString(16) +
rand_coord(128, 256).toString(16);
objects.push({
'id': i,
'x' : rand_coord(1, 50) * 10,
'y' : rand_coord(1, 50) * 10,
'tile': rand_tile,
'color': rand_color
});
}
Функция rand_coord изначально предназначалась для получения случайных координат, но в конце концов я её использовал для получения любого случайного числа в пределах от n до m.
Сервер Socket.IO запускается примерно так:
var io = require('./lib/socket.io');
var server = http.createServer(function(req, res){
// Здесь код основного сайта
});
server.listen();
io.listen(server, {
onClientConnect: function(client){
// Действие при подключении пользователя
},
onClientDisconnect: function(client){
// Действие при отключении пользователя
},
onClientMessage: function(message, client) {
// Действие при получении сообщения от пользователя
}
});
Socket.IO принимает в качестве аргумента уже существующий HTTP-сервер и как то “присоединяется” к нему. Клиентская часть тоже пишется довольно просто — по большому счёту, там всего один важный метод:
var socket = new io.Socket(null, {rememberTransport: false, port: 8080});
socket.connect();
socket.addEvent('message', function(data_raw) {
// Обрабатываем сообщение от сервера
});
Кроме этого есть события connect и disconnect.
Теперь нам нужно разобраться с сообщениями, отсылаемыми клиентом и сервером. Сначала обработаем сообщение от клиента, о том что робот игрока переместился:
onClientMessage: function(message, client) {
// Формируем исходящее сообщение
var msg = { message: [client.sessionId, message] };
// Сохраняем положение игрока в буфере (по большому счёту, без этого можно обойтись)
buffer.push(msg);
if (buffer.length > 15) {
buffer.shift();
}
// Рассылаем новое положение игрока остальным
client.broadcast(json(msg));
}
Здесь сервер вообще не залезает внутрь сообщения — он просто рассылает его в том же виде, в каком получил. Теперь надо обработать эти сообщения на клиентской стороне:
var process_message = function(message) {
newdata = message[1];
if (!players[message[0]]) {
// Если такого игрока мы ещё не видели, создаём для него спрайт
console.log('New player: ' + JSON.stringify(newdata));
var player_pic = $('<div></div>').attr({'class':'player', 'style':'color: '+ newdata.color}).text('@');
$(player_pic).css('left', newdata.x).css('top', newdata.y);
// Добавляем нового знакомого в общий пул
players[message[0]] = player_pic;
$('div#field').append(player_pic);
} else {
// Просто обновляем позицию игрока
$(players[message[0]]).css('left', newdata.x);
$(players[message[0]]).css('top', newdata.y);
$(players[message[0]]).css('color', newdata.color);
}
}
socket.addEvent('message', function(data_raw) {
// Парсим пришедшие сообщения
var data = JSON.parse(data_raw);
if (data.buffer) {
// Если пришёл буфер, обрабатываем сообщения по очереди
for (mid in data.buffer) {
process_message(data.buffer[mid].message);
}
} else if (data.message) {
// Если это просто сообщение: обрабатываем его
process_message(data.message);
}
}
Типы сообщений buffer и message обрабатываются отдельно, но все сообщения попадают в один обработчик process_message. Он создаёт тайлы для новых игроков и обновляет позиции уже знакомых.
Теперь разберёмся с буфером сообщений. Когда игрок подключается к серверу, нам надо выслать ему 15 последних сообщений и объекты карты, чтобы он был в курсе событий
onClientConnect: function(client){
// Отсылаем буфер сообщений
client.send(json({ 'buffer': buffer }));
// Отсылаем объекты, которые есть на карте
client.send(json({'objects' : objects}));
client.broadcast(json({ 'announcement': client.sessionId + ' connected' }));
}
Так же на событие onClientDisconnect можно повесить оповещение о том что клиент покинул игру (чтобы другие игроки убрали его тайл с поля). На клиенте нам теперь надо создать тайлы для объектов и разместить их на поле:
if (data.objects) {
for (var oid in data.objects) {
var server_obj = data.objects[oid];
objects[server_obj.id] = $('<div></div>').attr({'class':'object', 'style': 'color: #' + server_obj.color }).text(server_obj.tile);
$(objects[server_obj.id]).css('left', server_obj.x);
$(objects[server_obj.id]).css('top', server_obj.y);
$('div#field').append(objects[server_obj.id]);
}
}
Помимо размещения на поле объекты добавляются в массив objects, чтобы их потом проще было удалять по id.
Теперь игроки должны двигаться, объекты — появлятся и наша игра должна работать в разных браузерах (я проверил Chrome, IE8 и Opera). Настало время описать процесс нахождения объектов. Сервер при получении сообщения от клиента будет сравнивать его координаты с координатами объектов, и рассылать нужные сообщения в случае если объект был обнаружен. Изменим обработчик onClientMessage:
onClientMessage: function(message, client) {
//sys.puts('Client message: ' + json(message));
if (message.x && message.y) {
sys.puts('New message from ' + client.sessionId + ': ' + JSON.stringify(message))
// Проверяем, не врезался ли игрок во что нибудь
for (var oid in objects) {
if (message.x == objects[oid].x && message.y == objects[oid].y) {
// Всё таки врезался
var text = strings[rand_coord(0, strings.length)];
sys.puts('Player ' + client.sessionId + ' finds ' + objects[oid].tile + ' which is ' + text);
// Отсылаем счастливчику персональное сообщение
client.send(json({ 'foundobject' : text }));
// Отсылаем всем команду убрать найденный предмет с поля
client.send(json({ 'delobject' : objects[oid].id }));
client.broadcast(json({ 'delobject' : objects[oid].id }));
}
}
}
// Формируем исходящее сообщение
var msg = { message: [client.sessionId, message] };
// Сохраняем положение игрока в буфере (по большому счёту, без этого можно обойтись)
buffer.push(msg);
if (buffer.length > 15) {
buffer.shift();
}
// Рассылаем новое положение игрока остальным
client.broadcast(json(msg));
}
Client.broadcast() отсылает сообщение всем, кроме текущего игрока, так что нам надо добавить client.send() чтобы нашедший тоже удалил ненужный объект с поля. Список забавных сообщений я взял прямо из исходников оригинальной robotfindskitten — он слишком велик, чтобы его здесь приводить.
Надо научить клиент обрабатывать новые типы сообщений:
if (data.delobject) {
objects[data.delobject].remove();
} else if (data.foundobject) {
$('div#messages').append($('<p></p>').text(data.foundobject));
}
Вот и всё по части общения клиентов и сервера. Полный код игры как всегда можно найти на Github.


Интересная статья. Спасибо автору.
Искал инфу для создания чата с помощью Socket.IO, а тут оказывается можно онлайн игры писать. Будем экспериментировать
Сергей, а тебе не доводилось использовать socket.io-совместимый клиент не из js, а прямо из флэшки?
Нет, я Флешем почти не занимался. Знаю только что там свой двоичный протокол есть – AMF.
Понятно. AMF красив только со стороны клиента (флэшки), а мне, как server-side разработчику, от нее один геморрой. Формат бинарный без возможности деградации до человекочитаемого, снифферы все платные, не-flash-клиентов раз-два и обчёлся, а они нужны для автоматизации функционального тестирования…
и еще вопросик… я правильно понимаю, что в socket.io нет клиента для node.js? не тот client, который создается, когда к нам кто-нибудь приходит, а тот клиент, который нужен, если мы хотим открыть соединение с socket.io-сервером из node.js…
Вроде есть.
Пока не завелось. Вижу только, что у express.createServer() возникает событие connect, потом upgrade, и судя по всему, сервер закрывает соединение.
А если использовать socket.io из браузера, то там идут connect, upgrade и потом уже логи socket.io-сервера.
небольшой прогресс:
вебсокет надо создавать с указанием в урле того, куда мы хотим апгрэйдиться: new WebSocket(‘ws://localhost:8080/socket.io/websocket’, ‘borf’);
таким образом оно добирается до socket.io-сервера, выдает
10 Oct 13:33:27 – Initializing client with transport “websocket”
и падает на какой-то регулярке .-)
в мастере socket.io эта проблема решена…
боремся дальше: в message-listener приходят сообщения в таком виде: ‘~m~20~m~{“type”:”connected”}’
ну а это решается socket.io-шными функциями util.encode и util.decode
все, я пошел писать ботов для своей игрушки .-)
ну и надо уметь обрабатывать такую socket.io-шную надстройку над вебсокетами как хартбиты. Я взял код отсюда – Client.prototype._onMessage и немного переделал.