Skip to content

Netty - 05 配置项

在Netty中提供了一些配置项可以帮助帮助我们更好地设计网络应用程序。配置项分为两种情况:

  • 客户端:客户端使用new Bootstrap().option()设置配置项;

  • 服务端:

    • 配置ServerSocketChannel,可以使用new ServerBootstrap().option()方法进行设置;

    • 配置SocketChannel,可以使用new ServerBootstrap().childOption()方法进行设置;

1. CONNECT_TIMEOUT_MILLIS

CONNECT_TIMEOUT_MILLIS是客户端地配置项,主要用于配置连接超时时间,单位为毫秒。

如果超过了这个时间还没有连接上服务端,那么会抛出ConnectTimeoutException异常。

java
NioEventLoopGroup group = new NioEventLoopGroup();

Channel channel = null;
try {
    channel = new Bootstrap()
            .group(group)
            .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 1000)  // 设置超时时间为1秒
            .channel(NioSocketChannel.class)
            .handler(new ChannelInitializer<NioSocketChannel>() {
                @Override
                protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
                    nioSocketChannel.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
                }
            })
            .connect(new InetSocketAddress("172.12.2.32", 8888))  // 172.12.2.32是随便写的
            .sync()
            .channel();
}catch (Exception e){
    log.error(e.getMessage(), e);   // lombok
    group.shutdownGracefully();
}

异常报错:

txt
15:07:37.594 [main] ERROR com.lee.client.Client -- connection timed out after 1000 ms: /172.12.2.32:8888
io.netty.channel.ConnectTimeoutException: connection timed out after 1000 ms: /172.12.2.32:8888

如果我们不设置这个参数,其实操作系统底层仍然会抛异常,只不过等的时间比较久。

2. SO_BACKLOG

SO_BACKLOG是服务端设置ServerSocketChannel的。

首先回顾一下TCP三次握手原理:

  1. 第一次握手,client发送 SYNC 到 server,并将自己状态改为SYNC_SEND,server收到后将自己状态改为SYNC_RECD,并将该请求放入sync queue(半连接队列)中。
  2. 第二次握手,server 回复SYNC+ACK给client,client收到后将状态改为ESTABLISHED,并发送ACK给server。
  3. 第三次握手,server收到ACK,将状态改为ESTABLISHED,并将该请求从sync queue放入accept queue(全连接队列)。

其中:

  • 在Linux 2.2 之前,backlog大小包括了两个队列的大小,在Linux 2.2之后,分别用下面两个参数来控制
  • sync queue - 半连接队列
    • 大小通过/proc/sys/net/ipv4/tcp_max_syn_backlog指定,在syncookies启用的情况下,逻辑上没有最大值限制,这个设置便被忽略;
  • accept queue - 全连接队列
    • 其大小通过/proc/sys/net/core/somaxconn指定,在使用listen函数时,内核会根据传入的backlog参数与系统参数, 取二者的较小值;
    • 如果accept queue队列满了,server将发送一个拒绝连接的错误信息给client

在Netty中设置SO_BACKLOG

java
NioEventLoopGroup bossGroup = new NioEventLoopGroup();
NioEventLoopGroup workerGroup = new NioEventLoopGroup();

new ServerBootstrap()
        .group(bossGroup, workerGroup)
        .channel(NioServerSocketChannel.class)
        .option(ChannelOption.SO_BACKLOG, 2)  // 设置backlog大小
        .childHandler(new ChannelInitializer<NioSocketChannel>() {
            @Override
            protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
                nioSocketChannel.pipeline().addLast(new ChannelInboundHandlerAdapter(){
                    @Override
                    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                        String s = ByteBufUtil.prettyHexDump((ByteBuf) msg);
                        log.info(s);
                        super.channelRead(ctx, msg);
                    }
                });
            }
        })
        .bind(8888);

如何测试?

在服务端,使ServerSocketChannelaccept()方法加上断点,这样全连接队列就会不断增加元素直至队列满。之后再有连接请求过来,就会报错。

断点位置:NioEventLoop中方法processSelectedKey()unsafe.read()

SO_BACKLOG的默认值:

java
private static final class SoMaxConnAction implements PrivilegedAction<Integer> {
    @Override
    public Integer run() {
        // Determine the default somaxconn (server socket backlog) value of the platform.
        // The known defaults:
        // - Windows NT Server 4.0+: 200
        // - Linux and Mac OS X: 128
        int somaxconn = PlatformDependent.isWindows() ? 200 : 128;
        File file = new File("/proc/sys/net/core/somaxconn");
        BufferedReader in = null;
        try {
            if (file.exists()) {
                in = new BufferedReader(new FileReader(file));
                somaxconn = Integer.parseInt(in.readLine());
                if (logger.isDebugEnabled()) {
                    logger.debug("{}: {}", file, somaxconn);
                }
            } else {
                // Try to get from sysctl
                Integer tmp = null;
                if (SystemPropertyUtil.getBoolean("io.netty.net.somaxconn.trySysctl", false)) {
                    tmp = sysctlGetInt("kern.ipc.somaxconn");
                    if (tmp == null) {
                        tmp = sysctlGetInt("kern.ipc.soacceptqueue");
                        if (tmp != null) {
                            somaxconn = tmp;
                        }
                    } else {
                        somaxconn = tmp;
                    }
                }

                if (tmp == null) {
                    logger.debug("Failed to get SOMAXCONN from sysctl and file {}. Default: {}", file,
                            somaxconn);
                }
            }
        } catch (Exception e) {
            if (logger.isDebugEnabled()) {
                logger.debug("Failed to get SOMAXCONN from sysctl and file {}. Default: {}",
                        file, somaxconn, e);
            }
        } finally {
            if (in != null) {
                try {
                    in.close();
                } catch (Exception e) {
                    // Ignored.
                }
            }
        }
        return somaxconn;
    }
}

3. TCP_NODELAY

TCP_NODELAY 是一个用于优化 TCP 通信性能的选项,它的作用是禁用 Nagle 算法,从而减少数据传输延迟。

Nagle 算法是一种用于提高网络效率的技术,它通过将小的数据包合并成更大的数据包来减少网络中的数据包数量。具体来说:

  • 当应用程序发送少量数据时,Nagle 算法会等待,直到有足够的数据填满一个完整的 TCP 数据包,或者等到接收方确认之前发送的数据后,才会发送新的数据。
  • 这种机制可以有效减少网络拥塞,但可能会增加延迟,特别是在需要频繁发送小数据包的应用场景中。

使用场景与注意事项

  • 启用场景:如果应用需要低延迟、高实时性,建议启用 TCP_NODELAY。比如在线游戏、实时聊天、视频会议等。
  • 禁用场景:如果应用对带宽利用率更敏感(如文件传输),可以考虑保留 Nagle 算法以减少网络流量。
java
option(ChannelOption.TCP_NODELAY, true)  // 启用

4. ALLOCATOR

指定用于分配内存的 ByteBufAllocator 实现,在服务端需要使用childOption()配置。

java
public static void main(String[] args) {
    NioEventLoopGroup bossGroup = new NioEventLoopGroup();
    NioEventLoopGroup workerGroup = new NioEventLoopGroup();

    new ServerBootstrap()
            .group(bossGroup, workerGroup)
            .channel(NioServerSocketChannel.class)
            .childHandler(new ChannelInitializer<NioSocketChannel>() {
                @Override
                protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
                    nioSocketChannel.pipeline().addLast(new ChannelInboundHandlerAdapter(){
                        @Override
                        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                            ByteBuf buffer = ctx.alloc().buffer();
                            System.out.println(buffer);

                            super.channelRead(ctx, msg);
                        }
                    });
                }
            })
            .bind(8888);
}

在处理器中,ctx.alloc().buffer()创建的缓冲区默认如下:

txt
PooledUnsafeDirectByteBuf(ridx: 0, widx: 0, cap: 256)

我们可以通过ALLOCATOR修改配置:

java
childOption(ChannelOption.ALLOCATOR, UnpooledByteBufAllocator.DEFAULT)

创建的缓冲区如下:

txt
UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeDirectByteBuf(ridx: 0, widx: 0, cap: 256)

除了上面的方式,我们也可以修改虚拟机参数,因为ALLOCATOR的默认值是ByteBufAllocator.DEFAULT,跟踪默认缓冲区创建过程:

java
// 是否池化
static {
    String allocType = SystemPropertyUtil.get(
            "io.netty.allocator.type", PlatformDependent.isAndroid() ? "unpooled" : "pooled");

    ByteBufAllocator alloc;
    if ("unpooled".equals(allocType)) {
        alloc = UnpooledByteBufAllocator.DEFAULT;
    } else if ("pooled".equals(allocType)) {
        alloc = PooledByteBufAllocator.DEFAULT;
    } else if ("adaptive".equals(allocType)) {
        alloc = new AdaptiveByteBufAllocator();
    } else {
        alloc = PooledByteBufAllocator.DEFAULT;
    }

    DEFAULT_ALLOCATOR = alloc;
}

// 假如使用了池化技术,那么会执行alloc = PooledByteBufAllocator.DEFAULT;
// PooledByteBufAllocator.DEFAULT;会决定是否使用直接内存

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

public static boolean directBufferPreferred() {
        return DIRECT_BUFFER_PREFERRED;
    }

DIRECT_BUFFER_PREFERRED = CLEANER != NOOP && !SystemPropertyUtil.getBoolean("io.netty.noPreferDirect", false);

所以最终归纳为两个虚拟机参数值:

  • -Dio.netty.allocator.type:决定是否池化,值为pooledunpooled
  • -Dio.netty.noPreferDirect:决定是否使用直接内存,值为truefalse

5. RCVBUF_ALLOCATOR

  • 属于SocketChannel参数
  • 控制netty接收缓冲区大小
  • 负责入站数据的分配,决定入站缓冲区的大小(可动态调整),统一采用直接内存,具体是否池化由ALLOCATOR决定
java
public static void main(String[] args) {
    NioEventLoopGroup bossGroup = new NioEventLoopGroup();
    NioEventLoopGroup workerGroup = new NioEventLoopGroup();

    new ServerBootstrap()
            .group(bossGroup, workerGroup)
            .channel(NioServerSocketChannel.class)
            .childHandler(new ChannelInitializer<NioSocketChannel>() {
                @Override
                protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
                    nioSocketChannel.pipeline().addLast(new ChannelInboundHandlerAdapter(){
                        @Override
                        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                            ByteBuf byteBuf = (ByteBuf) msg;
                            System.out.println(byteBuf);
                            super.channelRead(ctx, msg);
                        }
                    });
                }
            })
            .bind(8888);

}

6. 其他

6.1 ulimit

ulimit -n 数字

限制一个进程可以打开最大的文件描述符的数量。在Linux中,网络连接也属于文件描述符,所以当该值设得较小时,会导致高并发情况时,后面的连接无法成功连接。