Netty 框架學習 —— 第一個 Netty 應用

低吟不作語發表於2021-05-05

概述

在本文,我們將編寫一個基於 Netty 實現的客戶端和服務端應用程式,相信通過學習該示例,一定能更全面的理解 Netty API

該圖展示的是多個客戶端同時連線到一臺伺服器。客戶端建立一個連線後,會向伺服器傳送一個或多個訊息,反過來,伺服器又會將每個訊息回送給客戶端


編寫 Echo 伺服器

所有 Netty 伺服器都需要以下兩部分:

  • 至少一個 CHannelHandler

    該元件實現了伺服器對從客戶端接收的資料的處理,即它的業務邏輯

  • 引導

    配置伺服器的啟動程式碼,將伺服器繫結到它要監聽連線請求的埠上

1. ChannelHandler 和業務邏輯

ChannelHandler 是一個介面族的父介面,它的實現負責接收並響應事件通知,即要包含資料的處理邏輯。我們的 Echo 伺服器需要響應傳入的訊息,所以需要實現 ChannelHandler 介面,用來定義響應入站事件的方法,又因為只需要用到少量的方法,所以繼承 ChannelHandlerAdapter 類就足夠了,它提供了 ChannelHandler 的預設實現

我們感興趣的方法有:

  • channelRead()

    對於每個傳入的訊息都要呼叫

  • channelReadComplete()

    通知 ChannelHandler 最後一次對 channelRead() 的呼叫是當前批量讀取的最後一條訊息

  • exceptionCaught

    在讀取操作期間,有異常丟擲時會呼叫

Echo 伺服器的 ChannelHandler 實現 EchoServerHandler 如下

@ChannelHandler.Sharable // 標識一個 ChannelHandler 可以被多個 Channel 安全的共享
public class EchoServerHandler extends ChannelHandlerAdapter {

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        ByteBuf in = (ByteBuf) msg;
        System.out.println("Server receiver: " + in.toString(CharsetUtil.UTF_8));
        // 將接收到的訊息寫給傳送者
        ctx.write(in);
    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) {
        // 將剩餘的訊息全部沖刷到遠端結點,並關閉 CHannel
        ctx.writeAndFlush(Unpooled.EMPTY_BUFFER)
                .addListener(ChannelFutureListener.CLOSE);
    }

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

應用程式通過實現或者擴充套件 ChannelHandler 來掛鉤到事件的生命週期,並且提供自定義的應用程式邏輯。ChannelHandler 有助於保持業務邏輯與網路處理程式碼的分離,簡化了開發過程

2. 引導伺服器

編寫完 EchoServerHandler 實現的核心業務邏輯之後,我們現在探討引導伺服器的過程,具體涉及內容如下:

  • 繫結伺服器將在其上監聽並接收傳入連線請求的介面
  • 配置 Channel,將入站訊息交給 EchoServerHandler 例項

EchoServer 類完整程式碼如下

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 group = new NioEventLoopGroup();
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(group)
                    // 指定所使用的 NIO 傳輸 Channel
                    .channel(NioServerSocketChannel.class)
                    // 使用指定的埠設定套接字地址
                    .localAddress(new InetSocketAddress(port))
                    // 新增一個 EchoServerHandler 到子 Handler 的 ChannelPipeline
                    .childHandler(new ChannelInitializer<>() {

                        @Override
                        protected void initChannel(Channel ch) {
                            ch.pipeline().addLast(serverHandler);
                        }
                    });
            // 非同步地繫結伺服器,呼叫 sync() 方法阻塞等待直到繫結完成
            ChannelFuture f = b.bind().sync();
            // 獲取 Channel 的 CloseFuture,並且阻塞當前執行緒直到它完成
            f.channel().closeFuture().sync();
        } finally {
            // 關閉 EventLoopGroup 釋放所有資源
            group.shutdownGracefully().sync();
        }
    }
}

到此為止,我們回顧一下伺服器實現中的幾個重要步驟:

  • EchoServerHandler 實現業務邏輯
  • main() 方法引導伺服器

引導伺服器過程的重要步驟如下:

  • 建立一個 ServerBootstrap 的例項以引導和繫結伺服器
  • 建立並分配一個 NioEventLoopGroup 例項以進行事件的處理,如接受新連線以及讀寫資料
  • 指定伺服器繫結的本地的 InetSocketAddress
  • 使用一個 EchoServerHandler 例項初始化每一個新的 Channel
  • 呼叫 ServerBootstrap.bind() 方法以繫結伺服器

編寫 Echo 客戶端

Echo 客戶端的作用:

  • 連線到伺服器
  • 傳送一個或多個訊息
  • 對於每個訊息,等待並接收從伺服器返回的響應
  • 關閉連線

和伺服器一樣,編寫客戶端所涉及的主要程式碼部分也是業務邏輯和引導

1. ChannelHandler 和客戶端邏輯

客戶端也要有一個用來處理資料的 ChannelHandler,這裡選擇 SimpleChannelInboundHandler 類處理所有必需的任務,要求重寫下面的方法:

  • channelActive()

    當與伺服器的連線建立之後被呼叫

  • messageReceived()

    當從伺服器接收到一條訊息時被呼叫

  • exceptionCaught()

    在處理過程中引發異常時被呼叫

@ChannelHandler.Sharable
public class EchoClientHandler extends SimpleChannelInboundHandler<ByteBuf> {

    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        // 當一個連線建立時被呼叫,傳送一條訊息
        ctx.writeAndFlush(Unpooled.copiedBuffer("Netty rocks!", CharsetUtil.UTF_8));
    }

    @Override
    protected void messageReceived(ChannelHandlerContext ctx, ByteBuf msg) {
        // 記錄已接收訊息的轉儲
        System.out.println("Client received: " + msg.toString(CharsetUtil.UTF_8));
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        // 發生異常時,記錄錯誤並關閉 Channel
        cause.printStackTrace();
        ctx.close();
    }
}

2. 引導客戶端

引導客戶端類似於伺服器,不同的是,客戶端是使用主機和埠引數來連線遠端地址

public class EchoClient {

    private final String host;
    private final int port;

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

    public static void main(String[] args) throws Exception {
        if (args.length != 1) {
            System.err.println("Usage: " + EchoClient.class.getSimpleName() + "<host> <port>");
            return;
        }
        String host = args[0];
        int port = Integer.parseInt(args[1]);
        new EchoClient(host, port).start();
    }

    public void start() throws Exception {
        EventLoopGroup group = new NioEventLoopGroup();
        try {
            // 建立 Bootstrap
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(group)
                    .channel(NioSocketChannel.class)
                    .remoteAddress(new InetSocketAddress(host, port))
                    .handler(new ChannelInitializer<SocketChannel>() {

                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ch.pipeline().addLast(new EchoClientHandler());
                        }
                    });
            // 連線到遠端節點,阻塞等待直到連線完成
            ChannelFuture future = bootstrap.connect().sync();
            // 阻塞,直到 Channel 關閉
            future.channel().closeFuture().sync();
        } finally {
            group.shutdownGracefully().sync();
        }
    }
}

到此為止,我們回顧一下客戶端實現中的幾個重要步驟:

  • 建立一個 Bootstrap 例項
  • 建立並分配一個 NioEventLoopGroup 例項以進行事件的處理,其中事件處理包括建立新的連線以及處理入站和出站資料
  • 為伺服器連線建立一個 InetSocketAddress 例項
  • 當連線建立時,一個 EchoClientHandler 例項會被安裝到該 Channel 的 ChannelPipeline 中
  • 呼叫 Bootstrap.connect() 方法連線遠端節點

執行客戶端和服務端

本文的專案使用 maven 構建,先啟動服務端並準備好接受連線。然後啟動客戶端,一旦客戶端建立連線,就會傳送訊息。伺服器接收訊息,控制檯會列印如下資訊:

Server receiver: Netty rocks!

同時將其回送給客戶端,客戶端的控制檯也會列印如下訊息,隨後退出:

Client received: Netty rocks!

相關文章