Netty4简单认知

Channel简介

在Netty中,Channel相当于一个Socket的抽象,它为用户提供了关于Socket状态(是连接还是断开)及对Socket的读、写等操作。每当Netty建立了一个连接,都创建一个与其对应的Channel实例。

Channel的注册过程所做的工作就是将Channel与对应的EventLoop进行关联。因此,在Netty中,每个Channel都会关联一个特定的EventLoop,并且这个Channel中的所有I/O操作都是在这个EventLoop中执行的;当关联好Channel和EventLoop后,会继续调用底层JavaNIO的SocketChannel对象的register()方法,将底层Java NIO的SocketChannel注册到指定的Selector中。通过这两步,就完成了Netty对Channel的注册过程。

而对于Channel的创建过程中,会传入参数Channel创建ChannelPipeline,创建ChannelPipeline时候会创建ChannelHandlerContext传入ChannelPipeline。


下图表示常用Channel:

NioSocketChannel的创建

    Bootstrap是Netty提供的一个便利的工厂类,可以通过它来完成客户端或服务端的Netty初始化。先来看一个例子,从客户端程序是如何启动的。首先,从客户端的代码片段开始。

package com.example.gateway;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;

public class NettyChatClient {
    public NettyChatClient connect(int port, String host, final String nickName) {
        EventLoopGroup group = new NioEventLoopGroup();
        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(group)
                    .channel(NioSocketChannel.class)
                    .option(ChannelOption.SO_KEEPALIVE, true)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {

                        }
                    });
            ChannelFuture channelFuture = bootstrap.connect(host, port).sync();
            channelFuture.channel().closeFuture().sync();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return this;
    }
}

在实例化NioSocketChannel的过程中,Unsafe就特别关键。Unsafe其实是对Java底层Socket操作的封装,因此,它实际上是沟通Netty上层和Java底层的重要桥梁。

  • NioSocketChannel创建过程(只是将构造函数赋值给了bootstrapt,实例化过程需要在bootstrap.connect链接时构建):

(1)调用NioSocketChannel.newSocket(DEFAULT_SELECTOR_PROVIDER)打开一个新的Java NioSocketChannel。

(2)初始化AbstractChannel(Channel parent)对象并给属性赋值,具体赋值的属性如下。

      id:每个Channel都会被分配一个唯一的id。

      parent:属性值默认为null。

      unsafe:通过调用newUnsafe()方法实例化一个Unsafe对象,它的类型是AbstractNioByteChannel.NioByteUnsafe内部类。

      pipeline:是通过调用new DefaultChannelPipeline(this)新创建的实例,而unsafe。DefaultChannelPipeline中还有两个特殊的属性,即Head和Tail,这两个属性是双向链表的头和尾。其实在DefaultChannelPipeline中维护了一个以AbstractChannelHandlerContext为节点元素的双向链表,这个链表是Netty实现Pipeline机制的关键。

// DefaultChannelPipeline的依赖
final AbstractChannelHandlerContext head;
final AbstractChannelHandlerContext tail;
// 构造函数的初始化
tail = new TailContext(this);
head = new HeadContext(this);

pipeline在connect的时候初始化init channel过程中注册被封装了ChannerHandler的AbstractChannelHandlerContext类,这个注册的过程是: tail.prev = newCtx;,之后在调用connect的时候会调用AbstractChannelHandlerContext的connect方法,最终调用的是Unsafe的connect方法

(3)AbstractNIOChannel中被赋值的属性如下。

      ch:被赋值为Java原生SocketChannel,即NioSocketChannel的newSocket()方法返回的Java NIO SocketChannel。

      readInterestOp:被赋值为SelectionKey.OP_READ。

      ch:被配置为非阻塞,即调用ch.configureBlocking(false)方法。

(4)NioSocketChannel中被赋值的属性:config=new NioSocketChannelConfig(this,socket.socket())。

  • EventLoop的初始化

MultithreadEventLoopGroup作为NioEventLoopGroup父类,基本操作都在MultithreadEventLoopGroup里完成,维护了线程池,大小如果不设置的话默认是2Ncpu,即CPU核数×2

EventLoopGroup的初始化过程。

(1)EventLoopGroup(其实是MultithreadEventExecutorGroup)内部维护一个类型为EventExecutor的children数组,其大小是nThreads,这样就构成了一个线程池。

(2)我们在实例化NioEventLoopGroup时,如果指定线程池大小,则nThreads就是指定的值,反之是CPU核数×2。

(3)在MultithreadEventExecutorGroup中调用newChild()象方法来初始化children数组。

(4)newChild()方法是在NioEventLoopGroup中实现的,它返回一个NioEventLoop实例。

(5)初始化NioEventLoop对象并给属性赋值,具体赋值的属性如下。

      ● provider:就是在NioEventLoopGroup构造器中,调用SelectorProvider.provider()方法获取的SelectorProvider对象。

      ● selector:就是在NioEventLoop构造器中,调用provider.openSelector()方法获取的Selector对象,这里边挺有意思的,利用了反射原理,重新设置了sun.nio.ch.SelectorImp的成员变量selectedKeys,过程如下:

第一步:反射获取字节码文件
Class.forName("sun.nio.ch.SelectorImpl", false,PlatformDependent.getSystemClassLoader());
第二部:获取字段
Field selectedKeysField = selectorImplClass.getDeclaredField("selectedKeys");
第三部:将包装SelectorImpl的类selectedKeys字段设置成openSelector()方法里边新定义的final SelectedSelectionKeySet selectedKeySet = new SelectedSelectionKeySet();
selectedKeysField.set(unwrappedSelector, selectedKeySet);
第四部:将selectedKeySet赋值给NioEventLoop的成员变量selectedKeys,这样每次使用代理类SelectedSelectionKeySetSelector调用selectedKeys的时候,获取到的值会自动注入到NioEventLoop的selectedKeySet里边
第五部:建立代理包装类
new SelectedSelectionKeySetSelector(unwrappedSelector, selectedKeySet)

至此,所要处理的key信息全部存储到了Netty的SelectedSelectionKeySet里,之后调用NioEventLoop的processSelectedKey方法进行处理读、写等过程
题外话:线程池数量的选择上的一般规律:

Nthreads=Ncpu*Ucpu*(1+w/c),其中

Ncpu=CPU核心数

Ucpu=cpu使用率,0~1

W/C=等待时间与计算时间的比率

Nthreads=Ncpu*(1+w/c)

IO密集型:一般情况下,如果存在IO,那么肯定w/c>1(阻塞耗时一般都是计算耗时的很多倍),但是需要考虑系统内存有限(每开启一个线程都需要内存空间),这里需要上服务器测试具体多少个线程数适合(CPU占比、线程数、总耗时、内存消耗)。如果不想去测试,保守点取1即,Nthreads=Ncpu*(1+1)=2Ncpu。这样设置一般都OK。

计算密集型:假设没有等待w=0,则W/C=0. Nthreads=Ncpu。

至此结论就是:

IO密集型=2Ncpu(可以测试后自己控制大小,2Ncpu一般没问题)(常出现于线程中:数据库数据交互、文件上传下载、网络数据传输等等)

计算密集型=Ncpu(常出现于线程中:复杂算法)

java中:Ncpu=Runtime.getRuntime().availableProcessors()

对于chooser的选择,使用的判断法,如果nThreads是2的平方,则使用PowerOfTwoEventExecutorChooser,否则使用GenericEventExecutorChooser。这里有趣的是判断2的倍数的方法,因为2倍数的相反数的反码的补码和源码相同,所以val & -val == val 就会证明了val是否是2的倍数

 public EventExecutorChooser newChooser(EventExecutor[] executors) {
        if (isPowerOfTwo(executors.length)) {
            return new PowerOfTwoEventExecutorChooser(executors);
        } else {
            return new GenericEventExecutorChooser(executors);
        }
    }
 private static boolean isPowerOfTwo(int val) {
        return (val & -val) == val;
    }

还有个有趣的事情就是chooser的两种方式的原因是,netty对next方法的优化,比如如下代码中的,AtomicInteger自增长取余的过程,如果是2的倍数,使用&会比%效率更高,因为位运算是直接在内存中进行,避免了10进制转成2进制到内存中进行计算,然后再把结果转换成10进制的过程。这一点对于很通用,比如hashmap的大小建议为2的n次方的原因,有比如负载均衡轮询的自增长取余过程,等等,当然还有个有趣的是,int无线增值的循环往复0-->2^31-->-2^31--0,因为计算机使用补码,可以将符号位和其它位统一处理。

 public EventExecutor next() {
            return executors[idx.getAndIncrement() & executors.length - 1];
        }
 public EventExecutor next() {
            return executors[Math.abs(idx.getAndIncrement() % executors.length)];
        }

 

  • 将Channel注册到Selector

     Channel会在Bootstrap的connect的initAndRegister()中进行初始化,并且这个方法还会将初始化好的Channe注册到NioEventLoop的Selector中。接下来我们分析一下Channel注册的过程。

Channel的注册过程,具体如下。

(1)在AbstractBootstrap的initAndRegister()方法中,通过group().register(channel)调用MultithreadEventLoopGroup的register()方法。

(2)在MultithreadEventLoopGroup的register()方法中,调用next()方法获取一个可用的SingleThreadEventLoop,然后调用它的register()方法。

  (3)在SingleThreadEventLoop的register()方法中,调用channel.unsafe().register(this,promise)方法获取Channel的unsafe()底层操作对象,然后调用Unsafe的register()方法。

(4)在AbstractUnsafe的register()方法中,调用register0()方法注册Channel对象。

(5)在AbstractUnsafe的register0()方法中,调用AbstractNioChannel的doRegister()方法。

  (6)AbstractNioChannel的doRegister()方法通过javaChannel().register(eventLoop().selector,0,this)将Channel对应的Java NIO的SocketChannel注册到一个eventLoop的Selector中,并且将当前Channel作为Attachment与SocketChannel关联。

 

  • connect过程如下图所示

 

总结:

NioEventLoopGroup 实际上就是个线程池,一个 EventLoopGroup 包含一个或者多个 EventLoop;
一个 EventLoop 在它的生命周期内只和一个 Thread 绑定;
所有有 EnventLoop 处理的 I/O 事件都将在它专有的 Thread 上被处理;
一个 Channel 在它的生命周期内只注册于一个 EventLoop;
每一个 EventLoop 负责处理一个或多个 Channel; 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值