![Piccy.info - Free Image Hosting](https://habrastorage.org/getpro/habr/post_images/a23/296/482/a232964828e360b7f7f2a26be5fb8fec.jpg)
Как и обещал, привожу описание производительного игрового сервера на Netty, который использую в своих проектах.
Все описанное ниже не является истинной в последней инстанции, а всего лишь опыт применения технологий в реальных проектах.
Начнем с начала.
Перед нами стоит задача сделать игровой сервер.
Каковы основные задачи игрового сервера?
Если коротко, то:
- Получить пакет от клиента
- Обработать этот пакет (расшифровка, десериализация и т.д. )
- Просчитать игровую ситуацию
- Разослать клиентам изменения игровой ситуации
…
Работу с БД и прочими вещами сейчас рассматривать не будем. Сосредоточимся на сетевой части.
Чем в этом деле нам может помочь Netty?
Netty это сетевая библиотека, которая возьмет на себя непосредственно работу с сокетами. Подключение, отключение клиентов. Прием, отправка и фрагментация пакетов. Т.е. всю низкоуровневую работу с сокетами netty возьмет на себя.
Как netty нам поможет?
Netty реализует очень удобную архитектуру. Например она позволяет подключить несколько обработчиков входящих данных. Т.е. в первом обработчике мы разделяем входящий поток данных на пакеты, а во втором уже обрабатываем эти пакеты. При этом можно гибко управлять настройками самой библиотеки, выделять ей необходимое число потоков или памяти и т.д…
Общая архитектура при использовании Netty может выглядеть так:
![Piccy.info - Free Image Hosting](https://habrastorage.org/getpro/habr/post_images/fbf/90b/4b1/fbf90b4b171791a62fb11e048483b6aa.png)
У нас получается 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](https://habrastorage.org/getpro/habr/conversation/4b8/02b/e89/4b802be897a0559ff2aec9ee1774702e.png)
Таким образом получаем довольно гибкую структуру. Пока нагрузки маленькие можно держать все на одном физическом сервере. При возрастании нагрузки можно TCP сервер на Netty выделить в отдельную машину. У которой хватит производительности для обслуживания нескольких физических серверов с игровой логикой.
Обработка сообщений происходит следующим образом. У нас есть Session это объект который хранит информацию связанную с подключенным клиентом, в нем так же хранятся 2 очереди, пришедших пакетов и готовых к отправке. Packet это объект хранящий информацию о сообщении полученном от клиента. Netty при получении пакета, добавляет его в очередь сессии на обработку и затем отправляет саму сессию на обработку игровой логике. Обработчик игровой логики берет сессию из очереди, потом берет пакет из очереди сессии и обрабатывает его согласно своей логике. И так в несколько потоков. Получается что пакеты обрабатываются последовательно по мере получения. И один клиент не будет тормозить остальных. Ух… вот завернул-то )) Если что, спрашивайте в каментах, поясню.
Вот картинка которая возможно понятнее будет.
![Piccy.info - Free Image Hosting](https://habrastorage.org/getpro/habr/post_images/70f/fbb/6bc/70ffbb6bc4874d65deaf0203df07c4d9.png)
Обработчик игровой логики.
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к сообщений в секунду. Что в общем неплохо.
P.S. Любые вопросы — велкам.