Netty(四)—客戶端流程

Netty客戶端

  • 建立一次通信,必然有着兩個對象。發送者和接收者

  • 如我們使用微信、qq進行聊天的時候,就是一個通信的過程

  • 當我們使用java NIO實現簡單的通信功能時,也必然存在着發送端(Client)和接收端(Server)

  • 接下來通過java NIO的實現,去逐步深入理解學習Netty的實現原理

Java BIO簡單通信功能

  • 服務端
public class Server{
    // 首先定義一個 ServerSocket 對象
    private ServerSocket server;
    // 在構造方法中進行初始化
    public Server(int port) {
        //  傳入需要監聽的端口號
        server = new ServerSocket(port);
    }
    
    public void listen() {
        //  輪詢
        while(true) {
            //  server.accept() 是一個阻塞(同步方法),在沒有請求接收的時候—阻塞
            Socket socket = server.accept();
            //  拿到請求之後,意味着建立了一個Sokdcet連接
            //  拿到 client 發送的數據
            InputStream is = socket.getInputStream();
            byte[] buff = new byte[10];
            // 把數據寫入到buff數組中去
            int length = is.read(buff);
            if(length > 0) {
                //  轉化爲 String 字符串
                String msg = new String(buff, 0, length);
            }
            // 關閉連接
            socket.close();
        }
    }
}
  • 客戶端
public class Client {

    public static void main(String[] args) throws IOException {
        //  localhost:和誰進行通信    port:服務器端口
        Socket client = new Socket("localhost", 8080);
        //  客戶端  輸出  數據:創建輸出流對象
        OutputStream os = client.getOutputStream();
        //  生成一個隨機字符串
        String uId = UUID.randomUUID().toString();
        System.out.println("客戶端準備開始發送數據:"+uId);
        System.out.println("發送數據字節長度:"+uId.getBytes().length);
        os.write(uId.getBytes());
        os.close();
        client.close();
    }
}
  • 通過簡單的BIO實現,腦海中定下一個基調
  • 客戶端發送,服務端接收,通過IP和端口地址進行關聯
  • 不管Netty如何複雜,其思想是不變的

Netty是如何發起一次連接的

  • 首先提出問題,假如使用Netty去發出一個請求
  • 必然會和java的實現類似通過IP和端口進行連接,這點是躲不開的。

BootStrap來了

  • 找到Netty中的BootStrap.java,偷來官方註釋瞧一瞧
  • 比較清晰明瞭,傳達了兩點
    • 客戶端通過Bootstrap使用Channel將會很簡單
    • 我們使用UDP協議的時候,可以使用bind()方法來進行綁定。使用TCP的時候,使用connect()方法進行綁定
      在這裏插入圖片描述

詳細註釋版本代碼

在這裏插入圖片描述在這裏插入圖片描述

  • 跟進connect方法,最終調用了Bootstrap的doResolveAndConnect方法
private ChannelFuture doResolveAndConnect(final SocketAddress remoteAddress, final SocketAddress localAddress) {
    final ChannelFuture regFuture = initAndRegister();
    final Channel channel = regFuture.channel();
    if (regFuture.isDone()) {
        if (!regFuture.isSuccess()) {
            return regFuture;
        }
        return doResolveAndConnect0(channel, remoteAddress, localAddress, channel.newPromise());
    } else {
        final PendingRegistrationPromise promise = new PendingRegistrationPromise(channel);
        regFuture.addListener(new ChannelFutureListener() {
            @Override
            public void operationComplete(ChannelFuture future) throws Exception {
                Throwable cause = future.cause();
                if (cause != null) {
                    promise.setFailure(cause);
                } else {
                    promise.registered();
                    doResolveAndConnect0(channel, remoteAddress, localAddress, promise);
                }
            }
        });
        return promise;
    }
}
  • 這裏面有幾個關鍵點需要我們注意
    • initAndRegister方法具體做了什麼事情
    • 客戶端時如何建立連接的

深入理解initAndRegister方法

  • 調用到AbstractBootstrap的 initAndRegister 方法

在這裏插入圖片描述


NioSocketChannel初始化

  • ​ 這裏走到 ReflectiveChannelFactory 通過反射的方式創建 Channel 對象
    • 創建Channel對象的類型 由 Bootstrap 的 channel() 方法指定,這裏實際上調用到了 NioSocketChannel 的構造方法

在這裏插入圖片描述

  • 通過 NioSocketChannel 方法的構造器,深入瞭解
    • newSocket(privider) 方法通過 SelectorProvider 對象新建一個 SocketChannel 對象

在這裏插入圖片描述

  • 通過層層調用到 AbstractNioChannel 的構造方法
    • 這裏parent屬性傳入爲null
    • 通過 ch 設置 SocketChannel 非阻塞
    • 通過 readInterestOp 設置 SelectionKey.OP_READ 爲初始狀態

在這裏插入圖片描述

  • 接着調用父類 AbstractChannel 的構造方法

    • 這裏把 Channel 對象設置爲 null

    • 爲已經創建的 Channel 對象設置一個 唯一 id

    • 對 Unsafe 對象進行初始化——其中封裝了Java底層Socket的操作

    • 對 Pipeline 對象進行初始化
      在這裏插入圖片描述


  • 至此 我們最開始設置的 NioSocketChannel 對象就初始化完成了

  • 接下來是 初始化完成之後的 init(Channel) 方法

NioSocketChannel註冊

  • 完成初始化之後調用 init(channel) 方法
  • 這裏體現了 Pipeline對象 和 Channel 對象的關聯
void init(Channel channel) throws Exception {
    //  這裏通過 Channel 對象獲取已經初始化完成的 Pipeline  對象
    ChannelPipeline p = channel.pipeline();
    //  把 handler  添加到 Pipeline 對象中
    p.addLast(config.handler());
    //  這裏獲取一些底層配置,如 ChannelOption.SO_KEEPALIVE 表示是否開啓TCP底層心跳機制,true爲開啓
    final Map<ChannelOption<?>, Object> options = options0();
    //  爲每一個  channel  進行配置
    synchronized (options) {
        for (Entry<ChannelOption<?>, Object> e: options.entrySet()) {
            try {
                if (!channel.config().setOption((ChannelOption<Object>) e.getKey(), e.getValue())) {
                    logger.warn("Unknown channel option: " + e);
                }
            } catch (Throwable t) {
                logger.warn("Failed to set a channel option: " + channel, t);
            }
        }
    }

    //  提供自定義屬性功能  存儲在 attrs 中,通過  channel.attr(key)方法可以獲取相對應屬性
    final Map<AttributeKey<?>, Object> attrs = attrs0();
    synchronized (attrs) {
        for (Entry<AttributeKey<?>, Object> e: attrs.entrySet()) {
            channel.attr((AttributeKey<Object>) e.getKey()).set(e.getValue());
        }
    }
}
  • 接下來會執行以下代碼
ChannelFuture regFuture = config().group().register(channel);
  • 調用鏈時序圖

在這裏插入圖片描述

  • 最終調用到 AbstractNioChannel 對象的 doRegister 方法
    • 到這裏完成了註冊關聯

在這裏插入圖片描述

在這裏插入圖片描述

  • channel的註冊過程可以總結爲:和對應的EventLoop對象的Selector相關聯
  • 初始化註冊完成之後,接下來就是客戶端連接操作了

客戶端連接實現

  • 首先通過時序圖觀察一下客戶端連接的調用過程,通過調用過程尋找關鍵代碼

在這裏插入圖片描述

  • 最終調用到 NioSocketChannel 方法完成連接
    • javaChannel()方法獲取java NIO SOcketChannel 完成連接

在這裏插入圖片描述

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