Разработка производительного игрового сервера на Netty + Java
Как и обещал, привожу описание производительного игрового сервера на Netty, который использую в своих проектах.
Все описанное ниже не является истинной в последней инстанции, а всего лишь опыт применения технологий в реальных проектах.
Начнем с начала.
Перед нами стоит задача сделать игровой сервер.
Каковы основные задачи игрового сервера?
Если коротко, то:
- Получить пакет от клиента
- Обработать этот пакет (расшифровка, десериализация и т.д. )
- Просчитать игровую ситуацию
- Разослать клиентам изменения игровой ситуации
Работу с БД и прочими вещами сейчас рассматривать не будем. Сосредоточимся на сетевой части.
Чем в этом деле нам может помочь Netty?
Netty это сетевая библиотека, которая возьмет на себя непосредственно работу с сокетами. Подключение, отключение клиентов. Прием, отправка и фрагментация пакетов. Т.е. всю низкоуровневую работу с сокетами netty возьмет на себя.Как netty нам поможет?
Netty реализует очень удобную архитектуру. Например она позволяет подключить несколько обработчиков входящих данных. Т.е. в первом обработчике мы разделяем входящий поток данных на пакеты, а во втором уже обрабатываем эти пакеты. При этом можно гибко управлять настройками самой библиотеки, выделять ей необходимое число потоков или памяти и т.д…Общая архитектура при использовании Netty может выглядеть так:
У нас получается 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 старается как можно быстрее принять пакеты от клиента и отправить их обработчику игровой логики, при этом в обработчике создается очередь из них.
Схематично весь процесс выглядит так.
Таким образом получаем довольно гибкую структуру. Пока нагрузки маленькие можно держать все на одном физическом сервере. При возрастании нагрузки можно TCP сервер на Netty выделить в отдельную машину. У которой хватит производительности для обслуживания нескольких физических серверов с игровой логикой.
Обработка сообщений происходит следующим образом. У нас есть Session это объект который хранит информацию связанную с подключенным клиентом, в нем так же хранятся 2 очереди, пришедших пакетов и готовых к отправке. Packet это объект хранящий информацию о сообщении полученном от клиента. Netty при получении пакета, добавляет его в очередь сессии на обработку и затем отправляет саму сессию на обработку игровой логике. Обработчик игровой логики берет сессию из очереди, потом берет пакет из очереди сессии и обрабатывает его согласно своей логике. И так в несколько потоков. Получается что пакеты обрабатываются последовательно по мере получения. И один клиент не будет тормозить остальных. Ух… вот завернул-то )) Если что, спрашивайте в каментах, поясню.
Вот картинка которая возможно понятнее будет.
Обработчик игровой логики.
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