`
annegu
  • 浏览: 99632 次
  • 性别: Icon_minigender_2
  • 来自: 杭州
文章分类
社区版块
存档分类
最新评论

解读Tomcat(三):请求处理解析Part_1

阅读更多
/**
*作者:annegu
*日期:2009-06-20
*/

在这第三部分里面我们主要看一下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的:

    protected boolean setSocketOptions(SocketChannel socket) {
        try {
            ... //ignore
			//从nioChannels中取出一个channel
            NioChannel channel = nioChannels.poll();
			//若没有可用的channel,根据不同情况新建一个channel
            if ( channel == null ) {
                if (sslContext != null) {
                    ...//ignore
                    channel = new SecureNioChannel(...);
                } else {
                    ...// normal tcp setup
                    channel = new NioChannel(...);
                }
            } else {                
                channel.setIOChannel(socket);
			   ...//根据channel的类型做相应reset
            }
            getPoller0().register(channel); // 把channel交给poller
        } catch (Throwable t) {
            try {            
            return false;   // 返回false,关闭socket
        }
        return true;
 }


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

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

Iterator iterator = keyCount > 0 ? selector.selectedKeys().iterator() : null;
while (iterator != null && iterator.hasNext()) {
    		SelectionKey sk = (SelectionKey) iterator.next();
         KeyAttachment attachment = (KeyAttachment)sk.attachment();
         attachment.access();
         iterator.remove();
         processKey(sk, attachment);
 }


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

protected boolean processKey(SelectionKey sk, KeyAttachment attachment) {
            boolean result = true;
            try {
                if ( close ) {
                    cancelledKey(sk, SocketStatus.STOP, false);
                } else if ( sk.isValid() && attachment != null ) {
                    attachment.access();
                    sk.attach(attachment);
                    NioChannel channel = attachment.getChannel();
            ①        if (sk.isReadable() || sk.isWritable() ) {
            ②            if ( attachment.getSendfileData() != null ) {
                            processSendfile(sk,attachment,true);
                        } else if ( attachment.getComet() ) {
                            if ( isWorkerAvailable() ) {
                                reg(sk, attachment, 0);
                                if (sk.isReadable()) {
                                    if (!processSocket(channel, SocketStatus.OPEN))
                                        processSocket(channel, SocketStatus.DISCONNECT);
                                } else {
                                    if (!processSocket(channel, SocketStatus.OPEN))
                                        processSocket(channel, SocketStatus.DISCONNECT);
                                }
                            } else {
                                result = false;
                            }
                        } else {
                            if ( isWorkerAvailable() ) {
                                unreg(sk, attachment,sk.readyOps());
     ③                           boolean close = (!processSocket(channel));
                                if (close) {
                                    cancelledKey(sk,SocketStatus.DISCONNECT,false);
                                }
                            } else {
                                result = false;
                            }
                        }                      }
                    } 
                } else {
                    //invalid key
                    cancelledKey(sk, SocketStatus.ERROR,false);
                }
            } catch ( CancelledKeyException ckx ) {
                cancelledKey(sk, SocketStatus.ERROR,false);
            } catch (Throwable t) {
                log.error("",t);
            }
            return result;
}


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

    protected boolean processSocket(NioChannel socket, SocketStatus status, boolean dispatch) {
        try {
            KeyAttachment attachment = (KeyAttachment)socket.getAttachment(false);
            attachment.setCometNotify(false); 
            if (executor == null) {
           ④     getWorkerThread().assign(socket, status);
            } else {
                SocketProcessor sc = processorCache.poll();
                if ( sc == null ) sc = new SocketProcessor(socket,status);
                else sc.reset(socket,status);
                if ( dispatch ) executor.execute(sc);
                else sc.run();
            }
        } catch (Throwable t) {
            return false;
        }
        return true;
}


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

           if (getUseExecutor()) {
                if ( executor == null ) {
                    executor = new ThreadPoolExecutor(...);
                }
            } else if ( executor == null ) {
                workers = new WorkerStack(maxThreads);
     }


好了,现在回到processSocket(),我们先来看有executor的情况,就是使用java自己的线程池。首先从processorCache中取出一个线程socketProcessor,然后把channel交给这个线程,启动线程的run方法。于是我们终于脱离主线程,进入了SocketProcessor的run方法啦!
  • 大小: 3.2 KB
  • 大小: 4.9 KB
  • 大小: 99.2 KB
分享到:
评论
4 楼 dicmo 2010-11-17  
楼主图都自己画?
3 楼 genius 2009-11-06  
楼主,您是我的偶像啊,我是tomcat源代码的忠实信徒
2 楼 annegu 2009-07-20  
嗯,我也觉得处理过程少了点图。
我也想再写写bio的,下次专门写一个吧。
1 楼 ixu 2009-07-20  
难得看到这么长的分析型文章,感觉作者(anne)很费了些功夫,再支持一下!
提两个小问题:
1、类图最重要的,是表现类之间的关联关系,包括导航方向、多重性和是否聚合等,但你的类图里却统统画成了虚线引用,因此没有达到应有的信息传递效果
2、剖析处理过程时,试试画时序图,那会比文字描述更直观简明
另,处理点餐的例子蛮形象,最好把bio的方式一起比喻,并对比一下效率,会发现现实生活中的餐馆其实采用的都是nio的方式,生活中处处有哲学啊。

相关推荐

    解决tomcat配置ssl错误的解决办法

    在 Tomcat 中,SSL 连接器是负责处理 SSL 加密连接的组件。需要正确地配置连接器的端口号、协议、证书文件和密码等信息。 再次,需要正确地配置 Tomcat 的 APR 模块。在 Tomcat 6.0.33 版本中,默认启用了 APR 模块...

    解析Tomcat处理请求的类Connector<三>

    【标题】:“解析Tomcat处理请求的类Connector&lt;三&gt;” 在Java的Web服务器领域,Tomcat无疑是最为广泛使用的轻量级应用服务器之一。它以其开源、免费、高效的特点深受开发者喜爱。在这个系列的第三部分,我们将深入...

    tomcat处理一个http请求的详细过程

    tomcat中server配置文件的结构,以及处理一个http请求的全过程

    整合apache与tomcat所需要的模块,各版本mod_jk

    Apache和Tomcat是两种常用的Web服务器,Apache主要处理静态内容,而Tomcat是Java Servlet和JSP的容器。为了在Apache上运行Java应用,通常会使用一种名为`mod_jk`的模块进行整合。`mod_jk`是Apache HTTP服务器的一个...

    JSP中常见的Tomcat报错错误解析

    ### JSP中常见的Tomcat报错错误解析 在Java Server Pages (JSP) 开发过程中,经常遇到Apache Tomcat服务器抛出的各种错误代码。这些错误不仅会影响应用的正常运行,而且有时还会导致服务不可用。因此,理解这些错误...

    park_moonx4u_tomcat_tomcat停车场_停车场程序_

    1. **Apache Tomcat**:Apache Tomcat是开源的Java Servlet容器,支持Servlet和JSP标准,常用于搭建Web服务器,运行Java Web应用。 2. **HTML与JavaScript**:HTML负责网页内容的结构,JavaScript负责网页的动态行为...

    Tomcat-Ajp-lfi_python_

    1. **Apache Tomcat**:Apache Tomcat是一款开源的Java Servlet容器,用于部署和运行Java Web应用程序。它实现了Java EE的Servlet和JSP规范,是许多开发者和企业的首选Web服务器。 2. **AJP协议**:AJP协议允许Web...

    2007年图书:JSP_JSF_Tomcat Web编程从入门到精通

    JSP_JSF_Tomcat Web编程从入门到精通》这本书主要涵盖了Java服务器页面(JSP)、JavaServer Faces(JSF)以及Tomcat应用服务器在Web开发中的应用,旨在帮助初学者及有一定基础的开发者深入理解和掌握这三大技术。...

    tomcat源码解析

    处理request对象涉及多个步骤,包括解析连接、解析请求等。 - **解析连接**:创建Socket连接并读取客户端请求。 - **解析request**:提取请求中的关键信息。 - **解析请求头**:处理请求中的HTTP头信息。 #### 第5...

    ServletaJSP_Tomcat_Api.rar_tomcat API_tomcat a_tomcat lang api_t

    `org.apache.tomcat.ajp`包提供了处理AJP请求的相关类,使得非Java应用服务器可以与Tomcat共享Java应用。 4. **Tomcat Lang API**: 这可能是Tomcat中特定的实用工具类库,比如在处理国际化、字符串操作、异常处理等...

    Tomcat请求处理UML序列图

    ### Tomcat请求处理UML序列图解析 #### 一、概述 Apache Tomcat是一个开源的Servlet容器,主要用于执行Java Servlet和展示JSP页面。在Tomcat中,HTTP请求的处理流程是一个复杂的过程,涉及到多个组件之间的交互。...

    Apache+Tomcat+mod_jk+mod_ssl配置笔记

    这里`LoadModule`加载了mod_jk模块,`JkWorkersFile`指定了workers.properties的位置,`JkMount`则将所有对`/app`路径的请求转发给名为`worker1`的Tomcat实例处理。 最后,mod_ssl模块使Apache支持HTTPS协议,提供...

    Apache_HTTP_Server_与_Tomcat_的三种连接方式介绍.doc

    当 Apache 收到请求时,它会根据配置判断是否应该转发给 Tomcat 处理,如果是,则通过 AJP 协议将请求发送给 Tomcat,Tomcat 处理完毕后再通过 AJP 返回结果给 Apache,最后由 Apache 将结果发送给客户端。...

    IIS与TOMCAT集成文档

    这个模块允许IIS将动态请求转发给Tomcat处理。访问http://tomcat.apache.org/download-connectors.cgi,选择JK 1.2的稳定版本进行下载。注意,旧版本可能存在严重安全漏洞,因此建议使用最新稳定版本。 然后,进入...

    tomcat_iis_connector工具

    4. 设置Tomcat,确保它可以处理来自IIS的请求,可能需要配置`server.xml`文件中的Connector元素。 5. 测试整合环境,确保IIS能够正确转发请求至Tomcat,且Tomcat可以返回响应给IIS。 这样的集成方式有助于优化资源...

    BBS1.rar_java oracle tomcat_java 后台 oracle_oracle_oracle java to

    标题"BBS1.rar_java oracle tomcat_java 后台 oracle_oracle_oracle java to"和描述中的关键词揭示了这个压缩包包含的是一个基于Java、Oracle数据库和Tomcat服务器的后台留言板系统的开发资源。以下是对这些技术栈...

    S19-再看tomcat架构与请求处理流程1

    1. **Endpoint 组件**:这是 Tomcat 的网络层面,负责处理连接请求。Endpoint 监听特定端口(如 `8080`),接收客户端通过 TCP/IP 协议发送的 HTTP 请求。它会创建线程来处理每个新的连接,并将接收到的数据传递给下...

    tomcat源码+文档pdf+源码解析

    1. **Catalina**:这是Tomcat的核心组件,负责Servlet容器的主要功能,如容器管理、生命周期管理和请求处理。Catalina包含Context、Host和Engine等层次结构,它们分别对应于Web应用、虚拟主机和整个服务器。 2. **...

    tomcat架构解析_PDF电子书下载 高清 带索引书签目录_刘光瑞(著)

    《Tomcat架构解析》这本书由刘光瑞撰写,由人民邮电出版社出版,是一本深入探讨Tomcat服务器架构的专业书籍。Tomcat作为Apache软件基金会的项目之一,是世界上最流行的Java应用服务器,尤其在轻量级Web应用领域,其...

    tomcat 分配请求之——socket获取请求

    标题“Tomcat分配请求之——Socket获取请求”主要涉及的是Tomcat服务器在处理HTTP请求时的核心机制。在Web服务器中,Tomcat是一个广泛使用的开源应用服务器,它负责解析并响应来自客户端(如浏览器)的HTTP请求。这...

Global site tag (gtag.js) - Google Analytics