12 просмотров
Рейтинг статьи
1 звезда2 звезды3 звезды4 звезды5 звезд
Загрузка...

Маппинг запросов на Netty

Netty: Запуск Socket сервера

Как было отмечено ранее, стабильная ветка Netty обладает отличным JavaDoc, а более новая четвертая ветка практически не задокументирована. Хочется еще отметить, что в JavaDoc’е Netty используется специальный JavaDoc доклет ApiViz, который позволяет отображать связи компонент прямо в документации ввиде графиков. В связи с этим обзор архитектуры будет вестись по 3ей версии. Именно по ней будет изучена терминология проекта, а уже с какими-то знаниями о компонентах системы мы посмотрим на отличия в реализации той или иной фичи в новой четвертой версии Netty.

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

Запуск сервера начинается с создания объекта helper класса ServerBootstrap . Этот объект позволяет сконфигурировать сервер, наполнить его ключевыми компонентами и наконец, запустить. В первую очередь необходимо указать фабрику каналов. Каналы представляют собой сетевое соединение и Netty поддерживает несколько их реализаций. Основные из них это соединения использующие:

  1. Old I/O Java package
  2. New I/O Java package

Если первый тип реализации – это скорее пережиток прошлого и нужен лишь для поддержки старых программ, то второй тип – это то, что нам нужно. ChannelFactory можно не задавать в конструкторе ServerBootstrap , а использовать ServerBootstrap#setChannelFactory метод. Setter-метод можно использовать только один раз. Это лишь логическое ограничение, которое проверяется естественно только в runtime’e.

NioServerSocketChannelFactory оперирует двумя пулами потоков для создание асинхронных соединений. Пулы можно задать в конструкторе ChannelFactory . В противном случае создаются стандартные кеширующие java пулы:

Зачем фабрике каналов два пула потоков? Первый из них – boss pool. Создает новые потоки для входящих соединений. Второй же worker pool, обеспечивает потоки для неблокирующих операций записи/чтения.

При создании соединения для канала создается свой pipeline – цепочка обработчиков асинхронных событий каналов. Поэтому ServerBootstrap должен обладать фабрикой pileline’ов. Создавая фабрику pipeline’ов определяются обработчики. В данном случае это один обработчик EchoServerHandler . Удивительно, что без фабрик каналов и pipeline’ов сервер работать не будет, но в архитектуре возможность создания сервера без фабрик есть, т.к. задаются они setter методами.

ServerBootstrap#bind запускает сервер, а точнее вешает сервер на определенный адрес. Рассмотрим на данном примере, как запускается NIO Socet Server.

Запуск сервера начинается с открытия нового канала, который создается с помощью фабрики каналов, которую мы засетили. В нашем случае будет создан NioServerSocketChannel . В конструкторе этого канала будет создан Java SocetChannel :

И если socket открыт удачно, то канал сигнализирует событие об открытии нового канала.

Стоит рассказать, что в событийной модели Netty присутствуют два вида сообщений:

Все сообщения циркулируют по pipeline. Можно рассматривать pipeline, как цепочку обработчиков событий. Любой обработчик может остановить на себе обработку сообщения. Посыл сообщений происходит соответственно через pipeline.

Канал может быть создан другим каналом. Тот будет для нового канала родительским. Для каналов клиентской стороны родительским каналом будет канал серверной стороны. Входящее сообщение об открытии канала посылается и родительскому каналу, если он есть (метод fireChildChannelStateChanged ), и обработчикам данного канала.

Boss-канал, открывающий соединение, на данном этапе кроме открытия сокета и порождения события об этом, ничего не делает и не может делать, так как не обладает нужной для этого информацией. Как же происходит слежение за сообщениями данного канала? Очевидно, что у данного канала должны быть свои обработчики, которые будут ожидать события открытия соединения и в этот момент выполнять всю необходимую для нас работу.

При создании в ServerBootstrap нового сокет канала инициализирует boss pipeline, куда добавляется обязательный обработчик Binder и пользовательский обработчик, который можно задать через метод ServerBootstrap#setParentHandler . Класс обязательного обработчика является внтуренним для класса ServerBootstrap , т.к. является неотъемлемой его частью. В момент получения события об открытии нового socket-канала, Binder получает ссылку на Netty канал из объекта события и задает ему пользовательский PipelineFactory , пользовательские настройки и привязывает открытое соединение к конкретному адресу. По аналогии с Java сокетами, Netty сокеты также обладают методом bind, который привязывает его к конкретному адресу.

Асинхронная модель работы Netty основана не тольно на событиях, но также и на Future объектах. Если события обеспечивают работу через callback’и и в данном случае поток, вызвавший операцию, никак не связан с асинхронным процессом, то Future объект предоставляет loop алгоритм, связывая асинхронный вызванный процесс с его родителем. Запуск сервера – как раз такой случай, т.к. этот метод должен вернуть открытый и сконфигурированный канал. Но если до данного момента работа шла в синхронном режиме, то привязка адреса к открытому соединению полноценно открывает сервер для клиентских соединений, запуская соответствующие потоки. Поэтому данный метод возвращает ChannelFuture объект с сылкой на текущий канал и вызывает исходящее событие привязки сервера к адресу.

В методах pipeline’а отправки сообщения происходит некоторое проксирование сообщений, т.к. разные реализации каналов могут работать по разному с разными сообщениями. Очевидно, что проксирование сообщений специфично для разных видов реализаций каналов, поэтому фабрика каналов, помимо типа самих каналов определяет конкретную реализацию проксирующего класса. Проксирующий класс определяет какие сообщения поддерживает канал и возможно даже обрабатывает какие-то из них, которые служат только для внутреннего использования. Такие объекты в Netty именуются Sink объектами. В данном случае обработка события привязки сервера к адресу обрабатывается внутри NioServerSocketPipelineSink :

В первую очередь вызывается привязка адреса к Java сокету. Далее ChannelFuture переводится в состояние успешного выполнения операции и посылается уже входящее сообщение pipeline’у канала о привязке сервера к адресу. Далее получается пул потоков, который мы передали фабрике каналов и с помощью него запускается boss-поток, который будет следить за соединениями по данному каналу. Теперь наш сервер может принимать соединения и обрабатывать их нашим pipeline’ом.

Читать еще:  Реактивное программирование со Spring Boot 2. Часть 1

Т.е. на данном этапе Binder обработчик связал сокет с адресом и осталось оповестить ServerBootstrap о статусе конфигурирования канала и вернуть наружу ссылку на сам канал. Но тут есть две проблемы:

  1. Событийная модель Netty работает в асинхронном режиме, что не дает вернуть напрямую результат работы обработчика
  2. Так как режим асинхронный, то ServerBootstrap работает в своем пространстве и, если не предпринято каких-то шаманских действий, то вполне возможно уже отработал.

А шаманские действия, как вы уже возможно успели заметить, предприняты. В ServerBootstrap до инициализации Binder обработчика создается BlockingQueue , которая передается в Binder. Далее, проинициализировав boss обработчик и создав канал, ServerBootstrap пытается прочитать из очереди ChannelFuture , но так как очередь блокирующая и в ней нет на данный момент сообщений, то поток ServerBootstrap подвешивается пока очередь не будет готова вернуть сообщение.

Как только Binder сконфигурировал открытый канал, он помещает ChannelFuture объект, который мы ранее рассмотрели в методе bind самого канала, в эту очередь:

В этот момент поток выполнения ServerBootstrap оживает и получает это сообщение из очереди и если статус ChannelFuture говорит, что все прошло хорошо, то наружу возвращает ссылка на открытый канал.

Вот так просто и изящно создаются socket соединения в Netty 3.5.

Posted by Code insiders 12:39 04.08.2012 Netty

Находки программиста

Решения конкретных задач программирования. Java, Android, JavaScript, Flex и прочее. Настройка софта под Linux, методики разработки и просто размышления.

четверг, 6 февраля 2014 г.

Netty: делаем лёгкий сервер с блэкджеком и аннотациями

Допустим вам нужно обрабатывать http-запросы в своём приложении. Пишем на servlet-ах! Spring!! ЕщёКакойТоФреймворк.
A теперь нам нужно слушать websocket. Выбор сужается? А завтра потребуется добавить поддержку SMPP или какого-нибудь ещё “необычного” протокола? Рано или поздно вам прийдётся создать консольное java-приложение и начать изучать “встраиваемые” сервера. Встроить можно много чего, но что если фантазия разработчиков “с той стороны internet-а” родит совсем уже неведомый протокол? И тут мы вспомним о Netty. На его основе можно реализовать практически что угодно, при чём такая универсальность не пойдёт в ущерб ни производительности ни простоте. Чтобы подтвердить свою мысль я ниже сделаю свой “крошечный” http-сервер, в который можно будет добавлять “сервлетообразные” обработчики, “навешивая” их на url с помощью аннотаций.

A теперь посмотрим на него подробнее.
Инициализируем наш сервер номером порта, который он должен слушать и вызываем метод start(). Тут создаются многопоточные обработчики сообщений, назначается handler и т.п.
В реализации ServerHandler-a как раз и кроется вся логика. Впрочем её там немного.
В реализации метода channelRead0() читаем очередное сообщение и в зависимости от его типа обрабатываем его или(и) пишем ответ. В ответе указываем все положенные заголовки. Обработку делаем вызывая тот или иной обработчик в зависимости от url. Все обработчики наследуют общего предка:

В нём определён абстрактный метод в котором нужно будет реализовать конкретную логику для каждого http-ресурса. Напремер, так:

Чтобы наш сервер смог найти какой url кем обслуживается, используем аннотацию:

В конструкторе заполняем таблицу обработчиков (поле handlers). Делаем это только если она пуста, т.е. при первом запуске. Все кастомные обработчики лежат в одном пакете. Достаём все классы из пакета с помощью такого вот инструмента:

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

Умный дом на Java. Обзор Netty — крутейшего Java-фреймворка

Содержание статьи

Фреймворк Netty появился относительно недавно, но с каждым годом он только набирает популярность. В 2011 году проект был удостоен награды Duke’s Choice за инновации в сетевом программировании. А уже сегодня его используют в своих разработках такие гиганты, как Apple, Twitter, Facebook, Google и Instagram. На базе Netty построены многие известные проекты с открытым кодом: Infinispan, HornetQ, Vert.x, Apache Cassandra и Elasticsearch. Попробуем разобраться, чем Netty так привлекает программистов со всего мира и что он может предложить такого, чего бы не было в JDK.

Работа с сетью в Java

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

Xakep #198. Случайностей не бывает

К счастью, сокеты можно настроить таким образом, чтобы сразу узнавать, есть там данные или нет, при чтении и не ждать передачи всех данных при записи. Это так называемые неблокирующие сокеты. Они позволяют одному потоку взаимодействовать с несколькими клиентами. В Java поддержка неблокирующих сокетов появилась только в версии 1.4 в виде Java NIO. Java NIO первоначально расшифровывалось как New IO, но теперь оно уже никакое не новое, поэтому и называется Non-blocking IO.

Читать еще:  Xls не является приложением win32 что делать?

Java NIO состоит из трех главных компонентов: каналов, буферов и селекторов.

Каналы похожи на потоки ввода/вывода (stream) с небольшими отличиями. Они обеспечивают двухстороннюю передачу данных, в один канал можно и писать, и читать. Чтение из канала и запись данных происходит асинхронно. Каналы всегда читают данные из буфера и пишут в буфер. Основные реализации каналов: FileChannel, DatagramChannel, SocketChannel, ServerSocketChannel.

Буфер представляет собой блок данных в памяти ограниченного размера и набор методов, облегчающих работу с ним. Буфер можно переключать в режим чтения и в режим записи. Один канал может работать с несколькими буферами, что позволяет не копировать данные на разных уровнях обработки. Если нужно, к примеру, добавить к данным заголовок, достаточно просто создать отдельный буфер для него, не копируя при этом сами данные. Основные классы буферов: ByteBuffer, MappedByteBuffer, CharBuffer, DoubleBuffer и другие.

Селектор — это компонент, который работает с каналами. В одном селекторе можно зарегистрировать несколько каналов, указав, какие именно события нужно отслеживать. Бывает четыре вида событий: «соединение с сервером установлено», «входящее соединение принято», «можно читать данные», «можно писать данные». Метод select блокирует выполнение кода, пока хотя бы в одном из каналов не произойдет интересующее селектор событие. Есть также и неблокирующий аналог метода select.

Сервер с неблокирующими сокетами

В Java 7 появилось расширение NIO под названием NIO2. В нем архитектура взаимодействия с каналами была немного изменена. Теперь стало возможным получать и отправлять данные асинхронно. Больше не нужно постоянно проверять, не появилось ли новое событие канала. Достаточно запустить операцию и зарегистрировать слушателя, который узнает о ее выполнении. Либо можно воспользоваться классом Future. Объект Future моделирует выполняемую операцию. С его помощью можно проверить, выполнена ли операция или заблокировать выполнение потока, пока не будет получен результат.

Таким образом, в Java существует три способа работы с сетевыми клиентами. Netty умеет работать со всеми тремя. Кроме того, Netty успешно борется с некоторыми проблемами NIO и NIO2. Например, NIO может работать немного по-разному на разных платформах, так как NIO реализован на низком уровне. И если какие-то тесты работают в Linux, нет стопроцентной гарантии, что они будут работать в Windows. NIO2 поддерживается только с седьмой версии Java. Так что для пользователей шестой Java придется писать свою версию сервера. Netty же предоставляет единый интерфейс работы с NIO, NIO2 и даже блокирующими сокетами — просто указываем, какой из фреймворков нужен.

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

Еще одна неприятность может подстерегать пользователей Linux-систем. Согласно документации, селектор должен ожидать появления события, но из-за ошибки в системе уведомлений epoll может возникать ситуация, когда селектор выходит из блокировки, даже если событий канала нет. В результате он начинает лихорадочно проверять каналы в цикле и съедает процессор на 100%. Netty справляется с этой и другими проблемами.

Netty также избавляет от сложностей асинхронного кода. Рассмотрим, к примеру, описанный выше селектор. Метод select может вернуть разнообразные события (можно писать в канал, можно читать, «клиент присоединился»), чтобы разобраться в этой мешанине, придется писать огромную if-else конструкцию, поддерживать которую — еще то веселье. Поэтому в Netty используется паттерн реактор. Каждому событию можно назначить свой обработчик. В Netty этот паттерн получил дальнейшее развитие. Каждому событию можно назначить цепочку обработчиков, выходное значение первого обработчика служит входным значением второго обработчика и так далее. Это облегчает последовательную обработку информации. К примеру, если клиент присылает сжатые зашифрованные данные, то первый обработчик разархивирует данные, второй расшифровывает, а третий уже непосредственно их обрабатывает. Если потребуется еще какая-то дополнительная обработка данных, то достаточно просто вставить новый обработчик в эту цепочку.

Основные компоненты Netty

Приложение Netty начинается с классов Bootstrap и ServerBootstrap. Bootstrap занимается инициализацией и конфигурацией инфраструктуры клиента, ServerBootstrap инициализирует сервер.

За обработку данных отвечают экземпляры ChannelHandler. Обработчики переводят объекты в бинарные данные и наоборот, а также предоставляют метод обработки ошибок, которые возникают в процессе. Таким образом, вся бизнес-логика происходит в обработчике. Обработчики разделяются на два типа: ChannelInboundHandler и ChannelOutboundHandler. Первый тип для работы с входящими данными, второй — с исходящими.

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

Все операции ввода-вывода для канала выполняются в цикле событий EventLoop. Несколько циклов событий объединяются в группу EventLoopGroup. Bootstrap клиента создает один экземпляр EventLoopGroup. ServerBootstrap для сервера создает два экземпляра EventLoopGroup. Один экземпляр только принимает соединения от клиентов, второй обрабатывает остальные события: чтение/запись данных и так далее. Это помогает избежать тайм-аута при подключении к серверу новых клиентов.

Читать еще:  Почему виртуальная машина не видит флешку?

Сервер Netty

Когда канал регистрируется, он привязывается к определенному циклу событий на все время своего существования. Цикл событий всегда выполняется в одном и том же потоке, поэтому не нужно заботиться о синхронизации операций ввода-вывода канала. Поскольку обычно один EventLoop работает с несколькими каналами, важно не выполнять никаких блокирующих операций в ChannelHandler. Но если это все же требуется, то Netty предоставляет возможность указать EventExecutorGroup при регистрации канала, EventExecutor которого будет выполнять все методы ChannelHandler в отдельном потоке, не нагружая EventLoop канала.

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

Пример работы: «умный дом»

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

Для протокола данных удобнее всего воспользоваться разработкой компании Google, утилитой protobuf. Protobuf — это механизм передачи структурированных данных, который не зависит от языка программирования или платформы. Нужно только написать каркас протокола передачи данных и сгенерировать специальный класс на нужном языке программирования.

Нам понадобятся два вида сообщений. SensorData — сообщение от датчика, в котором содержится статус, тип датчика и какое-то строковое значение. Command — сообщение от сервера с кодом команды, которую нужно выполнить.

Описываем протокол в файле с расширением proto и запускаем команду для генерации Java-класса:

Класс работы с нашим протоколом готов. Можно приступать к реализации сервера. Инициализацию сервера выполняет класс ServerBootstrap . Для работы с клиентами будем использовать неблокирующие сокеты, поэтому новые соединения принимает класс NioEventLoopGroup , а в качестве канала связи используется NioServerSocketChannel . Настройку канала общения с клиентом выполняет класс SmartHouseServerInitializer .

Открываем соединение на нужном порту и вызываем метод sync, который ожидает завершения операции. Метод sync вернет экземпляр ChannelFuture . Используем его, чтобы блокировать выполнение главного потока, пока канал сервера не будет закрыт:

В конце работы сервера освобождаем ресурсы и потоки, которыми пользовался цикл событий:

Класс SmartHouseServerInitializer наследуем от ChannelInitializer . В методе initChannel добавляем обработчики входящих и исходящих данных:

Когда ChannelPipeline получает новые входящие данные, он вызывает последовательно ProtobufVarint32FrameDecoder и ProtobufDecoder (и любые другие наследники от ChannelInboundHandler ), которые преобразуют входные данные из массива байтов в объект SensorData. Этот объект подается на вход SmartHouseServerHandler для обработки. Когда в ChannelPipeline поступают исходящие данные, вызываются последовательно ProtobufVarint32LengthFieldPrepender и ProtobufEncoder (и любые другие наследники от ChannelOutboundHandler ), которые переводят исходящий объект в байтовые данные. Подробнее о работе protobuf-обработчиков можно узнать из документации Netty.

Обработчик SmartHouseServerHandler наследован от SimpleChannelInboundHandler . SimpleChannelInboundHandler удобно использовать, если требуется прочитать сообщение определенного типа. В нашем случае это сообщения типа SensorData. Нам потребуется реализовать три метода. Метод channelRead0 позволяет обработать полученное сообщение указанного типа. Для простоты сервер будет просто посылать клиенту команду ничего не делать:

Метод write записывает исходящие данные в буфер, но не отправляет их клиенту. Для этого служит метод flush , который вызывается в channelReadComplete :

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

Теперь займемся клиентом. Для инициализации клиента используется класс Bootstrap . Настройкой канала соединения с сервером занимается класс SmartHouseClientInitializer . Клиенту не нужно держать соединение, поэтому после того, как данные отправлены, канал закрывается:

Реализация SmartHouseClientInitializer похожа на реализацию SmartHouseServerInitializer , только объект мы ожидаем класса Command и обрабатывать его будет класс SmartHouseClientHandler :

SmartHouseClientHandler тоже является наследником SimpleChannelInboundHandler . Для получения данных от сервера воспользуемся блокирующей очередью. Она приостановит выполнение программы, пока в нее не будут добавлены новые данные:

Отправлять данные на сервер будет метод sendUpdate . В нем используется writeAndFlush , чтобы сразу передать данные в сеть. Метод sendUpdate возвращает полученную от сервера команду:

Осталось передать на сервер тестовые данные, например, что температура дома 23 градуса:

Вывод

Таким образом, с помощью Netty можно быстро и просто написать любое быстрое клиент-серверное приложение, которое к тому же будет легко расширяться и масштабироваться. Если для обработки клиентов не хватает одного потока, следует всего лишь передать нужное число потоков в конструктор EventLoopGroup. Если на какой-то стадии развития проекта понадобится дополнительная обработка данных, не нужно переписывать код, достаточно добавить новый обработчик в ChannelPipeline, что значительно упрощает поддержку приложения.

Netty позволяет передавать данные через TCP или UDP, поддерживает множество протоколов, таких как HTTP, FTP, SMTP, WebSockets, SSL/TLC, SPDY. API хорошо документирован, на гитхабе выложено множество примеров с подробными комментариями. А все возрастающее сообщество свидетельствует о том, что продукт получился хороший, достойный внимания и активного использования.

голоса
Рейтинг статьи
Ссылка на основную публикацию
Статьи c упоминанием слов: