Netty 客户端和服务端启动的时候需要做什么?

参考自:Netty 核心原理剖析与 RPC 实践

仅仅是做了一些总结和整理,再加上了一些自己的思考,目的是方便自己后续复习巩固

一、编写一个简单的 HTTP 服务器

1. 服务端启动类

public class HttpServer {

    public void start(int port) throws Exception {
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            ServerBootstrap b = new ServerBootstrap();

            b.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .localAddress(new InetSocketAddress(port))
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        public void initChannel(SocketChannel ch) {
                            ch.pipeline()
                                    .addLast("codec", new HttpServerCodec())                  // HTTP 编解码
                                    .addLast("compressor", new HttpContentCompressor())       // HttpContent 压缩
                                    .addLast("aggregator", new HttpObjectAggregator(65536))   // HTTP 消息聚合
                                    .addLast("handler", new HttpServerHandler());             // 自定义业务逻辑处理器
                        }
                    })
                    .childOption(ChannelOption.SO_KEEPALIVE, true);

            ChannelFuture f = b.bind().sync();
            System.out.println("Http Server started, Listening on " + port);
            // 不做任何操作
            f.channel().closeFuture().sync();
        } finally {
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }
    }

    public static void main(String[] args) throws Exception {
        new HttpServer().start(8088);
    }
}
复制代码

2. 服务端业务逻辑类

如下代码所示,HttpServerHandler 是业务自定义的逻辑处理类。它是入站 ChannelInboundHandler 类型的处理器,负责接收解码后的 HTTP 请求数据,并将请求处理结果写回客户端。

public class HttpServerHandler extends SimpleChannelInboundHandler<FullHttpRequest> {

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest msg) {
        String content = String.format("Receive http request, uri: %s, method: %s, content: %s%n", msg.uri(), msg.method(), msg.content().toString(CharsetUtil.UTF_8));

        FullHttpResponse response = new DefaultFullHttpResponse(
                HttpVersion.HTTP_1_1,
                HttpResponseStatus.OK,
                Unpooled.wrappedBuffer(content.getBytes()));

        ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
    }
}
复制代码

3. HTTP 客户端类

可以通过浏览器直接访问上面编写的 HTTP 服务端,也可以自己编写客户端来和服务器建立连接。

public class HttpClient {

    public void connect(String host, int port) throws Exception {
        EventLoopGroup group = new NioEventLoopGroup();

        try {
            Bootstrap b = new Bootstrap();
            
            b.group(group);
            b.channel(NioSocketChannel.class);
            b.option(ChannelOption.SO_KEEPALIVE, true);
            b.handler(new ChannelInitializer<SocketChannel>() {
                @Override
                public void initChannel(SocketChannel ch) {
                    ch.pipeline().addLast(new HttpResponseDecoder());
                    ch.pipeline().addLast(new HttpRequestEncoder());
                    ch.pipeline().addLast(new HttpClientHandler());
                }
            });

            ChannelFuture f = b.connect(host, port).sync();
            URI uri = new URI("http://127.0.0.1:8088");
            String content = "hello world";
            DefaultFullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET,
                    uri.toASCIIString(), Unpooled.wrappedBuffer(content.getBytes(StandardCharsets.UTF_8)));
            
            request.headers().set(HttpHeaderNames.HOST, host);
            request.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
            request.headers().set(HttpHeaderNames.CONTENT_LENGTH, request.content().readableBytes());
            f.channel().write(request);
            f.channel().flush();
            f.channel().closeFuture().sync();
        } finally {
            group.shutdownGracefully();
        }
    }

    public static void main(String[] args) throws Exception {
        HttpClient client = new HttpClient();
        client.connect("127.0.0.1", 8088);
    }
}
复制代码

4. 客户端业务逻辑类

public class HttpClientHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        if (msg instanceof HttpContent) {
            HttpContent content = (HttpContent) msg;
            ByteBuf buf = content.content();
            
            System.out.println(buf.toString(io.netty.util.CharsetUtil.UTF_8));
            
            buf.release();
        }
    }
}

复制代码

二、引导器实践指南

Netty 服务端的启动过程大致分为三个步骤:

  1. 配置线程池;
  2. Channel 初始化;
  3. 端口绑定。

1. 配置线程池

Netty 是采用 Reactor 模型进行开发的,可以非常容易切换三种 Reactor 模型:单线程模型多线程模型主从多线程模型

单线程模型

Reactor 单线程模型所有 I/O 操作都由一个线程完成,所以只需要 EventLoopGroup 中只包含一个 EventLoop 即可。

// 1 代表只有一个 EventLoop 
EventLoopGroup group = new NioEventLoopGroup(1);

ServerBootstrap b = new ServerBootstrap();
b.group(group)
复制代码

图片来源于:gee.cs.oswego.edu/dl/cpjslide…

多线程模型

Reactor 单线程模型有非常严重的性能瓶颈,因此 Reactor 多线程模型出现了。

Netty 中使用 Reactor 多线程模型与单线程模型非常相似,区别是 NioEventLoopGroup 可以不需要任何参数,它默认会启动 2 倍 CPU 核数的线程。


EventLoopGroup group = new NioEventLoopGroup();

ServerBootstrap b = new ServerBootstrap();
b.group(group)
复制代码

image-20220805094148013

图片来源于:gee.cs.oswego.edu/dl/cpjslide…

源码证明默认的 CPU 核心数量为 2 倍

(1)NioEventLoopGroup 默认无参构造函数为:

image-20220805093255011

(2)一路跟下去会跟到这个方法,因为传的 nThreads 参数为 0,所以值就为 DEFAULT_EVENT_LOOP_THREADS,那么 DEFAULT_EVENT_LOOP_THREADS 这个参数的值又是什么呢?

image-20220805093104396

(3)DEFAULT_EVENT_LOOP_THREADS 的赋值就在这里进行。逻辑就是从 1,配置文件中的值,系统的 CPU 核心数量 * 2,这三者的最大值。

image-20220805093206925

所以就证明了,默认的线程数为当前系统的 CPU 核心数量的两倍。

主从多线程模型

在大多数场景下,我们采用的都是主从多线程 Reactor 模型。Boss 是主 Reactor,Worker 是从 Reactor。

它们分别使用不同的 NioEventLoopGroup,主 Reactor 负责处理 Accept 事件`,然后把 Channel 注册到从 Reactor 上,从 Reactor 主要负责 Channel 生命周期内的所有 I/O 事件。

// Main Reactor
EventLoopGroup bossGroup = new NioEventLoopGroup();
// Sub Reactor
EventLoopGroup workerGroup = new NioEventLoopGroup();

ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
复制代码

image-20220805094235485

图片来源于:gee.cs.oswego.edu/dl/cpjslide…

从上述三种 Reactor 线程模型的配置方法可以看出:Netty 线程模型的可定制化程度很高。它只需要简单配置不同的参数,便可启用不同的 Reactor 线程模型,而且无需变更其他的代码,很大程度上降低了用户开发和调试的成本。

2. Channel 初始化

设置 Channel 类型

NIO 模型是 Netty 中最成熟且被广泛使用的模型。因此,推荐 Netty 服务端采用 NioServerSocketChannel 作为 Channel 的类型,客户端采用 NioSocketChannel。设置方式如下:

ServerBootStrap b = new ServerBootStrap();
b.channel(NioServerSocketChannel.class);
复制代码

Netty 提供了多种类型的 Channel 实现类,你可以按需切换,例如 OioServerSocketChannel、EpollServerSocketChannel 等。

注册 ChannelHandler

在 Netty 中可以通过 ChannelPipeline 去注册多个 ChannelHandler,每个 ChannelHandler 各司其职,这样就可以实现最大化的代码复用,充分体现了 Netty 设计的优雅之处。

代码示例如下:

b.childHandler(new ChannelInitializer<SocketChannel>() {
    @Override
    public void initChannel(SocketChannel ch) {
        // 以下严格按照顺序
        ch.pipeline()
                .addLast("codec", new HttpServerCodec())
                .addLast("compressor", new HttpContentCompressor())
                .addLast("aggregator", new HttpObjectAggregator(65536)) 
                .addLast("handler", new HttpServerHandler());
    }
})
复制代码

ServerBootstrap 的 childHandler() 方法需要注册一个 ChannelHandler。ChannelInitializer是实现了 ChannelHandler 接口的匿名类,通过实例化 ChannelInitializer 作为 ServerBootstrap 的参数。

Channel 初始化时都会绑定一个 Pipeline,它主要用于服务编排。

Pipeline 管理了多个 ChannelHandler。I/O 事件依次在 ChannelHandler 中传播,ChannelHandler 负责业务逻辑处理。上述 HTTP 服务器示例中使用链式的方式加载了多个 ChannelHandler,包含HTTP 编解码处理器、HTTPContent 压缩处理器、HTTP 消息聚合处理器、自定义业务逻辑处理器

设置 Channel 参数

ServerBootstrap 设置 Channel 属性有 optionchildOption 两个方法,option 主要负责设置 Boss 线程组,而 childOption 对应的是 Worker 线程组。

经常设置的参数如下:

参数 含义
SO_KEEPALIVE 设置为 true 代表启用了 TCP SO_KEEPALIVE 属性,TCP 会主动探测连接状态,即连接保活
SO_BACKLOG 已完成三次握手的请求队列最大长度,同一时刻服务端可能会处理多个连接,在高并发海量连接的场景下,该参数应适当调大
TCP_NODELAY Netty 默认是 true,表示立即发送数据。如果设置为 false 表示启用 Nagle 算法,该算法会将 TCP 网络数据包累积到一定量才会发送,虽然可以减少报文发送的数量,但是会造成一定的数据延迟。Netty 为了最小化数据传输的延迟,默认禁用了 Nagle 算法
SO_SNDBUF TCP 数据发送缓冲区大小
SO_RCVBUF TCP数据接收缓冲区大小,TCP数据接收缓冲区大小
SO_LINGER 设置延迟关闭的时间,等待缓冲区中的数据发送完成
CONNECT_TIMEOUT_MILLIS 建立连接的超时时间

3. 端口绑定

在完成上述 Netty 的配置之后,bind() 方法会真正触发启动,sync() 方法则会阻塞,直至整个启动过程完成,具体使用方式如下:

ChannelFuture f = b.bind().sync();
复制代码

4. 总结

至此,服务端的启动流程就已经完成了,客户端的流程也类似,只有配置线程池那里不太一样,还有就是服务端的最后一步是绑定端口监听连接,而客户端的最后一步是和服务端绑定的端口进行连接。

猜你喜欢

转载自juejin.im/post/7128207616362151949