服务器端启动
-
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
扫描二维码关注公众号,回复: 12874603 查看本文章$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的空转
- ?