EpollEventLoopGroup 与 NioEventLoopGroup你知道吗

Java NIO根据操作系统不同,比如 macosx 是KQueueSelectorProvider、windows有WindowsSelectorProvider、Linux有EPollSelectorProvider Linux kernels >= 2.6,是epoll模式)或PollSelectorProvider(selector模式), 足以可见不同的系统对nio中的Selector有不同的实现,自4.0.16起,Netty为Linux通过JNI的方式提供了native socket transport。

Oracle jdk会自动选择合适的Selector,Oracle JDK在Linux已经默认使用epoll方式, 为什么netty还要提供一个基于epoll的实现呢?

stackoverflow也解释过,具体可参阅官方native-transportsTomcat Native

If you are running on linux you can use EpollEventLoopGroup and so get better performance, less GC and have more advanced features that are only available on linux.

  • Netty的 epoll transport使用 epoll edge-triggered 而 java的 nio 使用 level-triggered
  • Netty的 epoll transport 暴露了更多的nio没有的配置参数, 如 TCP_CORK, SO_REUSEADDR等。
  • C代码,更少GC,更少synchronized

总之,linux上使用EpollEventLoopGroup会有较少的gc有更高级的特性,性能更好~!

那该如何使用native socket transport(epoll)呢?

其实只需将相应的类替换即可

NioEventLoopGroupEpollEventLoopGroup
NioEventLoop EpollEventLoop
NioServerSocketChannel EpollServerSocketChannel
NioSocketChannel EpollSocketChannel

很多优秀的源码中对此都进行了兼容性处理,比如rocketmq

//源于org.apache.rocketmq.remoting.netty
public class NettyRemotingServer extends NettyRemotingAbstract implements RemotingServer {
    public NettyRemotingServer(final NettyServerConfig nettyServerConfig,
        final ChannelEventListener channelEventListener) {
        super(nettyServerConfig.getServerOnewaySemaphoreValue(), nettyServerConfig.getServerAsyncSemaphoreValue());
        this.serverBootstrap = new ServerBootstrap();
        this.nettyServerConfig = nettyServerConfig;
        this.channelEventListener = channelEventListener;

        int publicThreadNums = nettyServerConfig.getServerCallbackExecutorThreads();
        if (publicThreadNums <= 0) {
            publicThreadNums = 4;
        }

        this.publicExecutor = Executors.newFixedThreadPool(publicThreadNums, new ThreadFactory() {
            private AtomicInteger threadIndex = new AtomicInteger(0);

            @Override
            public Thread newThread(Runnable r) {
                return new Thread(r, "NettyServerPublicExecutor_" + this.threadIndex.incrementAndGet());
            }
        });

        if (useEpoll()) {
            this.eventLoopGroupBoss = new EpollEventLoopGroup(1, new ThreadFactory() {
                private AtomicInteger threadIndex = new AtomicInteger(0);

                @Override
                public Thread newThread(Runnable r) {
                    return new Thread(r, String.format("NettyEPOLLBoss_%d", this.threadIndex.incrementAndGet()));
                }
            });

            this.eventLoopGroupSelector = new EpollEventLoopGroup(nettyServerConfig.getServerSelectorThreads(), new ThreadFactory() {
                private AtomicInteger threadIndex = new AtomicInteger(0);
                private int threadTotal = nettyServerConfig.getServerSelectorThreads();

                @Override
                public Thread newThread(Runnable r) {
                    return new Thread(r, String.format("NettyServerEPOLLSelector_%d_%d", threadTotal, this.threadIndex.incrementAndGet()));
                }
            });
        } else {
            this.eventLoopGroupBoss = new NioEventLoopGroup(1, new ThreadFactory() {
                private AtomicInteger threadIndex = new AtomicInteger(0);

                @Override
                public Thread newThread(Runnable r) {
                    return new Thread(r, String.format("NettyNIOBoss_%d", this.threadIndex.incrementAndGet()));
                }
            });

            this.eventLoopGroupSelector = new NioEventLoopGroup(nettyServerConfig.getServerSelectorThreads(), new ThreadFactory() {
                private AtomicInteger threadIndex = new AtomicInteger(0);
                private int threadTotal = nettyServerConfig.getServerSelectorThreads();

                @Override
                public Thread newThread(Runnable r) {
                    return new Thread(r, String.format("NettyServerNIOSelector_%d_%d", threadTotal, this.threadIndex.incrementAndGet()));
                }
            });
        }

        loadSslContext();
    }
    private boolean useEpoll() {
        return RemotingUtil.isLinuxPlatform()
            && nettyServerConfig.isUseEpollNativeSelector()
            && Epoll.isAvailable();
    }
}

怎么读懂这块的源码?

首先从EventExecutorGroup开始,EventExecutorGroup是NioEventLoopGroup最上层的接口,它继承了java.util.concurrent.ScheduledExecutorService接口,因此它可以调度执行task。

  • EventExecutorGroup内部管理了n个EventExecutor,next()方法返回其中的一个
  • EventExecutor也是EventExecutorGroup(的子类)

 

EventExecutorGroup就像一个BOSS,每当有活儿的时候,就派一个小弟(EventExecutor)去干。

MultithreadEventExecutorGroup的每一个小弟都是一个SingleThreadEventExecutor,而且小弟的数量在构造的时候就确定了,这个BOSS的小弟分配逻辑相当简单,无非就是轮流使唤(next方法),这个BOSS的每一个小弟都是一个NioEventLoop。

public interface EventExecutorGroup extends ScheduledExecutorService, Iterable<EventExecutor> {
}
//实现了ScheduledExecutorService接口
public abstract class AbstractEventExecutorGroup implements EventExecutorGroup {
    public Future<?> submit(Runnable task) {
        return this.next().submit(task);
    }
    public <T> Future<T> submit(Callable<T> task) {
        return this.next().submit(task);
    }
}
public abstract class MultithreadEventExecutorGroup extends AbstractEventExecutorGroup {
    private final EventExecutor[] children;
    private final Set<EventExecutor> readonlyChildren;
    private final AtomicInteger terminatedChildren;
    private final Promise<?> terminationFuture;
    private final EventExecutorChooser chooser;

    public EventExecutor next() {
        return this.chooser.next();
    }
}
public final class DefaultEventExecutorChooserFactory implements EventExecutorChooserFactory {
    public static final DefaultEventExecutorChooserFactory INSTANCE = new DefaultEventExecutorChooserFactory();
    private static final class GenericEventExecutorChooser implements EventExecutorChooser {
        //调度逻辑,这里调用了Math.abs()方法以防止executors溢出
        public EventExecutor next() {
            return this.executors[Math.abs(this.idx.getAndIncrement() % this.executors.length)];
        }
    }
}
public abstract class MultithreadEventLoopGroup extends MultithreadEventExecutorGroup implements EventLoopGroup {
    public EventLoop next() {
        return (EventLoop)super.next();
    }

    protected abstract EventLoop newChild(Executor var1, Object... var2) throws Exception;

    public ChannelFuture register(Channel channel) {
        return this.next().register(channel);
    }

    public ChannelFuture register(ChannelPromise promise) {
        return this.next().register(promise);
    }
}
public class NioEventLoopGroup extends MultithreadEventLoopGroup {
    public void rebuildSelectors() {
        Iterator var1 = this.iterator();

        while(var1.hasNext()) {
            EventExecutor e = (EventExecutor)var1.next();
            ((NioEventLoop)e).rebuildSelector();
        }

    }
}

  • NioEventLoopGroup实际上就是个线程池
  • NioEventLoopGroup在后台启动了n个NioEventLoop来处理Channel事件
  • 每一个NioEventLoop负责处理m个Channel,可以理解为nio中Selector,它有两个职责:一是作为IO线程处理IO事件,二是作为普通的线程处理通过execute等方法提交上来的任务
  • NioEventLoopGroup从NioEventLoop数组里挨个取出NioEventLoop来处理Channel

NioEventLoop在单线程里同时处理IO事件和其他任务(单线程模式下的Reactor),NioEventLoop尽量(但不能保证)按照给定的比率(默认为50%)来分配花在这两种事情上的时间。换句话说,我们不应该在NioEventLoop里执行耗时的操作(比如数据库操作),这样会卡死NioEventLoop,降低程序的响应性。

<think>好的,我现在需要帮用户设计一个改进后的Spring Boot + Netty方案,实现服务端目标服务器之间的双向长连接,并且在接收到3001或3002端口的请求后,处理完成后通过本地客户端向对应端口发送响应。让我一步一步来思考这个问题。 首先,用户之前的方案可能已经建立了服务端,但需要改进以支持双向长连接。双向长连接意味着服务端和目标服务器之间需要保持持久的连接,而不是每次通信都重新建立连接。这需要服务端同时作为服务器和客户端,监听端口的同时也主动连接到目标服务器的端口。 接下来,用户提到当服务端接收到来自3001或3002端口的请求时,处理完成后需要通过本地客户端向对应的端口发送响应。这里可能存在混淆,因为通常端口是服务端监听的,而客户端连接到这些端口。需要明确的是,服务端需要作为客户端连接到目标服务器的3001和3002端口,建立出站连接,同时可能还需要监听其他端口以接收来自目标服务器的入站连接。 需要确定服务端和目标服务器各自的角色。例如,服务端可能需要同时监听某个端口(比如8080)来处理客户端的请求,同时作为客户端连接到目标服务器的3001和3002端口,保持这两个长连接。当服务端从自己的客户端(连接到目标服务器3001/3002的连接)收到请求时,处理完成后,通过相同的连接发送响应回去。或者,服务端可能在接收到来自目标服务器3001/3002端口的请求时,处理后通过对应的连接响应。 可能的结构是:服务端启动两个Netty客户端,分别连接到目标服务器的3001和3002端口,保持长连接。同时,服务端本身可能还需要有Netty服务器监听其他端口,接收其他客户端的请求。但根据用户描述,可能服务端需要同时处理来自目标服务器3001/3002的请求,并通过对应的连接发送响应。 这里的关键点是,服务端需要维护两个到目标服务器的长连接(3001和3002),并且当从这两个连接接收到数据时,处理完成后,通过对应的同一个连接发送响应。因此,每个连接(3001和3002)都是双向的,即服务端作为客户端连接到目标服务器,但这两个连接是持久的,可以双向通信。 接下来需要设计服务端的结构。服务端需要启动两个Netty客户端Bootstrap,分别连接到目标服务器的3001和3002端口,每个连接都保持活跃,并在Channel中保存这些连接,以便后续发送响应。当服务端接收到来自这些连接的数据时,处理业务逻辑,然后通过相同的Channel将响应写回目标服务器。 同时,可能还需要处理连接的重连机制,因为长连接可能会因为网络问题断开,需要自动重连。Netty的ChannelFutureListener可以用来检测连接是否成功,失败时进行重试。 另外,需要考虑如何区分来自不同端口(3001或3002)的请求,以便处理完成后通过对应的端口响应。这可以通过在Channel的属性中设置标识,或者在创建不同的Bootstrap时区分处理。 数据处理方面,需要定义好编解码器,比如使用LengthFieldBasedFrameDecoder处理粘包拆包问题,以及自定义的编解码器处理业务数据。接收到数据后,根据业务逻辑处理,然后构造响应数据,通过对应的Channel发送回去。 线程模型方面,Netty的EventLoopGroup需要合理配置,比如bossGroup和workerGroup,但如果是客户端连接,可能只需要一个EventLoopGroup来处理。 最后,需要考虑异常处理和资源释放,确保连接断开时能够正确清理资源,并重新连接。 总结步骤: 1. 服务端作为客户端,建立两个长连接到目标服务器的3001和3002端口。 2. 每个连接使用Netty的Bootstrap,配置编解码器、处理器等。 3. 在处理器中,处理接收到的数据,执行业务逻辑,然后通过相同的Channel发送响应。 4. 实现断线重连机制,确保长连接的稳定性。 5. 确保每个响应对应到正确的端口,即通过对应的Channel发送。</think>以下是根据需求改进后的Spring Boot+Netty架构设计方案,采用分层结构实现双向长连接通信: $$ \text{系统结构图} \rightarrow \text{SpringBoot服务端} \leftrightarrow \text{目标服务器}(3001/3002) $$ ### 一、核心组件设计 1. **连接管理器(ConnectionManager)** ```java public class ConnectionManager { private static Map<Integer, Channel> portChannelMap = new ConcurrentHashMap<>(); // 添加连接通道 public static void addChannel(Integer port, Channel ch) { portChannelMap.put(port, ch); } // 获取对应端口的通道 public static Channel getChannel(Integer port) { return portChannelMap.get(port); } } ``` 2. **双端口客户端初始化** ```java @PostConstruct public void init() { connectToTargetServer(3001); // 初始化3001端口连接 connectToTargetServer(3002); // 初始化3002端口连接 } private void connectToTargetServer(int port) { Bootstrap b = new Bootstrap(); b.group(new NioEventLoopGroup()) .channel(NioSocketChannel.class) .handler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) { ch.pipeline().addLast( new IdleStateHandler(0, 5, 0), // 5秒写空闲检测 new LengthFieldPrepender(4), new StringEncoder(CharsetUtil.UTF_8), new LengthFieldBasedFrameDecoder(1024, 0, 4, 0, 4), new StringDecoder(CharsetUtil.UTF_8), new TargetServerHandler(port) // 传入端口标识 ); } }); ChannelFuture f = b.connect("target.server.ip", port); f.addListener((ChannelFuture future) -> { if (future.isSuccess()) { ConnectionManager.addChannel(port, future.channel()); } else { scheduleReconnect(port); // 断线重连机制 } }); } ``` ### 二、关键处理逻辑 1. **请求接收处理(ServerHandler)** ```java @Override protected void channelRead0(ChannelHandlerContext ctx, String msg) { int sourcePort = getSourcePort(ctx.channel()); // 获取来源端口 // 异步处理保证不阻塞IO线程 CompletableFuture.supplyAsync(() -> processRequest(msg)) .thenAccept(response -> { Channel targetChannel = ConnectionManager.getChannel(sourcePort); if (targetChannel != null && targetChannel.isActive()) { targetChannel.writeAndFlush(response); } }); } private int getSourcePort(Channel channel) { // 通过Channel属性获取绑定的端口号 return channel.attr(AttributeKey.valueOf("port")).get(); } ``` 2. **心跳保持机制** ```java public class HeartbeatHandler extends ChannelDuplexHandler { @Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { if (evt instanceof IdleStateEvent) { ctx.writeAndFlush("HEARTBEAT|" + System.currentTimeMillis()); } } } ``` ### 三、异常处理增强 ```java @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { log.error("连接异常: {}", ctx.channel(), cause); ctx.close().addListener(future -> { if (!future.isSuccess()) { log.warn("关闭连接失败: {}", ctx.channel()); } }); scheduleReconnect(getPortFromCtx(ctx)); // 触发重连 } private void scheduleReconnect(int port) { eventLoopGroup.schedule(() -> connectToTargetServer(port), 5, TimeUnit.SECONDS); } ``` ### 四、配置优化建议 ```yaml netty: connection: timeout: 3000 # 连接超时3秒 max-retry: 5 # 最大重试次数 retry-interval: 5 # 重试间隔秒数 thread: boss: 1 # 主线程数 worker: 4 # 工作线程数 ``` ### 五、执行流程说明 $$ \begin{aligned} 1.&\text{建立双端口长连接} \rightarrow 3001/3002 \\ 2.&\text{接收请求} \rightarrow \text{解析来源端口} \\ 3.&\text{业务处理线程池} \rightarrow \text{异步处理} \\ 4.&\text{通过ConnectionManager} \rightarrow \text{获取对应通道} \\ 5.&\text{响应回写} \rightarrow \text{原路返回目标服务器} \end{aligned} $$ **性能优化点**: 1. 使用`EpollEventLoopGroup`提升Linux系统性能 2. 配置`WRITE_BUFFER_WATER_MARK`防止写缓冲区溢出 3. 采用对象池复用Message对象 4. 使用`FastThreadLocal`替代JDK原生ThreadLocal 该设计通过端口-通道映射管理实现精准路由,结合异步处理自动重连机制,确保系统在长连接场景下的高可用性。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值