前面,我們學習了 Netty 的基本 原理和架構 ,今天我們來大致瞭解一下 Netty 中的各個組件。
同我們 前面 學習 IO 與 NIO 一樣的套路,我們先通過 echo 服務 demo 來學習 netty 的使用。
開發環境
- JDK >= 8
- Netty 4.1.29.Final
編寫 Echo Server 代碼
Netty 服務端的開發主要有以下兩個步驟:
- 至少有一個 ChannelHandler —— 這個主要用於處理從 client 端接受到的信息,是主要的業務邏輯處理類。
- Bootstrapping —— 用於配置服務的啓動代碼。最簡單的就是,監聽一個端口。
實現 EchoServerHandler 邏輯
服務端用於處理入站的網絡請求,因此我們需要實現接口類 ChannelInboundHandler,它裏面定義了用於
處理入站請求的一些接口。由於我們這個例子比較簡單,只需要用到它的幾個方法即可,因此我們的實現類只需要繼承子類 ChannelInboundHandlerAdapter 即可,它默認實現了 ChannelInboundHandler 中的接口。
有幾個方法需要留意一下:
- channelRead() —— 每當有入站請求來臨時,該方法都會被調用
- channelReadComplete() —— 對 channelRead () 的最後一次調用是當前批處理中的最後一條消息時,該方法會被調用
- exceptionCaught() —— 在 read 操作執行期間,如果發生異常,該方法則會被調用。
代碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
// @Sharable象徵着該ChannelHandler實例在多個channels之間可以被安全地分享 @Sharable public class EchoServerHandler extends ChannelInboundHandlerAdapter { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { ByteBuf in = (ByteBuf) msg; // 打印消息日誌 System.out.println("Server received: " + in.toString(CharsetUtil.UTF_8)); // 將入站消息發送給發送者,但不沖刷出站消息 ctx.write(in); } @Override public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { // 將待處理的消息沖刷到遠程節點上,並關閉Channel ctx.writeAndFlush(Unpooled.EMPTY_BUFFER) .addListener(ChannelFutureListener.CLOSE); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { // 打印堆棧信息 cause.printStackTrace(); // 關閉channel ctx.close(); } } |
實現 EchoServer 邏輯
接下來,我們使用 ServerBootstrap 來實現服務端的開發,主要以下兩點:
- 綁定一個監聽端口
- 配置 Channels,當有入站消息到達時,通知 EchoServerHeadler 實例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
package nia.chapter2.echoserver; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelInitializer; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioServerSocketChannel; import java.net.InetSocketAddress; public class EchoServer { private final int port; public EchoServer(int port) { this.port = port; } public static void main(String[] args) throws Exception { if (args.length != 1) { System.err.println("Usage: " + EchoServer.class.getSimpleName() + " <port>"); return; } int port = Integer.parseInt(args[0]); new EchoServer(port).start(); } public void start() throws Exception { final EchoServerHandler serverHandler = new EchoServerHandler(); // 創建 EventLoopGroup 實例 EventLoopGroup group = new NioEventLoopGroup(); try { // 創建 ServerBootstrap 實例 ServerBootstrap b = new ServerBootstrap(); b.group(group) // 執行Channel的類型爲:NioServerSocketChannel .channel(NioServerSocketChannel.class) // 綁定端口 .localAddress(new InetSocketAddress(port)) // 將EchoServerHandler添加到ChannelPipeline中去 .childHandler(new ChannelInitializer<SocketChannel>() { @Override public void initChannel(SocketChannel ch) throws Exception { // EchoServerHandler有註解 @Sharable,因此我們總是可以使用改實例 ch.pipeline().addLast(serverHandler); } }); // 異步綁定服務,sync() 用於等待綁定完成 ChannelFuture f = b.bind().sync(); System.out.println(EchoServer.class.getName() + " started and listening for connections on " + f.channel().localAddress()); // 獲取Channel的CloseFutrue,在完成之前一直處於阻塞狀態 f.channel().closeFuture().sync(); } finally { // 關閉所有 EventLoopGroup,並釋放所有資源 group.shutdownGracefully().sync(); } } } |
編寫 Echo Client 代碼
Echo Client 代碼邏輯:
- 連接服務器
- 發送一個或多個消息
- 等待服務端返回同樣的消息
- 關閉連接
實現 EchoClientHandler 邏輯
同服務端一樣,客戶端也要實現 ChannelInboundHandler 接口,客戶端需要繼承 SimpleChannelInboundHandler ,有以下三個接口需要重寫:
- channelActive() —— 當連接建立時,調用該方法
- channelRead0() —— 當接收到服務端的消息時,調用該方法
- exceptionCaught() —— 當有異常發生時,執行該方法
代碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
package nia.chapter2.echoclient; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelHandler.Sharable; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.util.CharsetUtil; // @Sharable用於標記EchoClientHandler,可以在channel中分享使用 @Sharable public class EchoClientHandler extends SimpleChannelInboundHandler<ByteBuf> { @Override public void channelActive(ChannelHandlerContext ctx) { // 一旦連接建立,將會發送消息 ctx.writeAndFlush(Unpooled.copiedBuffer("Netty rocks!",CharsetUtil.UTF_8)); } @Override public void channelRead0(ChannelHandlerContext ctx, ByteBuf in) { // 記錄收到的消息 System.out.println("Client received: " + in.toString(CharsetUtil.UTF_8)); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { // 打印堆棧信息 cause.printStackTrace(); // 關閉channel ctx.close(); } } |
實現 EchoClient 邏輯
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
package nia.chapter2.echoclient; import io.netty.bootstrap.Bootstrap; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelInitializer; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioSocketChannel; import java.net.InetSocketAddress; public class EchoClient { private final String host; private final int port; public EchoClient(String host, int port) { this.host = host; this.port = port; } public void start() throws Exception { EventLoopGroup group = new NioEventLoopGroup(); try { // 創建Bootstrap Bootstrap b = new Bootstrap(); // 指定使用 NioEventLoopGroup 去處理客戶端事件 b.group(group) // 指定channel類型爲NIO .channel(NioSocketChannel.class) // 指定要連接的遠程地址 .remoteAddress(new InetSocketAddress(host, port)) // 將 EchoClientHandler 添加到 pipeline中 .handler(new ChannelInitializer<SocketChannel>() { @Override public void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast(new EchoClientHandler()); } }); // 連接遠程地址,一直等待直到連接完成 ChannelFuture f = b.connect().sync(); // 在channel關閉前一直處於block狀態 f.channel().closeFuture().sync(); } finally { // 關閉線程池,釋放所有資源 group.shutdownGracefully().sync(); } } public static void main(String[] args) throws Exception { if (args.length != 2) { System.err.println("Usage: " + EchoClient.class.getSimpleName() + " <host> <port>" ); return; } final String host = args[0]; final int port = Integer.parseInt(args[1]); new EchoClient(host, port).start(); } } |
爲什麼 client 使用 SimpleChannelInboundHandler ,而 server 端使用 ChannelInboundHandlerAdapter 的區別 ?
在 Client 中,channelRead0 () 完成時,消息已經處理完。當該方法返回時,SimpleChannelInboundHandler 會釋放保存該消息的 ByteBuf 的內存引用。
在 Server 中,接收完消息後,還需要將消息回傳給客戶端,並且 wirte () 是異步的,當 channelRead () 完成時,消息內存還沒有被釋放。需要等到 channelReadComplete () 中調用 writeAndFlush () 纔會被釋放。
Netty 組件
這裏我們先簡要了解一下以下幾個組件的作用,留個映像,後面我們會對每個組件做詳細深入。
Channel
同我們前面學習 Java NIO Channel 類似,Netty Channel 在此基礎上做了高度抽象的封裝,主要用於網絡 I/O 數據的基本操作,如 bind ()、connect ()、read ()、write () 等。
EventLoop
在網絡連接的整個生命週期內,發生的所有事件的處理主要有 EventLoop 來處理
ChannelFuture
在 Netty 中,I/O 操作主要都是異步進行,當操作發生時,我們需要通過一種方式來知道操作在未來的時間點的執行結果。ChannelFutrue 中的 addListener () 方法,可以註冊監聽器 ChannelFutureListener,當操作完成時,監聽器可以主動通知我們。
ChannelHandler
channelHandler 主要用於應用程序中的業務邏輯的處理,網絡中的進入與出去的數據都經由它處理,當有事件發生時,channelHandler 會被觸發執行。
ChannelPipeline
ChannelPipeline 提供了一種容器,用於定義數據流入與流出過程中的處理流程。可以將 Pipeline 看作是一條流水線,原始的原料 (字節流) 進來,經過加工,最後輸出。
Bootstrapping
主要用於配置服務端或客戶端的 Netty 程序的啓動信息。
ByteBuf
字節數據容器,提供比 Java NIO ByteBuffer 更好的的 API。