网络编程17

Channel系列源码分析

  • NioServerSocketChannel
  • NioSocketChannel

channel----channel相关顶级接口

  • 提供了很多跟网络通讯相关的操作,以及状态判定
    • read方法,读取到缓冲区,读取数据成功后,就会触发channel所属的pipeline进行handler的处理,触发channelRead,在各种handler中流转
    • flush方法,将我们要写到对端的数据,刷新到目标channel中,并发送给对方
    • config方法,获取当前channel的配置信息
    • isRegistered,当前channel是否注册到eventloop上
    • metadata,channel的元数据,tcp的配置,SO_XXX,每一个channel都有一个物理连接,每一个连接都有自己相关的配置参数
    • parent,表示当前channel的父channel是谁,对于服务器的channel肯定没有,服务器与客户端通讯后,产生的一个类似于socketChannel,这个就有父channel
    • id,唯一性id,结合ip地址加时间加mac地址计算出来的等等
  • 和channel相关的其他功能
  • 每一个channel都要和一个eventloop绑定
  • channel读写的时候,是需要获取一个buffer的

AbstractChannel

  • netty里面网络操作抽象相关的接口

  • 主要包括网络读写,客户端发起连接,主动关闭连接、链路关闭、获取通讯相关地址等等

  • 但是因为AbstractChannel是客户端和服务端都要用,所以所有网络通讯相关的都会存在,但是不限于上述功能

    • 从变量定义也可以看出,聚合了channel需要用到的能力对象,pipeline、unsafe、eventloop
  • 实际实现,比如bind方法,并没有自己去实现,而是调用pipeline进行相关的操作

  • 但是也有很多公共方法,比如获取远端地址、获取本端地址,都是通过unsafe实现的,是真正调用底层网络通讯相关的类

  • 几乎所有的方法,除了没实现的空方法,都是调用pipeline实现的

    • 与之前相比channel.write和ctx.write,一个是全部流转,一个是从最近的开始流转

AbstractNioChannel

  • 对nio操作的封装,封装了SelectableChannel、SelectionKey,等nio网络原生编程相关的内容

  • 不管是客户端还是服务端,拿到实例后,设置通道为非阻塞模式(configureBlocking(false)),在它的构造方法里面,也有设置为非阻塞的这么一句话,同时readInterestOp表示当前通道所关注的事情,构造方法中由外部传入

  • 几个重要的方法

    • doRegister:对应在nioHandler里面的channel.register,把当前通道和selector进行挂钩,同时表明当前channel对什么事件感兴趣,但是doRegister里面的unwrappedSelector,可以简单的理解为它就是NioServer中要使用的selector,然后原生Selector在注册的时候除了可以接受事件外,还可以attch,附件、绑定

      selectionKey = javaChannel().register(eventLoop().unwrappedSelector(), 0, this);
      

      1.0表示对任何事件都不感兴趣,同时把当前channel的实例挂到了selector上面

      2.什么时候才有值呢?

      3.一开始selected是false,如果调用成功了,可能会改成true,如果发生异常了,如果selected还是false,没有改成功,则会再次调用selectNow,如果selected是true,则直接抛出异常,这里注释写的是jdk bug?因为既然都改成功了,为什么还会抛出异常

    • doBeginRead:拿到当前channel的selectionKey,判断是否有效,如果有效,把当前key已经有的事件取出来,和当前channel的成员变量readInterestOp做一个与操作,如果等于0,说明没设置读操作位,再通过或操作,把当前channel关注的事件加入到selectionKey上面去,这样就可以监听相关的读事件了

  • AbstractNioChannel的构造方法是由其子类生成的

AbstractNioByteChannel

  • 它的构造方法就是调用父类的构造方法,把读感兴趣事件传进去

  • 所有基于AbstractNioByteChannel诞生的子类及其实例,都会设置当前channel所关注的事件为读

  • doWrite方法,是比较难以理解的,是channel.write和ctx.write最终往对端写的会调用的相关方法

    • 1.设置一个writeSpinCount,初始化缺省时默认是16

    • 2.进入一个循环,循环时会扣减writeSpinCount,终止条件是writeSpinCount小于0,循环里面从发送缓冲区里面取数据,如果没有,说明当前发送缓冲区里面的数据已经发送完成了,clearOpWrite()方法是去取消相关的写事件,如果不为null,就去写了,执行doWriteInternal方法,分成两块,首先判断发送数据类型,是ByteBuf,还是FileRegion,对于ByteBuf,首先做强制类型转换,如果没有可读内容,直接返回0,接下来读取buffer中的数据,进行相关的发送,而doWriteBytes是空实现,是一个抽象方法,由子类实现,具体怎么发送由子类决定,它的子类其实就两个,tcp和udp,如果说发送出去的数据大于0,就更新发送缓冲区里面的数据,并且返回1,说明发送成功1次,同时writeSpinCount扣减1

      为什么要设置16次?

      当socket.write去写数据时,其实是往发送缓冲区里面写数据,再由操作系统往对端发送,发送缓冲区默认是4k,如果要发送40M内容发送到对端,至少要写1000次,如果不做控制,缓冲区一有空,就往里面写,就会导致往发送缓冲区里面写数据的线程会很忙,一个eventloop是要管理多个channel,如果eventloop忙着一个channel,其他的channel就无法兼顾了

      为什么要取消写事件?

      大部分情况不注册写事件,因为发送缓冲区里面只有1个字节的空闲,就会触发写事件

      什么情况下doWriteBytes返回0?

      缓冲区满了,网络十分拥塞时,根本没地方让你写,此时doWrite中的doWriteInternal方法,返回一个很大的数,writeSpinCount直接变成负数了,从而终止循环

    • 3.没有写完的数据另外再做处理,执行incompleteWrite方法

      根据setOpWrite这个boolean值,发送缓冲区很满时,这个值为true,注册一个写事件,如果不是因为发送缓冲区很满,此时并不会注册写事件,反而清除写事件, 把写数据的任务(flushTask)放到eventloop所属的队列中去写

      为什么要这么设计?

      默认每个channel执行写操作,只写16次,如果超过16次也暂时不处理了,没写完的数据用一个task放到队列中空闲的时候去写,没有必要往selector再注册写事件,因为每一次selector调用都要从用户态切换到内核态,是一种系统调用,而且只需要把没发送完的数据发送完即可

AbstractNioMessageChannel

  • ServerSocketChannel上面的一个类,是不应该有实际的网络读写相关的操作的,虽然也有doWrite一些方法,虽然也实现了,但是没有意义,里面的实现思路跟AbstractNioByteChannel差不多,但是比如doWriteMessage方法,是抽象方法,看它的子类,ServerSocketChannel反而会抛出一个异常,说不支持此操作,还有doBeginRead方法,是调用它的父类,就是把当前channel所关注的事件往selector上进行注册,以及doReadMessages方法也是一个抽象方法,其实现也是交给子类

NioServerSocketChannel

  • 主要工作是接受连接,定义了ChannelMetadata,SelectorProvider(jdk中nio提供的,不管去拿selector还是channel,都由SelectorProvider提供),在下面静态方法newSocket中,调用provider.openServerSocketChannel()方法,拿到jdk nio中原生网络编程中的ServerSocketChannel,也就是把netty中的channel和jdk中的channel进行挂钩

  • 构造方法:调用父类的构造方法,表明当前channel是关注接受连接的事件,实际设置是在AbstractNioChannel的doBeginRead方法里面

  • 其他方法

    • isActive:简单调用了jdk里面socket的相关方法,判断当前socket的接口是否处于一种绑定的状态

    • localAddress0()

    • doBind:分了一下版本,不同版本,处理不一样,不同的地方是backlog,backlog可以指定

    • 除了和底层socket关联的方法外,还有一个方法doReadMessages,这个方法是netty来处理客户端连接的,

      1.首先通过底层SocketUtils.accept接收客户端的连接,如果channel不为null,说明当前这个由serverSoket所派生出来的socket已经和客户端建立起连接,然后把当前jdk的channel打包成一个NioSocketChannel,并加入到当前这个buffer里面去,这个buffer可以包含的内容和类型很多,可以是实际数据,也可以是一个一个的socketChannel,成功后返回1

    • doConnect等等和客户端相关的方法,serverSocketChannel是没有一个连接的说法的,统一处理都是抛出一个异常

NioSocketChannel

  • 主要是负责具体的网络读写,连接、写、读

  • 连接

    • 1.首先判断本地地址是不是null,如果不为null,则做绑定

    • 2.绑定完成后,调用jdk的socket进行连接操作,如果连接不成功,注册一个关注连接事件

      在之前的nio客户端实现中,连接不成功是怎么做的?

      连接成功注册一个读事件,连接失败注册一个关注连接事件,因为nio的连接是一个异步操作,可能正在连接当中

  • 写1,doWrite方法,把父类的doWrite方法进行了覆盖,和它的父类AbstractNioByteChannel总体设计思路是差不多的,但是也有区别,其中最大循环次数还有有的,但是do-while循环里面怎么写做了扩充,

    • 1.首先判断netty的输出缓冲区是否为null,如果为null,把关注写事件清除掉

    • 2.如果非null,然后从当前要写的,输出缓冲区里面获取bufffer,然后放到ByteBuffer数组中,in.nioBuffers(1024, maxBytesPerGatheringWrite)方法中前一个参数表示最多取buffer的数量是1024个,第二个参数是表示最大字节数限制

    • 3.然后从输出缓冲区里面获取nioBufferCnt,表示当前有多少个buffer要外面写

    • 4.根据取的数量来分情况处理

    • 5.如果nioBufferCnt等于0,表示当前写的可能并不是ByteBuf类型的数据,而是文件类型的数据,此时调用doWrite0方法,而doWrite0又是调用父类的doWriteInternal方法去写

    • 6.如果nioBufferCnt等于1,直接调用channel.write去写,如果写入数据<=0,说明发送缓冲区是满的,此时调用incompleteWrite方法,去注册一个写事件并返回

    • 7.如果nioBufferCnt大于1时,调用channel.write去聚集写

      但是这种写不同于6中的写。Nio原生网络编程中称为gather(聚集)和scatter(分散),聚集是指应用程序往channel写时,需要通过缓冲区中转,nio原生网络编程里面允许多个缓冲区进行中转,然后多个缓冲buffer同时往channel中写,然后channel一次性把多个buffer的数据发送出去;分散刚好相反,channel读取到数据后,允许把数据分散到多个buffer中去

  • 写2,由于doWriteFileRegion、doReadBytes、doWriteBytes在AbstractNioByteChannel中都是抽象方法,在NioSocketChannel都有具体的实现,都是通过byteBuf把Buffer的数据写往channel,这里面也用到了ByteBuf这个基础组件,而且doWriteFileRegion方法体现了零拷贝,region.transferTo(javaChannel(), position),transferTo对应于linux里面的sendfile

Unsafe系列

  • 在每一个channel里面都有一个unsafe,unsafe是channel的一个内部接口,聚合在channel里面辅助进行网络读写的一个类。

  • 几乎每一层channel的接口都有对应的一个unsafe类,比如channel对应unsafe,AbstractNioChannel对应AbstracNioUnsafe,safe里面的方法都跟对应channel的方法对应

AbstractUnsafe

  • register方法:把当前unsafe所对应的channel和eventloop进行挂钩,同时注册到selector上去,同时进行事件的触发

    • 1.首先各种前置判定

    • 2.判定我们当前执行register的线程,是不是eventloop对应的线程,如果是,不存在并发安全问题,直接调用register0方法,如果不是,将它投放到eventloop所对应的队列中去执行

    • 进一步解释register0方法

      1.首先检查,判定channel是否是打开的,如果没有打开,直接return

      2.然后调用doRegister方法,它的子类AbstractNioChannel实现了该方法,把当前channel和selector挂钩,但是不对任何事件感兴趣

      3.触发pipeline的相关handlerAdded这个事件

      4.然后再触发pipeline.fireChannelRegistered()事件

      5.如果当前通道是活动状态,又触发pipeline.fireChannelActive()事件,如果没有触发pipeline.fireChannelActive()事件,同时如果当前通道被配置成自动读取(缺省就是自动读取),这时会调用beginRead方法,发起读操作,把前面注册的不感兴趣事件,注册为当前channel应该感兴趣的事件,是接受连接的就是接受连接,是读事件的就是读事件

  • bind方法

  • flush方法

猜你喜欢

转载自blog.csdn.net/Markland_l/article/details/114587711