网络编程19

服务器端启动

  • 1.创建eventloopgroup

    • (1)其中设定了线程的数量,缺省时设置为cpu*2,
    • (2)同时创建一个executor,但是其中有一个threadFactory,从而可以为每一个eventloop来创建线程,但是不是此时创建的线程,而是后面创建的
    • (3)创建NioEventLoop,这其中有一个run方法,会进行selector调用,处理各种各样的事件集,netty有个小的特殊处理,eventloop不光处理io事件,还会处理系统内部任务,根据ioRatio比例来决定处理io事件的比例
    • (4)创建Nio通信需要的组件,selector、selectionKey等
  • 2.创建ServerBootstrap实例

  • 3.相关配置,网络通讯相关配置,指定channel类型,指定通讯地址,把eventloopgroup和serverBootstrap挂钩,同时把自定义处理各种业务的handler添加到serverBootstrap的子handler中

  • 4.做相关的绑定

    • 4.1首先调用AbstractBootstrap中的doBind方法

      首先调用initAndRegister方法

      (1)创建一个channel

      $1.通过反射创建NioServerSocketChannel,之前指定channel的时候是传递的class对象,此时还有创建SocketChannel

      $2.AbstractNioChannel构造方法里面,会设置当前channel关注的事件,也就是设置NioServerSocketChannel关注的事件是OP_ACCEPT,但是只是简单的赋值,并未注册到通道,同时设置通信模式为非阻塞

      $3.AbstractChannel构造方法,newChannelPipeline()创建了一个DefaultPipeline,增加了两个缺省的handler,HeadContext:处理入站和出站,TailContext:处理入站

      (2)初始化这个channel

      $1.通信参数赋值给channel和子channel

      $2.如果定义了主channel,把ServerBootstapAcceptor加入主channel的pipeline,而ServerBootstapAcceptor是ServerBootstrap的内部类,是一个入站handler,覆盖了channelRead方法和exceptionCaught方法,所有执行完这一步后,主channel的pipeline中有3个handler,HeadContext->ServerBootstrapAcceptor->TailContext

      (3)注册这个channel

      $1.调用MultithreadEventloopGroup的register方法,会在多个loop中挑选一个eventloop,然后进入到SingleThreadEventLoop的register方法,然后继续进入到AbstractChannel的内部抽象类AbstractUnsafe中的register方法

      $2.此时会判断当前执行register方法的线程和singleThreadEventLoop是否是同一个

      $3.如果是同一个,就直接执行register0方法,在AbstractNioChannel的doRegister()中注册了一个键值为0的事件,然后在pipeline中传递ChannelRegistered事件

      $4.如果不是同一个,执行eventloop.execute接受一个Runnable型的参数,当前Runnable里会执行register0方法,接着执行SingleThreadEventExecutor的execute方法,通过addTask将任务加入队列,而这个任务就是register0方法

      $5.判断执行当前execute方法的线程和EventLoop是否是同一个,如果不是同一个,调用startThread方法,又调用doStartThread方法,进一步执行excutor.excute方法,进入到ThreadPerTaskExecutor的execute方法,新启动一个实际的线程,这里面的runnable是以匿名内部类的形式定义好的,其中thread = Thread.currentThread()会将持有的Thread和当前执行execute方法的实际线程Thread进行挂钩

    • 4.2根据返回的regFuture判断是否将初始化和注册两项工作完成

      (1)如果注册完成了,执行doBind0方法

      $1.异步执行channel.bind方法,调用了pipeline的绑定方法,因为bind是一个出战事件,进入到TailContext的bind方法,通过findContextOutbound找到出战处理器HeadContext,通过HeadContext的invokeBind去执行HeadContext的bind方法,然后又进入到AbstractChannel的bind方法,然后再进入他的子类NioServerSocketChannel的doBind方法,这里是调用了jdk的方法进行了实际的绑定

      $2.当绑定完成后,会异步执行激活事件(ChannelActive事件)

      $3.DefaultChannelPipeline.fireChannelActive方法执行,事件开始再pipeline上流动,因为ChannelActive是个入站事件,所以按照head->tail的顺序执行inbound处理器,ServerBootstrapAcceptor和tail的channelActive方法都没有做任何实质性的事情,只有head有代码

      $4.调用DefaultChannelPipeline中的read方法,因为read是一个outbound事件,因此findContextOutbound会按照tail->…->head寻找前面最近的一个outbound处理器,但是目前只有head是outbound处理器,所以进入到HeadContext的read方法中,又进入到AbstractChannel的beginRead方法

      $5.调用AbstractNioChannel的doBeginRead方法,会将感兴趣的事件设置为OP_ACCEPT(16)

      (2)如果没有,注册一个Listener,Listener里依然会调用doBind0方法

事件执行

  • 1.NioEventLoop在run方法中收集了各种selectionKey,然后进入processSelectedKeys()方法进行执行

  • 2.processSelectedKeysPlain是一般的,其余一种是优化的,缺省情况就是进入第一种

  • 3.把key拿出来处理,区分了AbstractNioChannel和SelectableChannel,分别调用各自的processSelectedKey方法

  • 4.AbstractNioChannel的processSelectedKey方法,首先判定key是否有效,如果有效,读取相关事件,如果是连接事件,就处理连接事件,如果是写事件,就处理写事件,这里将读事件和接受连接事件写在了一起,里面是调用了unsafe.read()方法,这里是一个多态操作,连接事件由NioServerSocketChannel里的unsafe实现,读事件由NioSocketChannel中的unsafe实现

eventloop处理服务器接受连接事件–unsafe.read调用

  • 处理unsafe.read也可以是read事件,根据当前channel的类型进行相应的方法调用,此处的channel是ServerSocketChannel,如果是SocketChannel则是处理读事件

  • 由AbstractNioMessageChannel.read方法处理

  • read方法

    • 1.创建了一个readBuf,用来方法子channel的,之前写的Nio编程中,处理读事件,首先也是通过ServerSocketChannel.accept方法把socketChannel创建出来

    • 2.在do-while循环里面处理客户端连接----int localRead = doReadMessages(readBuf)

      $为什么放在一个循环中调用doReadMessages来处理子channel的连接?

      因为有可能由多个客户端同时发起连接,所以在循环中调用相关方法,也就是之前为什么要用list来装子channel,linux中也是用backlog在保存正在三次握手的连接

    • 3.进入到doReadMessages方法中,又跳到NioServerSocketChannel.doReadMessages方法上,拿到当前产生的具体的socketChannel-----SocketChannel ch = SocketUtils.accept(javaChannel()),并把它包装成NioSocketChannel,并加到buf这个list中

      $NioSocketChannel在创建的时候会做什么?

      (1)设置通道是非阻塞的

      (2)增加channel的pipiline,创建出HeadContext和TailContext

      (3)子channel配置关注的事件应该是OP_READ

    • 4.执行pipeline.fireChannelRead(readBuf.get(i))方法,把每一个子channel取出来,放到主channel的pipeline上,触发fireChannelRead事件

    • 5.由于ChannelRead是一个入站事件,进行处理

      (1)HeadContext往后传递

      (2)ServerBootstrapAcceptor中,首先给每一个子channel增加它应该有的handler,也就是我们所写的各种业务handler,然后设置各种相关的参数childOptions,也就是各种tcp的参数,接下来调用chilaGroup.register,把子channel作为参数进行处理,最终调用到unsafe.register方法,然后调用register0方法,又调用AbtractNioChannel.doRegister方法,将当前channel和selector挂钩,目前注册的事件是0,真正配置成读事件是在beginRead方法中,然后在doBeginRead进行子channel真正关注事件的注册,在这两步之间牵涉了很多事件传播,fireChannelRegistered和fireChannelActive

    • 6.触发fireChannelReadComplete事件,如果有异常,会触发fireExceptionCaught事件

      $netty中channelReadComplete和channelRead的区别?

      (1)看服务器端的不容易看明白,去看客户端AbstractNioByteChannel.read方法真正读取网络数据,里面也是创建buffer,从网络上读取数据到buffer中,反复循环读取,读取完成了,在读取的过程中每读一次就触发channelRead事件,读取完成后触发channelReadComplete事件,结合ByteToMessageDecoder方法中channelRead和channelReadComplete方法。

      (2)因为粘包半包问题存在,传递给socketBuffer解析出来的包可能是一整个、多个、半个等多种情况,如果解析出来是一个完整的数据包,channelRead就会往后面传递,如果是半个,channelRead不会被触发和往后面传递的,而channelReadComplete只要把缓冲区的数据读完了,就会触发一次,如果是有多个数据包的情况,依然只会触发一次,而channelRead是有几个完整的数据包就会触发几次,如果发送数据很大,被拆分成几个包,并且缓冲区解析出来一共只有半个包,channelRead是不会触发的,因为channelRead在Netty实现的时候,往后传递的要素是完整的应用级消息,而channelReadComplete只要缓冲区读完了就触发,不管数据包是不是完整的应用级消息,回顾之前写handler,如果要处理数据,不会在channelReadComplete处理,而是在channelRead中处理,这就是ByteToMessageDecoder中定义了一个cumulation参数的原因,会进行合并,其中callDecode方法只有是完整的数据包才会进行处理

eventloop处理写事件—unsafe().forceFlush()

  • 此处并没有讲上面的方法,而是讲的另一种,由ctx直接调用写事件方法,AbstractChannelHandlerContext.writeAndFlush

AbstractChannelHandlerContext.writeAndFlush

  • 调用标题方法后,进一步调用write(msg, true, promise)方法,其中msg是要写的消息,true表示向缓冲区写的同时强制向对端刷出

    • 1.通过findContextOutbound()方法找离它最近的出战处理器

    • 2.然后调用invokeWriteAndFlush方法进行处理

    • 3.再调用invokeWrite0方法和invokeFlush0方法

    • 4.在invokeWrite0方法中,调用((ChannelOutboundHandler) handler()).write(this, msg, promise)方法,如果传递到了最后一个出战处理器,此时需要看HeadContext.write方法里面是怎么处理的

    • 5.在HeadContext.write方法中,又调用了unsafe.write方法,进入到AbstractUnsafe.write方法中,拿到outboundBuffer,把当前方法的msg加到outboundBuffer中

    • 6.而在invokeFlush0中,同样调用各种handler,最后进入HeadContext.flush方法

    • 7.在HeadContext.flush方法中,又调用unsafe.flush方法,进入到AbstractUnsafe.flush方法中,然后flush0方法,在flush0方法中,又调用了doWrite方法,因为一般往外写最终是由NioSocketChannel方法(因为最终与对端通信的一个一个的socketChannel),所以又调用NioSocketChannel.doWrite方法,

      $NioSocketChannel.doWrite

      (1)首先用了一个16次的do-while循环,写16次,防止io线程总是在一个channel上发送数据

      (2)如果写16次没有写完,并没有注册一个写事件,而是把剩下的数据投放到一个队列中去,等io线程空闲了再往里面去写,

客户端启动流程

  • 1.创建EventLoopGroup

  • 2.创建Bootstrap

  • 3.进行bootstrap的相关配置

  • 4.ChannelFuture f = b.connect().sync(),执行b.connect方法,客户端连接主要是在这个方法里面,然后进入到doResolveAndConnect方法中

    • 1.initAndRegister

      (1).创建channel,不同之处是此处创建的是NioSocketChannel

      (2).对channel进行初始化,加入相关业务handler,同时配置相关channel参数

      (3).对channel进行注册,和服务器基本一样,和eventloop挂钩,如果eventloop没有线程,则创建一个线程,并挂钩,同时将当前channel和selector挂钩,并注册一个0事件,即当前对什么都不感兴趣的事件

    • 2.当初始化和注册完成后,执行doResolveAndConnect0方法,在这个方法中,最终进行连接的是doConnect方法

      (1)doConnect方法调用了channel.connect方法

      (2)而channel.connect方法,又调用pipeline.connect方法,head->业务->tail,因为是出战处理器,最终由HeadContext处理,然后又调用unsafe.connect方法,进入到AbstractNioUnsafe.connect中

      (3)在AbstractNioUnsafe.connect中,如果doConnect(remoteAddress, localAddress)方法返回为true,做一点连接完成的事情,如果返回false,则继续进行连接

      $NioSocketChannel.doConnect

      $1.首先执行doBind0方法

      $2.boolean connected = SocketUtils.connect(javaChannel(), remoteAddress)

      $3.如果连接没有成功,注册一个关注连接事件,连接成功,则返回true

      $doConnect连接成功后,是什么时候注册的read事件?

      %%看下面的eventloop处理连接事件%%

      $为什么要用if包住doConnect(remoteAddress, localAddress)方法,并且如果返回的是false则继续去连接?

      因为nio是非阻塞的,发起连接有可能很快完成,也有可能是在三次握手过程中,对于不同的情况有不同的处理方法,之前通过jdk实现的nio通讯中,如果连接成功,则注册一个读事件,如果连接失败,则注册一个连接事件

      (4)如果连接成功,如果当前通道是active状态,则触发fireChannelActive事件,在pipeline上进行相关传播

      (5)

eventloop处理连接事件

  • 首先取消selectionKey的连接位

  • 然后执行unsafe.finishConnect()方法

    • 1.执行doFinishConnect

      (1).调用javaChannel().finishConnect()方法

    • 2.执行fulfillConnectPromise,在这个方法里也会触发channelActive事件,channelActive是一个入站事件,HeadContext->业务->Tail

      (1).触发channelActive事件

      (2).在HeadContext.channelActive方法中,除了执行ctx.fireChannelActive()方法向后传递外,还执行了readIfIsAutoRead方法,触发了channel.read()事件,此处的read是出战方法,又交给HeadContext.read方法处理,调用unsafe.beginRead方法,进一步调用doBeginRead()方法,将当前channel感兴趣的事件设置为OP_READ

      $read和channelRead

      channelRead是当前通道读取到了数据,往后面的handler上进行传递,进行实际的处理,最终交给应用程序做实际业务处理,所以是一个入站。而read是由应用层向网络发出read的请求,要求网络进行数据的读取,由应用程序向io发出,所以是出站,可以这么理解,netty不仅仅是把数据在pipeline上流转,还有read、write、writeAndFlush、connect、bind等指令也在pipeline上流转,后面这些都可以看成是网络向io发送的指令,只要是网络端过来的数据,一定是入站,出站包括,像应用程序主动写往对端的数据,应用像网络发出的操作或者请求

netty源码的特点

  • 1.公共的操作放到父类中实现,真正跟子类业务相关的才放到子类中
  • 2.netty是事件驱动的模式,相关的数据在各种handler之间流转

处理selector的空转

猜你喜欢

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