深入解读Tomcat(三)

来源:互联网 发布:高德软件股票行情 编辑:程序博客网 时间:2024/04/30 08:20


/**
*作者:annegu
*日期:2009-06-20
*/

出处:http://annegu.iteye.com/blog/411807


在这第三部分里面我们主要看一下tomcat是如何接收客户端请求,并把这个请求一层一层的传递到子容器中,并最后交到应用程序中进行处理的。

首先,我们来了解一下什么叫做NIO和BIO。
在前面的解读tomcat里面,我们已经说到过了线程池。线程池,顾名思义,里面存放了一定数量的线程,这些线程用来处理用户请求。现在我们要讨论的NIO和BIO就是如何分配线程池中的线程来处理用户请求的方式。

BIO(Block IO):阻塞式IO。在tomcat6之前一直都是采用的这种方式。当一个客户端的连接请求到达的时候,ServerSocket.accept负责接收连接,然后会从线程池中取出一个空闲的线程,在该线程读取InputStream并解析HTTP协议,然后把输入流中的内容封装成Request和Response,在线程中执行这个请求的Servlet ,最后把Response的内容发送到客户端连接并关闭本次连接。这就完成了一个客户端的请求过程。我们要注意的是在阻塞式IO中,tomcat是直接从线程池中取出一个线程来处理客户端请求的,那么如果这些处理线程在执行网络操作期间发生了阻塞的话,那么线程将一直阻塞,导致新的连接一直无法分配到空闲线程,得不到响应。



NIO(Non-blocking IO):tomcat中的非阻塞式IO与阻塞式的不同,它采用了一个主线程来读取InputStream。也就是说当一个客户端请求到达的时候,这个主线程会负责从网络中读取字节流,把读入的字节流放入channel中。然后这个主线程就会到线程池中去找有没有空闲的线程,如果找到了,那么就会由空闲线程来负责从channel中取出字节,然后解析Http,转换成request和response,进行处理。当处理线程把要返回给客户端的内容放在Response之后,处理线程就可以把处理结束的字节流也放入channel中,最后主线程会给这个channel加个标识,表示现在需要操作系统去进行io操作,要把这个channel中的内容返回给客户端。这样的话,线程池中的处理线程的任务就集中在如何处理用户请求上了,而把与网络有交互的操作都交给主线程去处理。



对于这个非阻塞式IO,anne我想了一个很有趣的比喻,就好像去饭店吃饭点菜一样。餐馆的接待员就好像我们的操作系统,客人来了,他要负责记下客人的点菜,然后传达给厨房,厨房里面有好几位烧菜厨师(处理线程),主厨(主线程)呢比较懒,不下厨,只负责分配厨师的工作。现在来了一个客人,跟接待员说要吃宫宝鸡丁,然后接待员就写了张纸条,上面写了1号桌客人要点宫宝鸡丁,从厨房柜台上的一摞盘子里面拿了一个空的,把点菜单放在盘子里面。然后主厨就时刻关注这这些盘子,看到有盘子里面有点菜单的,就从厨房里面喊一个空闲的厨子说,你来把这菜给烧一下,这个厨子就从这个盘子里面拿出点菜单去烧菜了,好了这下这个盘子又空了,如果这时候还有客人来,那么接待员还可以把点菜单放到这个盘子里面去。等厨师烧好了菜,他就从柜台上找一个空盘子,把菜盛在里面,贴上纸条说这个是1号桌客人点的宫宝鸡丁。然后主厨看看,嗯,烧的还不错,可以上菜了,就在盘子上贴个字条说这盘菜烧好了,上菜去吧。最后接待员就来了,他一直在这候着呢,看到终于有菜可以上了,赶紧端去。嗯,自我感觉挺形象的,你们说呢?

因此,我们可以分析出tomcat中的阻塞式IO与非阻塞式IO的主要区别就是这个主线程。Tomcat6之前的版本都是只采用的阻塞式IO的方式,服务器接收了客户端连接之后,马上分配处理线程来处理这个连接;tomcat6中既提供了阻塞式的IO,也提供了非阻塞式IO处理方式,非阻塞式IO把接收连接的工作都交给主线程处理,处理线程只关心具体的如何处理请求。


好了,我们现在知道了tomcat是采用非阻塞式IO来分配请求的了。那么接下来我们就可以从发出一个请求开始看看tomcat是怎么把它传递到我们自己的应用程序中的。

程序员最爱看类图了,所以anne画了个类图,我们来照着类图,一个一个类来看。



我们首先从NioEndPoint开始,这个类是实际处理tcp连接的类,它里面包括一个操作线程池,socket接收线程(acceptorThread),socket轮询线程(pollerThread)。

首先我们看到的是start()方法,在这个方法里面我们可以看到启动了线程池,acceptorThread和pollerThread。然后,在这个类中还定义了一些子类,包括SocketProcessor,Acceptor,Poller,Worker,NioBufferHandler等等。SocketProcessor,Acceptor,Poller和Worker都实现了Runnable接口。
我想还是按照接收请求的调用顺序来讲会比较清楚,所以我们从Acceptor开始。

1、Acceptor负责接收socket,一旦得到一个tcp连接,它就会尝试去从nioChannels中去取出一个空闲的nioChannel,然后把这个连接的socket交给它,接着它会告诉轮询线程poller,我这里有个channel已经准备好了,你注意着点,可能不久之后就要有数据过来啦。下面的事情它就不管了,接着等待下一个tcp连接的到来。
我们可以看一下它是怎么把socket交给channel的:

Java代码 复制代码 收藏代码
  1.    protected boolean setSocketOptions(SocketChannel socket) {  
  2.        try {  
  3.            ... //ignore  
  4.         //从nioChannels中取出一个channel  
  5.            NioChannel channel = nioChannels.poll();  
  6.         //若没有可用的channel,根据不同情况新建一个channel  
  7.            if ( channel == null ) {  
  8.                if (sslContext != null) {  
  9.                    ...//ignore  
  10.                    channel = new SecureNioChannel(...);  
  11.                } else {  
  12.                    ...// normal tcp setup  
  13.                    channel = new NioChannel(...);  
  14.                }  
  15.            } else {                  
  16.                channel.setIOChannel(socket);  
  17.            ...//根据channel的类型做相应reset  
  18.            }  
  19.            getPoller0().register(channel); // 把channel交给poller  
  20.        } catch (Throwable t) {  
  21.            try {              
  22.            return false;   // 返回false,关闭socket  
  23.        }  
  24.        return true;  
  25. }  


要说明的是,Acceptor这个类在BIO的endpoint类中也是存在的。对于BIO来说acceptor就是用来接收请求,然后给这个请求分配一个空闲的线程来处理它,所以是起到了一个连接请求与处理线程的作用。现在在NIO中,我们可以看到Acceptor.run()里面是把processSocket(socket);给注释掉了(processSocket这个方法就是分配线程来处理socket的方法,这个anne打算在后面讲)。

2、Poller这个类其实就是我们在前面说到的nio的主线程。它里面也有一个run()方法,在这里我们就会轮询channel啦。看下面的代码:

Java代码 复制代码 收藏代码
  1. Iterator iterator = keyCount > 0 ? selector.selectedKeys().iterator() : null;  
  2. while (iterator != null && iterator.hasNext()) {  
  3.             SelectionKey sk = (SelectionKey) iterator.next();  
  4.          KeyAttachment attachment = (KeyAttachment)sk.attachment();  
  5.          attachment.access();  
  6.          iterator.remove();  
  7.          processKey(sk, attachment);  
  8.  }  


我们可以看到,程序遍历了所有selectedKeys,这个SelectionKey就是一种可以用来读取channel的钥匙。这个KeyAttachment又是个什么类型的对象呢?其实它记录了包括channel信息在内的又与这个channel息息相关的一些附加信息。MS很长的一句话,这么说吧,它里面有channel对象,还有lastAccess(最近一次访问时间),error(错误信息),sendfileData(发送的文件数据)等等。然后在processKey这个方法里面我们就可以把channel里面的字节流交给处理线程去处理了。
然后我们来看一下这个processKey方法:

Java代码 复制代码 收藏代码
  1. protected boolean processKey(SelectionKey sk, KeyAttachment attachment) {  
  2.             boolean result = true;  
  3.             try {  
  4.                 if ( close ) {  
  5.                     cancelledKey(sk, SocketStatus.STOP, false);  
  6.                 } else if ( sk.isValid() && attachment != null ) {  
  7.                     attachment.access();  
  8.                     sk.attach(attachment);  
  9.                     NioChannel channel = attachment.getChannel();  
  10.             ①        if (sk.isReadable() || sk.isWritable() ) {  
  11.             ②            if ( attachment.getSendfileData() != null ) {  
  12.                             processSendfile(sk,attachment,true);  
  13.                         } else if ( attachment.getComet() ) {  
  14.                             if ( isWorkerAvailable() ) {  
  15.                                 reg(sk, attachment, 0);  
  16.                                 if (sk.isReadable()) {  
  17.                                     if (!processSocket(channel, SocketStatus.OPEN))  
  18.                                         processSocket(channel, SocketStatus.DISCONNECT);  
  19.                                 } else {  
  20.                                     if (!processSocket(channel, SocketStatus.OPEN))  
  21.                                         processSocket(channel, SocketStatus.DISCONNECT);  
  22.                                 }  
  23.                             } else {  
  24.                                 result = false;  
  25.                             }  
  26.                         } else {  
  27.                             if ( isWorkerAvailable() ) {  
  28.                                 unreg(sk, attachment,sk.readyOps());  
  29.      ③                           boolean close = (!processSocket(channel));  
  30.                                 if (close) {  
  31.                                     cancelledKey(sk,SocketStatus.DISCONNECT,false);  
  32.                                 }  
  33.                             } else {  
  34.                                 result = false;  
  35.                             }  
  36.                         }                      }  
  37.                     }   
  38.                 } else {  
  39.                     //invalid key  
  40.                     cancelledKey(sk, SocketStatus.ERROR,false);  
  41.                 }  
  42.             } catch ( CancelledKeyException ckx ) {  
  43.                 cancelledKey(sk, SocketStatus.ERROR,false);  
  44.             } catch (Throwable t) {  
  45.                 log.error("",t);  
  46.             }  
  47.             return result;  
  48. }  


首先是判断一下这个selection key是否可用,没有超时,然后从sk中取出channel备用。然后看一下这个sk的状态是否是可读的,或者可写的,代码①处。代码②处是返回阶段,要往客户端写数据时候的路径,程序会判断是否有要发送的数据,这部分我们后面再看,先往下看request进来的情况。然后我们就可以在下面看到开始进行真正的处理socket的工作了,代码③处,进入processSocket()方法了。

Java代码 复制代码 收藏代码
  1. protected boolean processSocket(NioChannel socket, SocketStatus status, boolean dispatch) {  
  2.     try {  
  3.         KeyAttachment attachment = (KeyAttachment)socket.getAttachment(false);  
  4.         attachment.setCometNotify(false);   
  5.         if (executor == null) {  
  6.        ④     getWorkerThread().assign(socket, status);  
  7.         } else {  
  8.             SocketProcessor sc = processorCache.poll();  
  9.             if ( sc == null ) sc = new SocketProcessor(socket,status);  
  10.             else sc.reset(socket,status);  
  11.             if ( dispatch ) executor.execute(sc);  
  12.             else sc.run();  
  13.         }  
  14.     } catch (Throwable t) {  
  15.         return false;  
  16.     }  
  17.     return true;  


从④处可以看到明显是取了线程池中的一个线程来操作这个channel,也就是说在这个方法里面我们就开始进入线程池了。那么executor呢?executor可以算是一个配置项,如果使用了executor,那么线程池就使用java自带的线程池,如果不使用executor的话,就使用tomcat的线程池WorkerStack,这个WrokerStack我在后面有专门写它,现在先跳过。我们可以看到在start()方法里面,是这样写的:

Java代码 复制代码 收藏代码
  1.       if (getUseExecutor()) {  
  2.            if ( executor == null ) {  
  3.                executor = new ThreadPoolExecutor(...);  
  4.            }  
  5.        } else if ( executor == null ) {  
  6.            workers = new WorkerStack(maxThreads);  
  7. }  


好了,现在回到processSocket(),我们先来看有executor的情况,就是使用java自己的线程池。首先从processorCache中取出一个线程socketProcessor,然后把channel交给这个线程,启动线程的run方法。于是我们终于脱离主线程,进入了SocketProcessor的run方法啦!
0 0
原创粉丝点击