Netty 服务端/客户端配置

本篇文章将带你一起学习如何配置一个Netty的服务端和客户端的简单程序,并了解其部分配置属性

Netty 服务端简单案例

要启动一个Netty服务端,必须要指定三类属性,分别是线程模型、IO 模型、连接读写处理逻辑,有了这三者,之后在调用bind(8000),我们就可以在本地绑定一个 8000 端口启动起来

分类 Bootstrap ServerBootstrap
网络功能 连接到远程主机和端口 绑定本地端口

需要注意的是:服务端启动的引导类是ServerBootstrap

java 复制代码
public class NettyServer {
    private final int port = 8090; // 服务端监听的端口号

    /**
     * 启动Netty服务端
     */
    public void start() throws Exception {
        // 创建EventLoopGroup
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            // 创建ServerBootstrap并配置Channel
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .option(ChannelOption.SO_BACKLOG, 128)
                    .childOption(ChannelOption.SO_KEEPALIVE, true)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        public void initChannel(SocketChannel ch) throws Exception {
                            // 添加自定义的ChannelHandler
                            ch.pipeline().addLast("decoder", new StringDecoder());
                            ch.pipeline().addLast("encoder", new StringEncoder());
                            ch.pipeline().addLast(new NettyServerHandler());
                        }
                    });

            // 绑定端口,并启动Netty服务端
            ChannelFuture f = b.bind(port).sync();
            System.out.println("Netty server started on port " + port);

            // 等待服务端监听端口关闭
            f.channel().closeFuture().sync();
        } finally {
            // 关闭EventLoopGroup
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }
    }
}

Netty服务端的启动过程

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

  • 配置线程池;
  • Channel 初始化(IO 模型);
  • 端口绑定;

配置线程池

单线程模式

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

java 复制代码
EventLoopGroup group = new NioEventLoopGroup(1);
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(group)

多线程模式

Reactor 单线程模型有非常严重的性能瓶颈,因此 Reactor 多线程模型出现了。在 Netty 中使用 Reactor 多线程模型与单线程模型非常相似,区别是 NioEventLoopGroup 可以不需要任何参数,它默认会启动 2 倍 CPU 核数的线程。当然,你也可以自己手动设置固定的线程数。

java 复制代码
EventLoopGroup group = new NioEventLoopGroup();
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(group)

主从多线程模式

在大多数场景下,我们采用的都是主从多线程 Reactor 模型。Boss 是主 Reactor,Worker 是从 Reactor。它们分别使用不同的 NioEventLoopGroup,主 Reactor 负责处理 Accept,然后把 Channel 注册到从 Reactor 上,从 Reactor 主要负责 Channel 生命周期内的所有 I/O 事件。

java 复制代码
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup)

Channel 初始化

我们通过.channel(NioServerSocketChannel.class)来指定 IO 模型,推荐 Netty 服务端采用NioServerSocketChannel作为Channel的类型,客户端采用 NioSocketChannel

注册channelHandler

在 Netty 中可以通过 ChannelPipeline 去注册多个 ChannelHandler,使用childHandler方法

java 复制代码
serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
    @Override
    public void initChannel(SocketChannel ch) {
        ch.pipeline().addLast(xxxx)
    }
})

ChannelInitializer 是实现了 ChannelHandler接口的匿名类

自动绑定递增端口

serverBootstrap.bind(8000)这个方法呢,它是一个异步的方法,调用之后是立即返回的,他的返回值是一个ChannelFuture,我们可以给这个ChannelFuture添加一个监听器GenericFutureListener

  • 我们还可以通过 sync() 方法,不过它会阻塞,直至整个启动过程完成
java 复制代码
private static void bind(final ServerBootstrap serverBootstrap, final int port) {
    serverBootstrap.bind(port).addListener(new GenericFutureListener<Future<? super Void>>() {
        public void operationComplete(Future<? super Void> future) {
            if (future.isSuccess()) {
                System.out.println("端口[" + port + "]绑定成功!");
            } else {
                System.err.println("端口[" + port + "]绑定失败!");
                bind(serverBootstrap, port + 1);
            }
        }
    });
}

// 当然我们也可以阻塞绑定
ChannelFuture f = serverBootstrap.bind().sync();

ServerBootstrap服务端启动其他方法

带child的是针对连接的,不带的是针对服务器的

handler() 方法

java 复制代码
serverBootstrap.handler(new ChannelInitializer<NioServerSocketChannel>() {
    protected void initChannel(NioServerSocketChannel ch) {
        System.out.println("服务端启动中");
    }
})

childHandler() 用于指定处理新连接数据的读写处理逻辑,handler() 用于指定在服务端启动过程中的一些逻辑

attr() 方法

java 复制代码
serverBootstrap.attr(AttributeKey.newInstance("serverName"), "nettyServer")

attr() 方法可以给服务端的channel,也就是NioServerSocketChannel指定一些自定义属性,然后我们可以通过channel.attr() 取出这个属性。

比如,上面的代码我们指定我们服务端channel的一个serverName属性,属性值为nettyServer,其实说白了就是给NioServerSocketChannel维护一个map而已。

childAttr() 方法

java 复制代码
serverBootstrap.childAttr(AttributeKey.newInstance("clientKey"), "clientValue")

上面的childAttr可以给每一条连接指定自定义属性,然后后续我们可以通过channel.attr() 取出该属性。

客户端启动案例

创建一个引导类,然后给他指定线程模型,IO 模型,连接读写处理逻辑,连接上特定主机和端口,客户端就启动起来了。

需要注意的是:客户端启动的引导类是Bootstrap

java 复制代码
public class NettyClient {
    public static void main(String[] args) {
        NioEventLoopGroup workerGroup = new NioEventLoopGroup();
        
        Bootstrap bootstrap = new Bootstrap();
        bootstrap
                // 1.指定线程模型
                .group(workerGroup)
                // 2.指定 IO 类型为 NIO
                .channel(NioSocketChannel.class)
                // 3.IO 处理逻辑
                .handler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    public void initChannel(SocketChannel ch) {
                    }
                });
        // 4.建立连接
        bootstrap.connect("juejin.cn", 80).addListener(future -> {
            if (future.isSuccess()) {
                System.out.println("连接成功!");
            } else {
                System.err.println("连接失败!");
            }

        });
    }
}

失败重连

在网络情况差的情况下,客户端第一次连接可能会连接失败,这个时候我们可能会尝试重新连接,我们把建立连接的逻辑先抽取出来,然后在重连失败的时候,递归调用自身。

通常情况下,连接建立失败不会立即重新连接,而是会通过一个指数退避的方式,比如每隔 1 秒、2 秒、4 秒、8 秒,以 2 的幂次来建立连接,然后到达一定次数之后就放弃连接。

java 复制代码
connect(bootstrap, "juejin.cn", 80, MAX_RETRY);

private static void connect(Bootstrap bootstrap, String host, int port, int retry) {
    bootstrap.connect(host, port).addListener(future -> {
        if (future.isSuccess()) {
            System.out.println("连接成功!");
        } else if (retry == 0) {
            System.err.println("重试次数已用完,放弃连接!");
        } else {
            // 第几次重连
            int order = (MAX_RETRY - retry) + 1;
            // 本次重连的间隔
            int delay = 1 << order;
            System.err.println(new Date() + ": 连接失败,第" + order + "次重连......");
            bootstrap.config().group().schedule(() -> connect(bootstrap, host, port, retry - 1), delay, TimeUnit
                    .SECONDS);
        }
    });
}

在上面的代码中,我们看到,我们定时任务是调用bootstrap.config().group().schedule() , 其中bootstrap.config() 这个方法返回的是 BootstrapConfig,他是对Bootstrap配置参数的抽象,然后bootstrap.config().group() 返回的就是我们在一开始的时候配置的线程模型 workerGroup,调workerGroup的schedule方法即可实现定时任务逻辑。

Bootstrap客户端启动其他方法

handler() 方法

客户端相关的数据读写逻辑是通过handler()方法指定

attr() 方法

这个跟服务端的attr方法是一样的,只不过它是给客户端的channel配置属性

ChannelOption 参数

参数 含义
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 建立连接的超时时间
WRITE_BUFFER_HIGH_WATER_MARK 和 WRITE_BUFFER_LOW_WATER_MARK 表示发送缓冲区的高水位线和低水位线。当发送缓冲区的字节数超过高水位线时,Channel的isWritable()方法将返回false;当发送缓冲区的字节数低于低水位线时,Channel的isWritable()方法将返回true。默认值为64KB和32KB。
ALLOCATOR 表示使用的ByteBuf内存分配器。Netty提供了多种内存分配器,如UnpooledByteBufAllocator和PooledByteBufAllocator。默认值为PooledByteBufAllocator。

这些选项可以通过ServerBootstrapBootstrap类的option()方法和childOption()方法进行设置。

其中,option()方法用于设置服务端Channel,而childOption()方法用于设置接受客户端连接的子Channel,即SocketChannel

childOption和option的区别

在网络编程中,服务端和客户端是相对的概念。服务端通常是一个主动等待客户端连接的程序,而客户端通常是一个主动发起连接的程序。

在Netty中,ServerSocketChannel是服务端的 Channel ,它用于监听端口并接受客户端的连接请求 。当客户端发起连接请求时,ServerSocketChannel会创建一个新的SocketChannel,并将其交给业务逻辑处理。这个SocketChannel就是用于收客户端连接的子 Channel

简单来说,服务端Channel的作用是监听端口并接受客户端连接请求,而子Channel的作用是处理客户端的具体数据传输。

因为服务端 Channel SocketChannel有不同的行为,所以需要分别设置它们的 ChannelOption ,使用 option() childOption() 方法来进行区分设置。

childOption() 方法

java 复制代码
serverBootstrap
        .childOption(ChannelOption.SO_KEEPALIVE, true)
        .childOption(ChannelOption.TCP_NODELAY, true)

childOption()可以给每条连接设置一些TCP底层相关的属性,比如上面,我们设置了两种TCP属性,其中

  • ChannelOption.SO_KEEPALIVE表示是否开启TCP底层心跳机制,true为开启
  • ChannelOption.TCP_NODELAY表示是否开启Nagle算法,true表示关闭,false表示开启
    • 通俗地说,如果要求高实时性,有数据发送时就马上发送,就关闭,如果需要减少发送次数减少网络交互,就开启

option() 方法

除了给每个连接设置这一系列属性之外,我们还可以给服务端channel设置一些属性,最常见的就是so_backlog

java 复制代码
serverBootstrap.option(ChannelOption.SO_BACKLOG, 1024)
    .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)
  • ChannelOption.SO_BACKLOG表示系统用于临时存放已完成三次握手的请求的队列的最大长度,如果连接建立频繁,服务器处理创建新连接较慢,可以适当调大这个参数
  • ChannelOption.CONNECT_TIMEOUT_MILLIS表示连接的超时时间,超过这个时间还是建立不上的话则代表连接失败

总结

通过上述内容,我们可以知道 Netty 服务端和客户端的启动过程可以分为三个主要部分:线程模型的配置、IO模型的选择(NioServerSocketChannel或NioSocketChannel)、以及连接读写处理逻辑。

服务端使用ServerBootstrap引导类,客户端则使用Bootstrap引导类。对每条连接,可以设置TCP底层相关属性如心跳机制和Nagle算法。

同时,还可以给服务端Channel设置属性,如连接队列长度和连接超时时间。此外,Netty还提供了ChannelPipeline来注册多个ChannelHandler,加强数据处理逻辑。

相关推荐
Dola_Pan1 小时前
Linux文件IO(二)-文件操作使用详解
java·linux·服务器
wang_book1 小时前
Gitlab学习(007 gitlab项目操作)
java·运维·git·学习·spring·gitlab
蜗牛^^O^2 小时前
Docker和K8S
java·docker·kubernetes
从心归零3 小时前
sshj使用代理连接服务器
java·服务器·sshj
IT毕设梦工厂4 小时前
计算机毕业设计选题推荐-在线拍卖系统-Java/Python项目实战
java·spring boot·python·django·毕业设计·源码·课程设计
Ylucius4 小时前
动态语言? 静态语言? ------区别何在?java,js,c,c++,python分给是静态or动态语言?
java·c语言·javascript·c++·python·学习
凡人的AI工具箱5 小时前
AI教你学Python 第11天 : 局部变量与全局变量
开发语言·人工智能·后端·python
是店小二呀5 小时前
【C++】C++ STL探索:Priority Queue与仿函数的深入解析
开发语言·c++·后端
七夜zippoe5 小时前
分布式系统实战经验
java·分布式
canonical_entropy5 小时前
金蝶云苍穹的Extension与Nop平台的Delta的区别
后端·低代码·架构