# 3.2.HttpServer初赦

    Vert.x内置了HTTP服务器，而且封装的是纯异步Java服务器Netty，那么读者也许会有一个疑问：HttpServer的实例究竟是什么时候创建的，如果只是纯粹地编写，在哪里实例化都不影响，而本章就尝试把这些内容讲透，让大家对Vert.x中的HttpServer有一个更加深入的理解；官方的`vertx-web`项目中，创建该实例的位置是在一个`Verticle`中，那么我们做个简单的探索，看是不是只有在那里才能创建它。

    从进入Web的章节开始，我们将使用Vert.x的Web子项目，这是我们引入的额外角色：`vertx-web`；它需要您在`Maven`项目的`pom`文件中引入下边的配置：

```xml
<dependency>
  <groupId>io.vertx</groupId>
  <artifactId>vertx-web</artifactId>
  <version>4.3.3</version>
</dependency>
```

若您使用的是`Gradle`，则需要在您的`build.gradle`中引入：

```groovy
dependencies {
  compile 'io.vertx:vertx-web:4.3.3'
}
```

## 1. 开胃菜

### 1.1. 启动

    在您的主函数中写入下边代码：

```java
package io.vertx.up._02.http;

import io.vertx.core.Vertx;
import io.vertx.core.http.HttpServer;

public class DirectServer {

    public static void main(final String[] args) {
        final Vertx vertx = Vertx.vertx();
        final HttpServer server = vertx.createHttpServer();
        server.listen(8099);
    }
}
```

「**Exception**」运行的时候您会遇到下边的错误，这是本人无意间踩到的一个坑：

```shell
Exception in thread "main" java.lang.IllegalStateException: Set request or websocket handler first
    at io.vertx.core.http.impl.HttpServerImpl.listen(HttpServerImpl.java:221)
    at io.vertx.core.http.impl.HttpServerImpl.listen(HttpServerImpl.java:211)
    at io.vertx.up._02.http.DirectServer.main(DirectServer.java:11)
```

    异常的意思很清楚，那么来个完整版：

```java
package io.vertx.up._02.http;

import io.vertx.core.Vertx;
import io.vertx.core.http.HttpServer;

public class DirectServer {

    public static void main(final String[] args) {
        final Vertx vertx = Vertx.vertx();
        final HttpServer server = vertx.createHttpServer();
        server.requestHandler(handler -> {
            System.out.println(Thread.currentThread().getName());
            handler.response()
                    .putHeader("content-type", "text/plain")
                    .end("Hello Direct Server!");
        });
        server.listen(8099);
    }
}
```

    当您在浏览器中输入：[http://localhost:8099](https://github.com/silentbalanceyh/vertx-book/blob/master/chapter03/http:/localhost:8099%EF%BC%8C%E6%82%A8%E5%B0%86%E4%BC%9A%E7%9C%8B%E5%88%B0%E5%A6%82%E4%B8%8B%E4%BF%A1%E6%81%AF%EF%BC%9A/README.md)，您将会看到如下截图：

![](https://1702219450-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-Lv070Tm3mzVeCv0ENzP%2Fuploads%2Fgit-blob-e9f60038c4417be17f6ae1b4866280821ba88fc5%2F0001.png?alt=media)

    后台的Console中依旧是两行，为什么打印两次在第一章中说过了，这里不重复。

```shell
vert.x-eventloop-thread-1
vert.x-eventloop-thread-1
```

    实际上结合Vert.x的官方教程，可以得到一个结论：

> 在哪里创建HttpServer可随意，只要您可以拿到一个Vert.x实例的引用，您就可以调用它的createHttpServer()来创建对应的HttpServer的实例，而在Vert.x中，创建和启动一个HttpServer的实例往往是在一个Vertcile里面进行，为什么呢？

    是的，在我个人看来，实际上是没有影响的，但从Vert.x的本质上看，可以把这个问题当做一个趣味性思考。回到原始的话题，Vert.x是一个工具集，它本质的设计和Restful无关，和Web服务无关，和HttpServer更加没有直接的联系，应该这样描述：“这个工具集具备了作为Http服务器的功能”，从这种意义上说，在Verticle中创建HttpServer的实例就是理所当然了，也可以说使用Verticle是打开Vert.x工具集的正确方式，所以回头看上边的代码，很幼稚，幼稚得连Vert.x的入门代码都谈不上。对比下边两幅图来思考一下Vert.x的作者的原始意图：

![](https://1702219450-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-Lv070Tm3mzVeCv0ENzP%2Fuploads%2Fgit-blob-a9c368448c0c0a241b76ad1078625efac0301960%2F2020-08-17-17-29-14.jpg?alt=media)

    上边代码是左边这种方式，而官方教程中创建HttpServer的实例则是右边这种方式，虽然在`Verticle`实例中创建HttpServer同样要调用`createHttpServer()`的API，从某种意义上讲，右边的方式更加优雅，这里列举我个人的思考点做参考，则本质上没有对错之分。

    如果直接在`main`函数中通过Vertx创建`HttpServer`实例，本质上跳过了Verticle实例的创建，也就是说你在用一种没有Verticle的方式使用Vert.x，`Verticle`实例的本意就是为了让所有的东西能够平行扩展，并且组件足够小到可以实现“单一职责”。从上边的右图可以知道，一个`Verticle`实例本质上只做了一件事，要么创建`HttpServer`，要么创建`Rpc Server`或者其他，而Vert.x也提供了对`Vertcile`实例的很好的管理方式，比如可部署、也可撤销、同样支持热插拔，既然有了这样的一种管理模式，直接在`main`函数中创建似乎就显得多余了，不仅仅如此，自己管理一些配置以及相关数据时不得不说是噩梦（笔者曾经试过，主函数很重，代码很多而且乱）。所以，注意Vert.x中的`HttpServer`的正确打开方式：老老实实按照官方的教程在一个`Vertcle`中去创建，而不是直接在主函数中创建。

    很多使用过Vert.x中的`HttpServer`的小伙伴都知道官方有一段很简化的创建`HttpServer`的代码：

```java
HttpServer server = vertx.createHttpServer();

server.requestHandler(request -> {

  // 设置针对每个请求的Handler
  HttpServerResponse response = request.response();
  response.putHeader("content-type", "text/plain");

  // 生成请求数据
  response.end("Hello World!");
});

server.listen(8080);
```

    这段代码中隐藏了一个开发人员可能比较关心的东西：`io.vertx.core.http.HttpServerOptions`，本章节主要针对这个类解析说明，这个类是Options结构中的其中一个，它的目的是服务于HttpServer组件，如果您理解了Options结构，那么理解这个类就会变得轻车熟路。

### 1.2. 常用API

#### 1.1.1 createHttpServer

    `Vert.x`的源代码中，我们可以看到`createHttpServer`的方法签名如下：

```java
    HttpServer createHttpServer(HttpServerOptions httpServerOptions);

    HttpServer createHttpServer();
```

    从上述方法签名可以知道创建`HttpServer`的实例有两个核心API，一个是带参数（将是本章解析的Options结构），一个不带参数（官方例子），其实不带参数的方法会传入一个默认的Options结构，这是我们在Options章节提到的Vert.x中的常见做法。

#### 1.1.2. listen

    直接查看Vert.x中的listen方法源代码，该方法的方法签名如下：

```java
    // 无参同异步方法    
    @Fluent
    HttpServer listen();

    @Fluent
    HttpServer listen(Handler<AsyncResult<HttpServer>> handler);

    // 双参同异步方法 i - port（端口号）、s - host（Host地址）
    @Fluent
    HttpServer listen(int i, String s);

    @Fluent
    HttpServer listen(int i, String s, Handler<AsyncResult<HttpServer>> handler);

    // 单参同异步方法 i - port（端口号）
    @Fluent
    HttpServer listen(int i);

    @Fluent
    HttpServer listen(int i, Handler<AsyncResult<HttpServer>> handler);

    @Fluent
    HttpServer listen(Handler<AsyncResult<HttpServer>> listenHandler);
```

    关于`AsyncResult`的用法，后边会有相关说明。上述方法签名列表中有几点需要指出：

* HttpServerOptions有自己的默认实现，这里主要针对listen的两个参数说明：
  * **`DEFAULT_PORT`**：在不设置端口号的时候，`HttpServer`使用的默认端口号是80：`Default port the server will listen on = 80`；
  * **`DEFAULT_HOST`**：在不设置Host地址时，默认值是`0.0.0.0`，但是该配置的值不是在`HttpServerOptions`中定义，而是在它的父类`NetServerOptions`中定义的：`The default host to listen on = "0.0.0.0" ( meaning listen on all available interfaces )`；
* 「不设置HttpServerOptions」：如果创建`HttpServer`时不传入HttpServerOptions，则直接使用系统默认的`HttpServerOptions`中的定义，但是方法优先，在调用`listen`时传入了port或host则以listen传入参数中的值为主。
* 「设置HttpServerOptions」：如果创建`HttpServer`时传入HttpServerOptions，则直接使用传入的`HttpServerOptions`中的配置，同样是方法优先，调用`listen`时传入了port或host则还是以传入参数值为主。

    整体的配置优先级可归纳为（从高到低）：

1. listen（ port / host ）
2. HttpServerOptions（createHttpServer传入）
3. HttpServerOptions（默认）

    实际上listen方法只能设置host和port两个配置信息，HttpServerOptions其他配置信息还是要通过createHttpServer的方法来传入，而方法`createHttpServer(HttpServerOptions)`是开发人员在“编程方式“中唯一进行自定义配置的位置（使用命令行方式启动Vertx实例除外）。

「**Exception**」在不提供port和host直接启动Vert.x的过程中，您也许会遇到下边错误：

```shell
Sep 11, 2018 7:44:41 AM io.vertx.core.http.impl.HttpServerImpl
SEVERE: java.net.SocketException: Permission denied
```

    出现上述信息过后，证明HttpServer实际上是没有启动成功的，在Unix体系的操作系统中，使用非root账户设置的Web Server的端口号不可低于1024（不包含1024），而Vertx创建的HttpServer实例的默认端口号为80，所以这种情况下回看到上述错误信息，解决上述异常的办法很多，这里介绍几种解决办法：

1. 将您的账号权限提升，使用root账号启动该程序。
2. 调用listen方法时传入大于或等于1024的端口号。
3. 为应用程序单独设置用户的ID使它具有root权限，这个方法可以使得程序像root用户一样执行，不过有可能会带来安全上的风险（使用chown/chmod命令）。

   ```shell
   chown root.root <您的应用程序路径>
   chmod u+s <您的应用程序路径>
   ```
4. 关闭selinux，不推荐，并且该方法我没有验证过，不知道是不是对所有linux系统都适用。
5. 设置服务器的端口转发规则，而服务本身依然运行在高于1024的端口中。
6. 有些Linux系统支持能力的概念，即：普通用户也能够做只有超级用户才能做的任务——包括使用端口，这种情况下可以直接打开该用户的端口绑定能力，设置`CAP_NET_BIND_SERVICE`

   ```shell
   setcap cap_net_bind_service =+ep <您的应用程序路径>
   ```

   1. 注意该方法不是所有的Linux系统都使用，内核在2.1之前的系统中并没有提供“能力”的概念，所以需要检查该系统是否支持。
   2. 另外需要注意的是如果运行的程序是一个脚本，那么该方法是没有办法正常工作的。

    上边的方法可以解决您看到的异常信息，但不同的方法请酌情考虑。

#### 1.1.3. close

    服务器关闭的专用方法，这个方法有两个签名：

```java
    void close();

    void close(Handler<AsyncResult<Void>> completionHandler);
```

    这个方法相对简单，一个异步一个同步，主要就是将当前服务器直接停止。

## 2. 管中窥豹

    这个小节是本章的重头戏，通过对HttpServer的结构分析，希望读者对它彻底了解，虽然在实际项目过程中，我们只会用到`listen/close/createHttpServer`等相关的API，但理解了结构过后，就可以知其所以然，并且可以给读者更加不同的视觉，也希望读者对这部分内容理解过后有所收获，在Vert.x中，接口io.vertx.core.http.HttpServer对应的实现类是io.vertx.core.http.impl.HttpServerImpl。

> 为了方便读者理解，本章节的基础术语如下：
>
> 1. Handler：处理器，执行代码的函数，开发人也可以调用API设置，如实例中使用过的requestHandler。
> 2. Channel：通道，很多地方翻译成管道，这里统一使用通道表示。
> 3. 实例：真正创建好的对象信息，用于描述对象的数据结构，该对象就在当前实例中维护。
> 4. 引用：Java中引用对象的变量，该对象数据结构可能不在类中。
> 5. 池：内部哈希表，用于存储内存级别的 key = value 数据结构。

### 2.1. HttpServer结构

    Vert.x中的HttpServer实例的完整结构图如下：

![](https://1702219450-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-Lv070Tm3mzVeCv0ENzP%2Fuploads%2Fgit-blob-9eb1727e2bc51702125b49fc77a80920a891822d%2F2020-08-18-10-37-25.jpg?alt=media)

> 虚线框部分只是按照包对某些组件进行职责分类，将对应的组件摆放在正确的包中，方便读者理解，并没划分**组件结构**。

    HttpServer实例的内部维护了一个Vertx引用，但这个引用类型并非`io.vertx.core.Vertx`，而是`io.vertx.core.impl.VertxInternal`接口类型，该接口继承自`io.vertx.core.Vertx`，是Vert.x框架内部专用接口，它不对外，也不推荐开发人员直接使用该接口创建Vertx实例，有了它之后当前HttpServer实例就可以直接获取Vertx引用；不仅如此，每个HttpServer实例内部还维护了**两个上下文**引用，这两个上下文的引用类型也并非`io.vertx.core.Context`，而是`io.vertx.core.impl.ContextInternal`接口类型，同属内部专用接口。两个上下文引用一个是在HttpServer实例构造时初始化，称为创建上下文（creatingContext），另外一个则是在调用`listen`时初始化，称为监听上下文（listenContext）。

> Vert.x框架中所有内部专用的接口都是以`Internal`后缀命名，这种接口帮助Vert.x组件在框架内部协同工作，不对外，可它的访问控制符是public定义——理论上，可以从外部访问，对开发人员没有限制，但Vert.x编程中，推荐开发人员不直接使用它，所以这种类型的接口并没有出现在官方教程中。

    实例内部的VertxInternal引用的就是外部创建好的Vertx实例，每个Vertx实例在创建时会维护两个服务器池（内部的两个哈希表），一个服务器池维护的是HttpServer服务器（ServerID = HttpServer），另外一个服务器池维护的是Net服务器（ServerID = NetServer）。每个HttpServer实例内部还定义了一个额外的HttpServer引用（actualServer变量）用来表示真正运行时的HttpServer实例，这个引用会在调用`listen`方法时执行初始化，当我们从外部调用`listen`方法，HttpServer实例会调用`vertx.sharedHttpServers()`方法去获取HttpServer服务器**共享池**，并通过ServerID（host + port，主机名 + 端口）去查找池中是否有此实例，若已经存在则直接从池中初始化HttpServer实例（赋值给actualServer变量）；HttpServer实例内除了实际运行的实例（actualServer）之外，还存储了当前服务器实例运行的ServerID和端口（id变量，actualPort变量）。

    HttpServer实例内部通过Netty提供的**通道组**（`io.netty.channel.group.ChannelGroup`类型）来实现通道的管理，这个通道组的名称为vertx-acceptor-channels，外部`listen`方法调用后，HttpServer实例会创建底层Netty中的`io.netty.channel.Channel`组件，该组件创建过后会被添加到通道组中维护。HttpServer内部还包含了一个**通道连接**池，它将Netty中创建的通道组件（Channel）和HTTP连接（HttpConnection）绑定，每一个Netty中的Channel组件对应一个HttpConnection连接，这个池中的数据会在运行时缓存，关闭时（调用close方法）被清空。

    HttpServer实例中创建了一个**事件循环线程组**对象，是VertxEventLoopGroup类型，该类型实现了Netty中的`io.netty.channel.EventLoopGroup`接口，它用于维护内部线程，处理**事件循环**，这个线程组对象会提供给处理器管理器（HandlerManager）进行初始化，管理器会管理真正的处理器（Handler）。从图可知HttpServer实例内部有四种处理器：分别是**Http请求流处理器**、**WebSocket流处理器**、**Http连接处理器**和**异常处理器**，这四种处理器是HttpServer实例的核心组件，职责如下：

* Http请求流处理器/WebSocket流处理器：这两种处理器是开发人员必须设置的处理器，在创建了HttpServer实例过后，调用`listen`之前必须先调用`requestHandler`方法或`webSocketHandler/websocketHandler`方法来设置处理器，这两种处理器最少设置一个，否则就会遇到异常。——根据官方教程，开发人员应该调用`webSocketHandler`来设置WebSocket处理器（S大写的版本），两个API内部是一样的，没有区别。
* Http连接处理器：连接处理器可以让外部开发人员直接获取HttpConnection的实例，直接调用`connectionHandler`方法设置，在开发过程中内容更偏底层，控制力度更大。
* 异常处理器：异常处理器用于在出现异常时执行，调用`exceptionHandler`方法进行设置。

    这四种处理器主要分成两大类：**常规处理器**和**流处理器**，其中Http连接处理器和异常处理器是`io.vertx.core.Handler<T>`接口类型的常规处理器，而Http/WebSocket流处理器是`io.vertx.core.streams.ReadStream<T>`接口类型。这四种处理器必须在调用`listen`之前执行设置和初始化。Vert.x编程中，开发人员一定要分清楚**启动周期**和**请求周期**两个不同的代码执行时间，Vertcle的start方法是启动周期执行，本章中HttpServer实例的创建、监听都是在这个周期中完成的；处理器部分：启动周期只负责设置处理器，处理器内部的代码只会在请求周期执行，在调用启动周期`listen`方法之前，处理器的设置工作必须全部完成。

    除此之外，HttpServer实例内部还包含支持SSL的功能模块（由SSLHelper实现）以及健康检查模块（由HttpServerMetrics实现）。

### 2.2. HttpServer生命周期

    HttpServer的结构复杂，从功能上，它主要起到了一个衔接作用，Vert.x中使用的服务器是Netty内置的纯异步Java服务器，HttpServer主要负责就是把Vert.x和Netty连接起来，并且实现HTTP和WebSocket的访问——这也是我们开发RESTful容器的基础。看完了前一个小节中HttpServer的内部结构，那么这个小节我们换一个视角：创建、监听、销毁，从生命周期的角度看HttpServer实例内部执行原理。

#### 2.2.1. 创建

    在Vert.x框架中，通过`vertx.createHttpServer`方法的调用来实现，传入两个参数：一个是Vertx实例引用，从结构图中可以知道，这个Vertx实例引用会传递给内部的VertxInternal引用并执行初始化；另外一个是HttpServerOptions对象，该对象包含了初始化当前HttpServer实例的所有配置信息；它的构造函数签名如下：

```java
public HttpServerImpl(VertxInternal vertx, HttpServerOptions options){
    // ...
}
```

    传入的两个参数直接在HttpServer实例内部维护。除此之外，HttpServer在构造的时候还会做两件事：

* 初始化两个上下文中的创建上下文（creatingContext），创建上下文在初始化时直接调用`vertx.getContext()`实现，它不会创建新的上下文，而是直接从传入的Vertx实例中直接获取上下文；HttpServer实例在创建时还有一个限制：**multi-threaded worker**类型的Verticle上下文中不允许使用HttpServer，否则会抛出`IllegalStateException`异常。
* 构造SSLHelper对象。HttpServer中的SSL功能模块是依靠SSLHelper类来配置，该类会从传入的HttpServerOptions中读取SSL相关配置信息，然后执行内部计算，生成SSLHelper实例。计算内容包括：使用的SSLEngine是什么？证书是什么（keyCert, trust）？是否启用ALPN？这些和SSL相关的内容我们会在后边单独找个章节来详解。

    以上就是HttpServer实例在创建的时候所做的事情。

#### 2.2.2. 监听

    HttpServer实例最复杂的内部流程是监听（listen方法被调用），这个小结我们就来按步骤详解监听流程它做的事情，监听的上层API接口很多，但内部执行流程是统一的。先看两段代码：

```java
    // 不设置 Handler
    final Vertx vertx = Vertx.vertx();
    final HttpServer server = vertx.createHttpServer();
    server.listen(8099);
    // ---------------------- 分割线 --------------------
    // 重复监听
    server.listen(8099);
    server.listen(8000);
```

    「**Exception**」分别运行上边两段代码，您就可以在控制台看到如下异常输出：

```shell
// 不设置 Handler
Exception in thread "main" java.lang.IllegalStateException: Set request or websocket handler first
// 重复监听
Exception in thread "main" java.lang.IllegalStateException: Already listening
```

    **合法性检查**——前文中提到了HttpServer实例内部包含了四种处理器，在listen调用时，会检查其中的两种：Http流请求处理器和WebSocket流请求处理器，这两个处理器最少必须设置其中一个，否则就会遇到上述第一个异常；而listen方法不可以重复调用，重复调用就会遇到上边的第二个异常；——两个异常的区别在于：**如果出现了不设置处理器的异常，服务器直接无法启动，程序退出，但若遇到了重复监听的异常，服务器依旧会正常运行，但新监听的端口无效**，如示例中最终服务器会运行在`8099`端口而忽略第二次监听的`8000`端口。

    **环境初始化**——第二个步骤是环境初始化，该方法会先初始化监听上下文（listenContext），监听上下文的初始化方法和创建上下文有些区别，它调用的是`vertx.getOrCreateContext()`方法，如果无法获取到监听上下文信息，则会直接创建一个新的上下文环境。监听的环境初始化会做下边几件事：

1. 构造`io.vertx.core.net.SocketAddress`对象，然后解析该对象拿到主机名host（默认是localhost）、端口号port（默认0）。
2. 从HttpServerOptions中读取HTTP版本信息，Vert.x中支持HTTP的三种版本：1.0、1.1、2.0；由于Vert.x框架中的Verticle有两种：Agent和Worker，这两种的上下文类型不同，而HTTP 2.0是不支持Worker类型的，所以在读取版本信息时，针对Worker类型的Verticle会直接将2.0版本过滤掉。
3. 利用读取到的HTTP版本信息对SSLHelper模块进行协议设置。

    **实例初始化**——第三个核心步骤是计算内部HttpServer实例引用（actualServer变量），listen方法调用后，当前HttpServer实例会先从Vertx引用中获取HTTP共享服务器池，并对其进行同步加锁。加锁过后第一个步骤是配合HTTP共享服务器池计算共享标记、构造ServerID、获取实际运行HttpServer实例引用：

![](https://1702219450-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-Lv070Tm3mzVeCv0ENzP%2Fuploads%2Fgit-blob-c9f88c25a33e3951e43d164387211c5a12058955%2F2020-08-18-15-25-54.jpg?alt=media)

    上图中的最后一个分支（灰色部分）是第一次创建流程，其余两个分支是直接获取流程，两个流程会影响后续计算，如果通过新计算的ServerID可以直接从Vertx实例中的HTTP共享服务器池中读取到HttpServer实例信息，证明该实例已经被创建过一次，这种情况下，计算流程会相对简单：

1. 直接将找到的HttpServer实例引用传给内部actualServer引用。
2. 结合初始化好的**监听上下文**（listenContext）为该HttpServer实例添加Http处理器（包括上述的四种）。
3. 构造健康检查组件`io.vertx.core.spi.metrics.VertxMetrics`，如果启用了该模块则直接初始化。

    倘若没有在HTTP共享服务器池中找到对应的HttpServer实例，那么listen方法会走一个完整的创建流程：

1. 实例化Netty中的服务器通道组，构造一个`io.netty.channel.group.DefaultChannelGroup`实例，名称为vertx-acceptor-channels，默认执行器则使用`io.netty.util.concurrent.GlobalEventExecutor`。
2. 创建Netty中的服务器启动器对象（`io.netty.bootstrap.ServerBootstrap`类型），并且在创建完成过后，将当前HttpServer实例的事件循环组（EventLoopGroup）作为**子组**添加到外围Vertx实例的事件循环接收器组中；补充一下，Vertx实例内部的事件循环组有两种：IO类型（线程前缀`vert.x-eventloop-thread-`）和ACCEPTOR类型（线程前缀`vert.x-acceptor-thread-`），这两种事件循环组的职责有所不同，而在构造事件循环组时，ACCEPTOR类型的线程池大小是1，IO类型（开发人员常用类型）的线程池大小则是配置的。这里提到的启动器对象会将HttpServer实例中使用的事件循环组添加到ACCEPTOR类型事件循环组中，而不是IO类型。
3. 调用Vertx实例中的`io.vertx.core.net.impl.transport.Transport`对象设置服务器连接信息，这些连接信息都是在HttpServerOptions中配置。
4. 调用SSLHelper的validate方法对Vertx实例执行SSL校验并设置SSL环境信息，如果校验通过，所有的SSL的信息都会执行初始化；读者需要区别SSLHelper的**创建和校验**步骤，创建SSLHelper是在构造函数中进行，但它并不会初始化SSL的上下文环境，而真正初始化SSL执行上下文是在当前步骤中。
5. 设置当前HttpServer实例的子通道信息（childChannel），包括设置子处理器（childHandler），健康检查设置在这一步初始化。
6. 结合初始化好的**监听上下文**（listenContext）为该HttpServer实例添加Http处理器（包括上述的四种）。
7. 构造新的`Channel`通道组件，并且将该通道组件添加到**通道组**中，如果构造失败则标记当前HttpServer实例初始化失败，通过ServerID将它从HTTP共享服务器池中移除。
8. 初始化完成过后，若可共享（第一步计算的结果），会将当前初始化好的HttpServer实例添加到Vertx引用的HTTP共享服务器池中。

    **添加监听器/回调处理器**——上边步骤完成后，最早的结构图中的actualServer变量就有值了，而且也整体初始化完成以及更新了Vertx中的HTTP共享服务器池，最后一个步骤就是从**actualServer有值**开始，其实这个步骤是第三步的后续处理，看这个步骤之前先参考下图：

![](https://1702219450-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-Lv070Tm3mzVeCv0ENzP%2Fuploads%2Fgit-blob-19fd87bcaf54fe3f76c382d68ebbb0f54b508814%2F2020-08-18-16-38-47.jpg?alt=media)

    图中演示了从代码面上真正可以窥见的Channel后续流程，主要包括**监听器流程和回调处理流程**。通道Channel的创建在listen方法中是异步创建，内部存在三次转换：`Netty的Future`到`Promise`再到`Future`，监听器流程出现在`Netty的Future`到`Promise`转换过程，它用于监听Channel的创建结果。

* 如果创建成功，则调用complete方法通知Promise，去触发onComplete最后的回调。
* 如果创建失败，则调用fail方法，并且将当前HttpServer实例从HTTP共享服务器池中移除。

    回调处理器流程中，主要针对当前HttpServer的实际运行实例进行**校正**（比如actualPort的变化），并且运行监听上下文信息，如果出现错误，则需要将资源清除如关闭健康检查器、设置监听标记等。HttpServer在listen方法调用后，最后一个步骤创建Channel时是异步的，可能读者会担心在方法执行完后无法访问监听端口的信息，因为这个过程会有一个时间差，若想要知道什么时候启动完成，则可以在调用listen时使用异步调用接口（带`Handler<AsyncResult<HttpServer>>`参数的方法），这个处理器会在onComplete方法的最后执行，这样您就可以精确捕捉到服务器启动完成的时间戳，设置部分打印日志来标记服务器完成了。

#### 2.2.3. 销毁

    HttpServer实例的最后一个生命周期是销毁，直接调用close()方法来销毁该实例，它的内部步骤如下：

1. 读取Vertx实例中的监听上下文信息，根据监听标记（是否监听）来决定是否直接执行最后步骤清除上下文。
2. 获取内部HttpServer引用（actualServer变量），调用引用中的处理器管理器移除对应的Http处理器，这个过程完成后四种处理器都会被直接移除。
3. 执行上下文清除的最后步骤。
4. 移除当前HttpServer实例的CloseHooker。

***

### 「Netty中的Pipeline」

    在解析HttpServer实例的结构时，我们接触到了Netty中一个核心组件通道（Channel），为了让读者了解Channel的原理，我们一起来看看Netty中的Pipeline[^4](https://github.com/silentbalanceyh/vertx-book/blob/master/chapter03/%E3%80%8ANetty%E9%82%A3%E7%82%B9%E4%BA%8B%EF%BC%9AChannel%E4%B8%AD%E7%9A%84Pipeline%E3%80%8B%EF%BC%8C%3Chttps:/github.com/code4craft/netty-learning/blob/master/posts/ch3-pipeline.md%3E)的原理。讲Pipeline之前先看看Netty中的Unsafe，这里的Unsafe和`sun.misc.Unsafe`是不可以同日而语的，它实际上是Netty中定义的`io.netty.channel.Channel#Unsafe`，按照官方文档的说法——Unsafe函数不允许被用户代码使用，这些函数是真正用于数据传输操作的，而且必须被IO线程调用。换句话说，真正依赖于底层协议/方案的实现是通过Unsafe封装过后的结果。本节不详细讲解原生的`sun.misc.Unsafe`\[^3]，主要聚焦在Netty的Unsafe中，它在Channel内部定义，所以Channel才是核心。

#### 起源：Channel

    叫做起源是因为Channel几乎填充了整个Netty，在Netty中它是通讯的载体，简单说当Netty中的线程需要相互之间协同工作时，Channel就会登场。——但是真正在通讯过程执行逻辑的代码组件是ChannelHandler，Handler通常会翻译成处理器。

    其次是姗姗来迟的ChannelPipeline，它又是何物？——其实可以理解为一个ChannelHandler的集合，它是ChannelHandler的容器，而一个Channel中只有一个ChannelPipeline，所有的ChannelHandler都会按照一定的顺序在ChannelPipeline中组织起来。Netty数据和状态的载体一般使用事件对象`Event`来存储，如：

* 传输数据会使用`MessageEvent`；
* 状态改变会使用`ChannelStateEvent`；
* 当Channel执行操作（调用处理器处理时）会产生对应的`ChannelEvent`；

    这些`Event`对象最终都会发送给ChannelPipeline，最后按照ChannelPipeline对ChannelHandler的编排顺序执行，先看看下边的图

![](https://1702219450-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-Lv070Tm3mzVeCv0ENzP%2Fuploads%2Fgit-blob-43d0adc5a0fdc0baaeeeea6a35941899dac096ae%2F2020-08-19-00-34-26.jpg?alt=media)

    举个例子，一个数据最开始被Channel接受时应该是一个`MessageEvent`，如图所示，最终处理后会生成一个数据对象，最终生成一个新的`MessageEvent`并且发送给下一个Handler来处理。从上图结构可以知道，Channel真正的通讯执行者是在Handler中来完成的，而核心流程是位于ChannelPipeline（只有它才包含了多个Handler）。

#### 渗透：ChannelPipeline

    Netty中的Channel的核心流程实际上是`ChannelPipeline`，而它包含了两条线路：Upstream和Downstream：

* Upstream：上行，接收到的消息、被动的状态改变
* Downstream：下行，发送的消息、主动的状态改变

    实际上`ChannelPipeline`接口包含了两个重要的方法分别对应上述两条心路：`sendUpstream(ChannelEvent e)`和`sendDownstream(ChannelEvent e)`；对应的ChannelPipeline中的Handler也包含了两类，并且每个类中都包含了对应的方法

* ChannelUpstreamHandler：对应的方法为handleUpstream
* ChannelDownstreamHandler：对应的方法为handleDownstream

    最终看看下边的图来解释这个过程：

![](https://1702219450-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-Lv070Tm3mzVeCv0ENzP%2Fuploads%2Fgit-blob-3794f57f1f9135b362020c19a5b2091b3d06163e%2F2020-08-19-00-36-23.jpg?alt=media)

    从上图可以看到多了一个叫做`ChannelSink`的东西，那么什么是ChannelSink呢？它有一个重要方法`eventSink`，这个方法可以接受任意一个ChannelEvent。"sink"的本意是下沉，那么ChannelSink实际上可以理解为Channel下沉的地方，——其实也可以换一种说法：处于末尾的万能Handler。

    此处需要注意的地方就是：在一条”流”里，一个ChannelEvent并不会主动去经过所有的Handler，某个Handler中拿到的数据是由**上一个**Handler显示调用`sendUp(Down)stream`产生的，并且在处理完成过后交给下一个Handler。也就是说，每个Handler可以接收一个ChannelEvent，处理结束后若要继续，就显示调用`sendUp(Down)stream`主动发起一个新的事件，如果不需要继续处理，那么就在这里结束，只要不主动发起那么即使它后边有对应的Handler也不会执行。——这样的设计会导致整个结构中拥有最大的灵活性，当然对Handler本身的顺序也有严格要求。

#### 终焉：Pipeline解决的问题

    综上所述，实际上Netty的Pipeline的机制我相信读者已经有了大致的了解了，那么最后总结一下（这里就不按照引用作者的想法去阐述了）：

1. 从Handler这种机制可以看到，Vert.x中使用的就是这种机制，一个Handler和另外一个Handler编连成一个完整的Handler的链，请求经过处理链去执行。被执行的数据在Netty中抽象成了ChannelEvent，这样抽象的好处是在通讯过程中形成了统一的数据规范（相当于有了一个统一的VO——Value Object），那么Handler拿到数据过后就只关心ChannelEvent数据本身，不去考虑底层的事，如同引用文中提到的编码、解码等，Vert.x Web项目中的RoutingContext的Web请求处理链和这种设计就有异曲同工之处。
2. 不仅仅如此，Netty官方有一种说法，它是一个纯异步Java服务器。使用过OIO（线程发起请求）和NIO（Reactor模型）的工程师应该都清楚，这两种模式主要的区别在于编程风格，即便是NIO模式同步和异步的编程风格也很大，所以实际上ChannelPipeline还做了一个事情就是把同步和异步的原理封装在底层了，统一使用了这样一种机制来实现同步异步处理，那么开发人员就可以不去关心这个动作是同步还是异步的。
3. 回到Vert.x中，其实可以看到，在一个Handler的内部，还可以开启第二层异步（此时就需要工程师对异步的编程有一定的基础），在这种情况下，Netty的Pipeline的机制反而容易拆分过后去实现这种纯异步流。异步编程最复杂的地方是找到数据的同步点，只有运行的时候才能捕捉真正的程序数据流，不论使用了lambda表达式还是使用了Future，在Vert.x中编程时候都需要考虑到数据流的问题。

***

### 2.3. 小结

    到这里HttpServer实例的内部结构和核心生命周期就告一段落了，这部分内容比较复杂，有很多知识点在写本书时并没有拓展出去，不过读者不用担心，若仅做应用开发，可以把这部分内容当做**附加内容**仅作参考，实际开发中通常只会几行代码去完成HttpServer实例的创建，更多的实战步骤是在下一个小结中，但是了解它的内部结构和原理对您认识Vert.x框架是有好处的。

## 3. 又见Options

    实际开发过程中，和HttpServer实例最相关的是`HttpServerOptions`这个类，这是开发人员唯一可以近距离接触的一个类，它提供了HttpServer实例的所有配置信息，本小节对它进行结构剖析和解读，让开发人员可以根据自己的需求配置HttpServer实例。首先HttpServerOptions是一个家族企业，追溯到顶层有四个Options（按继承树）：

* io.vertx.core.net.NetworkOptions：网络配置
* io.vertx.core.net.TCPSSLOptions：TCP/SSL协议配置
* io.vertx.core.net.NetServerOptions：网络服务器配置
* io.vertx.core.http.HttpServerOptions：HTTP服务器配置

> 本章主要介绍每种配置的相关细节以及可配的内容，配置数据的原理方面不做过多的讨论，有必要引入的地方我会加上部分解析。

### 3.1. NetworkOptions

    第一个配置是网络配置，它是一个抽象类，定义如：

```java
public abstract class NetworkOptions
```

    它包含了以下配置项：

* **logActivity**：是否开启网络日志，基于Netty的Pipeline机制实现。
* **receiveBufferSize**：TCP网络接受缓冲区大小。
* **sendBufferSize**：TCP网络发送缓冲区大小。
* **reuseAddress**：是否启用地址重用。
* **reusePort**：是否启用端口重用。
* **trafficClass**：设置“流量等级”。

    这些配置项的属性和默认值对应参考下表（如果是JSON格式，则直接将属性名作为Json节点传入即可。）：

| 属性名               | set方法名               | 默认值                                 |
| ----------------- | -------------------- | ----------------------------------- |
| logActivity       | setLogActivity       | DEFAULT\_LOG\_ENABLED = false       |
| receiveBufferSize | setReceiveBufferSize | DEFAULT\_RECEIVE\_BUFFER\_SIZE = -1 |
| reuseAddress      | setReuseAddress      | DEFAULT\_REUSE\_ADDRESS = true      |
| reusePort         | setReusePort         | DEFAULT\_REUSE\_PORT = false        |
| sendBufferSize    | setSendBufferSize    | DEFAULT\_SEND\_BUFFER\_SIZE = -1    |
| trafficClass      | setTrafficClass      | DEFAULT\_TRAFFIC\_CLASS = -1        |

#### 3.1.1 IPv6 流量等级

    IPv6中的流量等级\[^1]（又称为“通信量等级”、“流量类别”），存在于IPv6的头部数据结构中，如下（RFC2460）：

![](https://1702219450-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-Lv070Tm3mzVeCv0ENzP%2Fuploads%2Fgit-blob-0ce5a6d710e2bfb7d9dee2e4e3cde8669c9ad76d%2F2020-08-18-17-46-16.jpg?alt=media)

    了解计算机网络的读者可以很清楚地知道，IPv6的头部是IPv4的精简版，它除去了不必要的低频使用字段，并且增加了可以更好支持实时传输的一个字段。

* 版本Version：该版本指定了IP协议的版本，默认为6，长度4位；不同的IP协议版本会使用不同的数据报格式。
* 流量等级Traffic Class：长度8位，它表示IPv6数据包的类型或优先级，它的功能和IPv4中的服务类型字段（Type Of Service，TOS字段）很相似。传输类型前边6位是DSCP字段（RFC 2474定义），后2位用于ECN（RFC 3168定义）。
* 流标签：长度20位，它表示当前数据包是源和目标中的一个，需要中间IPv6路由器进行特殊处理。它用来实现对数据包按照优先级发送，如优先发送实时数据（语音、视频），如果设置为0则表示采用默认的路由处理方法。如果想要将某个数据流量区分出来，中间路由可以通过这组数据包的源地址、目标地址、流标签进行判断，使用非零值即可，详细使用方法可以参考（RFC 3697）。
* 负载长度Payload Length：16位，它表示IPv6的有效负载长度，包括扩展头部和上层PDU（Protocol Data Unit，协议数据单元），这个值可支持65535字节的有效负载，若超过则该字段会设置成0，并动用逐跳（Hop by Hop）的可选扩展头部中的超大有效负载（Jumbo Payload）选项。
* 下一个头部Next Header：8位，如果IPv6没有扩展头部，那么这个值的作用和IPv4的上层协议字段一样，如果启用了扩展头部，那么这个值则表示第一个扩展头部或上层PDU的协议类型，它根据是否使用扩展头部其含义会有所区别。
* 跳数限制Hop Limit：8位，如果数据包转发，那么每台路由器对该字段的值减1，如果减为0则丢弃该包，所以它表示IPv6数据包能经过的最大链路数。这个值有点类似于IPv4中的TTL字段，只是它不像TTL一样可以表示数据包在路由队列中的时间；数据包被丢弃的同时，它还会发送ICMPv6超时信息到源主机。
* 源地址：表示起始主机的IPv6地址，长度128位。
* 目标地址：表示当前目的节点的IPv6地址，长度128位，大多数情况这个地址是最终目的地址，如果启用了路由扩展头部，那么目标地址会被设置成为中间地址。

    Vert.x中，流量等级默认值为-1，调用setTrafficClass可设置流量等级，它的设置范围只能是`0 ~ 255`，一旦越界，调用时会抛出异常。

    流量等级在IPv4到IPv6的升级过程中有所变更，最早在RFC791中定义了TOS中的前三位，划分为8个优先级，7和6保留，从5开始依次是：语音、视频会议、呼叫信号、高优先级数据、低优先级数据、尽力服务数据；随着网络在实际部署中的应用，八个优先级远远不够，所以RFC 2474\[^2]对它进行了重新定义，前6位是DSCP字段，后2位是ECN字段。

    DSCP是差分服务代码点（Differentiated Services Code Point），它有两种形式：数字形式、关键字形式。若使用数字形式，DSCP部分有6位，范围从`0 ~ 63`，可定义64个等级；若使用关键字形式，则它用来描述逐跳行为（PHB，Per-Hop Behavior），目前主要分成四大类：BE（000 000）默认、CS（aaa 000）类选择器、EF（101 110）加速转发、AF（aaa bb0）确保转发。逐跳行为选择的时候，ECN字段会被忽略，只要六位匹配，那么就会选择对应的逐跳行为，上述四种类型是IETF（The Internet Engineering Task Force，国际互联网工程任务组）已经标准化的一部分。从应用场景分析，逐跳行为会影响传输，所以针对特定的使用场景，设置该参数是可以提高传输效率的，所以除开Internal业务以外，若要开发一些类似直播、专线、语音相关的网络应用，可以考虑在这个参数上设置。

    ECN是显式拥塞通知（Explicit Congestion Notification），它的基本原理如：路由器在出现拥塞时通知TCP，当TCP段传递时，路由器使用ENC来记录拥塞，当TCP段数据到达后，接收方知道报文在某个位置经历过拥塞，然而，需要了解拥塞发生情况的是发送方，而非接收方。因此，接收方使用下一个ACK通知发送方有拥塞发生，然后，发送方做出响应，缩小自己的拥塞窗口。

#### 3.1.2. ReuseX

    reuseAddress和reusePort两个配置对应底层TCP套接字（Socket）中的`SO_REUSEADDR`和`SO_REUSEPORT`参数，直接影响了Socket绑定（host + ip）的成功与否，Socket起源于Unix系统，它的基本哲学是：“一切皆是文件！”。一个Socket的协议是在socket()初始化时设置好的，而在bind()阶段它会设置源地址和源端口，其次会调用connect()来设置目标地址和目标端口，如果在编程过程中，我们将端口设置成0（Vert.x的默认行为），它的语义就是：任意端口，此时系统会选择一个具体的端口绑定，它通过同样的方式选择地址（如IPv4中的`0.0.0.0`和IPv6中的`::`）。和端口不同的是，系统会选择具体的端口做绑定，而对于地址，只要是本机网络端口所有合法地址，它都可以绑定，而这个过程是发生在请求来临时，因为Socket无法在连接时同时绑定所有合法地址，TCP请求过来时，它会选择一个合适的源地址并且绑定。

    默认情况下，任意两个Socket都无法绑定到相同的源IP地址和源端口，若端口号不同，那么源地址实际上没什么关系。实际如下：

![](https://1702219450-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-Lv070Tm3mzVeCv0ENzP%2Fuploads%2Fgit-blob-f6a9d3434717b040e9e952b23d1340dbbbabb6d9%2F2020-08-18-20-16-39.jpg?alt=media)

    这种规则对所有的主流操作系统都一样，但一旦牵涉到**地址重用**和**端口重用**，操作系统之间的差异性就体现出来了，由于主流操作系统的Socket实现大部分都参考了BSD，这里主要分析BSD中两个参数`SO_REUSEADDR`和`SO_REUSEPORT`的作用。参考下表：

> 假设socketA已经绑定成功，再创建socketB，0.0.0.0 表示任意地址，最终结果就是创建socketB的结果。

| SO\_REUSEADDR | socketA          | socketB          | 结果             |
| ------------- | ---------------- | ---------------- | -------------- |
| N/A           | 10.0.0.1:1017    | 10.0.0.1:1017    | 错误, EADDRINUSE |
| N/A           | 0.0.0.0:1017     | 0.0.0.0:1017     | 错误，EADDRINUSE  |
| N/A           | 10.0.0.1:1017    | 192.168.0.1:1017 | 绑定成功           |
| N/A           | 192.168.0.1:1017 | 10.0.0.1:1017    | 绑定成功           |
| 禁用            | 0.0.0.0:1017     | 192.168.0.1:1017 | 错误，EADDRINUSE  |
| 禁用            | 192.168.0.1:1017 | 0.0.0.0:1017     | 错误，EADDRINUSE  |
| 启用            | 0.0.0.0:1017     | 192.168.0.1:1017 | 绑定成功           |
| 启用            | 192.168.0.1:1017 | 0.0.0.0:1017     | 绑定成功           |

    实际上`SO_REUSEADDR`对通配地址`0.0.0.0`会有影响。Socket中有一个发送缓冲区，调用send()函数后，并不意味着数据发送了，而是进入了发送缓冲区中，对TCP协议而言，如果不立刻发送，数据加入缓冲区和真正发送之间会有一个延迟，此时如果调用了close()关闭TCP套接字，可能发送缓冲区中还保留着等待发送的数据。为了不让数据丢失，TCP的套接字会进入`TIME_WAIT`状态，这种状态会持续到数据真正发送或者超时，这样的机制让TCP成为了数据可靠协议。

    操作系统如何处理`TIME_WAIT`呢？如果没有设置`SO_REUSEADDR`，一个处于`TIME_WAIT`的Socket仍然被认为绑定在源地址和端口，任何其他试图在同样地址和端口上绑定Socket的行为都会失败直到原始Socket真正被关闭。——所以绝大多数在一个Socket关闭后立即将地址和端口绑定到新的Socket的行为会失败。若设置了`SO_REUSEADDR`后，这种行为不会失败，但会被忽略，而且设置了该参数，您可以将相同地址绑定到不同的Socket上，虽然当Socket处于`TIME_WAIT`时，您做这样的事（绑定相同地址和端口到新的Socket）会导致未知结果，但幸运的是这种情况发生概率很小。最后：只有在您想绑定的Socket上开启了**地址重用**功能才会生效，它并不会检查已绑定或处于`TIME_WAIT`的Socket是否设置了该选项，简单说，就是绑定的成功与否只会检查当前绑定的Socket是否设置了`SO_REUSEADDR`参数。

    `SO_REUSEPORT`的含义与绝大部分人对`SO_REUSEADDR`的理解是一致的，只要这个Socket在绑定之前设置了该参数，它允许您将多个Socket绑定到相同的地址和端口。若第一个Socket没有设置此参数，那么后续所有的Socket都无法成功绑定源地址和端口，除非等到第一个Socket关闭过后。而且`SO_REUSEPORT`和`SO_REUSEADDR`并不等价，只要没有设置它，那么在相同地址和端口上绑定Socket的行为都会失败，哪怕这个原始的Socket处于释放阶段（`TIME_WAIT`），所以为了使得其他的Socket绑定成功，就必须设置`SO_REUSEADDR`或者`SO_REUSEPORT`又或者二者都设。

    最后说明一点，一个连接看起来是“协议 + 主机 + 端口”的三元组，但实际上它是被一个五元组“协议 + 主机 + 端口 + 地址重用 + 端口重用”定义的，任意两个连接的五元组不能完全一样，否则操作系统内核就无法识别，而在Vert.x框架中，默认只开启了**地址重用**，并没有开启**端口重用**功能，所以它默认使用四元组定义了连接，这个默认值的考虑是基于操作系统的，因为Windows系统上并没有**端口重用**的选项参数。

### 3.2. TCPSSLOptions

    第二个配置是TCP/SSL配置，它定义如下：

```java
public abstract class TCPSSLOptions extends NetworkOptions
```

    它包含了以下配置项：

* **tcpNoDelay**：设置`TCP-no-delay`的值（TCP中的`NO_DELAY`）；默认为true，该值为true时禁用了TCP中的Nagle算法。
* **tcpKeepAlive**：设置TCP连接的keep alive属性，启用KeepAlive机制，是否为一个长连接；默认为false——需要说明的是KeepAlive并不是TCP协议规范中的一部分，只是几乎所有的TCP/IP协议栈都支持这种机制。
* **soLinger**：设置（TCP中的`SO_LINGER`），它用于设置延迟关闭的时间，等待套接字发送缓冲区中的数据发送完成。
* **usePooledBuffers**：设置Netty服务器是否使用缓冲池，默认值为false。
* **ssl**：在连接中是否启用SSL，默认禁用。
* **sslHandshakeTimeout/sslHandshakeTimeoutUnit**：SSL握手时间值和时间单位，默认10秒。
* **idleTimeout/idleTimeoutUnit**：默认的空闲超时时间值和时间单位，默认为0秒；该设置需要和KeepAlive机制进行区分。
* **useAlpn**：是否在TLS协议中使用ALPN协议，ALPN全称为Application Layer Protocol Negotiation，它是TLS的一种扩展，允许在安全连接基础上进行应用层协议的协商，它支持任意应用层协议的协商，目前最多的是支持HTTP2的协商
* **tcpFastOpen**：设置（TCP中的`TCP_FASTOPEN`）选项，默认为false。
* **tcpCork**：设置（TCP中的`TCP_CORK`）选项，默认为false。
* **tcpQuickAck**：设置（TCP中的`TCP_QUICKACK`）选项，默认为false。

    这些配置项的属性和默认值对应参考下表：

| Json配置节点                | set方法名                     | 默认值                                    |
| ----------------------- | -------------------------- | -------------------------------------- |
| tcpNoDelay              | setTcpNoDelay              | DEFAULT\_TCP\_NO\_DELAY = true         |
| tcpKeepAlive            | setTcpKeepAlive            | DEFAULT\_TCP\_KEEP\_ALIVE = false      |
| soLinger                | setSoLinger                | DEFAULT\_SO\_LINGER = -1               |
| usePooledBuffers        | setUsePooledBuffers        | DEFAULT\_USE\_POOLED\_BUFFERS = false  |
| ssl                     | setSsl                     | DEFAULT\_SSL = false                   |
| idleTimeout             | setIdleTimeout             | DEFAULT\_IDLE\_TIMEOUT = 0             |
| idleTimeoutUnit         | setIdelTimeoutUnit         | TimeUnit.SECONDS                       |
| sslHandshakeTimeout     | setSslHandshakeTimeout     | DEFAULT\_SSL\_HANDSHAKE\_TIMEOUT = 10L |
| sslHandshakeTimeoutUnit | setSslHandshakeTimeoutUnit | TimeUnit.SECONDS                       |
| useAlpn                 | setUseAlpn                 | DEFAULT\_USE\_ALPN = false             |
| tcpFastOpen             | setTcpFastOpen             | DEFAULT\_TCP\_FAST\_OPEN = false       |
| tcpCork                 | setTcpCork                 | DEFAULT\_TCP\_CORK = false             |
| tcpQuickAck             | setTcpQuickAck             | DEFAULT\_TCP\_QUICK\_ACK = false       |

    上述配置都是简单属性，不牵涉到内置对象的创建，TCPSSLOptions中还包含了SSL相关的一些专用配置类，这些类将在后续章节中讨论，这些复杂属性包括：

* io.vertx.core.net.JdkSSSLEngineOptions
* io.vertx.core.net.JksOptions
* io.vertx.core.net.OpenSSLEngineOptions
* io.vertx.core.net.PemKeyCertOptions
* io.vertx.core.net.PemTrustOptions
* io.vertx.core.net.PfxOptions

    复杂属性对应的定义可参考下边表格：

| Json配置节点                        | set方法名                             | 实例化的类/格式                           |
| ------------------------------- | ---------------------------------- | ---------------------------------- |
| jdkSslEngineOptions             | setJdkSslEngineOptions             | JdkSSLEngineOptions                |
| keyStoreOptions                 | setKeyStoreOptions                 | JksOptions                         |
| openSslEngineOptions            | setOpenSslEngineOptions            | OpenSSLEngineOptions               |
| pemKeyCertOptions               | setPemKeyCertOptions               | PemKeyCertOptions                  |
| pemTrustOptions                 | setPemTrustOptions                 | PemTrustOptions                    |
| pfxKeyCertOptions               | setPfxKeyCertOptions               | PfxOptions                         |
| pfxTrustOptions                 | setPfxTrustOptions                 | PfxOptions                         |
| trustStoreOptions               | setTrustStroeOptions               | JksOptions                         |
| crlPaths                        | addCrlPath                         | 包含String元素的JsonArray               |
| crlValues                       | addCrlValue                        | 包含String元素的JsonArray，添加时转成Buffer处理 |
| enabledCipherSuites             | addEnabledCipherSuite              | 包含String元素的JsonArray               |
| enabledSecureTransportProtocols | setEnabledSecureTransportProtocols | 包含String元素的list                    |

    TCPSSLOptions配置中的大部分配置项信息都牵涉到TCP协议和SSL协议，书中提供了尽可能多的TCP协议中的原语如`TCP_CORK`，读者可以使用这些作为关键字去查找更多的资料，这里不做说明，SSL协议的部分内容会在后续SSL章节专程讨论，这里也略过。

### 3.3. NetServerOptions

    第三个配置是网络服务器配置，它包含以下配置项：

* **acceptBacklog**：默认值为1024，用于设置默认的连接缓存相关的参数，它一般表示在拒绝额外的请求之前，能接受的连接数。
* **port**：网络服务器的端口号，在NetServerOptions类中设置的默认参数为0，它表示随机选择一个可用的端口号。
* **host**：主机IP地址，默认值为`0.0.0.0`。
* **sni**：设置服务器是否支持SNI——SNI全称为Server Name Indication，它用于改善服务器和客户端SSL/TLS的一个扩展。
* **clientAuth**：设置SSL/TLS的模式，在Vert.x中，该属性的类型为一个枚举类型：`io.vertx.core.http.ClientAuth`，它用于描述客户端和服务器之间对SSL/TLS支持的种类——包含了三个值。
  * ClientAuth.**NONE**：不需要任何客户端认证。
  * ClientAuth.**REQUEST**：如果客户端提供了身份验证则接受该认证结果。设置了这种模式后，若客户端没有提供任何身份认证，那么服务端和客户端继续协商处理该请求。
  * ClientAuth.**REQUIRED**：严格要求客户端必须提供身份认证，若客户端没有提供，则服务端拒绝该请求。

> 3.9.x之前的版本还有一个clientAuthRequired，但这个配置现在的版本已经移除了，这里就不讲。

    这些配置项的属性和默认值对应参考下表：

| Json配置节点      | set方法名           | 默认值                                     |
| ------------- | ---------------- | --------------------------------------- |
| acceptBacklog | setAcceptBacklog | DEFAULT\_ACCEPT\_BACKLOG = -1           |
| port          | setPort          | DEFAULT\_PORT = 0                       |
| host          | setHost          | DEFAULT\_HOST = "0.0.0.0"               |
| sni           | setSni           | DEFAULT\_SNI = false                    |
| clientAuth    | setClientAuth    | DEFAULT\_CLIENT\_AUTH = ClientAuth.NONE |

> SNI全称是Server Name Indication，它是用来改善服务器与客户端SSL/TLS的一个扩展，主要解决一台服务器只能使用一个证书的缺点。

### 3.4. HttpServerOptions

    第四个配置就是Http服务器配置，它包含以下配置项：

* **compressionSupported/compressionLevel**：是否支持压缩流，支持压缩的情况下可设置压缩级别，默认不支持压缩，压缩级别为6。
* **handle100ContinueAutomatically**：是否自动处理`100-Continue`响应头，默认false。
* **maxChunkSize**：HTTP Chunk请求最大尺寸，默认值8192。
* **maxInitialLineLength**：HTTP请求首行（`GET / HTTP/1.0`）最大尺寸，默认值4096。
* **maxHeaderSize**：HTTP请求头最大尺寸，默认值8192。
* **decompressionSupported**：是否支持解压功能（支持该功能可直接读取压缩流内容），默认false。
* **decoderInitialBufferSize**：HTTP对象解码器的缓冲区初始化尺寸，默认128字节。
* **acceptUnmaskedFrames**：是否接收WebSocket中未标记的帧数据，默认false。
* **maxWebSocketFrameSize/maxWebSocketMessageSize**：WebSocket协议帧最大尺寸和消息最大尺寸，默认最大帧尺寸65536，最大消息尺寸为4帧。
* **perFrameWebSocketCompressionSupported/perMessageWebSocketCompressionSupported**：在WebSocket协议中是否支持单帧压缩，或者单消息压缩功能，默认都为true。
* **webSocketCompressionLevel**：WebSocket数据压缩级别，默认为6。
* **webSocketPreferredClientNoContext/webSocketAllowServerNoContext**：是否允许服务端和客户端无数据响应，默认都为false。
* **http2ConnectionWindowSize**：HTTP2中连接窗口尺寸，默认为-1。

    这些配置项的属性和默认值对应参考下表：

| Json配置节点                                | set方法名                                     | 默认值                                                             |
| --------------------------------------- | ------------------------------------------ | --------------------------------------------------------------- |
| compressionSupported                    | setCompressionSupported                    | DEFAULT\_COMPRESSION\_SUPPORTED = false                         |
| compressionLevel                        | setCompressionLevel                        | DEFAULT\_COMPRESSION\_LEVEL = 6                                 |
| handle100ContinueAutomatically          | setHandle100ContinueAutomatically          | DEFAULT\_HANDLE\_100\_CONTINE\_AUTOMATICALLY = false            |
| maxChunkSize                            | setMaxChunkSize                            | DEFAULT\_MAX\_CHUNK\_SIZE = 8192                                |
| maxInitialLineLength                    | setMaxInitialLineLength                    | DEFAULT\_MAX\_INITIAL\_LINE\_LENGTH = 4096                      |
| maxHeaderSize                           | setMaxHeaderSize                           | DEFAULT\_MAX\_HEADER\_SIZE = 8192                               |
| decompressionSupported                  | setDecompressionSupported                  | DEFAULT\_DECOMPRESSION\_SUPPORTED = false                       |
| decoderInitialBufferSize                | setDecoderInitialBufferSize                | DEFAULT\_DECODER\_INITIAL\_BUFFER\_SIZE = 128                   |
| acceptUnmaskedFrames                    | setAcceptUnmaskedFrames                    | DEFAULT\_ACCEPT\_UNMASKED\_FRAMES = false                       |
| maxWebSocketFrameSize                   | setMaxWebSocketFrameSize                   | DEFAULT\_MAX\_WEBSOCKET\_FRAME\_SIZE = 65536                    |
| maxWebSocketMessageSize                 | setMaxWebSocketMessageSize                 | DEFAULT\_MAX\_WEBSOCKET\_MESSAGE\_SIZE = 65536 \* 4             |
| perFrameWebSocketCompressionSupported   | setPerFrameWebSocketCompressionSupported   | DEFAULT\_PER\_FRAME\_WEBSOCKET\_COMPRESSION\_SUPPORTED = true   |
| perMessageWebSocketCompressionSupported | setPerMessageWebSocketCompressionSupported | DEFAULT\_PER\_MESSAGE\_WEBSOCKET\_COMPRESSION\_SUPPORTED = true |
| webSocketCompressionLevel               | setWebSocketCompressionLevel               | DEFAULT\_WEBSOCKET\_COMPRESSION\_LEVEL = 6                      |
| webSocketPreferredClientNoContext       | setWebSocketPreferredClientNoContext       | DEFAULT\_WEBSOCKET\_PREFERRED\_CLIENT\_NO\_CONTEXT = false      |
| webSocketAllowServerNoContext           | setWebSocketAllowServerNoContext           | DEFAULT\_WEBSOCKET\_ALLOW\_SERVER\_NO\_CONTEXT = false          |
| http2ConnectionWindowSize               | setHttp2ConnectionWindowSize               | DEFAULT\_HTTP2\_CONNECTION\_WINDOW\_SIZE = -1                   |

#### 3.4.1. 100-Continue

    HTTP/1.1协议中设计了100-Continue\[^5]的HTTP状态代码，设计这个状态代码的目的是：在客户端发送请求数据之前，HTTP/1.1协议允许客户端先判定服务器是否愿意接受客户端发来的消息主体，这种场景通常用于较大数据的传输。客户端在POST请求数据到达服务端之前，允许双方握手，如果匹配上了，客户端才真正开始发送较大数据。

> 如果客户端直接发送请求数据，而服务器因为特殊原因又将该请求拒绝的话，这种行为将带来很大的资源开销。

    协议对客户端的要求是，如果客户端等待到了预期的`100-Continue`应答，那么它发送的请求必须包含一个`Expect: 100-Continue`头信息。

**客户端策略**

* 如果客户端有POST数据要上传，可以考虑100-Continue，加入请求头`Expect: 100-Continue`。
* 如果没有POST数据，则禁止使用100-Continue，因为服务器可能会误解。
* 并不是所有的服务端都能正确实现100-Continue，如果客户端发送了这种消息后，在Timeout之内无响应，则它需要立即上传POST数据。
* 有些服务器会错误实现100-Continue，在不需要该协议时返回100，这种情况下客户端应该忽略。

**服务端策略**

* 正确的情况下，收到请求后，返回100或错误代码。
* 如果发送100-Continue之前收到了POST数据，则跳过发送100-Continue响应的流程。

```java
    // Vert.x中处理 100-Continue 的示例代码
    // 客户端：continueHandler 的使用
    HttpClientRequest request = client.put("some-uri", response -> {
        System.out.println("Received response with status code " + response.statusCode());
    });
    
    request.putHeader("Expect", "100-Continue");
    
    // 主要是调用 continueHandler
    request.continueHandler(v -> {
      // 发送剩余部分的代码
      request.write("Some data");
      request.write("Some more data");
      request.end();
    });

    // 服务端
    httpServer.requestHandler(request -> {
        if (request.getHeader("Expect").equalsIgnoreCase("100-Continue")) {
            // 发送 100-Continue 响应
            request.response().writeContinue();
    
            request.bodyHandler(body -> {
                // 处理请求Body
            });
            request.endHandler(v -> {
                request.response().end();
            });
        }
    });
```

#### 3.4.2. 分块传输

    当客户端向服务器请求一个静态页面或图片，Content-Length是可以直接计算的，这种情况下能直接传输数据；但是某些场景下，服务器的响应无法确定信息大小，此时Content-Length就无法写入长度，而是在执行过程实时计算消息长度，此时就需要使用Chunked编码实现分块传输。分块传输编码（Chunked Transfer Encoding）是超文本传输协议中的一种数据传输机制，它可以让您在传输数据时将发送给客户端的数据分成多个部分。

    Chuncked编码使用若干Chunk串连而成，由一个标明长度为0的Chunk结束，每一个Chunk都分为头部和正文两部分，头部会指定正文的字符总数（十六进制）和数量单位（一般置空），正文就是实际数据内容，两部分使用回车换行（CRLF）隔开，最后一个长度为0的Chunk中的正文部分就是通常提到的Footer内容，一般会存储一些附加信息。它的最终数据格式如下：

![](https://1702219450-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-Lv070Tm3mzVeCv0ENzP%2Fuploads%2Fgit-blob-a73c73292fe12a4a588b65e2933329bf44c37091%2F2020-08-19-11-04-38.jpg?alt=media)

    在执行Chunked编码传输时，在响应头信息中需要包含：`Transfer-Encoding: Chunked`，Chunked编码时，Chunk Size的大小是使用十六进制的ASCII码表示，如：86AE，实际十六进制应该是38366165，计算长度为34478，那么CRLF之后有连续34478字节的数据。某些场景中，为了保证计算简单，服务端还会使用空白补充来实现请求中数据对齐，这种多用于固定长度的Chunk传输中。

    Chunk的执行过程如：

1. 先判断数据是否使用了Chunked编码，查找`Transfer-Encoding: Chunked`响应头。
2. 然后在数据中查找开始标记，第一个Chunk Size开始的地方，通常使用`\r\n\r`（CRLF两次）——一般场景下，就是直接查找HTTP的响应Body部分的数据，在常用的响应数据格式中，HTTP Header和Body之间会有一个空行。
3. 用程序解析十六进制的ASCII码数值，并且转换成十进制计算字节数。
4. 获取长度后，查找和当前Chunk相关的数据部分，将数据部分读取出来，然后跳到下一个Chunk中（跳过CRLF）。
5. 重复第3，4步，直到找到Chunk Size = 0的数据块。

> \*\*「注」\*\*在解析过程中，有两个地方需要注意：第一是十六进制数在解析的时需要执行转换，第二就是空白字符以及特殊字符对中断字符的影响。

```java
    // Vert.x 中打开 Chunk 响应（不直接往Header中塞数据，Vert.x中带有专用API设置）
    HttpServerResponse response = request.response();
    response.setChunked(true);
```

#### 3.4.3. 压缩流

    HTTP协议定义了压缩，它是Web服务器和浏览器之间压缩文本数据的方法，它能大大减少网络传输的数据量，提高用户显示网页的速度，当然也会增加服务器的开销。区分内容编码和压缩：在HTTP协议中，可以对数据内容（Body部分）进行编码，当你采用类似gzip的编码时，就可以达到压缩的目的；在某些特殊场景，您也可以使用其他编码方式对内容进行加密、混淆等，以防止第三方看到文档内容。——其实压缩是内容编码的一种，也可以说是内容编码的子集。

    HTTP压缩过程如下：

1. 浏览器发送HTTP请求给服务器，请求头中带有客户端内容编码偏好：`Accept-Encoding: gzip, deflate`，它告诉服务器该浏览器支持这些内容编码。
2. 服务器接收到请求后，会生成原始的HTTP响应，其中包含了原始响应的`Content-Type`和`Content-Length`。
3. 服务器会使用gzip，对响应数据进行编码，编码后Header中会重算`Content-Length`（压缩后大小），并且增加`Content-Encoding: gzip`，将请求发送会客户端。
4. 浏览器收到响应后，根据`Content-Encoding: gzip`对响应进行解码，读取原始数据后呈现。

![](https://1702219450-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-Lv070Tm3mzVeCv0ENzP%2Fuploads%2Fgit-blob-28377e2d17e1ad1caa8a8de634d18cca57b1949e%2F2020-08-19-12-05-27.jpg?alt=media)

    HTTP中定义了一些内容编码的类型，它也允许使用扩展形式添加更多编码，常用的值如下：

* gzip：表示内容使用GNU zip编码。
* compress：表示内容使用Unix的文件压缩程序。
* deflate：表示内容使用zlib的格式压缩。
* sdch：全称为Shared Dictionary Compression over HTTP，Google发明的HTTP压缩算法，通过字典压缩算法对相同内容进行压缩，减少相同内容的传输。
* identity：表示没有对内容编码（默认情况）。

    其实gzip是一种数据格式，而deflate是一种算法，gzip内部默认使用了deflate算法对数据内容进行压缩。sdch压缩算法由于是Google出的，目前主流浏览器只有Chrome支持得比较好，这个算法现阶段还无法成为主流，但有兴趣的读者可以上网去搜索相关资料。Vert.x中如果打开压缩就直接使用`setDecompressionSupported/setCompressionSupported`两个方法来配置。

## 4. 总结

    本文我们俯瞰了整个HttpServer实例，也扩展讲解了某些参数的背景知识和相关意义，在`vertx-web`项目中，第一步就是`createHttpServer`，而本章节可以作为这行代码背后的知识补充，特别是在不同的应用场景对`HttpServerOptions`进行调参的相关处理。拓展出来的知识点主要是应用于实战开发，而和SSL、WebSocket、HTTP2相关的知识点在本章中并没有说明。初探完成后，相信读者对Vert.x中的HttpServer也有了更深入的了解，它也是开发REST的Web服务的基础。

\[^1]: 《深入解析IPv6（第3版）》，作者：Joseph Davies，翻译：汪海霖，人民邮电出版社，ISBN：978-7-115-33581-4

\[^2]: RFC 2474，<https://www.rfc-editor.org/rfc/rfc2474.html>

\[^3]: 《Java Magic. Part 4: sun.misc.Unsafe》，<http://mishadoff.com/blog/java-magic-part-4-sun-dot-misc-dot-unsafe/>

\[^5]: 《理解HTTP协议中的 Expect： 100-Continue》，<https://blog.csdn.net/skh2015java/article/details/88723028>，作者：思维的深度
