Scala Netty Как создать простой клиент для протокола байт данных?

71
9

Этот вопрос предназначен как презентация проблемы, с которой я столкнулся при работе над текущим проектом. Я отвечу ниже, представляя мое решение.

Я работаю над проектом, который требует, чтобы я подключился к серверу фида данных, который имеет собственный протокол для передачи данных, по существу закодированных в разделе данных протокола TCP в формате GZIP и должен быть извлечен.

Пример приложения для протокола данных от поставщика данных использует простой сокет в Java. Я хочу адаптировать его к scala/netty. Кроме того, стоит отметить, что предоставленные данные могут быть распределены по нескольким пакетам.

Я искал простые и краткие примеры того, как использовать Netty.io для создания простого клиентского приложения, но все примеры кажутся чрезмерно сложными и не имеют достаточного количества объяснений, чтобы просто достичь этой цели. Более того, многие примеры netty/scala ориентированы на серверные приложения.

В учебном пособии " Начало работы " нет достаточного количества объяснений, чтобы упростить навигацию при начале работы.

Вопрос в том, как реализовать простое приложение netty, которое подключается к серверу, получает данные и анализирует результаты?

Вот некоторые из примеров, которые я рассмотрел, чтобы попытаться понять эту концепцию:

спросил(а) 2015-10-25T01:43:00+03:00 5 лет назад
1
Решение
115

Я столкнулся с этой проблемой при попытке репликации приложения Java с использованием сокетов в более сложный подход к использованию Netty.

То, как я решил проблему, - это понять различные элементы библиотеки netty, необходимые для установления соединения:

Эти 3 элемента гарантируют, что соединение создано и управляется для дальнейшей обработки.

Кроме того, при работе с Netty необходимы некоторые другие элементы:

    инициализатор канала, обычно пользовательский объект, подклассифицированный из ChannelInitializer декодер, который может быть любым типом, основанным на типе сообщений, которые ожидаются получить, обычно это подклассы ChannelInboundHandlerAdapter кодер, аналогичный декодерам, но для исходящих сообщений, как правило, подкласс ChannelOutboundHandlerAdapter Handler, который по сути говорит netty, как работать с полученными данными.

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

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

I/O Request
via Channel or
ChannelHandlerContext
|
+---------------------------------------------------+---------------+
| ChannelPipeline | |
| \|/ |
| +---------------------+ +-----------+----------+ |
| | Inbound Handler N | | Outbound Handler 1 | |
| +----------+----------+ +-----------+----------+ |
| /|\ | |
| | \|/ |
| +----------+----------+ +-----------+----------+ |
| | Inbound Handler N-1 | | Outbound Handler 2 | |
| +----------+----------+ +-----------+----------+ |
| /|\ . |
| . . |
| ChannelHandlerContext.fireIN_EVT() ChannelHandlerContext.OUT_EVT()|
| [ method call] [method call] |
| . . |
| . \|/ |
| +----------+----------+ +-----------+----------+ |
| | Inbound Handler 2 | | Outbound Handler M-1 | |
| +----------+----------+ +-----------+----------+ |
| /|\ | |
| | \|/ |
| +----------+----------+ +-----------+----------+ |
| | Inbound Handler 1 | | Outbound Handler M | |
| +----------+----------+ +-----------+----------+ |
| /|\ | |
+---------------+-----------------------------------+---------------+
| \|/
+---------------+-----------------------------------+---------------+
| | | |
| [ Socket.read() ] [ Socket.write() ] |
| |
| Netty Internal I/O Threads (Transport Implementation) |
+-------------------------------------------------------------------+

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

Давайте начнем с рассмотрения основ подключения, которое будет инициировано как клиентское приложение:

import io.netty.bootstrap.Bootstrap
import io.netty.channel.nio.NioEventLoopGroup
import io.netty.channel.socket.nio.NioSocketChannel
import io.netty.channel.socket.SocketChannel

object App {
def main(args: Array[String]){
connect()
}

def connect() {
val host = "host.example.com"
val port = 9999
val group = new NioEventLoopGroup() // starts the event loop group

try {
var b = new Bootstrap() // creates the netty bootstrap
.group(group) // associates the NioEventLoopGroup to the bootstrap
.channel(classOf[NioSocketChannel]) // associates the channel to the bootstrap
.handler(MyChannelInitializer) // provides the handler for dealing with the incoming/outgoing data on the channel

var ch = b.connect(host, port).sync().channel() //initiates the connection to the server and links it to the netty channel

ch.writeAndFlush("SERVICE_REQUEST") // sends the request to the server

ch.closeFuture().sync() // keeps the connection alive instead of shutting down the channel after receiving the first packet
}
catch {
case t: Throwable => t.printStackTrace(); group.shutdownGracefully()
}
finally {
group.shutdownGracefully() // Shutdown the event group
}
}
}

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

import io.netty.channel.ChannelInitializer
import io.netty.channel.socket.SocketChannel
import io.netty.handler.codec.string.StringEncoder

object MyChannelInitializer extends ChannelInitializer[SocketChannel] {

val STR_ENCODER = new StringEncoder // Generic StringEecoder from netty to simply allow a string to be prepared and sent out to the server

def initChannel(ch: SocketChannel) {
val pipeline = ch.pipeline() // loads the pipeline associated with the channel

// Decode Message
pipeline.addLast("packet-decoder",MyPacketDecoder) // first data "filter" to extract the necessary bytes for the second filter
pipeline.addLast("gzip-inflater", MyGZipDecoder) // second "filter" to unzip the contents

// Encode String to send
pipeline.addLast("command-encoder",STR_ENCODER) // String encoder for outgoing data

// Handler
pipeline.addLast("message-handler", MyMessageHandler) // Handles the end data after all "filters" have been applied
}
}

В этом случае первый элемент конвейера, MyPacketDecoder был создан как подкласс ReplayingDecoder, который обеспечивает простой способ выполнения MyPacketDecoder пакета, чтобы иметь все необходимые байты для использования сообщения. (Проще говоря, дождитесь, пока все байты будут собраны в переменной ByteBuf, прежде чем двигаться дальше)

Понимание того, как работает ByteBuf, очень важно для этого типа приложений, особенно разница между методами read и get, которые позволяют читать и перемещать индекс чтения или просто читать данные, не влияя на индекс читателя.

Пример MyPacketDecoder приведен ниже

import io.netty.handler.codec.ReplayingDecoder
import io.netty.channel.ChannelHandlerContext
import io.netty.buffer.ByteBuf
import java.util.List

object MyPacketDecoder extends ReplayingDecoder[Int] {

val READ_HEADER = 0
val READ_CONTENT = 1

super.state(READ_HEADER) // sets the initial state of the Decoder by calling the superclass constructor

var blockSize:Int = 0 // size of the data expected, published by the received data from the server, will vary according to your case, there may be additional header bytes before the actual data to be processed

def decode(ctx: ChannelHandlerContext,in: ByteBuf,out: List[AnyRef]): Unit = {

var received_size = in.readableBytes()

if(state() == READ_HEADER){
blockSize = in.readInt() // header data with the size of the expected data to be received in the current and following packets if segmented

checkpoint(READ_CONTENT) // change the state of the object in order to proceed to obtaining all the required bytes necessary for the message to be valid
}
else if(state() == READ_CONTENT){

var bytes = new Array[Byte](blockSize)
in.getBytes(0,bytes,0,blockSize) // adds collected bytes to the by array for the expected size as defined by the blockSize variable

var frame = in.readBytes(blockSize) // creates the bytebuf to be passed to the next "filter"

checkpoint(READ_HEADER) // changes the state preparing for the next message
out.add(frame) // passes the data to the next "filter"
}
else {
throw new Error("Case not covered Exception")
}
}

}

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

Следующий уровень конвейера связан с декомпрессией GZIP полученных данных. Это обеспечивается объектом MyGZipDecoder, который определяется как подкласс абстрактного объекта ByteToMessageDecoder для обработки информации байта в виде полученных данных:

import io.netty.handler.codec.ByteToMessageDecoder
import io.netty.channel.ChannelHandlerContext
import io.netty.buffer.ByteBuf
import java.net._
import java.io._
import java.util._
import java.util.zip._
import java.text._

object MyGZipDecoder extends ByteToMessageDecoder {

val MAX_DATA_SIZE = 100000

var inflater = new Inflater(true)
var compressedData = new Array[Byte](MAX_DATA_SIZE)
var uncompressedData = new Array[Byte](MAX_DATA_SIZE)

def decode(ctx: ChannelHandlerContext,in: ByteBuf,out: List[AnyRef]): Unit = {

var received_size = in.readableBytes() // reads the number of available bytes

in.readBytes(compressedData, 0, received_size) // puts the bytes into a Byte array

inflater.reset();
inflater.setInput(compressedData, 0, received_size) // prepares the inflater for decompression of the data
var resultLength = inflater.inflate(uncompressedData) // decompresses the data into the uncompressedData Byte array

var message = new String(uncompressedData) // generates a string from the uncompressed data

out.add(message) // passes the data to the next pipeline level
}
}

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

Последней частью головоломки является объект MyMessageHandler который по существу делает окончательную обработку данных для требуемой цели приложения. Это подкласс SimpleChannelInboundHandler с параметром String, ожидаемым как сообщение для данных канала:

import io.netty.channel.{ChannelHandlerContext, SimpleChannelInboundHandler}
import io.netty.channel.ChannelHandler.Sharable

@Sharable
object QMMessageHandler extends SimpleChannelInboundHandler[String] {

def channelRead0(ctx: ChannelHandlerContext, msg: String) {

println("Handler => Received message: "+msg)
// Do your data processing here however you need for the application purposes

}
}

Это существенно дополняет требования к этому конкретному примеру подключения к серверу, который предоставляет данные в проприетарном протоколе данных, используя сжатие GZip для базовых пакетных данных.

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

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

Я заранее извиняюсь за любые ошибки, которые я, возможно, пропустил при написании этого ответа.

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

ответил(а) 2015-10-25T02:24:00+03:00 5 лет назад
Ваш ответ
Введите минимум 50 символов
Чтобы , пожалуйста,
Выберите тему жалобы:

Другая проблема