1. Pengenalan
Dalam artikel ini, kita akan melihat Netty - kerangka aplikasi rangkaian berdasarkan acara yang tidak segerak.
Tujuan utama Netty adalah membina pelayan protokol berprestasi tinggi berdasarkan NIO (atau mungkin NIO.2) dengan pemisahan dan gandingan longgar rangkaian dan komponen logik perniagaan. Ini mungkin melaksanakan protokol yang terkenal, seperti HTTP, atau protokol khusus anda sendiri.
2. Konsep Teras
Netty adalah rangka kerja yang tidak menyekat. Ini membawa kepada throughput yang tinggi berbanding dengan menyekat IO. Memahami IO yang tidak menyekat sangat penting untuk memahami komponen teras Netty dan hubungannya.
2.1. Saluran
Saluran adalah asas Java NIO. Ia mewakili sambungan terbuka yang mampu melakukan operasi IO seperti membaca dan menulis.
2.2. Masa Depan
Setiap operasi IO di Saluran di Netty tidak disekat.
Ini bermaksud bahawa setiap operasi dikembalikan sebaik sahaja panggilan dilakukan. Terdapat antara muka Future di perpustakaan Java standard, tetapi tidak sesuai untuk tujuan Netty - kami hanya dapat bertanya pada Masa Depan mengenai penyelesaian operasi atau untuk menyekat utas semasa sehingga operasi selesai.
Itulah sebabnya Netty mempunyai antara muka ChannelFuture sendiri . Kita boleh memberikan panggilan balik ke ChannelFuture yang akan dipanggil setelah operasi selesai.
2.3. Acara dan Pengendali
Netty menggunakan paradigma aplikasi berdasarkan peristiwa, jadi saluran pemprosesan data adalah rangkaian peristiwa yang melalui pengendali. Acara dan pengendali boleh berkaitan dengan aliran data masuk dan keluar. Acara masuk boleh menjadi seperti berikut:
- Pengaktifan dan penyahaktifan saluran
- Baca peristiwa operasi
- Acara pengecualian
- Acara pengguna
Acara keluar lebih sederhana dan, umumnya, berkaitan dengan membuka / menutup sambungan dan menulis / memerah data.
Aplikasi Netty terdiri daripada beberapa peristiwa rangkaian dan logik aplikasi dan pengendali mereka. Antaramuka asas untuk pengendali acara saluran adalah ChannelHandler dan leluhurnya ChannelOutboundHandler dan ChannelInboundHandler .
Netty menyediakan hierarki pelaksanaan ChannelHandler yang besar . Perlu diperhatikan adapter yang hanya implementasi kosong, misalnya ChannelInboundHandlerAdapter dan ChannelOutboundHandlerAdapter . Kami dapat memperluas penyesuai ini apabila kami hanya perlu memproses subkumpulan semua peristiwa.
Juga, terdapat banyak pelaksanaan protokol tertentu seperti HTTP, misalnya HttpRequestDecoder, HttpResponseEncoder, HttpObjectAggregator. Adalah baik untuk berkenalan dengan mereka di Netty's Javadoc.
2.4. Pengekod dan Penyahkod
Semasa kita bekerja dengan protokol rangkaian, kita perlu melakukan serialisasi data dan deserialisasi. Untuk tujuan ini, Netty memperkenalkan sambungan khas ChannelInboundHandler untuk penyahkod yang mampu menyahkod data masuk. Kelas asas kebanyakan penyahkod adalah ByteToMessageDecoder.
Untuk pengekodan data keluar, Netty mempunyai sambungan ChannelOutboundHandler yang disebut pengekod. MessageToByteEncoder adalah asas untuk kebanyakan pelaksanaan pengekod . Kita boleh menukar mesej dari urutan bait ke objek Java dan sebaliknya dengan pengekod dan penyahkod.
3. Contoh Aplikasi Pelayan
Mari buat projek yang mewakili pelayan protokol sederhana yang menerima permintaan, melakukan pengiraan dan menghantar respons.
3.1. Kebergantungan
Pertama sekali, kita perlu memberikan ketergantungan Netty dalam pom.xml kami :
io.netty netty-all 4.1.10.Final
Kita boleh mendapatkan versi terbaru di Maven Central.
3.2. Model Data
Kelas data permintaan akan mempunyai struktur berikut:
public class RequestData { private int intValue; private String stringValue; // standard getters and setters }
Mari kita anggap bahawa pelayan menerima permintaan dan mengembalikan nilai int dikalikan dengan 2. Respons akan mempunyai nilai int tunggal:
public class ResponseData { private int intValue; // standard getters and setters }
3.3. Minta Decoder
Sekarang kita perlu membuat pengekod dan penyahkod untuk mesej protokol kami.
Harus diingat bahawa Netty berfungsi dengan soket menerima penyangga , yang dilambangkan bukan sebagai barisan tetapi hanya sebagai sekumpulan bait. Ini bermaksud bahawa pengendali masuk kami dapat dipanggil apabila mesej penuh tidak diterima oleh pelayan.
Kita mesti memastikan bahawa kita telah menerima mesej penuh sebelum memproses dan terdapat banyak cara untuk melakukannya.
Pertama sekali, kita boleh membuat ByteBuf sementara dan menambahkannya ke dalam semua bait masuk sehingga kita mendapat jumlah bait yang diperlukan:
public class SimpleProcessingHandler extends ChannelInboundHandlerAdapter { private ByteBuf tmp; @Override public void handlerAdded(ChannelHandlerContext ctx) { System.out.println("Handler added"); tmp = ctx.alloc().buffer(4); } @Override public void handlerRemoved(ChannelHandlerContext ctx) { System.out.println("Handler removed"); tmp.release(); tmp = null; } @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { ByteBuf m = (ByteBuf) msg; tmp.writeBytes(m); m.release(); if (tmp.readableBytes() >= 4) { // request processing RequestData requestData = new RequestData(); requestData.setIntValue(tmp.readInt()); ResponseData responseData = new ResponseData(); responseData.setIntValue(requestData.getIntValue() * 2); ChannelFuture future = ctx.writeAndFlush(responseData); future.addListener(ChannelFutureListener.CLOSE); } } }
Contoh yang ditunjukkan di atas kelihatan agak pelik tetapi membantu kita memahami bagaimana Netty berfungsi. Setiap kaedah pengendali kami dipanggil apabila peristiwa yang sesuai berlaku. Oleh itu, kami menginisialisasi penyangga ketika pengendali ditambahkan, isi dengan data mengenai penerimaan bait baru dan mula memprosesnya apabila kami mendapat cukup data.
Kami dengan sengaja tidak menggunakan stringValue - penyahkodan sedemikian rupa akan menjadi rumit. Itulah sebabnya Netty menyediakan kelas penyahkod berguna yang merupakan pelaksanaan ChannelInboundHandler : ByteToMessageDecoder dan ReplayingDecoder .
Seperti yang kami nyatakan di atas, kami dapat membuat saluran pemprosesan saluran dengan Netty. Oleh itu, kita boleh meletakkan penyahkod kita sebagai pengendali pertama dan pengendali logik pemprosesan dapat mengikutinya.
Penyahkod untuk RequestData ditunjukkan seterusnya:
public class RequestDecoder extends ReplayingDecoder { private final Charset charset = Charset.forName("UTF-8"); @Override protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) throws Exception { RequestData data = new RequestData(); data.setIntValue(in.readInt()); int strLen = in.readInt(); data.setStringValue( in.readCharSequence(strLen, charset).toString()); out.add(data); } }
Idea penyahkod ini cukup mudah. Ia menggunakan pelaksanaan ByteBuf yang membuang pengecualian apabila tidak ada cukup data dalam penyangga untuk operasi membaca.
Apabila pengecualian ditangkap, buffer dikembalikan ke awal dan penyahkod menunggu bahagian data yang baru. Penyahkodan berhenti apabila keluar senarai tidak mengosongkan selepas decode pelaksanaan.
3.4. Pengekod Respons
Selain menyahkod RequestData, kita perlu menyandikan mesej. Operasi ini lebih mudah kerana kami mempunyai data mesej penuh ketika operasi menulis berlaku.
Kami dapat menulis data ke Saluran di pengendali utama kami atau kami dapat memisahkan logik dan membuat pengendali yang memperluas MessageToByteEncoder yang akan menangkap operasi tulis ResponseData :
public class ResponseDataEncoder extends MessageToByteEncoder { @Override protected void encode(ChannelHandlerContext ctx, ResponseData msg, ByteBuf out) throws Exception { out.writeInt(msg.getIntValue()); } }
3.5. Memproses Permintaan
Oleh kerana kami menjalankan penyahkodan dan pengekodan dalam pengendali yang berasingan, kami perlu mengubah ProcessingHandler kami :
public class ProcessingHandler extends ChannelInboundHandlerAdapter { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { RequestData requestData = (RequestData) msg; ResponseData responseData = new ResponseData(); responseData.setIntValue(requestData.getIntValue() * 2); ChannelFuture future = ctx.writeAndFlush(responseData); future.addListener(ChannelFutureListener.CLOSE); System.out.println(requestData); } }
3.6. Bootstrap Pelayan
Sekarang mari kita satukan semuanya dan jalankan pelayan kami:
public class NettyServer { private int port; // constructor public static void main(String[] args) throws Exception { int port = args.length > 0 ? Integer.parseInt(args[0]); : 8080; new NettyServer(port).run(); } public void run() throws Exception { EventLoopGroup bossGroup = new NioEventLoopGroup(); EventLoopGroup workerGroup = new NioEventLoopGroup(); try { ServerBootstrap b = new ServerBootstrap(); b.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) .childHandler(new ChannelInitializer() { @Override public void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast(new RequestDecoder(), new ResponseDataEncoder(), new ProcessingHandler()); } }).option(ChannelOption.SO_BACKLOG, 128) .childOption(ChannelOption.SO_KEEPALIVE, true); ChannelFuture f = b.bind(port).sync(); f.channel().closeFuture().sync(); } finally { workerGroup.shutdownGracefully(); bossGroup.shutdownGracefully(); } } }
Perincian kelas yang digunakan dalam contoh bootstrap pelayan di atas boleh didapati di Javadoc mereka. Bahagian yang paling menarik adalah baris ini:
ch.pipeline().addLast( new RequestDecoder(), new ResponseDataEncoder(), new ProcessingHandler());
Di sini kami menentukan pengendali masuk dan keluar yang akan memproses permintaan dan output dalam urutan yang betul.
4. Permohonan Pelanggan
Pelanggan harus melakukan pengekodan dan penyahkodan terbalik, jadi kita perlu mempunyai RequestDataEncoder dan ResponseDataDecoder :
public class RequestDataEncoder extends MessageToByteEncoder { private final Charset charset = Charset.forName("UTF-8"); @Override protected void encode(ChannelHandlerContext ctx, RequestData msg, ByteBuf out) throws Exception { out.writeInt(msg.getIntValue()); out.writeInt(msg.getStringValue().length()); out.writeCharSequence(msg.getStringValue(), charset); } }
public class ResponseDataDecoder extends ReplayingDecoder { @Override protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) throws Exception { ResponseData data = new ResponseData(); data.setIntValue(in.readInt()); out.add(data); } }
Juga, kita perlu menentukan ClientHandler yang akan menghantar permintaan dan menerima respons dari pelayan:
public class ClientHandler extends ChannelInboundHandlerAdapter { @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { RequestData msg = new RequestData(); msg.setIntValue(123); msg.setStringValue( "all work and no play makes jack a dull boy"); ChannelFuture future = ctx.writeAndFlush(msg); } @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { System.out.println((ResponseData)msg); ctx.close(); } }
Sekarang mari kita boot klien:
public class NettyClient { public static void main(String[] args) throws Exception { String host = "localhost"; int port = 8080; EventLoopGroup workerGroup = new NioEventLoopGroup(); try { Bootstrap b = new Bootstrap(); b.group(workerGroup); b.channel(NioSocketChannel.class); b.option(ChannelOption.SO_KEEPALIVE, true); b.handler(new ChannelInitializer() { @Override public void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast(new RequestDataEncoder(), new ResponseDataDecoder(), new ClientHandler()); } }); ChannelFuture f = b.connect(host, port).sync(); f.channel().closeFuture().sync(); } finally { workerGroup.shutdownGracefully(); } } }
Seperti yang kita lihat, terdapat banyak persamaan dengan bootstrapping pelayan.
Sekarang kita boleh menjalankan kaedah utama pelanggan dan melihat output konsol. Seperti yang dijangkakan, kami mendapat ResponseData dengan intValue sama dengan 246.
5. Kesimpulan
Dalam artikel ini, kami mendapat pengenalan ringkas mengenai Netty. Kami menunjukkan komponen utamanya seperti Channel dan ChannelHandler . Juga, kami telah membuat pelayan protokol tanpa sekatan sederhana dan pelanggan untuknya.
Seperti biasa, semua sampel kod boleh didapati di GitHub.