Development of a productive game server in Netty + Java

    Piccy.info - Free Image Hosting

    As promised, I give a description of a productive game server on Netty, which I use in my projects.
    Everything described below is not true in the last resort, but only the experience of applying technologies in real projects.



    Start over.



    We are faced with the task of making a game server.

    What are the main tasks of a game server?
    In short, then:
    • Receive package from customer
    • Process this package (decryption, deserialization, etc.)
    • Calculate the game situation
    • Send customers game changes

    ...

    Now we will not consider work with the database and other things. Let's focus on the network part.

    How can Netty help us in this matter?


    Netty is a network library that will take over direct work with sockets. Connect, disconnect clients. Receive, send and fragment packets. Those. netty will take care of all the low-level work with sockets.

    How will netty help us?


    Netty implements a very convenient architecture. For example, it allows you to connect multiple input handlers. Those. in the first handler, we split the incoming data stream into packets, and in the second we are already processing these packets. At the same time, you can flexibly manage the settings of the library itself, allocate the required number of threads or memory, etc. ...
    The general architecture when using Netty may look like this: We get 3 handlers: 1. Connection handler - this is the handler that is responsible for connecting / disconnecting clients . This will check whether clients can connect (black lists, white lists, IP filters, etc.) as well as correctly disconnect clients with the closure of all resources used. 2. Frame handler

    Piccy.info - Free Image Hosting




    - This is a handler dividing the data stream into separate packets.
    For convenience, we assume that the package consists of two parts. 1-header, which describes the length of the packet, and 2-directly packet data.

    3. Packet handler is already a game message handler. Here we will receive data from the package and process them further.

    We will analyze each part in the code.

    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.

    Here we use a replay decoder with two states. In one we read the length of the packet, in the other the data itself.
    public class FrameHandler extends ReplayingDecoder
    {
        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 );
        }
    }


    As you can see, the handler turned out to be very simple and fast. Packet is my class which contains all the necessary information for processing its game logic. It is very simple and its implementation will not be difficult.

    This is the most important handler. In principle, almost all logic can be described in it. The nuance here is that it cannot use blocking elements and time-consuming operations, such as connecting to a database. This will at least slow down all work. Therefore, we will architecturally divide our server into 2 parts. The first is a purely TCP server, whose main task is to receive a packet from the client as soon as possible and send the packet to the client as soon as possible. The second is the game logic processor itself. In principle, such a scheme can make not only a game server. After all, the logic of packet processing can be any.
    The beauty of this architecture also lies in the fact that the TCP server and the handlers can be distributed across different machines (for example, using Akka actors) and get a cluster for calculating game data. Thus, we obtain the following server operation scheme. The TCP part on Netty tries to receive packets from the client as soon as possible and send them to the game logic processor, and a queue is created from them in the processor.

    Schematically, the whole process looks like this. Thus, we obtain a rather flexible structure. While the loads are small, you can keep everything on one physical server. With increasing load, you can select a TCP server on Netty in a separate machine. Which has enough performance to serve several physical servers with game logic.

    Piccy.info - Free Image Hosting



    Message processing is as follows. We have a Session is an object that stores information related to the connected client, it also stores 2 queues, received packets and ready to be sent. Packet is an object that stores information about a message received from a client. When a packet is received, Netty adds it to the session queue for processing and then sends the session itself to the processing of the game logic. The game logic processor takes a session from the queue, then it takes a packet from the session queue and processes it according to its logic. And so in several threads. It turns out that packets are processed sequentially as they are received. And one client will not slow down the rest. Uh ... I’ve wrapped something up)) If anything, ask in comments, I’ll explain.

    Here is a picture that will probably be clearer.
    Piccy.info - Free Image Hosting

    Game logic handler.


    public final class ReadQueueHandler implements Runnable
    {
        private final BlockingQueue 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();
            }
        }    
    }


    There is also nothing complicated. Creates a pool of threads. We add sessions to the processing queue and in the handler we implement our game logic. Here you just need to strike a balance between the speed of the TCP server and the speed of the game logic processors. So that the queue does not fill up faster than it was processed. Netty is a very fast library. So it all depends on the implementation of your game logic game.

    As a protocol, I use protobuf. Very fast and convenient binary serialization. Made and used by Google, which speaks of library validation on large projects)

    With this architecture, on my AMD 1.4 GHz netbook (lenovo edge 13), about 18-20k messages are processed per second. Which is generally good.

    PS Any questions - Wellcome.

    Also popular now: