Разработка производительного игрового сервера на Netty + Java

Разработка производительного игрового сервера на Netty + Java

Piccy.info - Free Image Hosting

Как и обещал, привожу описание производительного игрового сервера на Netty, который использую в своих проектах.
Все описанное ниже не является истинной в последней инстанции, а всего лишь опыт применения технологий в реальных проектах.


Начнем с начала.

Перед нами стоит задача сделать игровой сервер.

Каковы основные задачи игрового сервера?
Если коротко, то:
  • Получить пакет от клиента
  • Обработать этот пакет (расшифровка, десериализация и т.д. )
  • Просчитать игровую ситуацию
  • Разослать клиентам изменения игровой ситуации


Работу с БД и прочими вещами сейчас рассматривать не будем. Сосредоточимся на сетевой части.

Чем в этом деле нам может помочь Netty?
Netty это сетевая библиотека, которая возьмет на себя непосредственно работу с сокетами. Подключение, отключение клиентов. Прием, отправка и фрагментация пакетов. Т.е. всю низкоуровневую работу с сокетами netty возьмет на себя.
Как netty нам поможет?
Netty реализует очень удобную архитектуру. Например она позволяет подключить несколько обработчиков входящих данных. Т.е. в первом обработчике мы разделяем входящий поток данных на пакеты, а во втором уже обрабатываем эти пакеты. При этом можно гибко управлять настройками самой библиотеки, выделять ей необходимое число потоков или памяти и т.д…
Общая архитектура при использовании Netty может выглядеть так:

Piccy.info - Free Image Hosting

У нас получается 3 обработчика:
1. Connection handler — это обработчик который отвечает за подключения/отключения клиентов. Здесь будет происходить проверка возможности подключения клиентов (черные, белые списки, IP фильтры и т.д.) а так же корректное отключение клиентов с закрытием всех используемых ресурсов.

2. Frame handler — это обработчик разделяющий поток данных на отдельные пакеты.
Для удобства работы примем, что пакет состоит из двух частей. 1-заголовок, в котором описана длинна пакета, и 2-непосредственно данные пакета.

3. Packet handler — это уже обработчик игровых сообщений. Здесь мы будем получать данные из пакета и дальше их обрабатывать.

Разберем каждую часть в коде.

Connection handler

public class ConnectionHandler extends SimpleChannelHandler
{

    @Override
    public void channelOpen( ChannelHandlerContext ctx, ChannelStateEvent e ) throws Exception
    {
        if( необходимые условия фильтрации подключения не выполнены )
        {
    // закроем подключение
            e.getChannel().close();
        }
    }
    
    @Override
    public void channelClosed( ChannelHandlerContext ctx, ChannelStateEvent e ) throws Exception
    {
        // Здесь, при закрытии подключения, прописываем закрытие всех связанных ресурсов для корректного завершения.
    }
  
    
    @Override
    public void exceptionCaught( ChannelHandlerContext ctx, ExceptionEvent e ) throws Exception
    {
    // Обработка возникающих ошибок
        log.error( "message: {}", e.getCause().getMessage() );
    }    
    
}


Frame handler.

Здесь используем реплей декодер с двумя состояниями. В одном читаем длину пакета, в другом сами данные.
public class FrameHandler extends ReplayingDecoder<DecoderState>
{
    public enum DecoderState
    {
        READ_LENGTH,
        READ_CONTENT;
    }  

    private int length;

    public ProtocolPacketFramer()
    {
        super( DecoderState.READ_LENGTH );
    }
    
    @Override
    protected Object decode( ChannelHandlerContext chc, Channel chnl, ChannelBuffer cb, DecoderState state ) throws Exception
    {
        switch ( state )
        {
            case READ_LENGTH:
                length = cb.readInt();
                checkpoint( DecoderState.READ_CONTENT );
                
            case READ_CONTENT:
                ChannelBuffer frame = cb.readBytes( length );
                checkpoint( DecoderState.READ_LENGTH );
                return frame;
                
            default:
                throw new Error( "Shouldn't reach here." );
        }
    }
}


Packet handler

public class ProtocolPacketHandler extends SimpleChannelHandler
{
    @Override
    public void messageReceived( ChannelHandlerContext ctx, MessageEvent e ) throws Exception
    {
    // получим сообщение
            byte[] msg = ((ChannelBuffer)e.getMessage()).array();
            
    // Соберем его для отправки обработчику
            Packet packet = new Packet();
            packet.setData( msg );
            packet.setSender( session );

    // Поставим пакет в очередь обработки сесссии
            session.addReadPacketQueue( packet );

    // Поставим сессию в очередь обработки логики
            Server.getReader().addSessionToProcess( session );
    }
}


Как видно, обработчик получился очень простой и быстрый. Packet это мой класс в котором содержится вся необходимая информация для обработки его игровой логикой. Он очень простой и его реализация не составит труда.

Это самый главный обработчик. В принципе можно практически всю логику описать в нем. Нюанс здесь в том, что в нем нельзя использовать блокирующие элементы и длительные по времени операции, например подключение к базе данных. Это как минимум затормозит всю работу. Поэтому мы архитектурно разделим наш сервер на 2 части. Первая это чисто TCP сервер, основная задача которого как можно быстрее принять пакет от клиента и как можно быстрее отослать пакет клиенту. Вторая это непосредственно обработчик игровой логики. В принципе, по такой схеме можно сделать не только игровой сервер. Ведь логика обработки пакетов может быть любой.
Прелесть такой архитектуры еще и в том, что TCP сервер и обработчики можно разнести по разным машинам (например с помощью Akka акторов) получив кластер для расчетов игровых данных. Таким образом получаем следующую схему работы сервера. TCP часть на Netty старается как можно быстрее принять пакеты от клиента и отправить их обработчику игровой логики, при этом в обработчике создается очередь из них.

Схематично весь процесс выглядит так.

Piccy.info - Free Image Hosting

Таким образом получаем довольно гибкую структуру. Пока нагрузки маленькие можно держать все на одном физическом сервере. При возрастании нагрузки можно TCP сервер на Netty выделить в отдельную машину. У которой хватит производительности для обслуживания нескольких физических серверов с игровой логикой.

Обработка сообщений происходит следующим образом. У нас есть Session это объект который хранит информацию связанную с подключенным клиентом, в нем так же хранятся 2 очереди, пришедших пакетов и готовых к отправке. Packet это объект хранящий информацию о сообщении полученном от клиента. Netty при получении пакета, добавляет его в очередь сессии на обработку и затем отправляет саму сессию на обработку игровой логике. Обработчик игровой логики берет сессию из очереди, потом берет пакет из очереди сессии и обрабатывает его согласно своей логике. И так в несколько потоков. Получается что пакеты обрабатываются последовательно по мере получения. И один клиент не будет тормозить остальных. Ух… вот завернул-то )) Если что, спрашивайте в каментах, поясню.

Вот картинка которая возможно понятнее будет.
Piccy.info - Free Image Hosting

Обработчик игровой логики.


public final class ReadQueueHandler implements Runnable
{
    private final BlockingQueue<Session> sessionQueue;
    private final ExecutorService        threadPool;
    private int                          threadPoolSize;
    
    public ReadQueueHandler( int threadPoolSize )
    {
        this.threadPoolSize = threadPoolSize;
        this.threadPool     = Executors.newFixedThreadPool( threadPoolSize );
        this.sessionQueue   = new LinkedBlockingQueue();
        
        initThreadPool();
    }
    
    private void initThreadPool()
    {
        for ( int i = 0; i < this.threadPoolSize; i++ )
        {
            this.threadPool.execute( this );
        }
    }
    
    // добавление сесси в очередь на обработку
    public void addSessionToProcess( Session session )
    {
        if ( session != null )
        {
            this.sessionQueue.add( session );
        }
    }    
    
    @Override
    public void run()
    {
        while ( isActive )
        {
                // Получаем следующую сессию для обработки
                Session session = (Session) this.sessionQueue.take();
                
                // Здесь происходит обработка игровых сообщений
                // получаем пакет
                packet = session.getReadPacketQueue().take();

                // далее получаем и обрабатываем данные из него
                data =  packet.getData();
        }
    }    
}


Тут тоже ничего сложного нет. Создает пул потоков. Добавляем в очередь сессии на обработку и в обработчике реализуем свою игровую логику. Здесь просто надо соблюсти баланс между скоростью TCP сервера и скоростью обработчиков игровой логики. Чтобы очередь не заполнялась быстрее чем обрабатывалась. Netty очень быстрая библиотека. Так что все зависит от реализации вашей игровой гейм логики.

В качестве протокола я использую protobuf. Очень быстрая и удобная бинарная сериализация. Сделана и используется гуглом, что говорит о проверенности библиотеки на больших проектах )

С такой архитектурой, на моем нетбуке AMD 1.4 ГГц (lenovo edge 13), обрабатывается порядка 18-20к сообщений в секунду. Что в общем неплохо.

Источник: http://habrahabr.ru