【自己读源码】Netty4.X系列(三) Channel Register

Netty源码分析(三)

前提概要

这次停更很久了,原因是中途迷茫了一段时间,不过最近调整过来了。不过有点要说下,前几天和业内某个大佬聊天,收获很多,所以这篇博文和之前也会不太一样,我们会先从如果是我自己去实现这个功能需要怎么做开始,然后去看netty源码,与自己的实现做对比。


Server端NIO复习

Netty有基于很多IO的实现(BIO/OIO/NIO...),而我们最常用的也就是NIO了,我们这次分析源码,也是基于NIO的实现,前提条件要先弄清楚NIO的流程,再分析Netty是怎么基于他开发出这个高性能网络框架的,这里我们先来简单的复习下,已经熟悉的同学可以跳过不看了。

四个步骤

抛开数据的读写,我们把NIO服务端监听分成四个步骤

  • channel初始化
  • 注册 selector到 channel上
  • channel绑定端口
  • 循环select 等待事件

其中第二步又分为几个小步骤

  • 创建selector
  • 调用channel的register

然后第四步也分为几个步骤

  • selector.select(或者几个带参的重写方法)
  • 处理所有的selectedKey

然后再继续分解:[处理所有的selectedKey]

  • 获取selectedKey的channel
  • 根据selectedKey的状态处理channel

其中需要注意的是:如果key的状态(或者说是readyops)是accept的话,需要把channel转成ServerSocktChannel然后通过accept获取新的channel,再把这个selector注册到这个channel中去。如果是其他的读写状态的话,获取的channel要转为SocketChannel,然后进行操作。


Register的实现

通过NIO的复习,我们可以看到一个完整的NIO的创建流程。在之前的两篇博文中,我们实现了第一步初始化以及第三步绑定端口的流程,现在我们先来实现channel的register。

这里多提一句,网上很多人都是在bind端口之后,再channel register的,但是其实这个前后顺序并没有关系,Netty中则是在bind之前进行的。

先看下之前的代码,我们在doBind方法中,先实例化了一个channel,然后调用channel的bind方法。

 private  void doBind(Integer port){
        Channel channel = channelFactory.newChannel();
        channel.bind(new InetSocketAddress(port));
}

这样看,我们现在需要做的就是在这两行代码中插入一段channel register的代码就可以了,那是不是可以这样,Channel中新增一个register接口,然后在实现类中去实现他,最后在这里中间写一句channel.register,貌似看起来没问题啊,不管怎么样,先动手实现下。

/* in Channel Interface */
void register(Selector selector);

/* in AbstractNioChannel */
@Override
public void register(Selector selector)  throws ClosedChannelException {
    ch().register(selector,0,null);
}
/*in AbstractBootstrap*/
try {
    channel.register(Selector.open());
} catch (IOException e) {
    e.printStackTrace();
}

我们最后的doBind看起来就会是这样

 private  void doBind(Integer port){
        Channel channel = channelFactory.newChannel();
        try {
            channel.register(Selector.open());
        } catch (IOException e) {
            e.printStackTrace();
        }
        channel.bind(new InetSocketAddress(port));
    }

功能貌似是实现了。。。但是,看起来是不是有些问题?

没错,首先看起来很丑,try/catch一大块的很难看,但其次抛去美观问题不谈,这么写不符合设计思想,我这个类是一个抽象的启动类,但是这个register只是NIO的写法,那BIO,AIO...其他IO实现怎么办?

那我们先来做下最简单的改造,软件不就是在一次次的重构中,慢慢成长起来的嘛。我简单的分了下步骤。

  • Channel接口更改,取消入参和异常抛出

        void register();
  • 抽象实现类中增加新的带参方法,供接口方法调用

    @Override
    public void register(){
        try {
                register(Selector.open());
        } catch (IOException e) {
                e.printStackTrace();
        }
    }
    
    private void register(Selector selector) throws ClosedChannelException {
        ch().register(selector,0,null);
    }
  • doBind内register调用

    private  void doBind(Integer port){
        Channel channel = channelFactory.newChannel();
        channel.register();
        channel.bind(new InetSocketAddress(port));
    }

这样看,代码是不是美观很多?最关键的是,此时的我们的抽象启动类里,真正做到了抽象,不管什么IO实现,都会去调用自身Channel实现的register方法。

目前来看还挺不错的,接下来看下Netty是怎么实现的吧,自己写的思路对的,但是肯定有很多地方没考虑到的。

Netty中的Register

How to do

Netty中,doBind方法里是包装了一个initAndRegister方法去完成初始化和注册地功能,这里我们直接看下initAndRegister这个方法体,看下Netty怎么做的
register1

是不是和我们写的差不多呢,先实例化一个channel,然后再进行register的,不过有两点不同的地方,其是register之前先要调用一个init方法,玩过Netty的应该都清楚,主要是处理我们自定义的一些配置的,这里我们先不提,等后面再说。先来说下第二个不同,也就是register方法。

我们的代码里,是直接通过扩展channel的接口,直接调用channel的register的方法的,但是Netty这里则是通过group(包含了Netty很重要的一个角色EventLoop,我们后面会详细说他),传入channel对象,然后再去调用channel的register方法,不信,我们看下他的调用链。

撇去Netty复杂的继承关系,我们最终定位到方法的最终调用的地方,SingleThreadEventLoop里
图片描述

我们可以看到,register方法里,先是包装了一个promise对象(实现promise接口的对象,这里维护了channel对象和这个eventloop对象,题外话:promise接口其实是future接口的一个超集),然后调用了promise里的channel的unsafe对象的register(ps:好绕口的感觉),这个unsafe就是channel的一个内部类,感觉越来越接近了,我们就到unsafe里面看下register方法吧。

图片描述

图片描述

这下应该很清晰了吧,unsafe的register里又调用了doRegister,然后就和我们的方法差不多了,这里的javaChannel其实就是JAVA NIO的channel对象,唯一不同的就是这里的selector不是open出来的,而是早在eventloop初始化的时候就存下来了,可能有人会问:这里eventloop是怎么来的呢?答案就在上面,在调用unsafe的register的时候,我们传入了两个对象,第一个this就是指向的eventloop(别忘了我们是在eventloop里调用的register)。然后就是把this(AbstractNioChannel)作为register的第三个参数(后面可以通过selectedKey.attachment获取到)

How to say

Netty中怎么做的我们也看过了,相信你和我一样,有很多疑问(大神们请自动无视)。这里我举出我觉得比较重要的两个。

  • 为什么一个注册这么复杂?明明用原生NIO只需要一行就搞定的,又是promise又是unsafe,在channel.register之间抽象出来两层。
  • 为什么最后的doRegister里,要用一个无限循环包起来呢?

答案还是要从源码里获得,先看下eventloop的register的接口注释

图片描述

简单来说,就是把eventloop对象注册到channel里,并且要能知道注册的结果,并且返回。这句话,可能有些同学不太明白,这是Java中的异步调用,我们传统开发的Java web程序,我们所处理的基本都是单线程的模式,首先这符合人的大脑的习惯嘛(人的大脑本质是串行处理事情的),其次是多线程的部分,框架和容器已经帮我做好了,所以可能会不太理解异步这个概念。

然后再看下unsafe这个register接口的注释

图片描述

看到了吧,执行promise的channel的注册并在完成的时候通知返回,这是靠一个叫观察者模式来完成的,具体的内容在下一章会详细讲解。

我想这很好的解释了Netty中,仅仅一个register都这么复杂,在单线程或者说串行的程序中,编程往往是很简单的,说白了就是调用,调用,调用然后返回。但Netty是个充满并行和异步的程序,所以光从设计上就会比较复杂,这让我想起了沈神说的一句话:简单的容易理解的模型性能都差,性能好的都很复杂。虽然他指的是数据库的设计,但是我觉得道理是相通的。

还有一点就是这些设计也是为了同时兼容服务端和客户端,软件开发的思想里,很重要的一点就是复用。这也是为什么会有第二个疑问,我们还是要看下注释。

图片描述
图片描述

jdk的register的注释说明,如果这个channel已经被注册过了,并且再次注册的过程中连接断了,则会抛出这个CanceledKeyException异常,那么这里捕获了异常并且调用select.selectNow,理论上会从selector中移除这个无效的channel,但是事实上并没有,所以下面也写到了,可能是个jdk bug,所以要显示的往外继续抛出异常。

仔细想想,如果仅仅是服务端的channel register,怎么可能发生上述的情况呢,所以这里的doRegister同时也是客户端channel复用的注册方法。

总结&&预告

这一章差不多就结束了,我们做了哪些事情呢?

  • 复习Java NIO 流程
  • 动手实现channel register
  • 读Netty源码,理解Netty是如何实现register以及为什么这样实现的
  • 软件开发的思想

然后画个简单的图来总结下把。

图片描述

接下来在实现循环select监听和处理事件之前,我们先实现一下Netty中一个很重要的EventLoop,并且了解下运用到的异步框架(Future and Promise),希望我的拖延症得到解决,应该在下周会写出来。。。

希望同学们看到后会有收获,如果我有理解不对的地方,欢迎来指正。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值