Netty 入門教程

前言

Netty是一個異步事件驅動的網絡應用程序框架,用於快速開發可維護的高性能協議服務器和客戶端。

Netty4的官方網站是:http://netty.io/

Netty 是一個廣泛使用的 Java 網絡編程框架(Netty 在 2011 年獲得了Duke's Choice Award,見https://www.java.net/dukeschoice/2011)。它活躍和成長於用戶社區,像大型公司 Facebook 和 Instagram 以及流行 開源項目如 Infinispan, HornetQ, Vert.x, Apache Cassandra 和 Elasticsearch 等,都利用其強大的對於網絡抽象的核心代碼。

  • 設計
    • 針對多種傳輸類型的統一接口 - 阻塞和非阻塞
    • 簡單但更強大的線程模型
    • 真正的無連接的數據報套接字支持
    • 鏈接邏輯支持複用
  • 易用性
    • 完善的Javadoc
    • 全面的代碼示例
  • 性能
    • 比核心的 Java API 更好的吞吐量,較低的延時
    • 資源消耗更少,這個得益於共享池和重用
    • 減少內存拷貝
  • 健壯性
    • 消除由於慢、快、或重載連接產生的OutOfMemoryError
    • 消除經常發現在 NIO 在高速網絡中的應用中的不公平讀/寫比
  • 安全
    • 完整的 SSL/ TLS 和 StartTLS 的支持
    • 運行在受限的環境例如 Applet 或 OSGI
  • 社區
    • 社區完善、更新/發佈頻繁

背景1 - Reactor模型

wiki:

The reactor design pattern is an event handling pattern for handling service requests delivered concurrently to a service handler by one or more inputs. The service handler then demultiplexes the incoming requests and dispatches them synchronously to the associated request handlers.

幾個關鍵點:

  • 事件驅動(event handling)
  • 可以處理一個或多個輸入源(one or more inputs)
  • 通過Service Handler同步的將輸入事件(Event)採用多路複用分發給相應的Request Handler(多個)處理

更多參考: https://my.oschina.net/u/1859679/blog/1844109

背景2 - Java網絡編程(BIO)

經典的BIO服務端:

  • 一個主線程監聽某個port,等待客戶端連接
  • 當接收到客戶端發起的連接時,創建一個新的線程去處理客戶端請求
  • 主線程重新回到監聽port,等待下一個客戶端連接

缺點:

  • 每個新的客戶端Socket連接,都需要創建一個Thread處理,將會創建大量的線程
  • 線程開銷較大,連接多時,內存耗費大,CPU上下文切換開銷也大

背景3 - Java NIO

Java NIO 由以下幾個核心部分組成:

  • Channels
  • Buffers
  • Selectors

傳統IO基於字節流和字符流進行操作,而NIO基於Channel和Buffer(緩衝區)進行操作,數據總是從通道讀取到緩衝區中,或者從緩衝區寫入到通道中。Selector(選擇區)用於監聽多個通道的事件(比如:連接打開,數據到達)。因此,單個線程可以監聽多個數據通道。

NIO和傳統IO(一下簡稱IO)之間第一個最大的區別是,IO是面向流的,NIO是面向緩衝區的。

更多「NIO相關基礎篇」參考: https://mp.weixin.qq.com/s?__biz=MzU0MzQ5MDA0Mw==&mid=2247483907&idx=1&sn=3d5e1384a36bd59f5fd14135067af1c2&chksm=fb0be897cc7c61815a6a1c3181f3ba3507b199fd7a8c9025e9d8f67b5e9783bc0f0fe1c73903&scene=21#wechat_redirect

以及帶你用生活大白話理解 NIO

Netty的重要組件

下面枚舉所有的Netty應用程序的基本構建模塊,包括客戶端和服務端。

BOOTSTRAP

Netty 應用程序通過設置bootstrap(引導)類的開始,該類提供了一個用於應用程序網絡配置的容器。Netty有兩種類型的引導: 客戶端(Bootstrap)和服務端(ServerBootstrap)

CHANNEL

底層網絡傳輸API必須提供給應用I/O操作的接口,傳入(入站)或者傳出(出站)數據的載體,如讀,寫,連接,綁定等等。對於我們來說,這結構幾乎總是會成爲一個"socket"。

CHANNELHANDLER

ChannelHandler 支持很多協議,並且提供用於數據處理的容器。我們已經知道ChannelHandler由特定事件觸發。ChannelHandler可專用於幾乎所有的動作,包括一個對象轉爲字節,執行過程中拋出的異常處理。

常用的一個接口是 ChannelInboundHandler,這個類型接收到入站事件(包括接收到的數據)可以處理應用程序邏輯。
當你需要提供相應時,你也可以從ChannelInboundHandler沖刷數據。一句話,業務邏輯經常存活於一個或者多個ChannelInboundHandler。

CHANNELPIPELINE

ChannelPipline提供了一個容器給 ChannelHandler鏈並提供了一個API用於管理沿着鏈入站和出站事件的流動。每個Channel都有自己的ChannelPipeline,當Channel創建時自動創建的。

EVENTLOOP

EventLoop 用於處理 Channel 的 I/O 操作,控制流、多線程和併發。一個單一的 EventLoop通常會處理多個 Channel 事件。一個 EventLoopGroup 可以含有多於一個的 EventLoop 和 提供了一種迭代用於檢索清單中的下一個。

CHANNELFUTURE

Netty 所有的 I/O 操作都是異步。因爲一個操作可能無法立即返回,我們需要有一種方法在以後確定它的結果。
出於這個目的,Netty 提供了接口 ChannelFuture,它的 addListener 方法註冊了一個 ChannelFutureListener ,當操作完成時,可以被異步通知(不管成功與否)。

以上組件的關係:

[站外圖片上傳中...(image-67dbed-1563459279939)]

幾點重要的約定:

  • 一個EventLoopGroup包含一個或多個EventLoop
  • 一個EventLoop在其生命週期內只能和一個Thread綁定
  • EventLoop處理的I/O事件都由它綁定的Thread處理
  • 一個Channel在其生命週期內,只能註冊於一個EventLoop
  • 一個EventLoop可能被分配處理多個Channel。也就是EventLoop與Channel是1:n的關係
  • 一個Channel上的所有ChannelHandler的事件由綁定的EventLoop中的I/O線程處理
  • 不要阻塞Channel的I/O線程,可能會影響該EventLoop中其他Channel事件處理

第一個 Netty 應用: Echo client / server

本應用的源碼請見 netty倉庫中的example目錄。

接下來,我們來構建一個完整的Netty客戶端和服務器,更完整地瞭解Netty的API是如何實現客戶端和服務器的。

先來看看 Netty 應用 - Echo client/server 總覽:

[站外圖片上傳中...(image-5996c5-1563459279939)]

echo應用的客服端和服務器的交互很簡單: 客戶端啓動後,建立一個連接併發送一個或多個消息到服務端,服務端接受到的每個消息再返回給客戶端。

服務端代碼

  • 一個信息處理器(handler): 這個實現是服務端的業務邏輯部分,當連接創建後和接收信息後的處理類。
  • 服務器: 主要通過ServerBootstrap設置服務器的監聽端口等啓動部分。
EchoServerHandler

通過繼承ChannelInboundHandlerAdapter,這個類提供了默認的ChannelInboundHandler實現,只需覆蓋以下的方法:

  • channelRead() - 每個消息入站都會調用
  • channelReadComplete() - 通知處理器最後的channelRead()是當前批處理中的最後一條消息時調用
  • exceptionCaught() - 捕獲到異常時調用
@ChannelHandler.Sharable // 標識這類的實例之間可以在 channel 裏面共享
public class EchoServerHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        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 {
        ctx.writeAndFlush(Unpooled.EMPTY_BUFFER) // 沖刷所有待審消息到遠程節點。關閉通道後,操作完成
            .addListener(ChannelFutureListener.CLOSE);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
}
EchoServer

創建ServerBootstrap實例來引導服務器,本服務端分配了一個NioEventLoopGroup實例來處理事件的處理,如接受新的連接和讀/寫數據,然後綁定本地端口,分配EchoServerHandler實例給Channel,這樣服務器初始化完成,可以使用了。

public class EchoServer {

    private final int port;

    public EchoServer(int port) {
        this.port = port;
    }

    public void start() throws Exception {
        NioEventLoopGroup group = new NioEventLoopGroup(); // 創建 EventLoopGroup

        try {
            ServerBootstrap bootstrap = new ServerBootstrap(); // 創建 ServerBootstrap
            bootstrap.group(group)
                    .channel(NioServerSocketChannel.class) // 指定使用 NIO 的傳輸 Channel
                    .localAddress(new InetSocketAddress(port)) // 設置 socket 地址使用所選的端口
                    .childHandler(new ChannelInitializer<SocketChannel>() { // 添加 EchoServerHandler 到 Channel 的 ChannelPipeline
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            socketChannel.pipeline().addLast(new EchoServerHandler());
                        }
                    });

            ChannelFuture future = bootstrap.bind().sync(); // 綁定的服務器;sync 等待服務器關閉
            System.out.println(EchoServer.class.getName() + " started and listen on " + future.channel().localAddress());
            future.channel().closeFuture().sync(); // 關閉 channel 和 塊,直到它被關閉
        } finally {
            group.shutdownGracefully().sync(); // 關閉 EventLoopGroup,釋放所有資源。
        }
    }

    public static void main(String[] args) throws Exception {
        int port = 4567;
        if (args.length == 1) {
            port = Integer.parseInt(args[0]);
        }
        new EchoServer(port).start(); // 設計端口、啓動服務器
    }
}

客戶端代碼

客戶端要做的是:

  • 連接服務器
  • 發送消息
  • 等待和接受服務器返回的消息
  • 關閉連接
EchoClientHandler

繼承SimpleChannelInboundHandler來處理所有的事情,只需覆蓋三個方法:

  • channelActive() - 服務器的連接被建立後調用
  • channelRead0() - 從服務器端接受到消息調用
  • exceptionCaught() - 捕獲異常處理調用
@ChannelHandler.Sharable // @Sharable 標記這個類的實例可以在channel裏共享
public class EchoClientHandler extends SimpleChannelInboundHandler<ByteBuf> {
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        ctx.writeAndFlush(Unpooled.copiedBuffer("Netty rocks!", CharsetUtil.UTF_8)); // 當被通知該 channel 是活動的時候就發送信息
    }

    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf) throws Exception {
        System.out.println("Client received: " + byteBuf.toString(CharsetUtil.UTF_8)); // 記錄接收到的消息
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        // 記錄日誌錯誤並關閉 channel
        cause.printStackTrace();
        ctx.close();
    }
}
EchoClient

通過Bootstrap引導創建客戶端,另外需要 host 、port 兩個參數連接服務器。

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 = new Bootstrap(); // 創建 Bootstrap
            bootstrap.group(group) // 指定EventLoopGroup來處理客戶端事件。由於我們使用NIO傳輸,所以用到了 NioEventLoopGroup 的實現
                    .channel(NioSocketChannel.class) // 使用的channel類型是一個用於NIO傳輸
                    .remoteAddress(new InetSocketAddress(host, port)) // 設置服務器的InetSocketAddr
                    .handler(new ChannelInitializer<SocketChannel>() { // 當建立一個連接和一個新的通道時。創建添加到EchoClientHandler實例到 channel pipeline
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            socketChannel.pipeline().addLast(new EchoClientHandler());
                        }
                    });

            ChannelFuture future = bootstrap.connect().sync(); // 連接到遠程;等待連接完成

            future.channel().closeFuture().sync(); // 阻塞到遠程; 等待連接完成
        } finally {
            group.shutdownGracefully().sync(); // 關閉線程池和釋放所有資源
        }
    }

    public static void main(String[] args) throws Exception {
        final String host = "127.0.0.1";
        final int port = 4567;
        new EchoClient(host, port).start();
    }
}

編譯和運行 Echo

首先編譯、運行服務端,會看到以下log:

me.icro.samples.echo.server.EchoServer started and listen on /0:0:0:0:0:0:0:0:4567

下一步是編譯、運行客服端後,服務端會先接收到信息:

Server received: Netty rocks!

然後客戶端收到反饋:

Client received: Netty rocks!

總結

以上,構建並運行你的第一 個Netty 的客戶端和服務器。雖然這是一個簡單的應用程序,它可以擴展到幾千個併發連接。

我們可以在Netty的Github倉庫看到的更多 Netty 如何簡化可擴展和多線程的例子。

下一步的深入學習,網上教程很多,大夥可以參考:

(完)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章