Ниже приведен общий абстрактный пакет, который должен быть родителем любого пакета, который вы хотите отправить. Любой результат действия пакета могут быть описаны в специфических методах(handleClientSide и handleServerSide).

ПРИМЕЧАНИЕ : Все дочерные классы этого класса ДОЛЖНЫ иметь пустой конструктор (Можно использовать несколько конструкторов, но один должен быть пустой!).

Ядро обработки пакетов. По сути, он автоматически сопоставляет пакет на дискриминатор, позволяющий в соответствии кодирования / декодирования конкретных данных пакетов.

ПРИМЕЧАНИЕ : Не забудьте переименовать канал на свой, а не как в туториале(«TUT»).

Регистрация PacketPipeline в главном классе мода.

Источник: forum.mcmodding.ru

Высокопроизводительный NIO-сервер на Netty

Здравствуйте. Я являюсь главным разработчиком крупнейшего в СНГ сервера Minecraft (не буду рекламировать, кому надо, те знают). Уже почти год мы пишем свою реализацию сервера, рассчитанную на больше чем 40 человек (мы хотим видеть цифру в 500 хотя бы). Пока всё было удачно, но последнее время система начала упираться в то, что из-за не самой удачной реализации сети (1 поток на ввод, 1 на вывод + 1 на обработку), при 300 игроках онлайн работает более 980 потоков (+ системные), что в сочетании с производительностью дефолтного io Явы даёт огромное падение производительности, и уже при 100 игроках сервер в основном занимается тем, что пишет/читает в/из сети.

как исправить ошибку в майнкрафте io netty channel abstractchannel$annotatedconnectexception connec

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

Здесь я постараюсь расписать серверную часть работы с сетью через Netty, может быть это кому-то будет полезно.

Создание сервера

ExecutorService bossExec = new OrderedMemoryAwareThreadPoolExecutor(1, 400000000, 2000000000, 60, TimeUnit.SECONDS); ExecutorService ioExec = new OrderedMemoryAwareThreadPoolExecutor(4 /* число рабочих потоков */, 400000000, 2000000000, 60, TimeUnit.SECONDS); ServerBootstrap networkServer = new ServerBootstrap(new NioServerSocketChannelFactory(bossExec, ioExec, 4 /* то же самое число рабочих потоков */)); networkServer.setOption(«backlog», 500); networkServer.setOption(«connectTimeoutMillis», 10000); networkServer.setPipelineFactory(new ServerPipelineFactory()); Channel channel = networkServer.bind(new InetSocketAddress(address, port));

Используется OrderedMemoryAwareThreadPoolExecutor для выполнения задач Netty, по опыту французских коллег они самые эффективные.

Можно использовать другие Executor’ы, например Executors.newFixedThreadPool(n). Ни в коем случае не используйте Executors.newCachedThreadPool(), он создаёт неоправданно много потоков и ни какого выигрыша от Netty почти нет. Использовать более 4 рабочих потоков нет смысла, т.к. они более чем справляются с огромной нагрузкой (программисты из Xebia-France на 4 потоках тянули более 100 000 одновременных подключений). Босс-потоки должны быть по одному на каждый слушаемый порт. Channel, который возвращает функция bind, а так же ServerBootsrap необходимо сохранить, чтобы потом можно было остановить сервер.

Ошибка io.netty.channel.abstractchannel$annotatedconnectexception что же делать?

PipelineFactory

То, как будут обрабатываться подключения и пакеты клиента, определяет PipelineFactory, которая при открытии канала с клиентом создаёт для него pipeline, в котором определены обработчики событий, которые происходят на канале. В нашем случае, это ServerPipelineFactory:

В данном коде PacketFrameDecoder, PacketFrameEncoder и PlayerHandler — обработчки событий, которые мы определяем. Функция Channels.pipeline() создаёт новый pipeline с переданными ей обработчиками. Будьте внимательны: события проходят обработчики в том порядке, в котором Вы передали из функции pipeline!

Протокол

Немного опишу протокол, чтобы дальше было понятно.

Обмен данными происходит с помощью объектов классов, расширяющих класс Packet, в которых определены две функции, get(ChannelBuffer input) и send(ChannelBuffer output). Соответственно, первая функция читает необходимые данные из канала, вторая — пишет данные пакета в канал.

public abstract class Packet < public static Packet read(ChannelBuffer buffer) throws IOException < int // Получаем ID пришедшего пакета, чтобы определить, каким классом его читать Packet packet = getPacket(id); // Получаем инстанс пакета с этим ID if(packet == null) throw new IOException(«Bad packet ID: » + id); // Если произошла ошибка и такого пакета не может быть, генерируем исключение packet.get(buffer); // Читаем в пакет данные из буфера return packet; >public statuc Packet write(Packet packet, ChannelBuffer buffer) < buffer.writeChar(packet.getId()); // Отправляем ID пакета packet.send(buffer); // Отправляем данные пакета >// Функции, которые должен реализовать каждый класс пакета public abstract void get(ChannelBuffer buffer); public abstract void send(ChannelBuffer buffer); >

Пример пары пакетов для наглядности:
// Пакет, которым клиент передаёт серверу свой логин public class Packet1Login extends Packet < public String login; public void get(ChannelBuffer buffer) < int length = buffer.readShort(); StringBuilder builder = new StringBuilder(); for(int i = 0; i < length ++i) builder.append(buffer.readChar()); login = builder.toString(); >public void send(ChannelBuffer buffer) < // Тело отправки пустое, т.к. сервер не посылает этот пакет >> // Пакет, которым сервер выкидывает клиента с указаной причиной, или клиент отключается от сервера public class Packet255KickDisconnect extends Packet < public String reason; public void get(ChannelBuffer buffer) < int length = buffer.readShort(); StringBuilder builder = new StringBuilder(); for(int i = 0; i < length ++i) builder.append(buffer.readChar()); reason = builder.toString(); >public void send(ChannelBuffer buffer) < buffer.writeShort(reason.length()); for(int i = 0; i < reason.length(); ++i) < buffer.writeChar(reason.getCharAt(i)); >> >

Читайте также:  Игра как Майнкрафт 2д

ChannelBuffer очень похож на DataInputStream и DataOutputStream в одном лице. Большинство функций если не такие же, то очень похожи. Заметьте, что я не забочусь о проверке того, хватает ли в буфере байт для чтения, как будто я работаю с блокирующим IO. Об этом далее…

Работа с клиентом


Работа с клиентом в основном определяется классом PlayerHandler:

Worker может посылать игроку данные просто функцией channel.write(packet), где channel — канал игрока, который передаётся ему при подключении, а packet — объект класса Packet. За кодирование пакетов будет отвечать уже Encoder.

Decoder и Encoder

Собственно, сама важная часть системы — они отвечают за формирование пакетов Packet из потока пользователя и за отправку таких же пакетов в поток.

Encoder очень прост, он отправляет пакеты игроку:

Decoder уже гораздо сложнее. Дело в том, что в буфере, пришедшем от клиента, может просто не оказаться достаточного количества байт для чтения всего пакета. В этом случае, нам поможет класс ReplayingDecoder. Нам всего лишь нужно реализовать его функцию decode и читать в ней данные из потока, не заботясь не о чём:

Спрашивается, как это работает? Очень просто, перед вызовом функции decode декодер помечает текущий индекс чтения, если при чтении из буфера в нём не хватит данных, будет сгенерировано исключение. При этом буфер вернётся в начальное положение и decode будет повторён, когда больше данных будет получено от пользователя. В случае успешного чтения (возвращён не null), декодер попытается вызвать функции decode ещё раз, уже на оставшихся в буфере данных, если в нём есть ещё хотя бы один байт.

Не медленно ли всё это работает, если он генерирует исключение? Медленнее, чем, например, проверка количества данных в буфере и оценка, хватит ли их для чтения пакета. Но он использует кэшированное исключение, поэтому не тратится время на заполнения stacktrace и даже создание нового объекта исключения. Подробнее об и некоторых других, повышающих эффективность, функцийя ReplayingDecoder можно почитать здесь

Вы так же можете поэкспериментировать с FrameDecoder’ом, если, например, Вы можете заранее определить размер пакета по его ID.

Кажется, это всё

Результаты получились отличными. Во-первых, сервер больше не сыпет тысячей потоков — 4 потока Netty + 4 потока обработки данных прекрасно справляются с 250+ клиентами (тестирование продолжается). Во-вторых, нагрузка на процессор стала значительно меньшей и перестала линейно расти от числа подключений. В-третьих, время отклика в некоторых случаях стало меньше.

Надеюсь кому-нибудь это будет полезно. Старалась передать как можно больше важных данных, могла переборщить. Примеров ведь много не бывает? Спрашивайте Ваши ответы и не судите строго — первый раз пишу на хабр.

Постскриптум: ещё несколько полезных вещей

У Netty есть ещё несколько интересных особенностей, которые заслуживают отдельного упоминания:

Во-первых, остановка сервера:

ChannelFuture future = channel.close(); future.awaitUninterruptibly();

Где channel — канал, который возвратила функция bind в начале. future.awaitUninterruptibly() дождётся, пока канал закроется и выполнение кода продолжится.

Самое интересное: ChannelFuture. Когда мы отправляем на канал пакет, функцией channel.write(packet), она возвращает ChannelFuture — это особый объект, который отслеживает состояние выполняемого действия. Через него можно проверить, выполнилось ли действие.

Например, мы хотим послать клиенту пакет отключения и закрыть за ним канал. Если мы сделаем

channel.write(new Packet255KickDisconnect(«Пока!»)); channel.close();

то с вероятностью 99%, мы получим ChannelClosedException и пакет до клиента не дойдёт. Но можно сделать так:

ChannelFuture future = channel.write(new Packet255KickDisconnect(«Пока!»)); try < future.await(10000); // Ждём не более 10 секунд, пока действие закончится >catch(InterruptedException e) <> channel.close();

Читайте также:  Как настроить автокликер на ПК для Майнкрафт

То всё будет супер, кроме того, что это может заблокировать поток выполнения, пока пакет не отправится пользователю. Поэтому на ChannelFuture можно повесит listener — объект, который будет уведомлён о том, что событие совершилось и выполнит какие-либо действия. Для закрытия соединения есть уже готовый listener ChannelFutureListener.CLOSE. Пример использования:

ChannelFuture future = channel.write(new Packet255KickDisconnect(«Пока!»)); furute.addListener(ChannelFutureListener.CLOSE);

Эффект тот же, блокировок нет. Разобраться в том, как создать свой листенер не сложно — там всего одна функция. Откройте любой готовый класс, здесь я не буду приводить пример.

Ещё важная информация

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

Так же в обработчике ни в коем случае нельзя «ждать будущего», т.е. выполнять .await() или .awaitUninterruptibly() на любом ChannelFuture. Во-первых, у Вас ничего не получится, их нельзя вызывать из обработчиков — система не даст сделать такую глупость и сгенерирует исключение. Во-вторых, если бы этого не было, Ваш поток опять же мог бы умереть оставив других клиентов без обслуживания.

Вообще, все действия, выполняемые в ChannelHandler’ах должны быть как можно более простыми и неблокирующими. Ни в коем случае не обрабатывайте данные прямо в них — кладите пакеты в очередь и обрабатывайте их в другом потоке.

Источник: habr.com

Введение в Netty

В этой статье мы собираемся взглянуть на Netty — фреймворк для асинхронных событийных сетевых приложений.

Основная цель Netty — создание высокопроизводительных протокольных серверов на основе NIO (или, возможно, NIO.2) с разделением и слабой связью компонентов сети и бизнес-логики. Он может реализовывать широко известный протокол, например HTTP, или ваш собственный протокол.

2. Основные концепции

Netty — это неблокирующий фреймворк. Это приводит к высокой пропускной способности по сравнению с блокировкой ввода-вывода. Понимание неблокирующего ввода-вывода имеет решающее значение для понимания основных компонентов Netty и их взаимосвязей.

2.1. Канал

Канал — это основа Java NIO. Он представляет собой открытое соединение, которое может выполнять операции ввода-вывода, такие как чтение и запись.

2.2. Будущее

Каждая операция ввода-вывода на канале в Netty не является блокирующей.

Это означает, что каждая операция возвращается сразу после вызова. В стандартной библиотеке Java есть интерфейс Future , но он неудобен для целей Netty — мы можем только запросить Future о завершении операции или заблокировать текущий поток до тех пор, пока операция не будет выполнена.

Вот почему у Netty есть собственный интерфейс ChannelFuture . Мы можем передать обратный вызов ChannelFuture, который будет вызываться после завершения операции.

2.3. События и обработчики

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

  • Активация и деактивация канала
  • Прочитать рабочие события
  • Исключительные события
  • Пользовательские события

Исходящие события проще и, как правило, связаны с открытием / закрытием соединения и записью / сбросом данных.

Приложения Netty состоят из пары событий сетевой и прикладной логики и их обработчиков. Базовыми интерфейсами для обработчиков событий канала являются ChannelHandler и его предки ChannelOutboundHandler и ChannelInboundHandler .

Netty предоставляет огромную иерархию реализаций ChannelHandler. Стоит отметить адаптеры, которые представляют собой просто пустые реализации, например ChannelInboundHandlerAdapter и ChannelOutboundHandlerAdapter . Мы могли бы расширить эти адаптеры, когда нам нужно обрабатывать только подмножество всех событий.

Кроме того, существует множество реализаций конкретных протоколов, таких как HTTP, например HttpRequestDecoder, HttpResponseEncoder, HttpObjectAggregator. Было бы хорошо познакомиться с ними в Javadoc Нетти.

2.4. Кодеры и декодеры

Поскольку мы работаем с сетевым протоколом, нам необходимо выполнить сериализацию и десериализацию данных. С этой целью Netty вводит специальные расширения ChannelInboundHandler для декодеров, которые могут декодировать входящие данные. Базовый класс большинства декодеров — ByteToMessageDecoder.

Для кодирования исходящих данных Netty имеет расширения ChannelOutboundHandler, называемые кодировщиками. MessageToByteEncoder — это основа для большинства реализаций кодировщика . Мы можем преобразовать сообщение из байтовой последовательности в объект Java и наоборот с помощью кодировщиков и декодеров.

Читайте также:  Как создать еду в Майнкрафте

3. Пример серверного приложения

Давайте создадим проект, представляющий простой сервер протокола, который получает запрос, выполняет расчет и отправляет ответ.

3.1. Зависимости

Прежде всего, нам нужно предоставить зависимость Netty в нашем pom.xml :

io.netty netty-all 4.1.10.Final

Мы можем найти последнюю версию на Maven Central.

3.2. Модель данных

Класс данных запроса будет иметь следующую структуру:

public class RequestData < private int intValue; private String stringValue; // standard getters and setters >

Предположим, что сервер получает запрос и возвращает intValue, умноженный на 2. Ответ будет иметь единственное значение int:

public class ResponseData < private int intValue; // standard getters and setters >

3.3. Запросить декодер

Теперь нам нужно создать кодировщики и декодеры для наших протокольных сообщений.

Следует отметить, что Netty работает с буфером приема сокетов , который представлен не как очередь, а просто как набор байтов. Это означает, что наш обработчик входящих сообщений может быть вызван, когда полное сообщение не получено сервером.

Мы должны убедиться, что получили полное сообщение перед обработкой, и есть много способов сделать это.

Прежде всего, мы можем создать временный ByteBuf и добавлять к нему все входящие байты, пока не получим требуемое количество байтов:

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

We deliberately did not use a stringValue — decoding in such a manner would be unnecessarily complex. That’s why Netty provides useful decoder classes which are implementations of ChannelInboundHandler: ByteToMessageDecoder and ReplayingDecoder.

As we noted above we can create a channel processing pipeline with Netty. So we can put our decoder as the first handler and the processing logic handler can come after it.

The decoder for RequestData is shown next:

An idea of this decoder is pretty simple. It uses an implementation of ByteBuf which throws an exception when there is not enough data in the buffer for the reading operation.

When the exception is caught the buffer is rewound to the beginning and the decoder waits for a new portion of data. Decoding stops when the out list is not empty after decode execution.

3.4. Response Encoder

Besides decoding the RequestData we need to encode the message. This operation is simpler because we have the full message data when the write operation occurs.

We can write data to Channel in our main handler or we can separate the logic and create a handler extending MessageToByteEncoder which will catch the write ResponseData operation:

3.5. Request Processing

Since we carried out the decoding and encoding in separate handlers we need to change our ProcessingHandler:

3.6. Server Bootstrap

Now let’s put it all together and run our server:

The details of the classes used in the above server bootstrap example can be found in their Javadoc. The most interesting part is this line:

ch.pipeline().addLast( new RequestDecoder(), new ResponseDataEncoder(), new ProcessingHandler());

Here we define inbound and outbound handlers that will process requests and output in the correct order.

4. Client Application

The client should perform reverse encoding and decoding, so we need to have a RequestDataEncoder and ResponseDataDecoder:

Also, we need to define a ClientHandler which will send the request and receive the response from server:

Now let’s bootstrap the client:

Как мы видим, есть много общих деталей с начальной загрузкой сервера.

Теперь мы можем запустить основной метод клиента и взглянуть на вывод консоли. Как и ожидалось, мы получили ResponseData с intValue, равным 246.

5. Заключение

В этой статье мы быстро познакомились с Netty. Мы показали его основные компоненты, такие как Channel и ChannelHandler . Также мы сделали простой неблокирующий протокол-сервер и клиент для него.

Как всегда, все образцы кода доступны на GitHub.

Популярные посты

  • Программирование
  • Программирование

Источник: ru.minecraftfullmod.com