玩Netty,從「Hello World」開始!

2023.11.21

玩Netty,從「Hello World」開始!

如果在專案的開發中,要實現穩定的網路通信,就得考慮網路的閃斷、客戶端的重複接入、客戶端的安全認證、訊息的編解碼、半包讀寫…

大家好,我是老三,之前裡,我們討論了Java的三種IO模型,提到了網路通訊框架Netty,它簡化和優化了NIO的使用,這期,我們正式開始走近Netty。

為什麼要用Netty?

首先當然是NIO的使用,本身比較複雜,而且還存在一些問題。

除此之外,如果在專案的開發中,要實現穩定的網路通信,就得考慮網路的閃斷、客戶端的重複接入、客戶端的安全認證、訊息的編解碼、半包讀寫…

所以,巧了,剛好有這麼一個成熟穩定、效能強大、開箱即用的網路框架擺在我們面前,相比較Java NIO,Netty更加出色:

  • 易用性: Netty 在NIO 基礎上進行了更高層次的封裝,屏蔽了NIO 的複雜性,大大降低了開發難度;Netty 提供了很多開箱即用的工具,例如常用的行解碼器、長度域解碼器等,不需要自己再實作。
  • 穩定性: Netty 更可靠穩定,修復並完善了JDK NIO 較多已知問題,例如臭名昭​​著的select 空轉導致CPU 消耗100%,TCP 斷線重連,keep-alive 檢測等問題。
  • 可擴充性: Netty 的可擴充性做的非常好,例如支援可客製化的執行緒模型。

我們有什麼理由拒絕這麼一款優秀的網路通訊框架呢?程式碼怎麼寫不是寫嘍!

初識Netty

什麼是Netty?

Netty官方是這麼定義Netty的:

Netty 是一個非同步事件驅動的網路應用程式框架,用於快速開發可維護的高效能協定伺服器和用戶端。

圖片

組成圖-來源官方

  • Netty是一個開源的、單執行緒模型的Java 網路程式框架。
  • Netty基於NIO ,廣泛應用於各種網路應用程式開發。
  • Netty支援多種協議,包括但不限於HTTP、WebSocket、TCP、UDP 和SSL/TLS 協定等。
  • Netty 是非阻塞的,事件驅動的框架。
  • Netty具有高性能、可擴展和易於使用的優點。

Netty的現狀?

Netty 由JBoss 社群開發和維護的,它的社群相對比較活躍:

  • https://github.com/netty/netty:Github已經收成31.2K星標
  • https://netty.io/:官方網站,提供了比較完整的文檔

目前官方最新的版本是5.x,,但是很不幸,已經被社區放棄開發維護,屬於廢棄版本,最新的穩定版本是4.x 。

一般使用,建議4.x,Netty 4.x對3.x不相容,我們後續的學習也基於Netty 4.x版本。

誰在用Netty?

作為最受歡迎的網路通訊框架,大量的公司選擇它作為底層網路通訊框架,包括不限於:

圖片

使用Netty的公司

我們可能自己沒有直接用過Netty,但其實熟悉的很多開源中間件,都用到了Netty,例如:

  • 服務治理:Apache Dubbo、gRPC。
  • 大數據:Hbase、Spark、Flink、Storm。
  • 搜尋引擎:Elasticsearch。
  • 訊息佇列:RocketMQ、ActiveMQ。

使用Netty的優秀產品非常多,大家有興趣可以看看:https://netty.io/wiki/related-projects.html。

從"Hello World"開始

氣氛襯托到這,不寫Demo也過不去,還是從"Hello World"開始,我們領略一下Netty的風采。

  1. 建立一個Maven專案:這個就不用多說了吧

圖片

建立Maven項目

  1. 導入依賴:我們直接用4.x最新的版本
<dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-all</artifactId>
            <version>4.1.92.Final</version>
        </dependency>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  1. 寫程式碼:那我們就開始寫這個Demo的伺服器和客戶端相關程式碼
  • NettyServer:基於Netty的客戶端
/**
 * <p>Date: 2023/5/14 10:29</p>
 * <p>Author: fighter3</p>
 * <p>Description: Netty服务端Demo</p>
 */
public class NettyServer{
    // 服务器监听的端口号
    private int port;

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

    /**
     * 启动Netty服务器
     * @throws InterruptedException
     */
    public void run() throws InterruptedException {
        // 创建boss线程组和worker线程组
        // bossGroup 用于监听客户端的连接请求,将连接请求发送给 workerGroup 进行处理
        NioEventLoopGroup bossGroup = new NioEventLoopGroup();
        // workerGroup 用于处理客户端连接的数据读写
        NioEventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            // 创建 ServerBootstrap 对象,用于启动 Netty 服务器
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            // 绑定线程池事件组
            serverBootstrap.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    // 通道初始化回调函数,在启动的时候可以自动调用
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        public void initChannel(SocketChannel ch) throws Exception {
                            ChannelPipeline pipeline = ch.pipeline();
                            // 添加消息处理器
                            pipeline.addLast(new NettyServerHandler());
                        }
                    });

            // 绑定端口,开始接收客户端请求
            ChannelFuture channelFuture = serverBootstrap.bind(port).sync();

            System.out.println("Netty服务器监听端口:"+port);

            // 等待服务端监听端口关闭
            channelFuture.channel().closeFuture().sync();
        } finally {
            //释放线程组资源
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        // 创建服务器对象,监听端口号为 8888
        NettyServer server = new NettyServer(8888);
        System.out.println("============Netty服务器启动...=============");
        // 启动服务器
        server.run();
        System.out.println("============Netty服务器停止...=============");
    }
}
  • 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.
  • NettyServerHandler:伺服器的訊息處理器,用於處理各種事件
/**
 * <p>Date: 2023/5/14 10:30</p>
 * <p>Author: fighter3</p>
 * <p>Description: Netty服务器消息处理器</p>
 */
public class NettyServerHandler extends ChannelInboundHandlerAdapter {

    /**
     * 当客户端上线的时候会触发这个方法
     * @param ctx
     * @throws Exception
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        String message="你好,靓仔!";
        ByteBuf hello = Unpooled.copiedBuffer(message, CharsetUtil.UTF_8);
        // 发送消息
        ctx.writeAndFlush(hello);
    }

    /**
     *当 Channel 中有来自客户端的数据时就会触发这个方法
     */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf buf = (ByteBuf) msg;
        System.out.println("客户端发来的消息:" + buf.toString(CharsetUtil.UTF_8)); // 接收消息并打印输出
    }

    /**
     * 当有异常时触发这个方法
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
}
  • 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.
  • NettyClient:使用Netty的客戶端,透過ip和連接埠連接服務端
/**
 * <p>Date: 2023/5/14 10:32</p>
 * <p>Author: fighter3</p>
 * <p>Description: Netty客户端Demo</p>
 */
public class NettyClient {
    // 服务器 IP
    private String host;
    // 服务器监听的端口号
    private int port;

    public NettyClient(String host, int port) {
        this.host = host;
        this.port = port;
    }

    /**
     * 启动 Netty 客户端
     */
    public void run() throws InterruptedException {
        // 创建事件循环组
        NioEventLoopGroup group = new NioEventLoopGroup();
        try {
            // 创建 Bootstrap 对象
            Bootstrap bootstrap = new Bootstrap();
            // 配置 Bootstrap 对象
            // 设置线程组
            bootstrap.group(group)
                    // 设置客户端通信的通道类型为NIO类型
                    .channel(NioSocketChannel.class)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        // 通道初始化回调函数,在启动的时候可以自动调用
                        @Override
                        public void initChannel(SocketChannel ch) throws Exception {
                            // 添加消息处理器
                            ch.pipeline().addLast(new NettyClientHandler());
                        }
                    });
            // 连接服务器,异步等待连接成功
            ChannelFuture channelFuture = bootstrap.connect(host, port).sync();
            System.out.println("===========Netty客户端连接服务端=========");

            // 等待客户端连接关闭
            channelFuture.channel().closeFuture().sync();
        } finally {
            //释放资源
            group.shutdownGracefully();
        }
    }

    public static void main(String[] args) throws InterruptedException {
       // 创建客户端对象,并连接到服务器
        NettyClient client = new NettyClient("127.0.0.1", 8888);
        // 启动客户端,开始发送消息
        client.run();
    }
}
  • 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.
  • NettyClientHandler:Netty客戶端處理器,用於處各種事件
/**
 * <p>Date: 2023/5/14 10:33</p>
 * <p>Author: fighter3</p>
 * <p>Description: Netty客户端处理器</p>
 */
public class NettyClientHandler extends ChannelInboundHandlerAdapter {

    /**
     * 当 Channel 准备就绪时就会触发这个方法
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        String message="大佬,带带我!";
        ByteBuf hello = Unpooled.copiedBuffer(message, CharsetUtil.UTF_8);
        // 发送消息
        ctx.writeAndFlush(hello);
    }

    /**
     * 当 Channel 中有来自服务器的数据时就会触发这个方法
     */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf buf = (ByteBuf) msg;
        System.out.println("服务端发来的消息:" + buf.toString(CharsetUtil.UTF_8)); // 接收消息并打印输出
    }

    /**
     * 发生异常就会触发这个方法
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
}
  • 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.
  1. 運行一下:先啟動NettyServer,再啟動NettyClient,看看運行結果
============Netty服务器启动...=============
Netty服务器监听端口:8888
客户端发来的消息:大佬,带带我!
  • 1.
  • 2.
  • 3.
===========Netty客户端连接服务端=========
服务端发来的消息:你好,靓仔!
  • 1.
  • 2.

好了,一個簡單的Netty入門Demo就寫完了,Netty是一個雙工通訊的網路框架,可以看到,服務端和客戶端,流程基本上一致,主要包括這麼多步驟:

  1. 建立事件循環群組和相關對象,用於監聽和處理網路事件;
  2. 設定Netty 伺服器或用戶端的啟動參數,包括執行緒群組、通道類型、TCP 參數等;
  3. 為伺服器或客戶端的ChannelPipeline 新增各種ChannelHandler,用於處理不同的網路事件;
  4. 綁定連接埠啟動伺服器或連接伺服器;
  5. 等待伺服器或客戶端連線關閉,釋放相關資源。

圖片

伺服器&客戶端初始化啟動流程

雖然這個Demo比較簡單,但其實已經用到了Netty裡幾個比較關鍵的組件:

  1. ByteBuf:Netty 的位元組容器,類似於Java 的ByteBuffer,但是提供了更強大、簡單且安全的API,用於在網路中傳遞二進位資料;
  2. EventLoopGroup:Netty 的事件循環群組,用於管理和調度連接到伺服器或從伺服器連接出去的所有Channel 上的事件循環;
  3. ServerBootstrap:Netty 的伺服器啟動類,用於啟動和設定一個TCP/IP 伺服器;
  4. Bootstrap:Netty 的客戶端啟動類,用於啟動和配置一個TCP/IP 用戶端;
  5. Channel:Netty 的核心概念,用來表示一個通訊通道,可以讀取和寫入資料;
  6. ChannelPipeline:Netty 的Channel 處理器,用於在傳入的資料上執行一組ChannelHandler;
  7. ChannelHandler:Netty 的核心元件,用於處理各種通訊事件,例如讀取資料、寫入資料、建立連線等;

圖片

Netty的重要元件

後續,我們也會和這些組件打更多的交道。

好了,那麼這期內容就到這了,這期裡我們初步了解了Netty,包括什麼是Netty、Netty現狀、Netty的應用,還寫了一個簡單的Demo。下一期,我們繼續深入了解Netty,敬請期待。

參考:

[1].https://netty.io/

[2].《Netty權威指南》

[3]. 《Netty核心原理剖析與RPC實務》