Channel的注册
为Channel选择Executor
- 负载均衡用简单的循环选择算法,列表中按照顺序选择下一个,直到选择到最后一个后,再从头开始选择,如此循环往复。
- 这两个Chooser的区别就是,用不用位运算的区别,位运算比取模快。
- 2的次幂转换为二进制后,只有最高位是1,其余位都是0,就是2的次幂的二进制表示只有一个1。例如,4的二进制表示为100,3的二进制表示为011。当我们对它们进行与运算时,相当于将4的二进制表示中的最高位1变成了0,也就是消掉了最高位的1。例如,4 & 3的结果为0。这样在位运算中可以非常方便地进行与、或、异或等操作。
ThreadPerTaskExecutor和SingleThreadEventExecutor的关系
- 在new NioEventLoopGroup()时会调用到如下代码:
- children会变成SingleThreadEventExecutor,追踪newChild(executor, args):
- 简单说就是SingleThreadEventExecutor里面封装了ThreadPerTaskExecutor。
- 也就说,SingleThreadEventExecutor执行任务就靠ThreadPerTaskExecutor。
- 简单说就是SingleThreadEventExecutor里面封装了ThreadPerTaskExecutor。
- 那么SingleThreadEventExecutor每执行一个任务就会导致ThreadPerTaskExecutor创建一个新线程吗?毕竟ThreadPerTaskExecutor是一任务一线程。
- 在 SingleThreadEventExecutor 内部的任务队列中,任务的执行是通过
ThreadPerTaskExecutor.execute()方法来完成的,而ThreadPerTaskExecutor.execute()方法是将任务提交给当前的线程去执行。也就是说,如果任务队列中已经有任务在等待执行,那么执行这些任务的线程就是当前的线程,不需要再创建新的线程。 - 换句话说,整一个死循环给ThreadPerTaskExecutor,对它来说这个死循环就是一个任务,所以是一个线程,但是SingleThreadEventExecutor可以执行很多任务,即,SingleThreadEventExecutor中的任务队列,对ThreadPerTaskExecutor来说其实是一个任务。
- 在 SingleThreadEventExecutor 内部的任务队列中,任务的执行是通过
- 最终
Channel的注册流程
- 调用serverBootstrap.bind()方法就会有Channel被注册:
- 追踪到如下代码:
- 最终会调用jdk提供的Nio提供的内容
- ServerSocketChannel应该要对OP_ACCEPT感兴趣的,而不是ops 0
- 避免了在注册和绑定之间产生不必要的事件通知,例如如果在注册时就对ACCEPT事件感兴趣,那么在绑定(bind)端口之前就可能收到ACCEPT事件的通知,但此时还没有绑定地址,无法处理新连接。
- Channel的注册结果就是被放入Selector中,准确的说是包装成SelectionKey,再放入Selector中。
那么NioEventLoop就好理解了
- 就是一个实现多路复用的类。
- 依旧是调用select(),等待对应的Channel感兴趣的事件就绪
- NioEventLoop是不断从Selector中获取就绪事件,然后根据事件的类型和对应的Channel,去执行相应的处理器(Handler)或者任务(Task)。但是NioEventLoop不仅仅是执行Selector中的事件,它还会执行一些其他的任务,比如定时任务(ScheduledTask),尾部任务(TailTask),或者用户提交的任务(UserTask)。这些任务都会被放入一个任务队列(TaskQueue)中,等待NioEventLoop执行。所以,NioEventLoop的执行流程大致如下:
- 调用Selector的select方法,阻塞等待就绪事件。
- 处理就绪事件,根据事件的类型和对应的Channel,调用相应的处理器或者任务。
- 执行所有的定时任务和尾部任务。
- 执行任务队列中的任务,直到达到一定的时间或者数量限制。
- 回到第一步,重复循环。
小结
- 一个EventLoopGroup当中会包含一个或多个EventLoop。
- 一个EventLoop在它的整个生命周期当中都只会与唯一一个Thread进行绑定。
- 一般是SingleThreadEventExecutor;
- 所有由EventLoop所处理的各种I/O事件都将在它所关联的那个Thread上进行处理。
- SingleThreadEventExecutor(EventLoop的父类)有一个成员变量Thread,其实是来自于ThreadPerTaskExecutor;
- 一个Channel在它的整个生命周期中只会注册在一个EventLoop上。
- chooser会选一个Executor给Channel。
- 也就是说,对Channel的所有操作都是线程安全的,单线程没有竞争。
- 一个EventLoop在运行过程当中,会被分配给一个或者多个Channel。
重要结论
- 在Netty中,Channel的实现一定是线程安全的;基于此、我们可以存储一个Channel的引用,并且在需要向远程端点发送数据时,通过这个引用来调用channel相应的方法;即便当时有很多线程都在使用它也不会出现多线程问题;而且,消息一定会按照顺序发送出去。
Channel channel = bootstrap.connect("localhost", 8080).sync().channel(); System.out.println("Connected to server: " + channel.remoteAddress()); // Create multiple threads that send messages to the same channel for (int i = 0; i < 10; i++) { new Thread(() -> { for (int j = 0; j < 5; j++) { channel.writeAndFlush("Thread " + Thread.currentThread().getId() + ", message " + "\n"); } }).start(); } - 我们在业务开发中,不要将长时间执行的耗时任务放入到EventLoop的执行队列中,因为它将会一直阻塞该线程所对应的所有Channel上的其他执行任务,如果我们需要进行阻塞调用或是耗时的操作(实际开发中很常见),那么我们就需要使用一个专门的EventBxecutor(业务线程池)。
- 通常会有两种实现方式:
- 在ChannelHandler的回调方法中,使用自己定义的业务线程池,这样就可以实现异步调用。
public class EchoServerHandler extends ChannelInboundHandlerAdapter { private ExecutorService executorService; public EchoServerHandler(ExecutorService executorService) { this.executorService = executorService; } @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { executorService.execute(() -> { // 异步处理数据 // ... // 发送数据回客户端 ctx.writeAndFlush(Unpooled.copiedBuffer("Echo: " + msg, CharsetUtil.UTF_8)); }); } } - 借助于Netty提供的向ChannelPipeline添加ChannelHandler时调用的addLast方法来传递EventExecutor.
- 说明:默认情况下(调用addLast(handler) ,ChannelHandler中的回调方法都是由I/O线程所执行,如果调用了ChannelPipeline addLast(EventExecutorGroup group,ChannelBandler... handlers);方法,那么ChannelHandler中的回调方法就是由参数中的group线程组来执行的。
// 创建业务线程池 EventExecutorGroup businessGroup = new DefaultEventExecutorGroup(10); // 添加业务处理器,指定使用业务线程池执行回调方法 pipeline.addLast(businessGroup, new MyBusinessHandler());
- 说明:默认情况下(调用addLast(handler) ,ChannelHandler中的回调方法都是由I/O线程所执行,如果调用了ChannelPipeline addLast(EventExecutorGroup group,ChannelBandler... handlers);方法,那么ChannelHandler中的回调方法就是由参数中的group线程组来执行的。
- 在ChannelHandler的回调方法中,使用自己定义的业务线程池,这样就可以实现异步调用。
- 通常会有两种实现方式:
- 现在有一个场景: 一个客户端c1与一个服务端s1,但是s1需要将客户端c1的数据转发到另外一个服务器s2,那么,s1就是既是服务器,又是客户端
- 最佳实践就是,c1与s1的channel和s1与s2的channel应该共用同一个EventLoop
public void channelActive(ChannelHandlerContext ctx) throws Exception { Channel c1Channel = ctx.channel();//与客户端c1的连接 Bootstrap bootstrap = new Bootstrap();//服务端充当客户端 bootstrap .group(c1Channel.eventLoop())//与客户端用同一个eventLoop .channel(NioSocketChannel.class)//客户端Channel .handler(new SimpleChannelInboundHandler<String>() { @Override protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception { //..... } }); Channel channel = bootstrap.connect("remoteHost", 8899).sync().channel(); channel.writeAndFlush("Hello World"); }不一定要在channelActive方法里搞,只是说明一下用意。