【轉】【Netty】Netty 核心組件介紹

前面,我們學習了 Netty 的基本 原理和架構 ,今天我們來大致瞭解一下 Netty 中的各個組件。

同我們 前面 學習 IO 與 NIO 一樣的套路,我們先通過 echo 服務 demo 來學習 netty 的使用。

Netty%20Echo%20Service.pnguploading.4e448015.gif轉存失敗重新上傳取消Netty Echo Service

開發環境

編寫 Echo Server 代碼

Netty 服務端的開發主要有以下兩個步驟:

  • 至少有一個 ChannelHandler —— 這個主要用於處理從 client 端接受到的信息,是主要的業務邏輯處理類。
  • Bootstrapping —— 用於配置服務的啓動代碼。最簡單的就是,監聽一個端口。

實現 EchoServerHandler 邏輯

服務端用於處理入站的網絡請求,因此我們需要實現接口類 ChannelInboundHandler,它裏面定義了用於

處理入站請求的一些接口。由於我們這個例子比較簡單,只需要用到它的幾個方法即可,因此我們的實現類只需要繼承子類 ChannelInboundHandlerAdapter 即可,它默認實現了 ChannelInboundHandler 中的接口。

有幾個方法需要留意一下:

  • channelRead() —— 每當有入站請求來臨時,該方法都會被調用
  • channelReadComplete() —— 對 channelRead () 的最後一次調用是當前批處理中的最後一條消息時,該方法會被調用
  • exceptionCaught() —— 在 read 操作執行期間,如果發生異常,該方法則會被調用。

代碼如下:

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
// @Sharable象徵着該ChannelHandler實例在多個channels之間可以被安全地分享

@Sharable
public class EchoServerHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        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 {
        // 將待處理的消息沖刷到遠程節點上,並關閉Channel
        ctx.writeAndFlush(Unpooled.EMPTY_BUFFER)
                .addListener(ChannelFutureListener.CLOSE);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        // 打印堆棧信息
        cause.printStackTrace();
        // 關閉channel
        ctx.close();
    }
}

實現 EchoServer 邏輯

接下來,我們使用 ServerBootstrap 來實現服務端的開發,主要以下兩點:

  • 綁定一個監聽端口
  • 配置 Channels,當有入站消息到達時,通知 EchoServerHeadler 實例。
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
package nia.chapter2.echoserver;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;

import java.net.InetSocketAddress;

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 實例
        EventLoopGroup group = new NioEventLoopGroup();
        try {
            // 創建 ServerBootstrap 實例
            ServerBootstrap b = new ServerBootstrap();
            b.group(group)
                	// 執行Channel的類型爲:NioServerSocketChannel
                    .channel(NioServerSocketChannel.class)
                	// 綁定端口
                    .localAddress(new InetSocketAddress(port))
                	// 將EchoServerHandler添加到ChannelPipeline中去
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        public void initChannel(SocketChannel ch) throws Exception {
                            // EchoServerHandler有註解 @Sharable,因此我們總是可以使用改實例
                            ch.pipeline().addLast(serverHandler);
                        }
                    });
			// 異步綁定服務,sync() 用於等待綁定完成
            ChannelFuture f = b.bind().sync();
            System.out.println(EchoServer.class.getName() +
                    " started and listening for connections on " + f.channel().localAddress());
            // 獲取Channel的CloseFutrue,在完成之前一直處於阻塞狀態
            f.channel().closeFuture().sync();
        } finally {
            // 關閉所有 EventLoopGroup,並釋放所有資源
            group.shutdownGracefully().sync();
        }
    }
}

編寫 Echo Client 代碼

Echo Client 代碼邏輯:

  1. 連接服務器
  2. 發送一個或多個消息
  3. 等待服務端返回同樣的消息
  4. 關閉連接

實現 EchoClientHandler 邏輯

同服務端一樣,客戶端也要實現 ChannelInboundHandler 接口,客戶端需要繼承 SimpleChannelInboundHandler ,有以下三個接口需要重寫:

  • channelActive() —— 當連接建立時,調用該方法
  • channelRead0() —— 當接收到服務端的消息時,調用該方法
  • exceptionCaught() —— 當有異常發生時,執行該方法

代碼如下:

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
package nia.chapter2.echoclient;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandler.Sharable;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.util.CharsetUtil;

// @Sharable用於標記EchoClientHandler,可以在channel中分享使用
@Sharable
public class EchoClientHandler extends SimpleChannelInboundHandler<ByteBuf> {
	
    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        // 一旦連接建立,將會發送消息
        ctx.writeAndFlush(Unpooled.copiedBuffer("Netty rocks!",CharsetUtil.UTF_8));
    }

    @Override
    public void channelRead0(ChannelHandlerContext ctx, ByteBuf in) {
        // 記錄收到的消息
        System.out.println("Client received: " + in.toString(CharsetUtil.UTF_8));
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        // 打印堆棧信息
        cause.printStackTrace();
        // 關閉channel
        ctx.close();
    }
}

實現 EchoClient 邏輯

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
63
64
65
package nia.chapter2.echoclient;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;

import java.net.InetSocketAddress;


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 b = new Bootstrap();
            // 指定使用 NioEventLoopGroup 去處理客戶端事件
            b.group(group)
                // 指定channel類型爲NIO
                .channel(NioSocketChannel.class)
                // 指定要連接的遠程地址
                .remoteAddress(new InetSocketAddress(host, port))
                // 將 EchoClientHandler 添加到 pipeline中
                .handler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    public void initChannel(SocketChannel ch) throws Exception {
                        ch.pipeline().addLast(new EchoClientHandler());
                    }
                });
            // 連接遠程地址,一直等待直到連接完成
            ChannelFuture f = b.connect().sync();
            // 在channel關閉前一直處於block狀態
            f.channel().closeFuture().sync();
        } finally {
            // 關閉線程池,釋放所有資源
            group.shutdownGracefully().sync();
        }
    }

    public static void main(String[] args)
            throws Exception {
        if (args.length != 2) {
            System.err.println("Usage: " + EchoClient.class.getSimpleName() +
                    " <host> <port>"
            );
            return;
        }

        final String host = args[0];
        final int port = Integer.parseInt(args[1]);
        new EchoClient(host, port).start();
    }
}

爲什麼 client 使用 SimpleChannelInboundHandler ,而 server 端使用 ChannelInboundHandlerAdapter 的區別 ?

在 Client 中,channelRead0 () 完成時,消息已經處理完。當該方法返回時,SimpleChannelInboundHandler 會釋放保存該消息的 ByteBuf 的內存引用。

在 Server 中,接收完消息後,還需要將消息回傳給客戶端,並且 wirte () 是異步的,當 channelRead () 完成時,消息內存還沒有被釋放。需要等到 channelReadComplete () 中調用 writeAndFlush () 纔會被釋放。

Netty 組件

這裏我們先簡要了解一下以下幾個組件的作用,留個映像,後面我們會對每個組件做詳細深入。

Channel

同我們前面學習 Java NIO Channel 類似,Netty Channel 在此基礎上做了高度抽象的封裝,主要用於網絡 I/O 數據的基本操作,如 bind ()、connect ()、read ()、write () 等。

EventLoop

在網絡連接的整個生命週期內,發生的所有事件的處理主要有 EventLoop 來處理

ChannelFuture

在 Netty 中,I/O 操作主要都是異步進行,當操作發生時,我們需要通過一種方式來知道操作在未來的時間點的執行結果。ChannelFutrue 中的 addListener () 方法,可以註冊監聽器 ChannelFutureListener,當操作完成時,監聽器可以主動通知我們。

ChannelHandler

channelHandler 主要用於應用程序中的業務邏輯的處理,網絡中的進入與出去的數據都經由它處理,當有事件發生時,channelHandler 會被觸發執行。

ChannelPipeline

ChannelPipeline 提供了一種容器,用於定義數據流入與流出過程中的處理流程。可以將 Pipeline 看作是一條流水線,原始的原料 (字節流) 進來,經過加工,最後輸出。

Bootstrapping

主要用於配置服務端或客戶端的 Netty 程序的啓動信息。

ByteBuf

字節數據容器,提供比 Java NIO ByteBuffer 更好的的 API。

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