HTTP 协议是我们最常见的协议之一,Netty 提供开箱即用的 ChannelHandler 方便用户构建一个高性能的 HTTP 服务器。构建一个能输出 Hello World! 服务器能精简两个步骤,分别是:

  1. 编写自定义 ChannelHanlder。
  2. 初始化 ServerBootstrap。

    1. 编写自定义 ChannelHandler

    ```java /**

    • 每次请求都返回 {@link #str} 字符串 */ @ChannelHandler.Sharable public class HelloWorldChannelInboundHandler extends SimpleChannelInboundHandler { static final String str = “

      Hello World!

      “;

      /**

      • Netty 属于异步框架,每次从通道中读取数据之后都会回调此方法
      • @param ctx the {@link ChannelHandlerContext} which this {@link SimpleChannelInboundHandler}
      • belongs to
      • @param request
      • @throws Exception */ @Override protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception { System.out.println(“Remote Address: “ + request.uri()); }

      /**

      • 消息读取完后,输出「Hello World!」字符串
      • @param ctx
      • @throws Exception */ @Override public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { // #1 创建一个「ByteBuf」对象用来存放「#str」字符串数据 ByteBuf buffer = ctx.alloc().buffer(str.length()); buffer.writeBytes(str.getBytes(StandardCharsets.UTF_8));

        // #2 构建一个「HttpResponse」对象 FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);

        // #3 设置内容属性 response.headers().set(HttpHeaderNames.CONTENT_TYPE, “text/html;charset=UTF-8”);

        // #4 写内容 response.content().writeBytes(buffer);

        // #5 把数据立即刷新到传输层,写入成功,则关闭通道 ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); }

  1. /**
  2. * 捕获异常
  3. * @param ctx
  4. * @param cause
  5. * @throws Exception
  6. */
  7. @Override
  8. public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
  9. cause.printStackTrace();
  10. // 关闭通道
  11. ctx.close();
  12. }

}

<a name="qliz2"></a>
## 2. ServerBootstrap
创建一个 Server 服务端大致分为三个步骤:

1. 配置线程池。创建两个 EventLoopGroup(采用 Reactor 主从设计模型)
1. 配置 ServerBootstrap
   1. 配置 Channel
   1. 添加 localAddress
   1. 配置 childHandler
3. 端口绑定

相关代码如下:
```java
/**
 * 基于HTTP协议的HelloWorld服务器
 */
public class HttpHelloWorldServer {
    /**
     * 绑定本地端口号
     */
    private final int port;

    HttpHelloWorldServer(int port) {
        if (port < 0)
            throw new IllegalArgumentException("port must not be less than 0!");
        this.port = port;
    }

    /**
     * 启动服务器
     * @throws InterruptedException
     */
    public void startServer() throws InterruptedException {
        // #1 创建两个线程组,采用主从Reactor设计模型
        NioEventLoopGroup boss = new NioEventLoopGroup(1);
        NioEventLoopGroup worker = new NioEventLoopGroup();

        // #2 创建「ServerBootstrap」
        ServerBootstrap b = new ServerBootstrap();
        try {
            // #3 配置「ServerBootstrap」
            b.group(boss, worker)
                    .channel(NioServerSocketChannel.class)     // 指定所使用的 NIO 传输通道,一般使用Java NIO,而如果想获得EPOLL性能,则可以填入EpollServerSocketChannel.class,相应的「NioEventLoopGroup
                                                               // 也会变成EpollEventLoopGroup,它们是一一对应的。
                    .localAddress(new InetSocketAddress(port)) // 使用指定的端口设置套接字地址
                    .childHandler(new ChannelInitializer<SocketChannel>() {  // 添加与HTTP相关的「ChannelHandler」,这些都是Netty内置的和HTTP相关的编、解码器,当然也别忘记添加我们自定义的「ChannelHandler」
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ChannelPipeline pipeline = ch.pipeline();
                            pipeline.addLast("http-decoder", new HttpRequestDecoder())
                                    .addLast("http-aggregator", new HttpObjectAggregator(Integer.MAX_VALUE))
                                    .addLast("http-encoder", new HttpResponseEncoder())
                                    .addLast("hello-handler", new HelloWorldChannelInboundHandler());
                        }
                    });

            // #4 异步绑定服务器,调用「sync()」方法阻塞等待直到绑定完成
            ChannelFuture f = b.bind().sync();

            // #5 获取「Channel」的「CloseFuture」,并阻塞当前线程直到它完成
            f.channel().closeFuture().sync();
        } finally {
            // #6 优雅关闭「NioEventLoopGroup」的资源
            boss.shutdownGracefully().sync();
            worker.shutdownGracefully().sync();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        HttpHelloWorldServer server = new HttpHelloWorldServer(9999);
        // 启动服务器
        server.startServer();
    }
}

虽然这是一个简单的应用程序,便是它可以弹性伸缩,支持数千并发连接不成问题,比普通的基于套接字的 Java 应用程序处理多得多的消息。而且,你可以把上面当成是一个模板,无论使用基于 Java NIO 的NioServerSocketChannel.class 或基于 Linux Epoll 的 EpollServerSocketChannel.class,我们只需要改动几个小点就能完成对底层框架的替换,从侧面证明了 Netty 强大的逻辑抽象能力和模块分层。
接下来,我们就按照创建一个 Server 服务端的步骤并通过源码的方法详解 Netty 服务端启动流程。

配置线程池

Netty 采用 Reactor 模型进行设计,可以非常容易切换三种不同的 Reactor 模型:

  • 单Reactor单线程模型
  • 单Reactor多线程模型
  • 主从Reactor模型

    单Reactor单线程模型

    EventLoopGroup group = new NioEventLoopGroup(1);
    ServerBootstrap b = new ServerBootstrap();
    b.group(group)
    

    单Reactor多线程模型

    EventLoopGroup group = new NioEventLoopGroup();
    ServerBootstrap b = new ServerBootstrap();
    b.group(group)
    

    主从Reactor模型

    EventLoopGroup bossGroup = new NioEventLoopGroup();
    EventLoopGroup workerGroup = new NioEventLoopGroup();
    ServerBootstrap b = new ServerBootstrap();
    b.group(bossGroup, workerGroup)
    

    从上述可以看出,Netty 对 Reactor 的三种模型的可定制化程序非常高,用户可以非常方便根据不同场景切换不同的 Reactor 模型,大大降低开发和维护成本。

    配置 ServerBootstrap

    配置通道类型

    NioServerSocketChannel 通道类型是 Netty 最成熟且运用最广泛的通道类型,底层基于 Java NIO,属于水平触发。而如果服务器部署在 Linux,也推荐使用 EpollServerSocketChannel 通道类型,因为 Linux Epoll 的性能是高于 select 的。

    注册 ChannelHandler

    每一个 Socket 连接都会创建一个新的 Channel,初始化时都会绑定一个新的 ChannelPiepline,它的功能是用于管理(编排) ChannelHandler,上述的代码中我们添加了多个 ChannelHandler,包含:

  • HttpRequestDecoder Http 请求解码器

  • HttpObjectAggregator Http 消息聚合处理器
  • HttpResponseEncoder Http 响应编码器
  • HelloWorldChannelInboundHandler 自定义业务逻辑处理器

I/O 事件依次按序在 ChannelHandler 之间传播。
内部使用一个 ChannelInitializer 匿名类帮助我们完成 ChannelHandler 的初始化编排工作。

设置 Channel 参数

设置与通道相关接近底层的配置选项,比如 TCP 发送、接收缓冲区大小、是否启用 Nagle 算法等。Netty 也提供了默认的参数设置,如果需要进一步优化, 可以通过 ServerBootstrap#option 方法修改参数即可。

参数 说明
SO_KEEPALIVE 连接保活,默认的 keepalive 超时需要 2 小时,探测次数为 5 次
SO_BACKLOG 存入已完成三次握手的请求队列的长度大小。
在海量连接的场景下,就适当调大此参数
TCP_NODELAY NO_DELAY 表示非延迟,Netty 默认为 true,即立即发送数据。如果为 false 表示启动 Nagle 算法,会把分组累积到一定量才会发送,虽然有效减少报文发送的次数,但是会造成一定时长的延迟。Netty 偏向最小化数据传输延迟,默认禁用 Nagle 算法
SO_SNDBUF TCP 数据发送缓冲区大小
SO_RCVBUF TCP 数据接收缓冲区大小
SO_LINGER 延迟关闭时间,目的是等待缓冲区中的数据完全发送完成
CONNECT_TIMEOUT_MILLIS 建立连接的超时时间

端口绑定

以上操作只是把数据填充到某个地点,真正触发 ServerBootstrap 启动的是 bind() 方法。在深入源码之前,我们需要 Bootstrap 体系有一个简单的了解。