Java程式設計方法論-Spring WebFlux篇 Reactor-Netty下HttpServer 的封裝

知秋z發表於2019-02-27

前言

本系列為本人Java程式設計方法論 響應式解讀系列的Webflux部分,現分享出來,前置知識Rxjava2 ,Reactor的相關解讀已經錄製分享視訊,併發布在b站,地址如下:

Rxjava原始碼解讀與分享:www.bilibili.com/video/av345…

Reactor原始碼解讀與分享:www.bilibili.com/video/av353…

NIO原始碼解讀相關視訊分享: www.bilibili.com/video/av432…

NIO原始碼解讀視訊相關配套文章:

BIO到NIO原始碼的一些事兒之BIO

BIO到NIO原始碼的一些事兒之NIO 上

BIO到NIO原始碼的一些事兒之NIO 中

BIO到NIO原始碼的一些事兒之NIO 下 之 Selector

BIO到NIO原始碼的一些事兒之NIO 下 Buffer解讀 上

BIO到NIO原始碼的一些事兒之NIO 下 Buffer解讀 下

Java程式設計方法論-Spring WebFlux篇 01 為什麼需要Spring WebFlux 上

Java程式設計方法論-Spring WebFlux篇 01 為什麼需要Spring WebFlux 下

其中,Rxjava與Reactor作為本人書中內容將不對外開放,大家感興趣可以花點時間來觀看視訊,本人對著兩個庫進行了全面徹底細緻的解讀,包括其中的設計理念和相關的方法論,也希望大家可以留言糾正我其中的錯誤。

HttpServer 的封裝

本書主要針對Netty伺服器來講,所以讀者應具備有關Netty的基本知識和應用技能。接下來,我們將對Reactor-netty從設計到實現的細節一一探究,讓大家真的從中學習到好的封裝設計理念。本書在寫時所參考的最新版本是Reactor-netty 0.7.8.Release這個版本,但現在已有0.8版本,而且0.70.8版本在原始碼細節有不小的變動,這點給大家提醒下。我會針對0.8版本進行全新的解讀。

HttpServer 的引入

我們由上一章可知Tomcat使用Connector來接收和響應連線請求,這裡,對於Netty來講,如果我們想讓其做為一個web伺服器,我們先來看一個Netty常見的一個用法(這裡摘自官方文件一個例子DiscardServer Demo):

import io.netty.bootstrap.ServerBootstrap;

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

/**
 * 丟棄任何進入的資料
 */
public class DiscardServer {

    private int port;

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

    public void run() throws Exception {
        EventLoopGroup bossGroup = new NioEventLoopGroup(); // (1)
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap b = new ServerBootstrap(); // (2)
            b.group(bossGroup, workerGroup)
             .channel(NioServerSocketChannel.class) // (3)
             .childHandler(new ChannelInitializer<SocketChannel>() { // (4)
                 @Override
                 public void initChannel(SocketChannel ch) throws Exception {
                     ch.pipeline().addLast(new DiscardServerHandler());
                 }
             })
             .option(ChannelOption.SO_BACKLOG, 128)          // (5)
             .childOption(ChannelOption.SO_KEEPALIVE, true); // (6)

            // 繫結埠,開始接收進來的連線
            ChannelFuture f = b.bind(port).sync(); // (7)

            // 等待伺服器  socket 關閉 。
            // 在這個例子中,這不會發生,但你可以優雅地關閉你的伺服器。
            f.channel().closeFuture().sync();
        } finally {
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }
    }

    public static void main(String[] args) throws Exception {
        int port;
        if (args.length > 0) {
            port = Integer.parseInt(args[0]);
        } else {
            port = 8080;
        }
        new DiscardServer(port).run();
    }
}
複製程式碼
  1. NioEventLoopGroup 是用來處理I/O操作的多執行緒事件迴圈器,Netty 提供了許多不同的 EventLoopGroup 的實現用來處理不同的傳輸。在這個例子中我們實現了一個服務端的應用,因此會有2個 NioEventLoopGroup 會被使用。第一個經常被叫做BossGroup,用來接收進來的連線。第二個經常被叫做WorkerGroup,用來處理已經被接收的連線,一旦BossGroup接收到連線,就會把連線資訊註冊到WorkerGroup上。如何知道多少個執行緒已經被使用,如何對映到已經建立的 Channel上都需要依賴於 EventLoopGroup 的實現,並且可以通過建構函式來配置他們的關係。
  2. ServerBootstrap 是一個啟動 NIO 服務的輔助啟動類。你可以在這個服務中直接使用 Channel,但是這會是一個複雜的處理過程,在很多情況下你並不需要這樣做。
  3. 這裡我們通過指定使用 NioServerSocketChannel來舉例說明一個新的 Channel 如何接收傳進來的連線。
  4. 這裡的事件處理類經常會被用來處理一個最近已經接收的 ChannelChannelInitializer 是一個特殊的處理類,目的是幫助使用者配置一個新的 Channel。 使用其對應的ChannelPipeline 來加入你的服務邏輯處理(這裡是DiscardServerHandler)。當你的程式變的複雜時,可能你會增加更多的處理類到 pipline 上,然後提取這些匿名類到最頂層的類上(匿名類即ChannelInitializer例項我們可以將其看成是一個代理模式的設計,類似於ReactorSubscriber的設計實現,一層又一層的包裝,最後得到一個我們需要的一個可以層層處理的Subscriber)。
  5. 你可以設定這裡指定的 Channel 實現的配置引數。如果我們寫一個TCP/IP 的服務端,我們可以設定 socket 的引數選項,如tcpNoDelaykeepAlive。請參考 ChannelOptionChannelConfig實現的介面文件來對ChannelOption 的有一個大概的認識。
  6. 接著我們來看 option()childOption() :option() 是提供給NioServerSocketChannel用來接收進來的連線。childOption() 是提供給由父管道 ServerChannel 接收到的連線,在這個例子中也是 NioServerSocketChannel
  7. 剩下的就是繫結埠然後啟動服務。這裡我們在伺服器上繫結了其 8080 埠。當然現在你可以多次呼叫 bind() 方法(基於不同繫結地址)。

針對bootstrap的option的封裝

在看了常見的Netty的一個伺服器建立用法之後,我們來看Reactor Netty給我們提供的Http伺服器的一個封裝:reactor.ipc.netty.http.server.HttpServer。由上面DiscardServer Demo可知,首先是定義一個伺服器,方便設定一些條件對其進行配置,然後啟動的話是呼叫其run方法啟動,為做到更好的可配置性,這裡使用了建造器模式,以便我們自定義或直接使用預設配置(有些是必須配置,否則會丟擲異常,這也是我們這裡面所設定的內容之一):

//reactor.ipc.netty.http.server.HttpServer.Builder
public static final class Builder {
    private String bindAddress = null;
    private int port = 8080;
    private Supplier<InetSocketAddress> listenAddress = () -> new InetSocketAddress(NetUtil.LOCALHOST, port);
    private Consumer<? super HttpServerOptions.Builder> options;

    private Builder() {
    }
    ...
    public final Builder port(int port) {
        this.port = port;
        return this;
    }

    /**
        * The options for the server, including bind address and port.
        *
        * @param options the options for the server, including bind address and port.
        * @return {@code this}
        */
    public final Builder options(Consumer<? super HttpServerOptions.Builder> options) {
        this.options = Objects.requireNonNull(options, "options");
        return this;
    }

    public HttpServer build() {
        return new HttpServer(this);
    }
}
複製程式碼

可以看到,此處的HttpServer.Builder#options是一個函式式動作Consumer,其傳入的引數是HttpServerOptions.Builder,在HttpServerOptions.Builder內可以針對我們在DiscardServer Demo中的bootstrap.option進行一系列的預設配置或者自行調控配置,我們的對於option的自定義設定主要還是針對於ServerBootstrap#childOption。因為在reactor.ipc.netty.options.ServerOptions.Builder#option這個方法中,有對它的父類reactor.ipc.netty.options.NettyOptions.Builder#option進行了相應的重寫:

//reactor.ipc.netty.options.ServerOptions.Builder
public static class Builder<BUILDER extends Builder<BUILDER>>
	extends NettyOptions.Builder<ServerBootstrap, ServerOptions, BUILDER>{...}
	
//reactor.ipc.netty.options.ServerOptions.Builder#option
/**
* Set a {@link ChannelOption} value for low level connection settings like
* SO_TIMEOUT or SO_KEEPALIVE. This will apply to each new channel from remote
* peer.
*
* @param key the option key
* @param <T> the option type
* @return {@code this}
* @see ServerBootstrap#childOption(ChannelOption, Object)
*/
@Override
public final <T> BUILDER option(ChannelOption<T> key, T value) {
this.bootstrapTemplate.childOption(key, value);
return get();
}
//reactor.ipc.netty.options.NettyOptions.Builder#option
/**
* Set a {@link ChannelOption} value for low level connection settings like
* SO_TIMEOUT or SO_KEEPALIVE. This will apply to each new channel from remote
* peer.
*
* @param key the option key
* @param value the option value
* @param <T> the option type
* @return {@code this}
* @see Bootstrap#option(ChannelOption, Object)
*/
public <T> BUILDER option(ChannelOption<T> key, T value) {
this.bootstrapTemplate.option(key, value);
return get();
}
複製程式碼

這是我們需要注意的地方。然後,我們再回到reactor.ipc.netty.http.server.HttpServer.Builder,從其build這個方法可知,其返回一個HttpServer例項,通過對所傳入的HttpServer.Builder例項的options進行判斷,接著,就是對bootstrap.group的判斷,因為要使用構造器配置的話,首先得獲取到ServerBootstrap,所以要先判斷是否有可用EventLoopGroup,這個我們是可以自行設定的,這裡設定一次,bossGroupworkerGroup可能都會呼叫這一個,這點要注意下(loopResources原始碼註釋已經講的很明確了):

//reactor.ipc.netty.http.server.HttpServer.Builder#build
public HttpServer build() {
    return new HttpServer(this);
}
//reactor.ipc.netty.http.server.HttpServer#HttpServer
private HttpServer(HttpServer.Builder builder) {
    HttpServerOptions.Builder serverOptionsBuilder = HttpServerOptions.builder();
    if (Objects.isNull(builder.options)) {
        if (Objects.isNull(builder.bindAddress)) {
            serverOptionsBuilder.listenAddress(builder.listenAddress.get());
        }
        else {
            serverOptionsBuilder.host(builder.bindAddress).port(builder.port);
        }
    }
    else {
        builder.options.accept(serverOptionsBuilder);
    }
    if (!serverOptionsBuilder.isLoopAvailable()) {
        serverOptionsBuilder.loopResources(HttpResources.get());
    }
    this.options = serverOptionsBuilder.build();
    this.server = new TcpBridgeServer(this.options);
}
//reactor.ipc.netty.options.NettyOptions.Builder
public static abstract class Builder<BOOTSTRAP extends AbstractBootstrap<BOOTSTRAP, ?>,
SO extends NettyOptions<BOOTSTRAP, SO>, BUILDER extends Builder<BOOTSTRAP, SO, BUILDER>>
implements Supplier<BUILDER> {
    ...
/**
* Provide a shared {@link EventLoopGroup} each Connector handler.
*
* @param eventLoopGroup an eventLoopGroup to share
* @return {@code this}
*/
public final BUILDER eventLoopGroup(EventLoopGroup eventLoopGroup) {
Objects.requireNonNull(eventLoopGroup, "eventLoopGroup");
return loopResources(preferNative -> eventLoopGroup);
}
/**
* Provide an {@link EventLoopGroup} supplier.
* Note that server might call it twice for both their selection and io loops.
*
* @param channelResources a selector accepting native runtime expectation and
* returning an eventLoopGroup
* @return {@code this}
*/
public final BUILDER loopResources(LoopResources channelResources) {
this.loopResources = Objects.requireNonNull(channelResources, "loopResources");
return get();
}

public final boolean isLoopAvailable() {
return this.loopResources != null;
}
...
}
複製程式碼

可以看到,這個類是Supplier實現,其是一個物件提取器,即屬於一個函式式動作物件,適合用於懶載入的場景。這裡的LoopResources也是一個函式式介面(@FunctionalInterface),其設計的初衷就是為io.netty.channel.Channel的工廠方法服務的:

//reactor.ipc.netty.resources.LoopResources
@FunctionalInterface
public interface LoopResources extends Disposable {

/**
* Default worker thread count, fallback to available processor
*/
int DEFAULT_IO_WORKER_COUNT = Integer.parseInt(System.getProperty(
    "reactor.ipc.netty.workerCount",
    "" + Math.max(Runtime.getRuntime()
                .availableProcessors(), 4)));
/**
* Default selector thread count, fallback to -1 (no selector thread)
*/
int DEFAULT_IO_SELECT_COUNT = Integer.parseInt(System.getProperty(
    "reactor.ipc.netty.selectCount",
    "" + -1));
/**
* Create a simple {@link LoopResources} to provide automatically for {@link
* EventLoopGroup} and {@link Channel} factories
*
* @param prefix the event loop thread name prefix
*
* @return a new {@link LoopResources} to provide automatically for {@link
* EventLoopGroup} and {@link Channel} factories
*/
static LoopResources create(String prefix) {
return new DefaultLoopResources(prefix, DEFAULT_IO_SELECT_COUNT,
        DEFAULT_IO_WORKER_COUNT,
        true);
}
static LoopResources create(String prefix,
			int selectCount,
			int workerCount,
			boolean daemon) {
		...
		return new DefaultLoopResources(prefix, selectCount, workerCount, daemon);
	}
...
/**
* Callback for server {@link EventLoopGroup} creation.
*
* @param useNative should use native group if current {@link #preferNative()} is also
* true
*
* @return a new {@link EventLoopGroup}
*/
EventLoopGroup onServer(boolean useNative);
...
}
複製程式碼

我們在自定義的時候,可以藉助此類的靜態方法create方法來快速建立一個LoopResources例項。另外通過LoopResources的函式式特性,可以做到懶載入(將我們想要實現的業務藏到一個方法內),即,只有在使用的時候才會生成所需要的物件例項,即在使用reactor.ipc.netty.options.NettyOptions.Builder#loopResources(LoopResources channelResources)方法時,可進行loopResources(true -> new NioEventLoopGroup()),即在拿到LoopResources例項後,只有呼叫其onServer方法,才能拿到EventLoopGroup。這樣就可以大大節省記憶體資源,提高效能。

小結

至此,我們將由netty的普通使用到HttpServer的封裝完成通過本章給大家展示出來,目的也是告訴大家這個東西是怎麼來的,基於什麼樣的目的,接下來,我們會依照這個思路一步步給大家揭開Reactor-netty的面紗以及其與Spring webflux是如何對接設計的。

相關文章