1. 程式人生 > >Netty4.x 原始碼實戰系列(二):服務端bind流程詳解

Netty4.x 原始碼實戰系列(二):服務端bind流程詳解

在上一篇《ServerBootstrap 與 Bootstrap 初探》中,我們已經初步的瞭解了ServerBootstrap是netty進行服務端開發的引導類。 且在上一篇的服務端示例中,我們也看到了,在使用netty進行網路程式設計時,我們是通過bind方法的呼叫來完成伺服器埠的偵聽:

EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
    ServerBootstrap b = new ServerBootstrap(); 
b.group(bossGroup, workerGroup)   .channel(NioServerSocketChannel.class) .handler(new LoggingHandler()) .childHandler(new ChannelInitializer<SocketChannel>() { @Override public void initChannel(SocketChannel ch) throws Exception { ch.pipeline
().addLast(new DiscardServerHandler()); } }) .option(ChannelOption.SO_BACKLOG, 128) .childOption(ChannelOption.SO_KEEPALIVE, true); // 偵聽8000埠 ChannelFuture f = b.bind(8000).sync(); f.channel().closeFuture().sync(); } finally { workerGroup.shutdownGracefully
(); bossGroup.shutdownGracefully(); }

從上面的服務端示例中可以看到,我們只是定義了主執行緒組及worker執行緒組,以及指定了channel型別為NioServerSocketChannel等等一些簡單的配置, 然後繫結偵聽埠,用於網路服務的主體程式碼基本就完了(業務程式碼在Handler中實現,後面的文章會詳細介紹。
這真的大大簡化並方便了java程式設計師使用netty來進行網路開發,但是想要深入學習netty的人可能會有下面的一些疑問:

netty是繼續Java NIO的,那麼ServerSocketChannel是什麼時候初始化的?
我怎麼沒有看到Java NIO中的selector, netty是如何實現多路複用的?
我們設定的handler 或者 childHandler,是如何應用的?
boss執行緒組 以及 worker執行緒組 其實如何分配執行緒的?
為什麼是boss執行緒組,難道接收使用者請求是多個執行緒一起工作?
。。。

本篇將帶著上面這些疑問,我們將進入bind方法內部,深入淺出,對netty的工作機制一探究竟。

當我們呼叫ServerBootstrap的bind方法時,其實是呼叫的是父類AbstractBootstrap的bind方法:

public ChannelFuture bind(int inetPort) {
    return bind(new InetSocketAddress(inetPort));
}

進而呼叫另一個過載bind方法:

public ChannelFuture bind(SocketAddress localAddress) {
    validate();
    if (localAddress == null) {
        throw new NullPointerException("localAddress");
    }
    return doBind(localAddress);
}

此bind(SocketAddress localAddress)內部有兩個呼叫:
1、 呼叫validate()
顧名思義,validate應該是做校驗,由於ServerBootstrap覆蓋了AbstractBootstrap方法,因此此validate其實是呼叫ServerBootstrap中的validate方法:

@Override
public ServerBootstrap validate() {
    super.validate();
    if (childHandler == null) {
        throw new IllegalStateException("childHandler not set");
    }
    if (childGroup == null) {
        logger.warn("childGroup is not set. Using parentGroup instead.");
        childGroup = config.group();
    }
    return this;
}

在子類ServerBootstrap的validate方法中,首先它有呼叫了基類的validate()方法:

public B validate() {
    if (group == null) {
        throw new IllegalStateException("group not set");
    }
    if (channelFactory == null) {
        throw new IllegalStateException("channel or channelFactory not set");
    }
    return self();
}

所以通過validate方法我們得出如下結論:

1) 服務端程式必須要設定boss執行緒組 以及 worker執行緒組,分別用於Acceptor 以及 I/O操作;
2)必須通過Boostrap的channel()來指定通道型別,用來生成相應的通道(ServerSocketChannel或SocketChannel);
3) 因為是服務端程式,所以必須設定ChildHandler,來指定業務處理器,否則沒有業務處理的伺服器hi沒有意義的;

2、 呼叫doBind(localAddress)
首先我們先看一下AbstractBootstrap中doBind方法程式碼片段:

private ChannelFuture doBind(final SocketAddress localAddress) {
    final ChannelFuture regFuture = initAndRegister();      // (1)
    final Channel channel = regFuture.channel();            // (2)
    if (regFuture.cause() != null) {
        return regFuture;
    }

    if (regFuture.isDone()) {
        ChannelPromise promise = channel.newPromise();
        doBind0(regFuture, channel, localAddress, promise);     // (3)
        return promise;
    } 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();

                    doBind0(regFuture, channel, localAddress, promise);  // (3)
                }
            }
        });
        return promise;
    }
}

剝去無用程式碼,其實doBind方法內部,只做了兩件事:

一、呼叫了initAndRegister方法
二、呼叫用了doBind0方法

到底這兩個方法做了啥工作,我們繼續往下分析。

一、首先看看 initAndRegister方法內部程式碼:

final ChannelFuture initAndRegister() {
    Channel channel = null;
    try {
        // (1) 呼叫工廠方法,生成channel例項
        channel = channelFactory.newChannel();

        // (2) 初始化通道資訊
        init(channel);
    } catch (Throwable t) {
        if (channel != null) {
            channel.unsafe().closeForcibly();
        }

        return new DefaultChannelPromise(channel, GlobalEventExecutor.INSTANCE).setFailure(t);
    }

    // (3) 註冊通道
    ChannelFuture regFuture = config().group().register(channel);
    if (regFuture.cause() != null) {
        if (channel.isRegistered()) {
            channel.close();
        } else {
            channel.unsafe().closeForcibly();
        }
    }

    return regFuture;
}

通過上面程式碼,我們可以看出initAndRegister方法做了三件事:

①、呼叫channelFactory生成通道channel例項:

在上一篇中,我們已經知道,通過serverbootstrap的channel方法來指定通道型別,其實是呼叫基類AbstractBoostrap的channel方法,其內部其實是例項化了一個產生指定channel型別的channelFactory。

所以,initAndRegister中的channelFactory.newChannel()方法就是生成了一個NioServerSocketChannel的例項。 關於NioServerSocketChannel內部細節,我會有專門的文章進行分析,此處不做詳述。

②、呼叫init(channel)初始化通道資訊

init方法在基類AbstractBootstrap中是一個抽象方法:

abstract void init(Channel channel) throws Exception;

所以此處init的具體實現在子類ServerBootstrap類中:

@Override
void init(Channel channel) throws Exception {
    // 設定引導類配置的option
    final Map<ChannelOption<?>, Object> options = options0();
    synchronized (options) {
        setChannelOptions(channel, options, logger);
    }
    // 設定引導類配置的attr
    final Map<AttributeKey<?>, Object> attrs = attrs0();
    synchronized (attrs) {
        for (Entry<AttributeKey<?>, Object> e: attrs.entrySet()) {
            @SuppressWarnings("unchecked")
            AttributeKey<Object> key = (AttributeKey<Object>) e.getKey();
            channel.attr(key).set(e.getValue());
        }
    }

    // 獲取當前通道的pipeline
    ChannelPipeline p = channel.pipeline();

    final EventLoopGroup currentChildGroup = childGroup;
    final ChannelHandler currentChildHandler = childHandler;
    final Entry<ChannelOption<?>, Object>[] currentChildOptions;
    final Entry<AttributeKey<?>, Object>[] currentChildAttrs;
    synchronized (childOptions) {
        currentChildOptions = childOptions.entrySet().toArray(newOptionArray(childOptions.size()));
    }
    synchronized (childAttrs) {
        currentChildAttrs = childAttrs.entrySet().toArray(newAttrArray(childAttrs.size()));
    }

    // 給NioServerSocketChannel的pipeline中新增一個ChannelInitializer型別的Handler
    p.addLast(new ChannelInitializer<Channel>() {
        @Override
        public void initChannel(final Channel ch) throws Exception {
            final ChannelPipeline pipeline = ch.pipeline();
            ChannelHandler handler = config.handler();
            if (handler != null) {
                pipeline.addLast(handler);
            }

            ch.eventLoop().execute(new Runnable() {
                @Override
                public void run() {
                    pipeline.addLast(new ServerBootstrapAcceptor(
                            ch, currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs));
                }
            });
        }
    });
}

init內部主要做了一下幾件事:
ⅰ、 設定channel的options

final Map<ChannelOption<?>, Object> options = options0();
synchronized (options) {
    setChannelOptions(channel, options, logger);
}

ⅱ、設定channel的attribute

final Map<AttributeKey<?>, Object> attrs = attrs0();
synchronized (attrs) {
    for (Entry<AttributeKey<?>, Object> e: attrs.entrySet()) {
        @SuppressWarnings("unchecked")
        AttributeKey<Object> key = (AttributeKey<Object>) e.getKey();
        channel.attr(key).set(e.getValue());
    }
}

ⅲ、給NioServerSocketChannel的pipeline中新增一個ChannelInitializer型別的Handler(根據類繼承ChannelInitializer繼承自ChannelInboundHandlerAdapter)

關於pipeline到底是什麼,本篇不做詳述,下一篇我會跟NioServerSocketChannel來一起給大家分析一下。

③、完成通道的註冊
通道初始化完成後,然後就可以註冊通道了:

 ChannelFuture regFuture = config().group().register(channel);

config()在AbstractBootstrap中也是個抽象方法:

public abstract AbstractBootstrapConfig<B, C> config();

所以具體的實現細節還是在子類ServerBootstrap中:

@Override
public final ServerBootstrapConfig config() {
    return config;
}

此方法只會返回了config例項物件,此屬性是在ServerBootstrap初始化時就建立了

public class ServerBootstrap extends AbstractBootstrap<ServerBootstrap, ServerChannel> {
    ...
    private final ServerBootstrapConfig config = new ServerBootstrapConfig(this);
    ...

    @Override
    public final ServerBootstrapConfig config() {
        return config;
    }
}

我們先看一下ServerBootstrapConfig的類繼承結構圖:
這裡寫圖片描述

ServerBootstrapConfig初始化時傳入的this物件,此this表示ServerBootstrap,而且ServerBootstrapConfig構造方法內部呼叫了其基類AbstractBootstrapConfig的構造方法:

ServerBootstrapConfig(ServerBootstrap bootstrap) {
    super(bootstrap);
}

所以config().group()就對應ServerBootstrap的group屬性引用(由上一篇得知group指向boss執行緒組),因此register其實是呼叫的NioEventLoopGroup的register方法。

對於NioEventLoopGroup,目前大家只知道是個執行緒組,其內部到底如何實現的,它的作用到底是什麼,大家也都不太清楚,由於篇幅原因,這裡不作詳細介紹,後面會有文章作專門詳解。

二、我們再回到doBind(localAddress)方法,內部在呼叫玩initAndRegister之後,就是呼叫doBind0方法

 private static void doBind0(
            final ChannelFuture regFuture, final Channel channel,
            final SocketAddress localAddress, final ChannelPromise promise) {

    // This method is invoked before channelRegistered() is triggered.  Give user handlers a chance to set up
    // the pipeline in its channelRegistered() implementation.
    channel.eventLoop().execute(new Runnable() {
        @Override
        public void run() {
            if (regFuture.isSuccess()) {
                channel.bind(localAddress, promise).addListener(ChannelFutureListener.CLOSE_ON_FAILURE);
            } else {
                promise.setFailure(regFuture.cause());
            }
        }
    });
}

此方法內部就是完成channel的偵聽埠的繫結。

至此ServerBootstrap的bind工作執行完成。

此篇對服務端繫結的流程做了大體介紹,但由於篇幅問題,下面幾個問題未做詳盡分析:
1、 NioServerSocketChannel是如何例項化的
2、 Pipeline是什麼,為什麼要通過它新增handler
3、 NioEventLoopGroup內部細節是什麼,為什麼要通過它註冊Channel, Java NIO中channel初始化後不是要註冊到selector中嗎?

帶著上面這些疑問,歡迎大家繼續關注接下來的幾篇文章,在這幾篇文章中,bind操作會一直貫穿其中:

Netty4.x 原始碼實戰系列(三):NioServerSocketChannel全剖析

Netty4.x 原始碼實戰系列(四):深入淺出學Pipeline

Netty4.x 原始碼實戰系列(五):深入淺出學NioEventLoopGroup

Netty4.x 原始碼實戰系列(六):深入淺出學NioEventLoop

相關推薦

Netty4.x 原始碼實戰系列服務bind流程

在上一篇《ServerBootstrap 與 Bootstrap 初探》中,我們已經初步的瞭解了ServerBootstrap是netty進行服務端開發的引導類。 且在上一篇的服務端示例中,我們也看到了,在使用netty進行網路程式設計時,我們是通過bind方法

Netty4.x 原始碼實戰系列深入淺出學Pipeline

在netty中,每一個channel都有一個pipeline物件,並且其內部本質上就是一個雙向連結串列 本篇我們將深入Pipeline原始碼內部,對其一探究竟,給大家一個全方位解析。 Pipeline初始化過程分析 在上一篇中,我們得知ch

Netty4.x 原始碼實戰系列NioServerSocketChannel全剖析

根據上一篇《Netty4.x 原始碼實戰系列(二):服務端bind流程詳解》所述,在進行服務端開發時,必須通過ServerBootstrap引導類的channel方法來指定channel型別, channel方法的呼叫其實就是例項化了一個用於生成此channel

MVC之前的那點事兒系列7WebActivator的實現原理

文章內容 上篇文章,我們分析如何動態註冊HttpModule的實現,本篇我們來分析一下通過上篇程式碼原理實現的WebActivator類庫,WebActivator提供了3種功能,允許我們分別在HttpApplication初始化之前,之後以及ShutDown的時候分別執行指定的程式碼,示例如下: [

Android音訊實時傳輸與播放服務

我偷懶就用java寫了個簡單的伺服器,大家明白原理就好。 服務端共開放兩個埠,一個udp上行埠用來接收amr音訊流,另一個tcp下行埠用來發送amr音訊流。 我這裡寫的服務端實現了組播的功能,即一個人在錄音,可以同時讓很多人同時聽到。 簡而言之,服務端做的唯一一件

Netty服務客戶例項分析

package com.netty; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.*; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.ch

實戰】Unity3d實戰之Unity3d網路遊戲實戰6服務框架的搭建

Unity3d實戰之Unity3d網路遊戲實戰篇(6):服務端框架的搭建 學習書籍《Unity3d網路遊戲實戰》 羅培羽著 機械工業出版社 本文是作者在學習過程中遇到的認為值得記錄的點,

Redis系列Redis的RESP協議

一、什麼是RESP   Redis是Redis序列化協議,Redis客戶端RESP協議與Redis伺服器通訊。Redis協議在以下幾點之間做出了折衷: 簡單的實現 快速地被計算機解析 簡單得可以能被人工解析 二、RESP協議描述   RESP協議在Redis 1.2中引入,但在Redis 2.0中成為與R

Quartz.Net系列十三DateBuilder中的API

1.DateOf、ToDayAt、TomorrowAt DateOf:指定年月日時分秒 public static DateTimeOffset DateOf(int hour, int minute, int second) { ValidateSe

faster rcnn pytorch 復現系列generate_anchors原始碼解析

目錄​ 1. 總函式 generate_anchors 2. 函式分功能寫,首先是ratios的實現,其次是scale的實現 3. anchor2WHXY函式+WsHsXsYs2anchors函式[s表示複數] 4.  _ratio_enum(anchor,r

Python3爬蟲入門實戰系列爬取貓眼電影排行榜

在進行本節實戰之前,希望您對requests庫以及正則表示式有所瞭解。 執行平臺:windows Python版本: Python3.x 一、依賴庫的安裝 在本節實戰之前,請確保已經正確安裝了requests庫 requests庫的安裝 pip3 instal

Hadoop Yarn原始碼閱讀系列Yarn原始碼目錄組織結構

Hadoop Yarn分為5部分:API、Common、Applications、Client和Server,他們的內容具體如下: YARN API(hadoop-yarn-api目錄):給出了YARN記憶體涉及的4個主要RPC協議的Java宣告和Protocol Buffers定義,這4個RP

Java原始碼解析系列ArrayList原始碼解析

備註:以下都是基於JDK8 原始碼分析 ArrayList簡介        ArrayList 是一個數組佇列,相當於 動態陣列。與Java中的陣列相比,它的容量能動態增長。它繼承於AbstractList,實現了List, RandomAccess, Clonea

Python爬蟲入門實戰系列爬取貓眼電影排行榜

在進行本節實戰之前,希望您對requests庫以及正則表示式有所瞭解。 執行平臺:windows **Python版本: Python3.x ** 一、依賴庫的安裝 在本節實戰之前,請確保已經正確安裝了requests庫 requests庫的安裝 pip3 i

SparkSQLSpark-1.4.0)實戰系列——DataFrames進階

本節主要內容如下 DataFrame與RDD的互操作實戰 不同資料來源構建DataFrame實戰 DataFrame與RDD的互操作實戰 1 採用反映機制進行Schema型別推導(RDD到DataFrame的轉換) SparkSQL支援RDD到D

EMQ X 規則引擎系列儲存訊息到 MySQL 資料庫

場景介紹 該場景需要將 EMQ X 指定主題下且滿足條件的訊息儲存到 MySQL 資料庫。為了便於後續分析檢索,訊息內容需要進

SpringBoot基礎實戰系列springboot解析json與HttpMessageConverter

# SpringBoot解析Json格式資料 ### @ResponseBody 注:該註解表示前端請求後端controller,後端響應請求返回 `json` 格式資料前端,實質就是將java物件序列化 #### 1.建立Controller 注:springboot預設就已經支援返回`json`格式資料,

基於 abp vNext 和 .NET Core 開發部落格專案 - Blazor 實戰系列

## 系列文章 1. **[基於 abp vNext 和 .NET Core 開發部落格專案 - 使用 abp cli 搭建專案](https://www.cnblogs.com/meowv/p/12896177.html)** 2. **[基於 abp vNext 和 .NET Core 開發部落格專案

回顧2017系列移動APP設計趨勢

原型設計 交互設計 界面設計 設計師 移動端APP在2017年經歷了諸多的變化, 人工智能、聊天式的界面、響應式設計、虛擬現實(VR)和增強現實(AR)讓設計師不斷面臨新的挑戰。研究表明,用戶每天耗費在手機和平板上的平均時長為158分鐘,其中127分鐘是耗費在各類APP中,可以看出移動端

容器開啟數據服務之旅系列Kubernetes如何助力Spark大數據分析

容器 控制臺 摘要: 容器開啟數據服務之旅系列(二):Kubernetes如何助力Spark大數據分析 (二):Kubernetes如何助力Spark大數據分析 概述 本文為大家介紹一種容器化的數據服務Spark + OSS on ACK,允許Spark分布式計算節點對阿裏雲OSS對象存儲的直接訪問。