【Netty「优化进阶」(四)探索 Netty 的配置参数,打造卓越的网络应用

2023年 7月 12日 60.4k 0

前言

本篇博文是《从0到1学习 Netty》中进阶系列的第四篇博文,主要内容是通过源码与示例结合分析,研究 Netty 常见的配置常数,实现控制底层网络操作的行为,往期系列文章请访问博主的 Netty 专栏,博文中的所有代码全部收集在博主的 GitHub 仓库中;

介绍

作为网络通信框架,Netty 提供了广泛的配置参数,以便我们根据应用程序的需求来控制底层网络操作的行为。下面是一些常见的配置参数示例:

  • CONNECT_TIMEOUT_MILLIS:连接超时时间。
  • SO_BACKLOG:连接请求的队列大小。
  • TCP_NODELAY:禁用 Nagle 算法以减少延迟。
  • SO_SNDBUFSO_RCVBUF:发送缓冲区和接收缓冲区的大小。
  • ALLOCATOR:内存分配器,用于分配 ByteBuf 对象。
  • RCVBUF_ALLOCATOR:接收缓冲区分配器,用于分配 SocketChannel 接收数据的缓冲区。
  • 除了上述示例之外,Netty 还提供了更多参数,用于配置不同层面的网络操作,例如 SSL/TLS 支持、代理设置等。这些参数可以通过 BootstrapServerBootstrap 对象的 option() 方法进行设置。

    在使用 Netty 时,了解和正确配置这些参数是非常重要的,以确保网络通信的性能、稳定性和安全性。根据具体应用的需求,我们可以根据文档和实践经验来选择合适的参数值,并通过调优来优化网络通信的效率。

    CONNECT_TIMEOUT_MILLIS

    在 Netty 中,CONNECT_TIMEOUT_MILLIS 是一个用于设置连接超时时间的参数。它指定了当尝试建立连接时,客户端等待服务器响应的最大时间。

    当我们使用 Netty 创建客户端连接时,会发送一个连接请求到服务器,并等待服务器响应。如果服务器在指定的超时时间内没有响应,连接将被视为超时而失败。

    通过设置 CONNECT_TIMEOUT_MILLIS 参数,可以控制连接的超时时间。如果超过了该时间限制仍未建立连接,则 Netty 将抛出 ConnectTimeoutException 异常,以便我们在代码中进行相应的处理。

    下面是几个示例代码:

  • 创建一个 Bootstrap 实例,用于客户端的启动和连接,调用 option() 方法并传递 ChannelOption.CONNECT_TIMEOUT_MILLIS 参数,将连接超时时间设置为 5 秒,这意味着在尝试建立与服务器的连接时,如果连接建立时间超过了 5 秒,则会抛出连接超时异常。

    new Bootstrap().option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000);
    
  • 创建一个 ServerBootstrap 实例,用于服务器端的启动和连接。这里的 option() 方法设置的是服务器级别的参数,影响所有接受到的连接。

    new ServerBootstrap().option(ChannelOption.CONNECT_TIMEOUT_MILLIS,5000);
    
  • 创建一个 ServerBootstrap 实例,用于服务器端的启动和连接。这里的 childOption() 方法设置的是连接的子级别参数,影响每个已接受连接的 SocketChannel。

    new ServerBootstrap().childOption(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000);
    
  • 想要更加深入地理解连接超时设置原理,请移步博主的上一篇文章:「源码解析」(三)设置连接超时:深入分析 ChannelFuture.sync() 的执行过程;

    SO_BACKLOG

    参数设置

    概念介绍

    在Netty中,SO_BACKLOG 参数用于设置服务器套接字的等待连接队列的大小,它控制着当服务器已经建立了所有可用的处理线程,而连接请求还在不断到达时,可以保存在队列中的最大连接数。

    具体来说,当一个客户端尝试连接到服务器时,服务器会将该连接放入等待连接队列中,如果等待连接队列已满,新的连接请求将被拒绝或忽略,导致客户端无法连接成功。

    通过调整 SO_BACKLOG 参数的值,可以控制等待连接队列的大小,从而影响服务器可以同时接受的连接数,较大的 SO_BACKLOG 值允许服务器处理更多的并发连接请求,但也会增加服务器的内存消耗和处理压力。

    需要注意的是,SO_BACKLOG 参数的实际行为可能因操作系统而异,在某些操作系统上,SO_BACKLOG 参数的值会被截断为该操作系统允许的最大值。此外,如果操作系统对等待连接队列的大小没有限制,那么 SO_BACKLOG 参数的值将没有明确的上限。

    在代码实现前,我们先来了解一下 TCP 建立连接的过程,如下图所示:

    TCP 连接.png

    在上图中,服务端先通过 bind()listen() 函数绑定和监听端口,客户端通过 connect() 函数向服务端发送连接建立请求:

  • 客户端向服务端发送 SYN 数据包,请求建立连接。
  • 服务端在接收到数据包之后,会将封装着连接信息的对象存储到半连接队列 syns queue 中,同时,服务端还会向客户端发送 SYN+ACK 的应答包。
  • 客户端在接收到应答包之后,进入到连接建立的状态 ESTABLISHED,并会向服务端发送 ACK 应答包。
  • 服务端在接收到应答包之后,会将半连接队列中的对象转移存储到全连接队列 accept queue 中,同时服务端的状态转变为 ESTABLISHED。
  • 在完成 TCP 的三次握手之后,服务端通过 accept() 函数将连接信息从全连接队列中取出,完成连接建立。

    想要进一步了解 TCP 协议的读者,欢迎阅读博主往期的博文:

    • 【网络协议】万文长篇,带你深入理解 TCP;场景复现,掌握鲜为人知的细节(上);
    • 【网络协议】万文长篇,带你深入理解 TCP;场景复现,掌握鲜为人知的细节(中);
    • 【网络协议】万文长篇,带你深入理解 TCP;场景复现,掌握鲜为人知的细节(下);

    代码演示

    通过 option() 方法将服务端的全连接队列大小设置为 2:

    new ServerBootstrap().option(ChannelOption.SO_BACKLOG, 2);
    

    运行结果:

    image.png

    为什么同时启动了三个客户端,明明超出了我们设置的全连接大小,但却没有抛出异常呢?

    这是因为 Netty 的处理效率是十分优秀的,它在短时间内就处理完了这三个连接请求,因此并没有造成请求堆积。

    接下来,我们使用打断点的方式,来模拟请求堆积的情况,如下图所示:

    image.png

    通过概念介绍,我们知道服务端最后会调用 accept() 方法从全连接队列中取出连接信息,因此,我们就在如上图所示的地方进行断点,使得连接信息一直放在全连接队列当中,从而造成队列阻塞。

    将服务端以 DEBUG 模式启动,然后同时启动三个客户端,运行结果如下所示:

    image.png

    上述结果就比较符合我们预期的结果了。

    需要完整代码的读者请访问博主的 Github:TestBacklogServer,TestBacklogClient;

    默认值

    因为是 bind() 方法会使用到 BACKLOG,因此我们就从此方法入手,去查看其默认设置。

    在 ServerSocketChannel 类中找到 bind() 方法,通过 查找方法 功能,找到其在 NioSocketChannel.doBind 方法被使用,如下所示:

    image.png

    doBind() 源码如下所示:

    @Override  
    protected void doBind(SocketAddress localAddress) throws Exception {  
        if (PlatformDependent.javaVersion() >= 7) {  
            javaChannel().bind(localAddress, config.getBacklog());  
        } else {  
            javaChannel().socket().bind(localAddress, config.getBacklog());  
        }  
    }
    

    通过上述源码,我们可得知 BACKLOG 的默认值是在 config 中,对 config 进行跟踪,发现其值是调用方法 NioServerSocketChannelConfig 获取的,而 BACKLOG 的初始值正是在 NioServerSocketChannelConfig 的父类 DefaultServerSocketChannelConfig 中进行初始化,代码如下:

    public class DefaultServerSocketChannelConfig extends DefaultChannelConfig  
                                                  implements ServerSocketChannelConfig {  
      
        private volatile int backlog = NetUtil.SOMAXCONN;
    
    }
    

    继续跟踪到 NetUtil 类中进行查看:

    /**  
    * The SOMAXCONN value of the current machine. If failed to get the value, {@code 200} is used as a  
    * default value for Windows or {@code 128} for others.  
    */  
    public static final int SOMAXCONN;
    

    正如注释说的那样,如果能从配置文件中获取到 SOMAXCONN 值,则直接使用,不然,Windows 操作系统的 SOMAXCONN 值默认为 200,而其他操作系统的 SOMAXCONN 值默认为 128。

    image.png

    TCP_NODELAY

    在 Netty 中,TCP_NODELAY 是一个 TCP 参数,用于控制是否启用 Nagle 算法。Nagle 算法是一种网络优化算法,它通过将小的数据包合并成更大的数据包进行传输,以降低网络传输的开销。

    TCP_NODELAY 参数的作用是控制是否禁用 Nagle 算法。当 TCP_NODELAY 参数被设置为 true 时,表示禁用 Nagle 算法,数据会立即发送,而不会等待缓冲区填满或者延迟定时器到期,这对于需要低延迟的应用程序非常重要,例如实时通信或者交互式应用程序。

    然而,在某些特定的场景下,如果你需要最大限度地减少网络传输,可以考虑启用 Nagle 算法,即将 TCP_NODELAY 参数设置为 false

    代码示例如下所示:

    new ServerBootstrap().childOption(ChannelOption.TCP_NODELAY, true)
    

    SO_SNDBUF & SO_RCVBUF

    在 Netty 中,SO_SNDBUFSO_RCVBUF 是两个与套接字缓冲区大小相关的参数,它们用于设置底层操作系统中用于传输和接收数据的套接字缓冲区的大小。

  • SO_SNDBUF(发送缓冲区):它表示发送数据时应用程序可以使用的套接字缓冲区的大小。当应用程序通过套接字发送数据时,数据将首先被复制到发送缓冲区,然后由操作系统发送出去。如果发送缓冲区太小,可能会导致发送速度较慢或发送失败。通过增大 SO_SNDBUF 的值,可以提高发送数据的效率和性能。
  • SO_RCVBUF(接收缓冲区):它表示接收数据时应用程序可以使用的套接字缓冲区的大小。当操作系统接收到数据时,数据将被存储在接收缓冲区中,然后应用程序从该缓冲区中读取数据。如果接收缓冲区太小,可能会导致数据丢失或接收速度较慢。通过增大 SO_RCVBUF 的值,可以提高接收数据的效率和性能。
  • 这两个参数的默认值通常由操作系统决定,并且可以通过 ChannelOption.SO_SNDBUFChannelOption.SO_RCVBUF 选项进行配置。在 Netty 中,可以通过 ServerBootstrapBootstrap 类的 option() 方法设置这些选项的值。

    代码示例如下所示:

    new ServerBootstrap()  
            .option(ChannelOption.SO_SNDBUF, 1024 * 1024)  
            .option(ChannelOption.SO_RCVBUF, 1024 * 1024)
    
    new ServerBootstrap()
            .childOption(ChannelOption.SO_SNDBUF, 1024 * 1024)  
            .childOption(ChannelOption.SO_RCVBUF, 1024 * 1024)
    

    在上述代码中,将服务端的发送缓冲区大小与接收缓冲区大小都设置为 1 MB,在客户端中,可以通过 Bootstrap 类进行设置。

    ALLOCATOR

    在 Netty 中,ALLOCATOR 参数用于指定用于分配内存的分配器。Netty 提供了不同类型的分配器,可以根据具体需求进行选择,通过使用适当的分配器,可以优化内存分配和释放的性能。

    BootstrapServerBootstrap 中,可以通过 option(ChannelOption.ALLOCATOR, allocator) 来设置 ALLOCATOR 参数。其中,allocator 是实现了 ByteBufAllocator 接口的对象。

    现在需要在客户端中添加写入代码,在服务端中添加读取代码,如下所示:

    // client
    @Override  
    public void channelActive(ChannelHandlerContext ctx) throws Exception {  
        ctx.writeAndFlush(ctx.alloc().buffer().writeBytes("Hello, World! --sidiot.".getBytes()));  
    }
    
    // server
    @Override  
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {  
        log.debug(String.valueOf(ctx.alloc().buffer()));  
    }
    

    运行结果:

    image.png

    默认值

    PooledUnsafeDirectByteBuf 正是 ALLOCATOR 参数的默认值,我们将继续按照之前的方式,对其源码进行分析,找到 ChannelConfig 类中的 ALLOCATOR 选项:

    image.png

    继续跟进发现 DefaultChannelConfig 继承了 ChannelConfig,并为 allocator 进行了初始化:

    public class DefaultChannelConfig implements ChannelConfig {
        private volatile ByteBufAllocator allocator = ByteBufAllocator.DEFAULT;
    }
    

    继续深入 ByteBufAllocator 类,发现 ByteBufAllocator.DEFAULT 值是由 ByteBufUtil 类赋予的:

    public interface ByteBufAllocator {
        ByteBufAllocator DEFAULT = ByteBufUtil.DEFAULT_ALLOCATOR;
    }
    

    深入 ByteBufUtil 可以发现,其类内部实现了静态代码块为 DEFAULT_ALLOCATOR 进行赋值,如果是安卓系统,则默认使用非池化的 ByteBuf;同时,如果没有对 VM 选项进行设置,则默认使用池化 ByteBuf:

    image.png

    接下来,我们对 VM 选项进行设置,将其设置为非池化类型,运行结果如下:

    image.png

    但是,我们一直使用的是直接内存,那如果要使用堆内存应该怎么办呢?

    我们深入 PooledByteBufAllocator.DEFAULT 进行探究,发现其调用了 PlatformDependent.directBufferPreferred():

    public static final UnpooledByteBufAllocator DEFAULT =  
            new UnpooledByteBufAllocator(PlatformDependent.directBufferPreferred());
    

    通过获取系统变量来判断是否使用直接内存:

    image.png

    继续通过设置 VM 选项来变更 ByteBuf 的类型:

    image.png

    参数设置

    ALLOCATOR 的使用方式如下:

    new ServerBootstrap().childOption(ChannelOption.ALLOCATOR, new PooledByteBufAllocator());
    

    第二个参数需要传入一个 ByteBufAllocator,用于指定生成的 ByteBuf 的类型:

    • 池化 & 直接内存:

      new PooledByteBufAllocator(true);
      
    • 池化 & 堆内存:

      new PooledByteBufAllocator(false);
      
    • 非池化 & 直接内存:

      new UnpooledByteBufAllocator(true);
      
    • 非池化 & 堆内存:

      new UnpooledByteBufAllocator(false);
      

    想要继续深入了解 ByteBuf,欢迎阅读博主的其他博文:

    • 「萌新入门」(六)ByteBuf 的基本使用;
    • 「萌新入门」(七)ByteBuf 的性能优化;
    • 「源码解析」(一)ByteBuf 的动态扩容策略与实现原理;
    • 「源码解析」(二)HeapBuffer 创建过程详解:高效可靠的内存管理技巧;

    后记

    Netty 作为一种强大的网络通信框架,提供了丰富的配置参数,使我们能够根据应用程序的需求精确地控制底层网络操作的行为。

    通过对文章中参数的配置,我们可以优化连接超时时间、管理全连接队列长度、调整数据传输策略、控制发送和接收缓冲区的大小以及实现高效的内存分配。这些配置参数的灵活性与可定制性,使得 Netty 成为构建高性能、可扩展的网络应用程序的首选框架。

    以上就是 探索 Netty 的配置参数,打造卓越的网络应用 的所有内容了,希望本篇博文对大家有所帮助!

    参考:

    • Netty API reference;
    • 黑马程序员Netty全套教程 ;

    📝 上篇精讲:「源码解析」(三)设置连接超时:深入分析 ChannelFuture.sync() 的执行过程

    💖 我是 𝓼𝓲𝓭𝓲𝓸𝓽,期待你的关注,创作不易,请多多支持;

    👍 公众号:sidiot的技术驿站;

    🔥 系列专栏:探索 Netty:源码解析与应用案例分享

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论