Skip to content

Socket.IO и Node.js: пробное использование

15/06/2010

Я давно хотел попробовать поиграться с библиотекой 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.

Ссылки по теме

Реклама
13 комментариев
  1. Интересная статья. Спасибо автору.
    Искал инфу для создания чата с помощью Socket.IO, а тут оказывается можно онлайн игры писать. Будем экспериментировать 🙂

  2. Wicked permalink

    Сергей, а тебе не доводилось использовать socket.io-совместимый клиент не из js, а прямо из флэшки?

    • Нет, я Флешем почти не занимался. Знаю только что там свой двоичный протокол есть — AMF.

      • Wicked permalink

        Понятно. AMF красив только со стороны клиента (флэшки), а мне, как server-side разработчику, от нее один геморрой. Формат бинарный без возможности деградации до человекочитаемого, снифферы все платные, не-flash-клиентов раз-два и обчёлся, а они нужны для автоматизации функционального тестирования…

  3. Wicked permalink

    и еще вопросик… я правильно понимаю, что в socket.io нет клиента для node.js? не тот client, который создается, когда к нам кто-нибудь приходит, а тот клиент, который нужен, если мы хотим открыть соединение с socket.io-сервером из node.js…

      • Wicked permalink

        Пока не завелось. Вижу только, что у express.createServer() возникает событие connect, потом upgrade, и судя по всему, сервер закрывает соединение.

        А если использовать socket.io из браузера, то там идут connect, upgrade и потом уже логи socket.io-сервера.

      • Wicked permalink

        небольшой прогресс:

        вебсокет надо создавать с указанием в урле того, куда мы хотим апгрэйдиться: new WebSocket(‘ws://localhost:8080/socket.io/websocket’, ‘borf’);

        таким образом оно добирается до socket.io-сервера, выдает
        10 Oct 13:33:27 — Initializing client with transport «websocket»
        и падает на какой-то регулярке .-)

        • Wicked permalink

          в мастере socket.io эта проблема решена…

          боремся дальше: в message-listener приходят сообщения в таком виде: ‘~m~20~m~{«type»:»connected»}’ 🙂

          • Wicked permalink

            ну а это решается socket.io-шными функциями util.encode и util.decode

            все, я пошел писать ботов для своей игрушки .-)

            • Wicked permalink

              ну и надо уметь обрабатывать такую socket.io-шную надстройку над вебсокетами как хартбиты. Я взял код отсюда — Client.prototype._onMessage и немного переделал.

Trackbacks & Pingbacks

  1. progg.ru
  2. Dorian Gray оффициальный сайт » Пример использования socket.io и Node.js

Добавить комментарий

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход / Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход / Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход / Изменить )

Google+ photo

Для комментария используется ваша учётная запись Google+. Выход / Изменить )

Connecting to %s

%d такие блоггеры, как: