设为首页 收藏本站
查看: 1121|回复: 0

[经验分享] tomcat架构分析 (connector NIO 实现)

[复制链接]

尚未签到

发表于 2018-12-5 07:16:22 | 显示全部楼层 |阅读模式
     整个tomcat是一个比较完善的框架体系,各个组件之间都是基于接口的实现,所以比较方便扩展和替换。像这里的“org.apache.coyote.http11.Http11NioProtocol”和BIO的“org.apache.coyote.http11.Http11Protocol”都是统一的实现org.apache.coyote.ProtocolHandler接口,所以从整体结构上来说,NIO还是与BIO的实现保持大体一致。
  首先来看一下NIO connector的内部结构,箭头方向还是消息流;
DSC0000.jpg

  还是可以看见connector中三大块

  •   Http11NioProtocol
  •   Mapper
  •   CoyoteAdapter
  基本功能与BIO的类似,参见tomcat架构分析(connector BIO实现)。重点看看Http11NioProtocol.
  和JIoEndpoint一样,NioEndpoint是Http11NioProtocol中负责接收处理socket的主要模块。但是在结构上比JIoEndpoint要复杂一些,毕竟是非阻塞的。但是需要注意的是,tomcat的NIOconnector并非完全是非阻塞的,有的部分,例如接收socket,从socket中读写数据等,还是阻塞模式实现的,在后面会逐一介绍。
  如图所示,NioEndpoint的主要流程;
DSC0001.jpg

  图中Acceptor及Worker分别是以线程池形式存在,Poller是一个单线程。注意,与BIO的实现一样,缺省状态下,在server.xml中没有配置,则以Worker线程池运行,如果配置了,则以基于javaconcurrent 系列的java.util.concurrent.ThreadPoolExecutor线程池运行。
  Acceptor
  接收socket线程,这里虽然是基于NIO的connector,但是在接收socket方面还是传统的serverSocket.accept()方式,获得SocketChannel对象,然后封装在一个tomcat的实现类org.apache.tomcat.util.net.NioChannel对象中。然后将NioChannel对象封装在一个PollerEvent对象中,并将PollerEvent对象压入eventsqueue里。这里是个典型的生产者-消费者模式,Acceptor与Poller线程之间通过queue通信,Acceptor是events queue的生产者,Poller是events queue的消费者。
  注意:其实tomcat会有多个poller(可以配置,默认是Math.min(2,Runtime.getRuntime().availableProcessors())),所以不同的PollerEvent对象会分到某一个poller上。
  Poller
  Poller线程中维护了一个Selector对象,NIO就是基于Selector来完成逻辑的。在connector中并不止一个Selector,在socket的读写数据时,为了控制timeout也有一个Selector,在后面的BlockSelector中介绍。可以先把Poller线程中维护的这个Selector标为主Selector。
  Poller是NIO实现的主要线程。首先作为events queue的消费者,从queue中取出PollerEvent对象,然后将此对象中的channel以OP_READ事件注册到主Selector中,然后主Selector执行select操作,遍历出可以读数据的socket,并从Worker线程池中拿到可用的Worker线程,然后将socket传递给Worker。整个过程是典型的NIO实现。
  Worker
  Worker线程拿到Poller传过来的socket后,将socket封装在SocketProcessor对象中。然后从Http11ConnectionHandler中取出Http11NioProcessor对象,从Http11NioProcessor中调用CoyoteAdapter的逻辑,跟BIO实现一样。在Worker线程中,会完成从socket中读取httprequest,解析成HttpServletRequest对象,分派到相应的servlet并完成逻辑,然后将response通过socket发回client。在从socket中读数据和往socket中写数据的过程,并没有像典型的非阻塞的NIO的那样,注册OP_READ或OP_WRITE事件到主Selector,而是直接通过socket完成读写,这时是阻塞完成的,但是在timeout控制上,使用了NIO的Selector机制,但是这个Selector并不是Poller线程维护的主Selector,而是BlockPoller线程中维护的Selector,称之为辅Selector。
  NioSelectorPool
  NioEndpoint对象中维护了一个NioSelecPool对象,这个NioSelectorPool中又维护了一个BlockPoller线程,这个线程就是基于辅Selector进行NIO的逻辑。以执行servlet后,得到response,往socket中写数据为例,最终写的过程调用NioBlockingSelector的write方法。
  Java代码  

  •   public int write(ByteBuffer buf, NioChannel socket, long writeTimeout,MutableInteger lastWrite) throws IOException {
  •   SelectionKey key = socket.getIOChannel().keyFor(socket.getPoller().getSelector());
  •   if ( key == null ) throw new IOException("Key no longer registered");
  •   KeyAttachment att = (KeyAttachment) key.attachment();
  •   int written = 0;
  •   boolean timedout = false;
  •   int keycount = 1; //assume we can write
  •   long time = System.currentTimeMillis(); //start the timeout timer
  •   try {
  •   while ( (!timedout) && buf.hasRemaining()) {
  •   if (keycount > 0) { //only write if we were registered for a write
  •   //直接往socket中写数据
  •   int cnt = socket.write(buf); //write the data
  •   lastWrite.set(cnt);
  •   if (cnt == -1)
  •   throw new EOFException();
  •   written += cnt;
  •   //写数据成功,直接进入下一次循环,继续写
  •   if (cnt > 0) {
  •   time = System.currentTimeMillis(); //reset our timeout timer
  •   continue; //we successfully wrote, try again without a selector
  •   }
  •   }
  •   //如果写数据返回值cnt等于0,通常是网络不稳定造成的写数据失败
  •   try {
  •   //开始一个倒数计数器
  •   if ( att.getWriteLatch()==null || att.getWriteLatch().getCount()==0) att.startWriteLatch(1);
  •   //将socket注册到辅Selector,这里poller就是BlockSelector线程
  •   poller.add(att,SelectionKey.OP_WRITE);
  •   //阻塞,直至超时时间唤醒,或者在还没有达到超时时间,在BlockSelector中唤醒
  •   att.awaitWriteLatch(writeTimeout,TimeUnit.MILLISECONDS);
  •   }catch (InterruptedException ignore) {
  •   Thread.interrupted();
  •   }
  •   if ( att.getWriteLatch()!=null && att.getWriteLatch().getCount()> 0) {
  •   keycount = 0;
  •   }else {
  •   //还没超时就唤醒,说明网络状态恢复,继续下一次循环,完成写socket
  •   keycount = 1;
  •   att.resetWriteLatch();
  •   }

  •   if (writeTimeout > 0 && (keycount == 0))
  •   timedout = (System.currentTimeMillis() - time) >= writeTimeout;
  •   } //while
  •   if (timedout)
  •   throw new SocketTimeoutException();
  •   } finally {
  •   poller.remove(att,SelectionKey.OP_WRITE);
  •   if (timedout && key != null) {
  •   poller.cancelKey(socket, key);
  •   }
  •   }
  •   return written;
  •   }
  也就是说当socket.write()返回0时,说明网络状态不稳定,这时将socket注册OP_WRITE事件到辅Selector,由BlockPoller线程不断轮询这个辅Selector,直到发现这个socket的写状态恢复了,通过那个倒数计数器,通知Worker线程继续写socket动作。看一下BlockSelector线程的逻辑;
  Java代码  

  •   public void run() {
  •   while (run) {
  •   try {
  •   ......

  •   Iterator iterator = keyCount > 0 ? selector.selectedKeys().iterator() : null;
  •   while (run && iterator != null && iterator.hasNext()) {
  •   SelectionKey sk = (SelectionKey) iterator.next();
  •   KeyAttachment attachment = (KeyAttachment)sk.attachment();
  •   try {
  •   attachment.access();
  •   iterator.remove(); ;
  •   sk.interestOps(sk.interestOps() & (~sk.readyOps()));
  •   if ( sk.isReadable() ) {
  •   countDown(attachment.getReadLatch());
  •   }
  •   //发现socket可写状态恢复,将倒数计数器置位,通知Worker线程继续
  •   if (sk.isWritable()) {
  •   countDown(attachment.getWriteLatch());
  •   }
  •   }catch (CancelledKeyException ckx) {
  •   if (sk!=null) sk.cancel();
  •   countDown(attachment.getReadLatch());
  •   countDown(attachment.getWriteLatch());
  •   }
  •   }//while
  •   }catch ( Throwable t ) {
  •   log.error("",t);
  •   }
  •   }
  •   events.clear();
  •   try {
  •   selector.selectNow();//cancel all remaining keys
  •   }catch( Exception ignore ) {
  •   if (log.isDebugEnabled())log.debug("",ignore);
  •   }
  •   }
  使用这个辅Selector主要是减少线程间的切换,同时还可减轻主Selector的负担。以上描述了NIO connector工作的主要逻辑,可以看到在设计上还是比较精巧的。NIO connector还有一块就是Comet,有时间再说吧。需要注意的是,上面从Acceptor开始,有很多对象的封装,NioChannel及其KeyAttachment,PollerEvent和SocketProcessor对象,这些不是每次都重新生成一个新的,都是NioEndpoint分别维护了它们的对象池;
  Java代码  

  •   ConcurrentLinkedQueue processorCache = new ConcurrentLinkedQueue()
  •   ConcurrentLinkedQueue keyCache = new ConcurrentLinkedQueue()
  •   ConcurrentLinkedQueue eventCache = new ConcurrentLinkedQueue()
  •   ConcurrentLinkedQueue nioChannels = new ConcurrentLinkedQueue()
  当需要这些对象时,分别从它们的对象池获取,当用完后返回给相应的对象池,这样可以减少因为创建及GC对象时的性能消耗。


运维网声明 1、欢迎大家加入本站运维交流群:群②:261659950 群⑤:202807635 群⑦870801961 群⑧679858003
2、本站所有主题由该帖子作者发表,该帖子作者与运维网享有帖子相关版权
3、所有作品的著作权均归原作者享有,请您和我们一样尊重他人的著作权等合法权益。如果您对作品感到满意,请购买正版
4、禁止制作、复制、发布和传播具有反动、淫秽、色情、暴力、凶杀等内容的信息,一经发现立即删除。若您因此触犯法律,一切后果自负,我们对此不承担任何责任
5、所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其内容的准确性、可靠性、正当性、安全性、合法性等负责,亦不承担任何法律责任
6、所有作品仅供您个人学习、研究或欣赏,不得用于商业或者其他用途,否则,一切后果均由您自己承担,我们对此不承担任何法律责任
7、如涉及侵犯版权等问题,请您及时通知我们,我们将立即采取措施予以解决
8、联系人Email:admin@iyunv.com 网址:www.yunweiku.com

所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其承担任何法律责任,如涉及侵犯版权等问题,请您及时通知我们,我们将立即处理,联系人Email:kefu@iyunv.com,QQ:1061981298 本贴地址:https://www.yunweiku.com/thread-643375-1-1.html 上篇帖子: linux下tomcat安装配置 下篇帖子: TOMCAT下JSTL的配置和应用
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

扫码加入运维网微信交流群X

扫码加入运维网微信交流群

扫描二维码加入运维网微信交流群,最新一手资源尽在官方微信交流群!快快加入我们吧...

扫描微信二维码查看详情

客服E-mail:kefu@iyunv.com 客服QQ:1061981298


QQ群⑦:运维网交流群⑦ QQ群⑧:运维网交流群⑧ k8s群:运维网kubernetes交流群


提醒:禁止发布任何违反国家法律、法规的言论与图片等内容;本站内容均来自个人观点与网络等信息,非本站认同之观点.


本站大部分资源是网友从网上搜集分享而来,其版权均归原作者及其网站所有,我们尊重他人的合法权益,如有内容侵犯您的合法权益,请及时与我们联系进行核实删除!



合作伙伴: 青云cloud

快速回复 返回顶部 返回列表