Разработка высоконагруженного игрового сервера на Java. Исходник сетевой части

Как обещали, выкладываем продолжение статьи о реализации на Java сетевой части нагруженного сервера. Эта статья посвящена целиком исходному коду и комментариям к нему. Архив исходников, естественно, прилагается. :)

В предыдущем посте мы обсудили один из вариантов архитектуры сетевой части. Само собой, это решение не единственное, и ,вероятно, не самое оптимальное. Но оно работает, и это вызывает уважение! :)

Главный класс – NettyServer.java

package core.NettyServer;

import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.util.concurrent.Executors;
import org.jboss.netty.bootstrap.ServerBootstrap;
import org.jboss.netty.channel.ChannelFactory;
import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory;

public class NettyServer {
public NettyServer(int port) throws Exception{
ChannelFactory factory = new NioServerSocketChannelFactory(Executors.newCachedThreadPool(), Executors.newCachedThreadPool()); //Создаем фабрику каналов. Так надо :)

ServerBootstrap bootstrap = new ServerBootstrap(factory);
bootstrap.setPipelineFactory(new NettyServerPipeLineFactory()); //Кастомный класс имплементирующий ChannelPipelineFactory

bootstrap.setOption("child.tcpNoDelay", true); //Не очень понятная опция, но вроде бы нужна для поддержки постоянного соединения с клиентом
bootstrap.setOption("child.keepAlive", true); //Аналогично

bootstrap.bind(new InetSocketAddress(port)); //Вешаем слушатель на переданный в параметрах порт

System.out.print("NettyServer: Listen to users on "+InetAddress.getLocalHost().toString()+":"+port+"\n");
}
}

Где-то в инициализации приложения нужно вписать вот такую строчку:
new NettyServer(12345); //12345 - номер порта
После этого на указанном порту будет висеть слушатель входящих соединений.

Пайплайн фэктори – NettyServerPipeLineFactory.java

package core.NettyServer;

import static org.jboss.netty.channel.Channels.*;

import org.jboss.netty.channel.ChannelPipeline;
import org.jboss.netty.channel.ChannelPipelineFactory;
import org.jboss.netty.handler.codec.frame.DelimiterBasedFrameDecoder;
import org.jboss.netty.handler.codec.frame.Delimiters;
import org.jboss.netty.handler.codec.string.StringDecoder;
import org.jboss.netty.handler.codec.string.StringEncoder;

public class NettyServerPipeLineFactory implements ChannelPipelineFactory {
public ChannelPipeline getPipeline() throws Exception {
// Create a default pipeline implementation.
ChannelPipeline pipeline = pipeline();

// Здесь указываем размер буфера (8192 байта) и символ-признак конца пакета.
//Свои пакеты мы обычно терминируем символом с кодом 0, что соответствует nulDelimiter() в терминологии нетти
pipeline.addLast("framer", new DelimiterBasedFrameDecoder(8192, Delimiters.nulDelimiter()));
pipeline.addLast("decoder", new StringDecoder()); //Стандартный строковый декодер. Когда пакеты идут в текстовом, XML, JSON или подобном виде. Для бинарных пакетов - другие кодеки.
pipeline.addLast("encoder", new StringEncoder());

pipeline.addLast("handler", new NettyServerHandler()); //И наконец, указываем, какой класс у нас будет уведомляться о входящих соединениях и пришедших данных

return pipeline;
}
}

В этом классе пока еще идет инициализация части структуры. То, ради чего мы это все создаем, начинается в следующем классе. :)

Самый нужный для нас класс – NettyServerHandler.java

package core.NettyServer;

import org.jboss.netty.channel.ChannelEvent;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.ChannelStateEvent;
import org.jboss.netty.channel.ExceptionEvent;
import org.jboss.netty.channel.MessageEvent;
import org.jboss.netty.channel.SimpleChannelUpstreamHandler;

public class NettyServerHandler extends SimpleChannelUpstreamHandler {
private boolean _debug;

public NettyServerHandler() {
_debug = true; //Это мы просто выставляем режим отладки, чтобы видеть в консоли, что происходит в канале
}

@Override
public void handleUpstream(ChannelHandlerContext ctx, ChannelEvent e) throws Exception {
super.handleUpstream(ctx, e);
//Для чего оверрайдим этот метод - не помню. :) Возможно, это не нужно.
}

@Override
public void channelConnected(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
//Этот метод вызывается Нетти, когда к серверу подключается новый пользователь
log("Client connected from "+ctx.getChannel().getRemoteAddress()+" ("+ctx.getChannel().getId()+")");
}

@Override
public void channelClosed(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
//Нетти вызывает этот метод, чтобы проинформировать нас о том, что соединение с каким-то юзером разорвано
log("Connection closed from "+ctx.getChannel().getRemoteAddress()+" ("+ctx.getChannel().getId()+")");
}

@Override
public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) {
//Самый главный метод. Вызывается, когда от клиента приходит очередной пакет. Причем доставание пакета из сокета и его сборку из кусков делает сам Нетти! :)
//Создаем новый поток для обработки пакета. Передаем пришедшие данные, а так же сетевой контекст
try {
new CommandProcessorSmall("cmdProcessor", new NetContext(e.getChannel(), (String) e.getMessage()),_debug).start();
} catch (Exception ex) {
ex.printStackTrace();
}
}

@Override
public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) {
//Метод вызывается в случае ошибок, возникших в процесс работы Netty-сервера
log("Error ("+ctx.getChannel().getRemoteAddress()+"): "+e.getCause()+" ("+ctx.getChannel().getId()+")");
e.getChannel().close();
}

private void log(String txt) {
if (_debug) {
System.out.print("NettyServerHandler: "+txt+"\n");
}
}
}

Простая вэошка для удобства работы – NetContext.java
package core.NettyServer;

import org.jboss.netty.channel.Channel;

public class NetContext {
public Channel channel;
public String message;

public NetContext(Channel channel, String message) {
this.channel = channel;
this.message = message;
}
}

Почти все! Остался последний класс – тред, обрабатывающий входящий пакет и отсылающий юзеру результат обработки.

Обработчик входящего пакета – CommandProcessorSmall.java

package core.NettyServer;

import org.jboss.netty.channel.Channel;

public class CommandProcessorSmall extends Thread {
private NetContext _nCtx;
private boolean _debug;

public CommandProcessorSmall(String name, NetContext nCtx, boolean debug) {
super();
setName(name);

_combat = combat;
_nCtx = nCtx;
_debug = debug;
}

@Override
public void run() {
try {
if (_debug) {
System.out.print("IN > "+_nCtx.message+"\n");
}

//Здесь вызывается игровая логика нашего сервера и формируется ответ клиенту
//String response = (String)_combat.processCommand(_nCtx);

String response = "Response text in XML format"; //Вместо игровой логики сгенерируем рыбу :)

((Channel)(_nCtx.channel)).write(response+"\0"); //Отправляем ответ клиенту

if (_debug) {
System.out.print("OUT > "+response+"\n");
}
} catch (Exception e) {
e.printStackTrace();
}
}
}

В заключение опишем схему работы всей этой кухни. Где-то в начале приложения создаем инстанс NettyServer, указав ему номер порта, на который надо повеситься. После этого, наш сервер готов принимать входящие соединения и обрабатывать данные, приходящие от клиентов. За прием соединений и пакетов отвечает наш класс NettyServerHandler – именно в нем происходит организация рабочих процессов – его кастомайзим в первую очередь. Методы этого класса дергаются ядром Нетти при возникновении различных сетевых событий. Когда приходит очередной пакет от одного из юзеров (NettyServerHandler.messageReceived), нами создается экземпляр класса CommandProcessorSmall, который представляет собой наследник от java.lang.Thread. Ему передается сетевой контекст (пришедший пакет и обратный канал клиента). Запускается тред. происходит обработка, результат отсылается юзеру, тред умирает. Вот такой нехитрый алгоритм. :)

Напоследок, как обещали – архив с исходным кодом сетевой части на Java с использованием библиотеки Netty

Заранее благодарны за то, что вы не будете размещать у себя на сайтах этот архив, а будете вместо этого постить ссылку на эту статью. :)
Ждем вопросы и комментарии.

Эта запись была опубликована в рубрике Мысли о разработке и отмечена метками , , , . Добавить в закладки ссылку.

12 в ответ на Разработка высоконагруженного игрового сервера на Java. Исходник сетевой части:

  1. Tolik пишет:

    Спасибо, чувак:) Очень понятно
    Сделал

    import core.NettyServer.*;

    а потом через try-catch

    try {
    NettyServer server = new NettyServer(34340);
    }
    catch (Exception e) {
    System.out.println(“Can’t create a server”);
    }

    и вроде нормально работает, в консоле написало что “слушает” такой-то-такой порт на таком-то-таком хосте.
    Если создам игрушку покруче DarkOrbit то обязательно тебя отблагодарю=)

  2. xSofa пишет:

    Возник такой вопрос. Я скачал архив, подключил библиотеку, но NetBeans выдает 2 ошибки:
    NettyServerHandler.java -> 43 строка -> The type of CommandProcessorSmall(String,NextContext,boolean) is erronius
    CommandProcessorSmall.java -> 28 строка -> not a statement; ‘;’ expected
    Что с этим богатством делать? :-)

    • antares пишет:

      Надо глянуть в код CommandProcessorSmall.java – там, видимо, закралась досадная опечатка.

  3. taluks пишет:

    говорят что вместо Executors.newCachedThreadPool() лучше использовать OrderedMemoryAwareThreadPoolExecutor или Executors.newFixedThreadPool(n), можно ли пошире рассмотреть данную тему? хочу тоже разобраться как собрать сервер

    • antares пишет:

      Честно говоря, не пробовали пока другие варианты. Выбранные нами вполне устраивает нас. Мы довольны. :) Если есть интерес – копните глубже, узнайте разницу между этими типами пулов. А если спрашиваете чисто из практических соображений (чтоб работало) – то попробуйте любой из них. Уверены, они все вполне работоспособны.

  4. Akirus пишет:

    пожайлуста покажите как отправлять сообщения на этот сервер
    вот так не выходит :(
    Socket s = new Socket("localhost", 4404);
    PrintStream ps = new PrintStream(s.getOutputStream());
    ps.print("hello server!");
    ps.flush();
    ps.close();
    s.close();

    сервер просто не получает никакого сообщения(или не говорит об этом) debug выставлен на true

    • antares пишет:

      Все просто. Нужно к строке добавить в конец символ с кодом 0.
      Вот так:
      ps.print("hello server!"+"\0");
      Этот символ для сервера – признак того, что дошло всё сообщение, а не его часть.

  5. memo пишет:

    А почему на каждое входящее вы создаёте новый поток? Почему бы не создать небольшой пул потоков, которые будут ждать прихода ваших сообщений и их обрабатывать? Ложите сообщение в очередь и первый свободный поток из пула его обработает. Ведь сообщения от netty приходят асинхронно и вам некуда спешить с их обработкой. Разве это не снизит нагрузку на сервер, вместо постоянного создания/уничтожения новых потоков?

    За статью большое спасибо!

    • antares пишет:

      Первоначально так и сделал. Но иногда случались ситуации, когда очереди обработки переполнялись из-за большого онлайна и небольшого количества потоков-обработчиков, и начинались лаги.
      Увеличение количества потоков-обработчиков привело бы к тому, что на N пользователей онлайн создавалось бы не менее M потоков. То есть, на 1000 юзеров, например, запускалось бы 100 потоков. В итоге пришли бы к тому, с чего начали – один юзер = один поток.

      Поэтому было решено сделать обработку каждого пакета в отдельном потоке, умирающем сразу после обработки. Решение оказалось вполне жизнеспособным, и радует нас по сей день. :)

  6. privet пишет:

    Спасибо! Буду разбираться.