Netty基礎組件

之前的文章Netty基礎篇:Netty是什麼?介紹了傳統IO編程存在的問題,及Java NIO編程在解決IO編程的問題中的侷限性,由此引出IO編程問題的理想解決方案——Netty。在上篇文章中簡單展示了Netty的基本使用,本篇文章通過一個Netty服務端的demo來了解一下Netty的基本組件。

1. Netty服務端

public class EchoServer {
    private final int port;

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

    public static void main(String[] args) throws InterruptedException {
        new EchoServer(8888).start();
    }

    public void start() throws InterruptedException {
        //創建EventLoopGroup,處理事件
        EventLoopGroup boss = new NioEventLoopGroup();
        EventLoopGroup worker = new NioEventLoopGroup();
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(boss, worker)
                    //指定所使用的NIO傳輸 Channel
                    .channel(NioServerSocketChannel.class)
                    //使用指定的端口設置套接字地址
                    .localAddress(new InetSocketAddress(port))
                    //設置ChannelHandler
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel) {
                            ChannelPipeline channelPipeline = socketChannel.pipeline();
                            channelPipeline.addLast(new HttpServerCodec());
                            channelPipeline.addLast(new HttpObjectAggregator(10 * 1024 * 1024));
                            channelPipeline.addLast(new NettyServerHandler());
                        }
                    });
            //異步的綁定服務器,調用sync()方法阻塞等待直到綁定完成
            ChannelFuture future = b.bind().sync();
            future.channel().closeFuture().sync();
        } finally {
            //關閉EventLoopGroup,釋放所有的資源
            boss.shutdownGracefully().sync();
            worker.shutdownGracefully().sync();
        }
    }
}
public class NettyServerHandler extends ChannelInboundHandlerAdapter {

    /**
     * 收到客戶端請求,返回信息
     * @param ctx
     * @param msg
     */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        String result;
        if (!(msg instanceof FullHttpRequest)) {
            result = "未知請求!";
            send(ctx, result, HttpResponseStatus.BAD_REQUEST);
            return;
        }
        FullHttpRequest httpRequest = (FullHttpRequest) msg;
        try {
            String path = httpRequest.uri();            //獲取路徑
            String body = getBody(httpRequest);    //獲取參數
            HttpMethod method = httpRequest.method();//獲取請求方法
            System.out.println("接收到:" + method + " 請求");

            //如果是GET請求
            if (HttpMethod.GET.equals(method)) {
                System.out.println("body:" + body);
                result = "GET請求";
                send(ctx, result, HttpResponseStatus.OK);
                return;
            }
            //如果是POST請求
            if (HttpMethod.POST.equals(method)) {
                System.out.println("body:" + body);
                result = "POST請求";
                send(ctx, result, HttpResponseStatus.OK);
                return;
            }

            //如果是PUT請求
            if (HttpMethod.PUT.equals(method)) {
                System.out.println("body:" + body);
                result = "PUT請求";
                send(ctx, result, HttpResponseStatus.OK);
                return;
            }
            //如果是DELETE請求
            if (HttpMethod.DELETE.equals(method)) {
                System.out.println("body:" + body);
                result = "DELETE請求";
                send(ctx, result, HttpResponseStatus.OK);
            }
        } catch (Exception e) {
            System.out.println("處理請求失敗!");
            e.printStackTrace();
        } finally {
            //釋放請求
            httpRequest.release();
        }
    }

    /**
     * 獲取body參數
     *
     * @param request
     * @return
     */
    private String getBody(FullHttpRequest request) {
        ByteBuf buf = request.content();
        return buf.toString(CharsetUtil.UTF_8);
    }

    /**
     * 發送的返回值
     *
     * @param ctx     返回
     * @param context 消息
     * @param status  狀態
     */
    private void send(ChannelHandlerContext ctx, String context, HttpResponseStatus status) {
        FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status, Unpooled.copiedBuffer(context, CharsetUtil.UTF_8));
        response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=UTF-8");
        ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
    }

    /**
     * 建立連接時,返回消息
     * @param ctx
     * @throws Exception
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("連接的客戶端地址:" + ctx.channel().remoteAddress());
        ctx.writeAndFlush("客戶端" + InetAddress.getLocalHost().getHostName() + "成功與服務端建立連接! ");
        super.channelActive(ctx);
    }
}

當啓動main方法,瀏覽器訪問127.0.0.1:8888/hello,可以在瀏覽器獲取響應結果“GET請求”,服務端可以看到請求日誌:

接收到:GET請求
body:

說明Netty實現了服務端並正常運行,下面我們結合上面的demo來看一下Netty的基礎組件。

2. Netty基礎組件

根據demo,我們可以總結出Netty服務端啓動流程:

  1. 創建 ServerBootStrap實例
  2. 設置EventLoopGroup線程池
  3. 通過ServerBootStrap的channel方法設置Channel類型,會在bind方法調用後根據該類型初始化Channel
  4. 綁定Socket訪問地址
  5. 設置ChannelHandler(通過ChannelPipeline將ChannelHandler組織爲一個邏輯鏈)
  6. 調用bind方法啓動服務端

該流程主要涉及如下圖所示的幾個Netty組件:

2.1 BootStrap

BootStrap是Netty提供的啓動輔助類,幫助Netty客戶端或服務端的Netty初始化,客戶端對應的是Bootstrap類,服務端對應的是 ServerBootStrap引導類。

2.2 Channel

Channel是Netty中的網絡操作抽象類,對應JDK底層的Socket,它除了包含基本的I/O操作,如 bind()、connect()、read()、write()之外,還包括了Netty框架相關的一些功能,如獲取 Channel的EventLoop。

2.3 EventLoop & EventLoopGroup

EventLoop定義了Netty的核心抽象,用於處理連接的生命週期中所發生的事件。EventLoop 爲Channel處理I/O操作,下圖是 Channel,EventLoop,Thread以及EventLoopGroup之間的關係(摘自《Netty In Action》):

它們之間的關係是:

  • 一個EventLoopGroup 包含一個或者多個EventLoop
  • 一個 EventLoop 在它的生命週期內只和一個Thread綁定
  • 所有由 EventLoop處理的 I/O事件都將在它專有的Thread上被處理
  • 一個 Channel 在它的生命週期內只註冊一個EventLoop
  • 一個 EventLoop 可能會被分配給一個或多個 Channel

EventLoopGroup實際上就是處理I/O操作的線程池,負責爲每個新註冊的Channel分配一個EventLoop,Channel在整個生命週期都有其綁定的 EventLoop來服務。

而上面服務端用的 NioEventLoop 就是 EventLoop的一個重要實現類,NioEventLoop 是Netty內部的I/O線程,而 NioEventLoopGroup是擁有 NioEventLoop的線程池,在Netty服務端中一般存在兩個這樣的NioEventLoopGroup線程池,一個 “Boss” 線程池,用於接收客戶端連接,實際上該線程池中只有一個線程,一個 “Worker”線程池用於處理每個連接的讀寫。而Netty客戶端只需一個線程池即可,主要用於處理連接中的讀寫操作。

2.4 ChannelHandler

ChannelHandler主要用於對出站和入站數據進行處理,它有兩個重要的子接口:

  • ChannelInboundHandler——處理入站數據
  • ChannelOutboundHandler——處理出站數據

2.5 ChannelPipeline

ChannelPipeline是ChannelHandler的容器,通過ChannelPipeline可以將ChannelHandler組織成一個邏輯鏈,該邏輯鏈可以用來攔截流經Channel的入站和出站事件,當 Channel被創建時,它會被自動地分配到它的專屬的 ChannelPipeline。

當一個消息或者任何其他的入站事件被讀取時,那麼它會從 ChannelPipeline的頭部開始流動,並被傳遞給第一個 ChannelInboundHandler,第一個處理完成之後傳遞給下一個 ChannelInboundHandler,一直到ChannelPipeline的尾端,與之對應的是,當數據被寫出時,數據從 ChannelOutboundHandler 鏈的尾端開始流動,直到它到達鏈的頭部爲止。

2.6 ChannelOption

ChannelOption用於對Channel設置TCP層面通用參數,比如TCP長連接設置,比如可以通過如下代碼實現TCP層面的keepAlive機制:

//設置TCP的長連接,默認的keepAlive的心跳時間是兩個小時
.option(ChannelOption.SO_KEEPALIVE, true)

2.7 ChannelFuture

ChannelFuture用於獲取異步IO的處理結果,其 addListener()方法註冊了一個 ChannelFutureListener,以便在某個操作完成時(無論是否成功)得到通知。比如可以通過如下方式實現Netty客戶端斷線重連:

//客戶端斷線重連邏輯
ChannelFuture future = bootstrap.connect();
future.addListener((ChannelFutureListener) future1 -> {
    if (future1.isSuccess()) {
        log.info("連接Netty服務端成功");
    } else {
        log.info("連接失敗,進行斷線重連");
        future1.channel().eventLoop().schedule(() -> start(), 20, TimeUnit.SECONDS);
    }
});
socketChannel = (SocketChannel) future.channel();

2.8 ByteBuf

在之前的文章中提到,Java IO編程使用字節流來處理輸入輸出,效率較差,Java NIO中使用內存塊爲單位進行數據處理。而ByteBuf就是字節緩衝區,用於高效處理輸入輸出。

參考鏈接:

1. Netty 系列文章之基本組件概覽

2. Netty中的ChannelOption

3. Netty4 學習筆記之四- Netty HTTP服務的實現

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