Guildmaker.Ru logo
Guildmaker.Ru

Платформа для создания сайтов кланов и гильдий MMORPG

Создать сайт клана

Для входа в панель управления созданным сайтом используйте пароль, который будет сгенерирован в личном кабинете (хеш-код после слова "Активен").

Мониторинг сервера Counter-Strike своими руками

Пишем свой мониторинг сервера CS для сайта

Эта статья для тех, кто собирается сделать мониторинг серверов Counter-Strike или просто хочет добавить на сайт клана информацию о состоянии своего игрового сервера. Начнем с теории. Для получения информации о состоянии сервера CS, а также статистики активных игроков нам необходимо установить соединение с этим сервером по протоколу UDP. Именно по протоколу udp:// от вашего клиента к серверу передаются данные, куда вы перемещаетесь на карте и в какую точку стреляете. Одна из причин, почему разработчики онлайн игр предпочитают использовать UDP, а не TCP/IP/HTTP является высокая скорость передачи и обработки таких пакетов, достигаемая засчет отсутствия лишних прелюдий и рукопожатий между клиентом и сервером следующего вида:

– Сервер, привет ты жив?

– Да.

– Там один игрок выстрелил в другого, лови пакет с инфой.

– Ага. Кидай... Ну, где он там? Все, вижу.

– Сервер, подтверди получение пакета.

– Подтверждаю, пойду подготовлю ответ.

– Хорошо, жду.

– Клиент, лови ответ! Тот игрок убит, не забудь нарисовать лужу крови и покрупнее!

– Хорошо, давай ответ сюда.

– Клиент, подтверди что получил мой ответ.

– Подтверждаю, получил.

– Отлично, ну тогда до связи!

– Пока!


Вот так бы выглядело общение сервера и клиента, если бы разработчики Counter-Strike выбрали TCP в качестве протокола для передачи данных по сети. По UDP это общение выглядит так:

– Сервер... Не знаю слышишь ты меня или нет, но мне нужно тебе кое что сказать.

– Сервер, игрок 1 выстрелил вот в такую точку.

– Сервер, продублирую еще раз. Игрок 1 выстрелил вот в такую точку. Все, до связи, подтверждений о получении ненадо.

(пауза)

– Клиент... В той точке был другой игрок и он убит, нарисуй на том месте анимацию смерти и лужу крови! Все пока, никаких подтверждений ненадо.


Вы, наверное, уже слышали что-то про потери пакетов, когда у вас медленный интернет, либо игровой сервер перегружен. Это и есть минус UDP. Сервер и клиент не запрашивают у друг друга подтверждение о доставке UDP-пакетов, не отправляют их повторно при потерях. Да и в сетевых играх, где действия происходят очень быстро это могло бы только прибавить уйму лишних багов. Таким образом, клиент может не успевать вовремя обрабатывать пакеты, а сервер – отдавать, если он нагружен. Прибавим к этому уже отмеченные выше ситуации, когда пакеты и вовсе не доходят и получим ту самую ложку дегтя, которая не дает нам право называть UDP более удобным протоколом, чем тот же TCP. Каждое решение эффективно для разного круга задач.

Итак, мы немного познакомились с протоколом, который будем использовать для общения с сервером CS. Теперь перейдем к программированию. Напишем функцию, которая будет открывать сокет – интерфейс, с помощью которого мы и будем осуществлять обмен пакетами между клиентом (веб-сервером, выполняющим наш php-скрипт) и сервером (игровым сервером Counter-Strike).

/**
 *
 * @brief     Установка соединения с игровым сервером
 * 
 * @param     String             IP-адрес или домен игрового сервера
 * @param     Integer            Порт игрового сервера
 * 
 * @return    FALSE              Если открыть соединение с игровым сервером не удалось
 *            socket(handler)    Дескриптор соединения с игровым сервером
 */
function Connect( $host, $port )
{        
    $connection = fsockopen( CONNECTION_PROTOCOL.$host, $port, $errno, $errstr, 0 );

    if( !$connection )
        return FALSE;

    // Настройка сокета: ждем ответа от сервера, читаем данные только при полном ответе.
    stream_set_timeout( $connection, 1, 0 );
    stream_set_blocking( $connection, true );

    return $connection;
}

Затем напишем функцию, которая будет "писать в сокет" определенный запрос, представляемый строкой. Это и будет функция отправки запроса и получения ответа.

/**
 *
 * @brief     Запрос к игровому серверу (и получение ответа)
 * 
 * @param     socket(handler)    Дескриптор соединения с игровым сервером
 * @param     String             Строка с текстовым запросом к игровому серверу
 *                               Документация Valve: https://developer.valvesoftware.com/wiki/Server_queries
 * 
 * @return    String             4096 байт ответа от игрового сервера, прочтенных через сокет
 */
function Query( $connection, $query )
{
    if( !$connection )
        return FALSE;

    fwrite( $connection, $query );

    $response = fread( $connection, 4096 );

    return $response;
}

Напоследок оформим функцию, которая будет закрывать соединение с игровым сервером.

/**
 *
 * @brief    Закрытие соединения с игровым сервером
 * 
 * @param    socket(handler)    Дескриптор соединения с игровым сервером
 */
function Disconnect( $connection )
{
    fclose( $connection );
}

Итак, базовые функции, обеспечивающие работу нашего мониторинга, мы реализовали. Подошло время самой интересной части кода – как же выглядят сами запросы и как обрабатывать ответ от игрового сервера Counter-Strike? В этом нам поможет документация от Valve: https://developer.valvesoftware.com/wiki/Server_queries

А именно, формат запроса на получение информации о сервере (A2S_INFO):



И формат ответа от сервера на этот запрос:



Помимо информации о сервере и его активности, нам необходимо получить ники всех игроков, кто сейчас на нем играет, а также их рейтинг на текущей карте. Согласно документации, этот запрос называется A2S_PLAYER и его формат имеет следующий вид:



Формат ответа на этот запрос несколько проще, чем на тот, который мы отправляем для получения информации о нашем сервере CS:



Для удобства представим весь функционал мониторинга серверов CS в виде единого класса CSMonitoring. Функции, написанные чуть ранее, станут его методами. В этом классе мы не будем объявлять ни конструктора, ни деструктора. У нас нет нужды плодить множество объектов. Серверов много. Мониторинг – один. Поэтому, все методы нашего класса сделаем статическими. Используя документацию, напишем метод getServerInfo, в который будем передавать IP и порт сервера CS, а на выходе получать массив с информацией о его состоянии и активных игроках. Итоговый код класса выглядит так:

class CSMonitoring
{
    ///////////
    // SETUP //
    ///////////
    
    const CONNECTION_PROTOCOL = 'udp://';
    
    const QUERY_GET_SERVER_INFO    = "\xFF\xFF\xFF\xFFTSource Engine Query\x00";
    const QUERY_GET_SERVER_PLAYERS = "\xFF\xFF\xFF\xFF\x55\x00\x00\x00\x00";
    
    const ERROR_CONNECTION = 'Не удалось установить соединение с игровым сервером...';
    const ERROR_RESPONSE_1 = 'Не удалось получить информацию о игровом сервере...';
    const ERROR_RESPONSE_2 = 'Не удалось получить информацию о игроках на сервере...';
    
    /////////
    // API //
    /////////
    
    /**
     * 
     * @brief     Получение информации о игровом сервере и статистики активных игроков
     * 
     * @param     String     IP-адрес или домен сервера Counter-Strike
     * @param     Integer    Порт сервера Counter-Strike
     * @param     String     В случае ошибки в эту переменную будет записан текст ошибки.
     * 
     * @return    False      Если информацию о сервере не удалось получить.
     *            Array      Массив с информацией о состоянии сервера Counter-Strike.
     * 
     *                       nm – Название карты
     *                       nh – Название хоста
     *                       pc – Количество игроков на сервере
     *                       pm – Максимальное количество игроков на сервере
     *                       dr – Описание сервера
     *                       ps – Массив со статистикой игроков на сервере
     *                       os – Операционная система сервера
     * 
     *                          [n] – Ник игрока на сервере
     *                          [s] – Текущий счет игрока на сервере
     */
    public static function getServerInfo( $host = '127.0.0.1', $port = 27015, &$error )
    {
        $connection = self::Connect( $host, $port );
        
        if( !$connection )
        {
            $error = self::ERROR_CONNECTION;
            return FALSE;
        }
        
        $response = self::Query( $connection, self::QUERY_GET_SERVER_INFO );
        
        self::Disconnect( $connection );
        
        if( !$response )
        {
            $error = self::ERROR_RESPONSE_1;
            return FALSE;
        }

        $response = trim( substr( $response, 4 ) );	
        
        $server_info = array();
        
        $source = explode( "\x00", $response );
        $offset = strlen( $source[ 0 ].
                          $source[ 1 ].
                          $source[ 2 ].
                          $source[ 3 ].
                          $source[ 4 ] ) + 5;
        
        $server_info['nm'] = $source[ 2 ];
        $server_info['nh'] = $source[ 1 ];
        $server_info['pc'] = ord( $response[ $offset + 0 ] );
        $server_info['pm'] = ord( $response[ $offset + 1 ] );
        $server_info['dr'] = $source[ 4 ];
        $server_info['os'] = 'l' == $response[ $offset + 4 ] ? 'Linux' : 'Windows';
        $server_info['ps'] = FALSE;
        
        $connection = self::Connect( $host, $port );
        
        if( !$connection )
        {
            $error = self::ERROR_CONNECTION;
            return $server_info;
        }
        
        $response = self::Query( $connection, self::QUERY_GET_SERVER_PLAYERS );
        
        if( !$response )
        {
            self::Disconnect( $connection );
            $error = self::ERROR_RESPONSE_2;
            return $server_info;
        }
        
        $response = self::Query( $connection, "\xFF\xFF\xFF\xFFU".substr( $response, 5, 4 ) );
        
        self::Disconnect( $connection );
    
        if( !$response )
        {
            $error = self::ERROR_RESPONSE_2;
            return $server_info;
        }
    
        $response = trim( substr( $response, 4 ) );	
    
        $resp_len   = strlen( $response );
        $player_num = 0;
        $position   = 2;    
        $players    = array();

        while( $position < $resp_len )
        {            
            ++$player_num;
            ++$position;
            
            while( $response[ $position ] != "\x00" && $position < 4000 )
                $players[ $player_num ]['n'] .= $response[ $position++ ];
            
            $players[ $player_num ]['s'] = ( ord( $response[ $position + 1 ] ) )
                                         + ( ord( $response[ $position + 2 ] ) * 256 )
                                         + ( ord( $response[ $position + 3 ] ) * 65536 )
                                         + ( ord( $response[ $position + 4 ] ) * 16777216 );
            
            if( 2147483648 < $players[ $player_num ]['s'] )
                $players[ $player_num ]['s'] -= 4294967296;
            
            $position += 9;
        }
        
        usort( $players, function( $first, $second )
        {
            return $second['s'] - $first['s'];
        });
        
        $server_info['ps'] = $players;
        
        return $server_info;
    }
    
    //////////////
    // INTERNAL //
    //////////////
    
    /**
     *
     * @brief     Установка соединения с игровым сервером
     * 
     * @param     String             IP-адрес или домен игрового сервера
     * @param     Integer            Порт игрового сервера
     * 
     * @return    FALSE              Если открыть соединение с игровым сервером не удалось
     *            socket(handler)    Дескриптор соединения с игровым сервером
     */
    private static function Connect( $host, $port )
    {        
        $connection = fsockopen( self::CONNECTION_PROTOCOL.$host, $port, $errno, $errstr, 0 );

        if( !$connection )
            return FALSE;
        
        // Ждем ответа от сервера, читаем данные только при полном ответе.
        stream_set_timeout( $connection, 1, 0 );
        stream_set_blocking( $connection, true );
        
        return $connection;
    }
    
    /**
     *
     * @brief    Закрытие соединения с игровым сервером
     * 
     * @param    socket(handler)    Дескриптор соединения с игровым сервером
     */
    private static function Disconnect( $connection )
    {
        fclose( $connection );
    }
    
    /**
     *
     * @brief     Запрос к игровому серверу (и получение ответа)
     * 
     * @param     socket(handler)    Дескриптор соединения с игровым сервером
     * @param     String             Строка с текстовым запросом к игровому серверу
     *                               Документация Valve: https://developer.valvesoftware.com/wiki/Server_queries
     * 
     * @return    String             4096 байт ответа от игрового сервера, прочтенных через сокет
     */
    private static function Query( $connection, $query )
    {
        if( !$connection )
            return FALSE;
        
        fwrite( $connection, $query );
        
        $response = fread( $connection, 4096 );
        
        return $response;
    }
}

Проверим, как работает наш скрипт. Укажите IP-адрес и порт своего сервера Counter-Strike в форме ниже:



А если Вы серьезно решили поставить свой игровой сервер, то наверняка уже задумывались о борьбе с читерами и умниками, которые хотят навариться за Ваш счет, не так ли? Ведь и на Вашем сервере найдутся игроки, знающие как заработать на онлайн игре. Например, когда продают персонажа, прокачанного на Вашем сервере – да, это повод для гордости, но стоит задуматься и о последствиях.