基于Tomcat9.0.21源码的请求过程分析

基于Tomcat9.0.21

Tomcat的IO模型
  1. BIO:一个连接对应一个线程。(在tomcat8.5后被淘汰)
  2. NIO:一个请求对应一个线程。(多个长连接,有连接,不一定有请求,如果用BIO就会有大量闲置线程。)
  3. APR:Apache Portable Runtime。Apache可移植运行库,以JNI的形式调用阿帕奇HTTP服务器的核心动态链接库,调用系统底层的IO接口。(不通过java的nio包)
  • NIO概述:Tomcat的NIO是基于I/O复用模型。

    可以发现,tomcat6以后利用java对read request body与write response使用了BIO,对read request headers和wait for next request实现了NIO。Tomcat8.5后全面取消BIO

    • 为什么要这样设置?nio在网络不好的时候,处理http request head的时候根本不会不浪费socketProcesser线程去等待读(bio是阻塞的,nio直接继续丢到poller池轮询,就绪了再分配socketProcesser线程处理)。另外bio在处理长连接的时候天生就弱势,必须要浪费一个socketProcesser等待读下一次http请求,指导超时才会释放socket(然而tomcat一般默认200个socketProcesser线程,bio超过75%长连接后面就直接当短连接处理就知道bio对长连接处理多弱),而nio是poller处理TCP连接,有数据就绪就分配到线程池处理http;单线程优势啊,几乎无内核用户态切换,不浪费cpu。
  • (Tomcat6.x)执行过程:客户端连接到达 -> nio接收连接 -> nio使用轮询方式读取文本并且解析HTTP协议(单线程) -> 生成ServletRequest、ServletResponse,取出请求的Servlet -> 从线程池取出线程,并在该线程执行这个Servlet -> 把ServletResponse的内容发送到客户端连接 -> 关闭连接。

组件与框架概述
  1. server对应多个service,service中具有connectors(多个connector)和一个contaioner,在tomcat9.x源码中,connector和engine持有service的引用,但是engine和connector之间并没有引用关系。
    图中server表示tomcat启动的服务,service表示webapp,每个webapp都包括n组engine和connector,connector负责接收用户请求,engine是逻辑容器。host为主机名,因为虚拟主机的存在,因此host也可能有多个,context就是web.xml转化而来的,wrapper就是servlet。

  2. 组件详细介绍

    • server 读取server.xml中的配置文件,将配置文件加载到Server的实现类StandardServer中。若关闭server,将关闭全部组件。包含方法有获取端口,地址,持有多个service引用。
    • service Service接口实现类standardService,逻辑上持有Connector和Container。并且持有向server的引用。service和server是相互关联的,server向外提供访问这些service的接口
    • connector,负责处理客户端发来的连接,默认协议有http,https和AJP。主要作用为根据不同请求解析客户端请求,将解析好的请求转发给connector关联的engine容器,即container。这个组件就是IO模型应用的地方,本质上是为了解决请求等待的问题。100000个长连接,不可能开那么多线程,那就开少量的selector线程轮询,将请求放到线程池中执行即可。

    以下四个都继承自Container接口,都属于Container

    manger用于管理session,resources对每个webaap对应部署结构的封装,loader对每个webapp自有的classloader的封装,mapper封装了请求资源uri和相对应处理wrapper容器的映射关系。

    • 以上组件中的cluster用于tomcat管理,Realm实现用户权限管理,pipeline和value利用职责链模式处理pipeline上的各个value。
生命周期、启动、停止
  1. tomcat中所有组件都继承LifeCycle接口,lifecycle的抽象实现类中有initt方法,如果当前状态为NEW,那么就将状态转换为INITIALIZING状态并进行初始化,初始化完毕后状态为INITIALIZED。

    //LifeCycleBase.java
    //init方法
    @Override
    public final synchronized void init() throws LifecycleException {
        if (!state.equals(LifecycleState.NEW)) {
            invalidTransition(Lifecycle.BEFORE_INIT_EVENT);
        }
    
        try {
            setStateInternal(LifecycleState.INITIALIZING, null, false);
            initInternal();
            setStateInternal(LifecycleState.INITIALIZED, null, false);
        } catch (Throwable t) {
            handleSubClassException(t, "lifecycleBase.initFail", toString());
        }
    }
    ``
    
  2. 以StandardServer为例,他的initInternal方法中调用类加载器加载jar包,这是双亲委派的,然后遍历services组件并init。service组件对其中的connectors和engine还有Executors进行init。

    //StandardServer.java
    //initInternal方法
    for (int i = 0; i < services.length; i++) {
        services[i].init();
    }
    
  3. init之后是start,以StandardServer为例,让内部的services调用start,以此类推。

    //LifeCycleBase.java
    //start方法节选
    try {
        setStateInternal(LifecycleState.STARTING_PREP, null, false);
        startInternal();
        if (state.equals(LifecycleState.FAILED)) {
            // This is a 'controlled' failure. The component put itself into the
            // FAILED state so call stop() to complete the clean-up.
            stop();
        } else if (!state.equals(LifecycleState.STARTING)) {
            // Shouldn't be necessary but acts as a check that sub-classes are
            // doing what they are supposed to.
            invalidTransition(Lifecycle.AFTER_START_EVENT);
        } else {
            setStateInternal(LifecycleState.STARTED, null, false);
        }
    }
    
  4. 小结:每个组件的生命周期是统一的,init->start->stop->destory,这是由LifeCycleBase统一通知的,每个组件单独实现initInternal、startInternal、stopInternal、destoryInternal,使用方法前先判断LifeCycle枚举类状态,然后根据状态执行不同的生命周期方法,执行完成后改变状态。

  5. 观察者设计模式。为什么要用观察者设计模式呢?多个观察者对象同时监听一个主体对象,当主体对象作出某种操作时,主体对象会通知观察者,然后观察者作出相应动作。当一个对象改变的同时需要改变其他对象时,可以采用观察者模式,易于扩展且降低耦合。 所以狼来了的故事里,小孩是通知者,也就是subject,大人们是监听者,在大话设计模式里,前台是通知者,同事们是监听者。

    //监听者(观察者)
    //public class EngineConfig implements LifecycleListener
    @Override
    public void lifecycleEvent(LifecycleEvent event) {
    
        // Identify the engine we are associated with
        try {
            engine = (Engine) event.getLifecycle();
        } catch (ClassCastException e) {
            log.error(sm.getString("engineConfig.cce", event.getLifecycle()), e);
            return;
        }
    
        // Process the event that has occurred
        if (event.getType().equals(Lifecycle.START_EVENT))
            start();
        else if (event.getType().equals(Lifecycle.STOP_EVENT))
            stop();
    
    }
    
    //被监听者(被观察者) server组件
    protected void startInternal() throws LifecycleException {
    
            fireLifecycleEvent(CONFIGURE_START_EVENT, null);
            setState(LifecycleState.STARTING);
    
    //当被监听者start的时候,监听器会通知所有
    protected void fireLifecycleEvent(String type, Object data) {
        LifecycleEvent event = new LifecycleEvent(this, type, data);
        for (LifecycleListener listener : lifecycleListeners) {
            listener.lifecycleEvent(event);
        }
    }
    
  6. EngineConfig,HostConfig,ContextConfig都是通过监听器来实现的,达到了配置逻辑和容器的解耦,修改配置逻辑不需要改动容器,修改容器也不用改动配置逻辑。

  7. 当我们运行tomcat/bin/startup的时候发生了什么?我们相当于调用Bootstrap类的main方法,并传入一个“start”参数,Bootstrap会根据这个字符串反射调用方法。

    //Bootstrap.java
    public static void main(String args[]) {
        synchronized (daemonLock) {
            if (daemon == null) {
                // Don't set daemon until init() has completed
                Bootstrap bootstrap = new Bootstrap();
                try {
                    bootstrap.init();
                } catch (Throwable t) {
                    handleThrowable(t);
                    t.printStackTrace();
                    return;
                }
                daemon = bootstrap;
            } else {
                // When running as a service the call to stop will be on a new
                // thread so make sure the correct class loader is used to
                // prevent a range of class not found exceptions.
                Thread.currentThread().setContextClassLoader(daemon.catalinaLoader);
            }
        }
    
        try {
            String command = "start";
            if (args.length > 0) {
                command = args[args.length - 1];
            }
    
            if (command.equals("startd")) {
                args[args.length - 1] = "start";
                daemon.load(args);
                daemon.start();
            } else if (command.equals("stopd")) {
                args[args.length - 1] = "stop";
                daemon.stop();
            } else if (command.equals("start")) {
                daemon.setAwait(true);
                daemon.load(args);
                daemon.start();
                if (null == daemon.getServer()) {
                    System.exit(1);
                }
            } else if (command.equals("stop")) {
                daemon.stopServer(args);
            } else if (command.equals("configtest")) {
                daemon.load(args);
                if (null == daemon.getServer()) {
                    System.exit(1);
                }
                System.exit(0);
            } else {
                log.warn("Bootstrap: command \"" + command + "\" does not exist.");
            }
        } catch (Throwable t) {
            // Unwrap the Exception for clearer error reporting
            if (t instanceof InvocationTargetException &&
                    t.getCause() != null) {
                t = t.getCause();
            }
            handleThrowable(t);
            t.printStackTrace();
            System.exit(1);
        }
    }
    

  1. 在这个这个类中还值得关注一点类加载器。根据我之前的了解,除了webapp的类加载器默认子加载器优先外,其他都还是执行双亲委派的。

    private void initClassLoaders() {
        try {
            commonLoader = createClassLoader("common", null);
            if (commonLoader == null) {
                // no config file, default to this loader - we might be in a 'single' env.
                commonLoader = this.getClass().getClassLoader();
            }
            //commonLoader是catalinaLoader和sharedLoader的父加载器。根据上面的英文注释,默认情况下这三个加载器的加载范围是同一个,通过查阅配置文件也可以得知,只有common有加载路径,shared和catalina都是没有加载路径的。
            catalinaLoader = createClassLoader("server", commonLoader);
            sharedLoader = createClassLoader("shared", commonLoader);
        } catch (Throwable t) {
            handleThrowable(t);
            log.error("Class loader creation threw exception", t);
            System.exit(1);
        }
    }
    
    

    catalina.properties关于类加载器配置如下:
    common.loader=" c a t a l i n a . b a s e / l i b &quot; , &quot; {catalina.base}/lib&quot;,&quot; catalina.base/lib","{catalina.base}/lib/.jar"," c a t a l i n a . h o m e / l i b &quot; , &quot; {catalina.home}/lib&quot;,&quot; catalina.home/lib","{catalina.home}/lib/.jar"
    server.loader=
    shared.loader=

    源码中解析路径的时候还要做${字符判断的原因就在这里。

  2. 上文中看到了catalina_base和catalina_home,有什么区别呢?home是安装目录,base是工作目录。home主要包括bin和lib,base包括conf,logs,temps,webapps,work和shared。所以IDEA运行tomcat其实是为每个webapp都复制了一份catalina_base(work,logs,conf),但是公用同一个tomcat的bin和lib,同一个版本但是可以多实例的运行,部署难度降低,不然你就要安装多个tomcat。然后还要再conf->catalina->localhost中放一个工程.xml以表示工程位置在哪里。



  3. 总结

启动过程为:main方法接收到参数start交给bootstrap类,boostrap类委托给catalina类,catalina类中initserver,一旦开始组件的初始化就一发不可收拾了,就链式将该组件下的所有组件init。然后执行boostrap的start,委托给catalina进行start,然后server先start,之后所有组件start。

请求过程
  1. tomcat9.x的IO模型
    • nio模式是基于Java nio包实现的,能提供非阻塞I/O操作,拥有比传统I/O操作(bio)更好的并发运行性能。在Tomcat9中是默认模式。
    • apr模式(Apache Portable Runtime/Apache可移植运行时),是Apache HTTP服务器的支持库。可以简单地理解为Tomcat将以JNI的形式调用Apache HTTP服务器的核心动态链接库来处理文件读取或网络传输操作,从而大大地提高了对静态文件的处理性能。
  2. 概述
  3. 启动顺序
    • Connector组件的构造方法通过反射创建Http11NioProtocol类实例,这个实例内部有一个NioEndpoint(负责接收请求)和一个ConnectorHandler(负责处理请求)。NioEndpoint和ConnectorHandler在创建Http11NioProtocol的时候被创建出来。
  4. NioEndPoint包含三个组件:Acceptor(负责监听请求)、Poller(接收监听到的请求socket)、SocketProcessor(Worker,处理socket,本质上委托给ConnectionHandler处理)。
Acceptor
  1. Acceptor在NioEndpoint中被创建出来,Acceptor这个类本身是实现Runnable的,因此是一个线程。在9.0.21版本中,这里是直接运行的,在9.x的早期版本中应该还有一个acceptors保存了所有的acceptor。

    AbstractNioEndPoint.java
    protected void startAcceptorThread() {
        acceptor = new Acceptor<>(this);
        String threadName = getName() + "-Acceptor";
        acceptor.setThreadName(threadName);
        Thread t = new Thread(acceptor, threadName);
        t.setPriority(getAcceptorThreadPriority());
        t.setDaemon(getDaemon());
        t.start();
    }
    
    //public class Acceptor<U> implements Runnable 
    

    在Acceptor.java -> run()中,socket = endpoint.serverSocketAccept(); 这里的endpoint.serverSocketAccept本质上是调用的serverSocketChannel.accept。再看下一段代码,NioEndpoint->initServerSocket()中,serverSock.configureBlocking(true); 说明是阻塞的,因此Acceptor在监听连接的时候是阻塞的。当有请求过来时,endpoint.setSocketOptions(socket),这里是将socket包装起来生成secketWrapper(设置socket发送、接收缓存大小,心跳检测),将SocketChannel包装成NioChannel,调用poller池中的register方法提交给poller。

    public class NioEndpoint {
        protected boolean setSocketOptions(SocketChannel socket) {
                // Process the connection
                try {
                    // Disable blocking, polling will be used
                    socket.configureBlocking(false);
                    Socket sock = socket.socket();
                    socketProperties.setProperties(sock);
    
                    NioChannel channel = null;
                    if (nioChannels != null) {
                        channel = nioChannels.pop();
                    }
                    if (channel == null) {
                        SocketBufferHandler bufhandler = new SocketBufferHandler(
                                socketProperties.getAppReadBufSize(),
                                socketProperties.getAppWriteBufSize(),
                                socketProperties.getDirectBuffer());
                        //根据是否是HTTPS协议返回不同的channel的包装类
                        if (isSSLEnabled()) {
                            channel = new SecureNioChannel(socket, bufhandler, selectorPool, this);
                        } else {
                            channel = new NioChannel(socket, bufhandler);
                        }
                    } else {
                        channel.setIOChannel(socket);
                        channel.reset();
                    }
                    NioSocketWrapper socketWrapper = new NioSocketWrapper(channel, this);
                    channel.setSocketWrapper(socketWrapper);
                    socketWrapper.setReadTimeout(getConnectionTimeout());
                    socketWrapper.setWriteTimeout(getConnectionTimeout());
                    socketWrapper.setKeepAliveLeft(NioEndpoint.this.getMaxKeepAliveRequests());
                    socketWrapper.setSecure(isSSLEnabled());
                    poller.register(channel, socketWrapper);
                    return true;
                } catch (Throwable t) {
                    ExceptionUtils.handleThrowable(t);
                    try {
                        log.error(sm.getString("endpoint.socketOptionsError"), t);
                    } catch (Throwable tt) {
                        ExceptionUtils.handleThrowable(tt);
                    }
                }
                // Tell to close the socket
                return false;
            }
    }
    
Poller和PollerEvent
  1. public class Poller implements Runnable,而且每个poller都持有一个selector。那PollerEvent呢?这个类的run方法中进行socketChannel.register(selector,OP_XX)。Poller的run方法中调用events()方法,这个方法就是遍历一个元素的PollerEvent的队列,然后依次run,即依次register。这样每个poller上就注册了全部socketChannel,任何一个socketChannel发来请求的时候,poller都能通过select获得。根据之前JAVA NIO的那一节,select结束阻塞后,可以通过Set keys = selector.SelectedKeys获得selector上所有selectionkey,每个key都可以获得selector和channel。换而言之,poller可以通过select获得所有因为请求而触发的事件,而每个事件都对应着一个NioSocketChannel,这个channel可以读取请求的数据(因为持有SocketWrapper引用)。下一步就是交给SocketProcessor。
SocketProcessor和ConnectionHandler
  1. 这个小组件主要任务是吧NioSocket这个Channel的socket交给ConnectionHandler这个组件去处理。ConnectionHandler主要是解析http协议,并封装成request和response对象交给CoyoteAdapter。
小结
  • 9.0.21版本IO模型的特点为,acceptor和poller都是单线程的,早期版本acceptor是有数组的,9.x早期版本poller也是有数组的,但是9.0.21都换成了单线程,且不交给线程池运行,这两个线程是从EndPoint.startInternal开始就创建并运行的。Acceptor组件负责以阻塞的方式监听连接,监听到后交给NioEndpoint封装NioSocketChannel和SocketWrapper,poller负责创建PointEvent实例(NioSocketChannel是核心)并加入events这个队列中。P.S.虽然PointEvent也是Runnable的实现类,但是他从来没有当做线程执行过,而是在poller的select前,遍历events并全部run一遍来保证注册所有事件。select后,遍历所有的SelectionKey,然后找到对应的NioSocketChannel,并取出socket,交给SocketProcessor。SocketProcessor先尝试从缓存中拿,缓存中不够的话会new一个,缓存的目的是为了避免反复的生成对象和GC,然后交给线程池去执行。这个线程池的阻塞队列是TaskQueue extends LinkedBlockingQueue,核心池为10,最大池为200。但是值得注意的是,虽然acceptor是单线程的,但是一个service有多个connector,因此用于接收客户端连接还是有多个NIO线程,不同的connector的SocketProcessor应该是交给不同线程池去运行,因为无论是executor还是acceptor都是属于某一个connector组件的。
  • socketprocessor交给connectionHandler的process方法,处理socket。代码中是socketprocessor的run方法中调用doRun方法,doRun方法中先getHandler,然后connectionHandler.process()->AbstractProcessorLight.process(socketWrapper)->Http11Processor.service()
  • reactor模式:事件驱动,有多个并发请求,有一个(或多个)Selector响应请求,并分发给其他线程处理。
Container
  1. 概览


  2. 书接上回,SocketProcessor.doRun()->ConnectionHandler.process()->Http11Processor.process()->Http11Processor.service(socketWrapper)->Adapter.service(request,)。根据这个执行链,到了CoytoteAdapter,此时已经封装好了request和response,并且作为参数传到了adapter的service中。这里的request和response还不是servlet中的,但是有转换关系,具体来说,adapter中解析出来的request和response比servlet中的更强

  3. PipeLine之间使用valve连接,valve之间又通过单链表的形式组织而成。

    PipeLine之间就是职责链,通过父子节点的方式连接。这里有个疑问,父节点如何找到自己的子节点呢?Tomcat的解决方案是,Engine与Host之间被一个Valve连接,这个Valve用于指明下一个子节点是谁,Engine有StrandEngineVavle,关系为:

    上图所有的Value都拼错了,应该是Valve。这些Valve可以用户自己写,然后配置在xml中即可,相当于起到了拦截器的作用。Valve的组织形式是单链表。

    (1)每个Pipeline都有特定的Valve,而且是在管道的最后一个执行,这个Valve叫做BasicValve,BasicValve是不可删除的;(2)在上层容器的管道的BasicValve中会调用下层容器的管道。

  4. 每个Valve的作用:

    • StandardEngineValve:传递到Host容器中
    • AccessLogValve:Host容器1号阀门,用于记录日志
    • ErrorReportValve:Host容器2号阀门用于记录异常并封装到response中
    • StandardHostValve:从request中找到映射的context容器(说明1个host有多个context),更新session的访问时间
    • StandardContextValve:禁止对WEB-INF/META-INF目录下资源的重定向,从request中获取Wrapper(Mapper组件将request映射到正确的servlet)
    • StandardServerWrapperValve:调用StandardWrapper的loadServlet方法生成servlet,调用ApplicationFilterFactory生成filter链条。
  5. 在Wrapper组件中,每个wrapper对应这样一个servlet和一条filterChain。每个请求会通过Mapper组件映射到一个Wrapper中,然后这个请求进入StandardWrapperValve中,先用filterChain进行处理,然后执行service方法。

Mapper
  1. Mapper组件由service管理,具有MappedHost,MappedContext,MappedWrapper。我们熟知的servlet的映射匹配方式,比如精确匹配,通配符匹配,扩展名匹配就是通过wrapperContext中不同的数组实现的。
  2. CoyoteAdapter中,之前说到用service方法处理request和response,在9.0.21版本中,service()方法会调用postParseRequest()方法,在此方法中,connector.getService().getMapper().map(serverName, decodedURI, version, request.getMappingData()); 来找到对应的host,context和wrapper,用于之后BasicValve向下一层容器派发,这些都是保存在request中的。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值