netty线程模型

1 篇文章 0 订阅

1 概述

netty本质上还是采用了java的nio,只是对nio进行了相关的封装,使用起来更加的方便。在netty服务端开发时,一般会有两个线程池。一个是用于接收链接申请的线程池,如果并发量不是巨大的话,可以设置为1,一般可以称之为boss线程。一个是用于io读写的线程池,可以称之为工作线程池,即boss线程收到的tcp链接,会转给这个工作线程池进行io读写的处理。一般推荐线程池内线程数量为cpu核数*2。

       EventLoopGroup bossgroup=new NioEventLoopGroup(1);  //接入线程池
		EventLoopGroup workgroup=new NioEventLoopGroup(5);  //读写线程池

2、NioEventLoop介绍

在netty中,对io线程进行了包装,即采用NioEventLoop来进行io的accept和读写的管理。可以理解为每个NioEventLoop是对io操作的线程的包装,但是又不仅限于io的读写等。
这里以工作线程池的NioEventLoop为例(这是我们经常要打交道的地方)。一个workgroup中可能会包含多个NioEventLoop(比如10个),用于处理成千上万个channel的读写请求(这里的channel可以理解为一个socket连接)。所以,一个NioEventLoop会被分配给多个channel进行读写的操作。即其内部通过select监听多路socket读或者写的请求,扫描完成后会对已经有数据的socket进行后续的操作(即触发channelPipleline的handler链)。
但是NioEventLoop不仅仅是一个io线程,其还可以执行系统任务和定时任务。
比如下面的代码中,新建一个线程(可以理解为用户线程),在新建线程中执行 ctx.writeAndFlush(msg);通过查看源码可以知道,最终的操作是将这个writeAndFlush包装成一个task,扔给NioEventLoop内部的queue。我们的NioEventLoop在执行io操作之余,也会从这个task中取出runnable任务,然后执行。

	@Override
	protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
		// TODO Auto-generated method stub
		
		
		   System.out.println("当前evenloop--》"+ctx.executor());
           Thread thread=new Thread(
        		   new Runnable() {
					
					@Override
					public void run() {
						// TODO Auto-generated method stub
						ctx.writeAndFlush(msg);    //这个操作会在io线程执行
					}
				}
        		   );
           thread.start();
           }

在执行定时任务时,NioEventLoop会通过fetchfromScheduledTaskQueue来取出到时间的定时任务到自己的taskqueue,然后执行。

    private boolean fetchFromScheduledTaskQueue() {
        long nanoTime = AbstractScheduledEventExecutor.nanoTime();    
        Runnable scheduledTask  = pollScheduledTask(nanoTime);
        while (scheduledTask != null) {
            if (!taskQueue.offer(scheduledTask)) {
                // No space left in the task queue add it back to the scheduledTaskQueue so we pick it up again.
                scheduledTaskQueue().add((ScheduledFutureTask<?>) scheduledTask);
                return false;
            }
            scheduledTask  = pollScheduledTask(nanoTime);
        }
        return true;
    }

pollScheduledTask是从一个定时任务队列中取出到时间点的queue。其中这个定时任务队列是基于时间间隔排序的PriorityQueue(根据时间的先后执行顺序进行堆排序)。
以常用的netty定义的心跳链接handler new IdleStateHandler(5, 0, 0, TimeUnit.SECONDS)为例。查看其源码:
其在handler被激活是完成监听读、写等的心跳链接操作

  @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        // This method will be invoked only if this handler was added
        // before channelActive() event is fired.  If a user adds this handler
        // after the channelActive() event, initialize() will be called by beforeAdd().
        initialize(ctx);
        super.channelActive(ctx);
    }

以常用的读心跳链接为例,

    private void initialize(ChannelHandlerContext ctx) {
        // Avoid the case where destroy() is called before scheduling timeouts.
        // See: https://github.com/netty/netty/issues/143
        switch (state) {
        case 1:
        case 2:
            return;
        }

        state = 1;

        EventExecutor loop = ctx.executor();

        lastReadTime = lastWriteTime = System.nanoTime();
        if (readerIdleTimeNanos > 0) {        //这里开始定义一个readerIdleTimeNanos后的执行ReaderIdleTimeoutTask任务,比如5s后
            readerIdleTimeout = loop.schedule(
                    new ReaderIdleTimeoutTask(ctx),
                    readerIdleTimeNanos, TimeUnit.NANOSECONDS);
        }
        if (writerIdleTimeNanos > 0) {
            writerIdleTimeout = loop.schedule(
                    new WriterIdleTimeoutTask(ctx),
                    writerIdleTimeNanos, TimeUnit.NANOSECONDS);
        }
        if (allIdleTimeNanos > 0) {
            allIdleTimeout = loop.schedule(
                    new AllIdleTimeoutTask(ctx),
                    allIdleTimeNanos, TimeUnit.NANOSECONDS);
        }
    }

在ReaderIdleTimeoutTask方法中,每次执行时候,会统计现在时间和上一次读到数据时间的差是否大于我们设置的心跳间隔,如果大于,说明链路出了问题,需要触发IdleStateEvent事件,让我们后面的handler进行相关事件的订阅,以便进行处理。

  private final class ReaderIdleTimeoutTask implements Runnable {

        private final ChannelHandlerContext ctx;

        ReaderIdleTimeoutTask(ChannelHandlerContext ctx) {
            this.ctx = ctx;
        }

        @Override
        public void run() {
            if (!ctx.channel().isOpen()) {
                return;
            }

            long nextDelay = readerIdleTimeNanos;
            if (!reading) {
                nextDelay -= System.nanoTime() - lastReadTime;
            }

            if (nextDelay <= 0) {
                // Reader is idle - set a new timeout and notify the callback.
                readerIdleTimeout =
                    ctx.executor().schedule(this, readerIdleTimeNanos, TimeUnit.NANOSECONDS);
                try {
                    IdleStateEvent event = newIdleStateEvent(IdleState.READER_IDLE, firstReaderIdleEvent);
                    if (firstReaderIdleEvent) {
                        firstReaderIdleEvent = false;
                    }

                    channelIdle(ctx, event);
                } catch (Throwable t) {
                    ctx.fireExceptionCaught(t);
                }
            } else {
                // Read occurred before the timeout - set a new timeout with shorter delay.
                readerIdleTimeout = ctx.executor().schedule(this, nextDelay, TimeUnit.NANOSECONDS);
            }
        }
    }

3、NioEventLoop使用注意事项

3.1io线程不要执行长时间的操作

netty官方建议不要在io中处理长时间的操作,比如读写数据库等。比如我们在下面的代码中加入sleep操作

@Override
	public void channelRead(ChannelHandlerContext ctx,Object msg){
		

		try {
			TimeUnit.SECONDS.sleep(10);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}	
		
	}

那么这个chandler所在的NioEventLoop就被休眠了10s中,这10秒NioEventLoop完全不能干其他事了,包括扫描,监听读事件等。

如果需要执行长时间的操作,那么需要我们另起用户线程执行。参考《netty进阶之路》的说法,这个时候需要分两种情况考虑:
1、大量用户同时接入,但是每个用户的每秒业务量不大,这种情况,可以单独配置一个DefaultEventLoopGroup线程池,然后让handle与这个线程池进行绑定,那么在handler的channelRead等操作就是在DefaultEventLoopGroup这个线程池的某个线程执行,而不是在io线程中。

public class EchoServer {
	
	
	 static EventLoopGroup eventLoopGroup = new DefaultEventLoopGroup(10);

	/**
	 * @param args
	 * @throws InterruptedException 
	 */
	public static void main(String[] args) throws InterruptedException {
		// TODO Auto-generated method stub
		new EchoServer().start();

	}
	
	public void start() throws InterruptedException{
		
		ResourceLeakDetector.setLevel(ResourceLeakDetector.Level.ADVANCED);
		EventLoopGroup group=new NioEventLoopGroup(1);  
		EventLoopGroup bossgroup=new NioEventLoopGroup(1);   //work线程池故意设置为1
		
		
		
		try {
		ServerBootstrap bootstrap=new ServerBootstrap();  //引导?
		
		EchoServerHandler echoServerHandler = new EchoServerHandler();
		
		bootstrap.group(bossgroup,group)
			.option(ChannelOption.TCP_NODELAY, true)
			.channel(NioServerSocketChannel.class)
			.localAddress(8080)
			//.handler(new ChannelInboundHandlerAdapter())  //为什么SimpleinBoundHandler不行????
			.childHandler(new ChannelInitializer<SocketChannel>() {

				@Override
				protected void initChannel(SocketChannel ch) throws Exception {
					ChannelPipeline addLast = ch.pipeline().addLast(eventLoopGroup,new EchoServerHandler());  //EchoServerHandler内的操作都在eventLoopGroup这个线程池执行
			
				}
				
			});
		
		ChannelFuture sync = bootstrap.bind().sync();
		sync.channel().closeFuture().sync();
		
	}
		finally {
		// TODO: handle finally clause
			group.shutdownGracefully().sync();
		}

	}

}

2、 第二种情况是每个用户每秒的业务量很大,这个时候上述方法就不行了,因为上述方法中,每个channel只能使用eventLoopGroup的一个线程。那么这种情况,只能是在每个ChannelInboundHandlerAdapter中单独配置线程池用于长时间的业务操作。

3.2自定义的ServerHandler线程安全问题

可能由于spring默认的单例使用情况,又比如servlet也不是线程安全的,因为默认情况下servlet载tomcat容器中是单例的。所以会有考虑,我们自定义的ServerHandler是否是线程安全的呢。通过打印ServerHandler的地址来验证:

public class EchoServerHandler extends ChannelInboundHandlerAdapter {
	
	
	 static ExecutorService execute=Executors.newFixedThreadPool(10);
	 PooledByteBufAllocator allocator=new PooledByteBufAllocator(false);
	 
	 private int count=0;    
	//从channel引导客户端,避免再生成eventloop
   @Override	
  public void channelActive(ChannelHandlerContext ctx) throws Exception {
		//
	   count++;
	   System.out.println("channelActive--->"+count+"---this address-->"+this);   		
	   ctx.fireChannelActive();
	}
	}

上面方法中,为了测试默认情况下EchoServerHandler 是否是线程安全的,分别设置一个count变量,然后打印其EchoServerHandler 实例的地址,查看结果:

channelActive--->1---this address-->com.andy.netty.direct.EchoServerHandler@76e0dec2
channelActive--->1---this address-->com.andy.netty.direct.EchoServerHandler@3ec59670
channelActive--->1---this address-->com.andy.netty.direct.EchoServerHandler@32285672
channelActive--->1---this address-->com.andy.netty.direct.EchoServerHandler@75d21cb
channelActive--->1---this address-->com.andy.netty.direct.EchoServerHandler@2c6c7f26

上面结果说明EchoServerHandler 是线程安全的。内存地址不一致,每个count都从0开始计数。
在netty中,会为每一个channel配置一个channelpipleline,而channelpipleline包含的这些ServerHandler都是单独实例化的handler,是互相隔离的。

但是,有时候,业务会需要共享channel,netty也提供了@Sharable注解,以便我们达到这种功能。其使用如下:

	try {
		ServerBootstrap bootstrap=new ServerBootstrap();  //引导?
		
		EchoServerHandler echoServerHandler = new EchoServerHandler();
		
		bootstrap.group(bossgroup,group)
			.option(ChannelOption.TCP_NODELAY, true)
			.channel(NioServerSocketChannel.class)
			.localAddress(8080)
			//.handler(new ChannelInboundHandlerAdapter())  //为什么SimpleinBoundHandler不行????
			.childHandler(new ChannelInitializer<SocketChannel>() {

				@Override
				protected void initChannel(SocketChannel ch) throws Exception {
					// TODO Auto-generated method stub
					ChannelPipeline addLast = ch.pipeline().addLast(echoServerHandler);  //这里传入进来的就不是new  EchoServerHandler,而是实例化好的echoServerHandler 
			
				
				}
				
			});
		
		ChannelFuture sync = bootstrap.bind().sync();
		sync.channel().closeFuture().sync();
		
	}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值