Netty 入門教程

前言

Netty是一個異步事件驅(qū)動的網(wǎng)絡(luò)應(yīng)用程序框架,用于快速開發(fā)可維護(hù)的高性能協(xié)議服務(wù)器和客戶端。

Netty4的官方網(wǎng)站是:http://netty.io/

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

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

背景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.

幾個關(guān)鍵點:

  • 事件驅(qū)動(event handling)
  • 可以處理一個或多個輸入源(one or more inputs)
  • 通過Service Handler同步的將輸入事件(Event)采用多路復(fù)用分發(fā)給相應(yīng)的Request Handler(多個)處理

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

背景2 - Java網(wǎng)絡(luò)編程(BIO)

經(jīng)典的BIO服務(wù)端:

  • 一個主線程監(jiān)聽某個port,等待客戶端連接
  • 當(dāng)接收到客戶端發(fā)起的連接時,創(chuàng)建一個新的線程去處理客戶端請求
  • 主線程重新回到監(jiān)聽port,等待下一個客戶端連接

缺點:

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

背景3 - Java NIO

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

  • Channels
  • Buffers
  • Selectors

傳統(tǒng)IO基于字節(jié)流和字符流進(jìn)行操作,而NIO基于Channel和Buffer(緩沖區(qū))進(jìn)行操作,數(shù)據(jù)總是從通道讀取到緩沖區(qū)中,或者從緩沖區(qū)寫入到通道中。Selector(選擇區(qū))用于監(jiān)聽多個通道的事件(比如:連接打開,數(shù)據(jù)到達(dá))。因此,單個線程可以監(jiān)聽多個數(shù)據(jù)通道。

NIO和傳統(tǒng)IO(一下簡稱IO)之間第一個最大的區(qū)別是,IO是面向流的,NIO是面向緩沖區(qū)的。

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

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

Netty的重要組件

下面枚舉所有的Netty應(yīng)用程序的基本構(gòu)建模塊,包括客戶端和服務(wù)端。

BOOTSTRAP

Netty 應(yīng)用程序通過設(shè)置bootstrap(引導(dǎo))類的開始,該類提供了一個用于應(yīng)用程序網(wǎng)絡(luò)配置的容器。Netty有兩種類型的引導(dǎo): 客戶端(Bootstrap)和服務(wù)端(ServerBootstrap)

CHANNEL

底層網(wǎng)絡(luò)傳輸API必須提供給應(yīng)用I/O操作的接口,傳入(入站)或者傳出(出站)數(shù)據(jù)的載體,如讀,寫,連接,綁定等等。對于我們來說,這結(jié)構(gòu)幾乎總是會成為一個"socket"。

CHANNELHANDLER

ChannelHandler 支持很多協(xié)議,并且提供用于數(shù)據(jù)處理的容器。我們已經(jīng)知道ChannelHandler由特定事件觸發(fā)。ChannelHandler可專用于幾乎所有的動作,包括一個對象轉(zhuǎn)為字節(jié),執(zhí)行過程中拋出的異常處理。

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

CHANNELPIPELINE

ChannelPipline提供了一個容器給 ChannelHandler鏈并提供了一個API用于管理沿著鏈入站和出站事件的流動。每個Channel都有自己的ChannelPipeline,當(dāng)Channel創(chuàng)建時自動創(chuàng)建的。

EVENTLOOP

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

CHANNELFUTURE

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

以上組件的關(guān)系:

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

幾點重要的約定:

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

第一個 Netty 應(yīng)用: Echo client / server

本應(yīng)用的源碼請見 netty倉庫中的example目錄。

接下來,我們來構(gòu)建一個完整的Netty客戶端和服務(wù)器,更完整地了解Netty的API是如何實現(xiàn)客戶端和服務(wù)器的。

先來看看 Netty 應(yīng)用 - Echo client/server 總覽:

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

echo應(yīng)用的客服端和服務(wù)器的交互很簡單: 客戶端啟動后,建立一個連接并發(fā)送一個或多個消息到服務(wù)端,服務(wù)端接受到的每個消息再返回給客戶端。

服務(wù)端代碼

  • 一個信息處理器(handler): 這個實現(xiàn)是服務(wù)端的業(yè)務(wù)邏輯部分,當(dāng)連接創(chuàng)建后和接收信息后的處理類。
  • 服務(wù)器: 主要通過ServerBootstrap設(shè)置服務(wù)器的監(jiān)聽端口等啟動部分。
EchoServerHandler

通過繼承ChannelInboundHandlerAdapter,這個類提供了默認(rèn)的ChannelInboundHandler實現(xiàn),只需覆蓋以下的方法:

  • channelRead() - 每個消息入站都會調(diào)用
  • channelReadComplete() - 通知處理器最后的channelRead()是當(dāng)前批處理中的最后一條消息時調(diào)用
  • exceptionCaught() - 捕獲到異常時調(diào)用
@ChannelHandler.Sharable // 標(biāo)識這類的實例之間可以在 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); // 將所接收的消息返回給發(fā)送者
    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        ctx.writeAndFlush(Unpooled.EMPTY_BUFFER) // 沖刷所有待審消息到遠(yuǎn)程節(jié)點。關(guān)閉通道后,操作完成
            .addListener(ChannelFutureListener.CLOSE);
    }

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

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

public class EchoServer {

    private final int port;

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

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

        try {
            ServerBootstrap bootstrap = new ServerBootstrap(); // 創(chuàng)建 ServerBootstrap
            bootstrap.group(group)
                    .channel(NioServerSocketChannel.class) // 指定使用 NIO 的傳輸 Channel
                    .localAddress(new InetSocketAddress(port)) // 設(shè)置 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(); // 綁定的服務(wù)器;sync 等待服務(wù)器關(guān)閉
            System.out.println(EchoServer.class.getName() + " started and listen on " + future.channel().localAddress());
            future.channel().closeFuture().sync(); // 關(guān)閉 channel 和 塊,直到它被關(guān)閉
        } finally {
            group.shutdownGracefully().sync(); // 關(guān)閉 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(); // 設(shè)計端口、啟動服務(wù)器
    }
}

客戶端代碼

客戶端要做的是:

  • 連接服務(wù)器
  • 發(fā)送消息
  • 等待和接受服務(wù)器返回的消息
  • 關(guān)閉連接
EchoClientHandler

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

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

    @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 {
        // 記錄日志錯誤并關(guān)閉 channel
        cause.printStackTrace();
        ctx.close();
    }
}
EchoClient

通過Bootstrap引導(dǎo)創(chuàng)建客戶端,另外需要 host 、port 兩個參數(shù)連接服務(wù)器。

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

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

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

    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

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

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

下一步是編譯、運行客服端后,服務(wù)端會先接收到信息:

Server received: Netty rocks!

然后客戶端收到反饋:

Client received: Netty rocks!

總結(jié)

以上,構(gòu)建并運行你的第一 個Netty 的客戶端和服務(wù)器。雖然這是一個簡單的應(yīng)用程序,它可以擴(kuò)展到幾千個并發(fā)連接。

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

下一步的深入學(xué)習(xí),網(wǎng)上教程很多,大伙可以參考:

(完)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容