服务器之Tomcat分析

本文详细解析了Tomcat服务器的架构原理,包括组件层级、初始化与启动过程、类加载机制、热部署、定制线程池及异步Servlet的实现。揭示了Tomcat如何处理Web请求,以及其内部组件如Connector、Container、Service的职责划分。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录

1.Tomcat基本介绍

2.Tomcat的类加载和热部署

3.Tomcat的定制版线程池

4.Tomcat的异步servlet


1.Tomcat基本介绍


1.1概念

  • Server:接受请求并解析,完成相关任务,返回处理结果
  • Connector:开启Socket并监听客户端请求,返回响应数据
  • Container:负责具体的请求处理
  • Engine:Container是一个通用的概念,而Engine是一个具体概念来负责具体的处理器请求
  • Service:服务,对Connector和Container的封装
  • Host:代表主机域名IP(不同IP会通过DNS映射到同一个网站地址)
  • Context:web应用,在域名上在进行划分,对应到不同资源地址 
  • wrapper(代表Servlet):对资源地址链接请求进行包装

ps:通用概念上的Container包含Engine、Host、Context、wrapper

  • 一个Tomcat可以包含一个Server。
  • 一个Server可以包含多个Service(可以实现通过不同的端口号来访问同一台机器上部署的不同应用)。
  • 扩展:可以适用于多端口多应用的场景;
  • 用一个Tomcat建立多个Server_摩西的博客 爱it 爱生活 爱折腾 -优快云博客
  • 一个Service负责维护多个Connector和一个Container,这样来自Connector的请求只能由Service维护的一个Container处理。
  • 一个Engine可以包含多个Host。
  • 一个Host可以包含多个Context。
  • 扩展:将电脑的静态资源映射到tomcat,同一个域名下不同的路径,默认可以拷贝到webapps目录下访问,但是不想拷贝就需要配置Context的docBase和path。
  • tomcat <context path>的意义及作用 - 码农时刻 - 博客园
  • 一个Context(web应用)可以包含多个wrapper(用来支持Servlet)。

1.3初始化过程

  • 启动类Bootstrap:
  • main方法,初始化类加载器-common类加载器【common的父类加载器是应用类加载器catalina和share类加载器都会默认使用父类common类加载器】和创建启动类catalina对象,在反射调用catalina的 load 和 start 方法;
  • 外嵌Tomcat服务器初始化:调用Catalina对象的load方法
  • Catalina,调用load方法 通过digester【基于SAX解析事件驱动回调实现】解析server.xml文件,并且初始化Server;
  • Server,初始化多个Service;
  • Service,依次初始化  Engineexecutor,MapperListener,ConnectorHost、Context、Wrapper三个执行器是在start开始时才会被初始化】; 
  • Connector,初始化protocolHandler(Http11NioProtocol类),给protocolHandler设置CoyoteAdapter适配器等等;
  • protocolHandler,初始化EndPoint进行socket绑定端口【NioEndPoint】;
  • 基本呢--就是初始化这些类,并且通过JMX进行管理Bean类;
  • 父类的方法:通过观察者模式,每次设置状态时就会创建一个事件,遍历调用对应的监听器处理该事件;
  • 扩展:springboot是如何初始化的;
  • 因为内嵌tomcat没有xml文件,它是通过new创建对象来代替digester进行初始化对象的,SpringBoot的将applicationcontext的refresh之后通过代理类代替了Catalina的工作;

1.4启动过程调用Catalina对象的strat方法

  • Catalina,调用start方法 启动Server
  • Server,启动多个Service
  • Service,启动Engine、启动executor、启动MapperListener【将监听器自身添加到容器,Engin、Host、context、wrapper等待发送对应的事件】、启动Connector【会启动protocolHandler---启动endpoint创建acceptor和poller线程--会创建线程池等等
    • poller线程,如果CPU是多核就开启2个,否则开启一个(非阻塞的)
    • pollerThreadCount = Math.min(2,Runtime.getRuntime().availableProcessors())
    • accept线程,默认是一个(阻塞的),可以设置参数调整。
    • acceptorThreadCount = 1
  • engine,启动cluster,启动realm,启动子容器【children.start()利用线程池】,启动pipeline,启动线程【ContainerBackgroundProcessor后台处理线程,实现热加载热部署等功能
  • host,启动cluster,启动realm,启动子容器【children.start()】,启动pipeline,
  • context,发送一个CONFIGURE_START_EVENT事件会解析web.xml,,在将web.xml的listener,filter,servlet【针对配置了 load-on-startup 属性的 Servlet】的实例进行构造,将信息放入到context和wrapper里,然后启动子容器【context表示一个web应用】;
    • 启动listener,spring就是增加了一个ContextLoaderListener来进行初始化spring的。
  • valve,在StandardContext、StandardWrapperValve等容器类创建的时候会添加到StandardPipeline里,会在请求到来的时候依次调用,符合责任链设计模式。
  • 扩展:ContainerBackgroundProcessor后台处理线程运行将web应用加载进来;
  • 线程每10秒启动一次,调子容器的处理方法发布一个循环事件,HostConfig监听器会进行实际的加载部署的app应用--启动事件也会进行触发;
  • 监听器这些类都是在digester里进行设置的;
  • 扩展:web.xml是如何解析到context的;
  • 后台处理线程在加载应用时,创建子容器时需要start开启容器,发布一个config的事件,通过ContextConfig监听器处理将web.xml解析到context里,进行配置;
  • 扩展:热加载也是通过process后台处理线程重新init、start实现的;
  • 扩展:MapperListener的作用;
  • 在startInternal的时候给Host、Context、Wrapper这三个容器添加MapperListener自身,容器启动之后发送AFTER_START_EVENT事件,MapperListener接收到之后在lifecycleEvent里将信息解析到Mapper;
  • 热加载、热部署都会造成容器重新start,所以也会发送AFTER_START_EVENT事件,进行路径更新。

1.5web请求和处理

  • NioEndPoint(包含Acceptor和Poller线程)-->Http11Processor
  • 发送一个请求,Acceptor线程通过accept阻塞的接收请求,并包装成PollerEvent事件放入队列;
  • Poller线程从队列取元素在将socketChannel注册到selector上,然后select选出IO准备就绪的连接,通过线程池来具体的执行;
    • select会阻塞住,如果队列有事件了,会进行weakup唤醒。
  • 线程池默认为LinkedBlockingQueue
  • 扩展:acceptor和poller的线程通信;
  • 通过一个Event队列来进行通信;
  • 扩展:Acceptor限制最大连接数;
  • 通过AQS来进行实现,如果超过了最大连接数那么会在AQS的同步队列等待,阻塞当前线程;
  • 创建一个processor对象(Http11Processor类),将socket的数据进行解析请求行/请求头/请求体封装出request;
  • 通过适配器adaptor(CoyoteAdaptor类)service方法,实际的调用container容器的valve执行链,实际会调用invoke方法进行处理;
  • 通过责任链模式一直调用到Wrapper的valve,在这里会调用一个filterchain,最后调用servlet的service进行执行(servlet一般都会继承HttpServlet这个抽象类来编写),完成之后将response结果阻塞的写到socket,最终返回到浏览器;
  • 扩展:Tomcat的Nio2EndPoint实现异步IO;
  • 首先,应用程序在调用read API的同时告诉内核两件事情:数据准备好了以后拷贝到哪个Buffer,以及调用 哪个回调函数去处理这些数据。
  • 之后,内核接到这个read指令后,等待网卡数据到达,数据到了后,产生硬件中断,内核在中断程序里把数 据从网卡拷贝到内核空间,接着做TCP/IP协议层面的数据解包和重组,再把数据拷贝到应用程序指定的 Buffer,最后调用应用程序指定的回调函数。
  • Nio2SocketWrapper线程,创建连接会调用socket.accet(null,this) 不会阻塞的,自己实现了回调函数,在回调函数里会在调用一次accept构成循环,整个过程要保证连接数不超过max,在给线程池发送一个Read事件的任务;
  • 线程池的线程执行到读数据的时候,第一次read读会发现没有数据,然后会调用socket.read(buffer,readCompletionHandler),然后释放掉线程池的线程,当数据拷贝到buffer后会执行回调,回调函数会再给线程池发一个读事件的任务,第二次read读肯定就可以读取到数据了,然后就可以进行协议解析等一系列操作了。
  • 处理完请求之后,将response结果会write这是第一次写,实际是没有写出去数据的,然后会调用socket.write(buffer,readCompletionHandler),释放掉当前线程,在回调方法中会进行write写入或者重新给线程池提交写任务,第二次write写是会成功写出去数据的。
  • 扩展:SpringMVC与Tomcat的关系;
  • Tomcat执行到最后,在Wrapper会创建一个过滤链并且进行调用,然后调用servlet的Service方法,SpringMVC就是在web.xml里配置了DispatcherServlet,通过DispatcherServlet继承HttpServlet来实现请求路由的分发;
  • 扩展:pipeline包含basic_valve和普通的valve;
  • basic_valve(每一层容器的valve类)会调用下一层容器的第一个valve,来连接到每层容器;
  • 扩展:为什么要引出适配器来整合--连接器和容器;
  • 连接器封装的Request跟容器需要的ServletRequest接口不适配,不能直接调用接口需要进行一层转换才可以,所以引出了适配器模式;
  • 扩展:适配器通过连接器的Mapper保存请求对应具体的容器;
  • 适配器adaptor在connector的mapper属性里保存request请求对应的host、context、wrapper组件的引用;
  • 端口号和协议是连接器决定;
  • 域名是Host决定;
  • 访问webapps下的哪一个应用是Context决定,一般路径是文件名;
  • 访问具体应用下的哪一个业务类是Wrapper决定,路径是配置servlet的;
  • http:///host:port/context/wrapper;
  • 扩展:不允许直接访问WEB-INF或META-INF下的资源;
  • 这个是在StandardContextValve里进行判断的,它会比较请求路径然后直接返回的;
  • 扩展:处理流程图;

2.Tomcat的类加载和热部署


2.1有哪些地方打破了双亲委托机制?

  • 第一处打破:Tomcat使用WebAppClassLoader加载器;
  • Tomcat 可以通过WebappClassLoader加载web应用的 class 文件【在 Tomcat 中 web 应用内WEB-INF\classes目录下的 class 文件都是用这个类加载器来加载的应用的业务代码,如果加载不了当前类,会交给父类加载器走双亲委托。 
  • 具体的实现就是重写了ClassLoader的两个方法:findClass【实际加载类】和loadClass【实现加载方式,打破了双亲委派】;
  • 第二处打破:Tomcat给线程上下文设置类加载器;
  • 通过SharedClassLoader等父加载器来共享的第三方JAR包,而第三方的JAR包比如Spring如何加载特定的web应用的类呢?通过设置线程上下文加载器来解决。
  • Tomcat可以在父类加载器SharedClassLoader请求子类加载器WebAppClassLoader完成加载的动作--使用线程上下文类加载器
  • Tomcat的类加载模型体系:
  • Web应用之间的类需要隔离(两个web应用如果有相同的类,那么会出现冲突)--引出了WebAppClassLoader;
  • Tomcat和Web应用之间需要将类隔离--引出了与WebAppClassLoader平行的CatalinaClassLoader类加载器;
  • 两个Web应用都依赖同个第三方的JAR包(各自加载会浪费内存)--引出了SharedClassLoader类加载器来给不同的web应用共享JAR包;
  • Tomcat和各Web应用之间需要共享些类--引出了CommonClassLoader类加载器作为他们的父类加载器;
  • 扩展:CommonClassLoader,SharedClassLoaderWebAppClassLoader,CatalinaClassLoader类加载器都是URIClassLoader,加载的类需要在配置文件进行指定,
  • 默认情况只指定了CommonClassLoader加载的目录,所以Common、Shared、Catalina是同一个类加载器,加载${catalina.base}/lib目录(Tomcat自身的jar包

2.2Tomcat的热加载和热部署;

  • 热加载的实现:
  • 当Context标签的reloadable属性的值为true时,就实现了热加载。 tomcat服务器在运行状态下会监视在WEB-INF/classes和WEB-INF/lib目录下class文件的改动,如果监测到有class文件被更新的,服务器会自动重新加载Web应用。
  • 后台处理线程ContainerBackgroundProcessor,线程每10秒启动一次;
  • 调用StandContext的backgroundProcess后台处理方法,最后是在classloader的后台处理方法里进行判断;
  • 当类检测到了修改(检查class文件修改时间,在检查 web 应用中的 jar 包是否有添加/删除)。
  • 先进行stop清除实例的引用,并且把加载该应用的WebappClassLoader设为null,然后在进行start重新加载类和设置类加载器;
  • 热部署的实现;
  • 后台处理线程ContainerBackgroundProcessor,实时将在webapps下发布的web应用加载进来,当Tomcat已经启动了,手动往webapps目录下拷贝文件,10秒钟就会走到对应的逻辑将应用加载进来。
  • 线程每10秒启动一次,调子容器的处理方法发布一个循环事件,HostConfig监听器会进行实际的加载部署的app应用--启动事件也会进行触发;
  •  扩展:热加载和热部署;

3.Tomcat的定制版线程池


  • 定制版拒绝策略:
  • 继承ThreadPoolExecutor重写execute方法,拒绝策略默认抛出异常,然后铺获后会尝试一定时间将任务加入工作队列
  • 定制版工作队列:
  • TaskQueue继承LinkedBlockingQueue重写offer方法,避免在只有达到工作队列的最大长度时才可以来创建非核心线程;
  • 当前线程数没超过最大线程,提交的任务数量小于核心线程数,就可以创建一个非核心线程;
  • @Override
    public boolean offer(Runnable o) {
        //如果线程数已经到了最大值,不能创建新线程了,只能把任务添加到任务队列。
        if (parent.getPoolSize() == parent.getMaximumPoolSize()) 
            return super.offer(o);
    
        //执行到这,表明当前线程数大于核心线程数,并且小于最大线程数。 
        //表明是可以创建新线程的,那到底要不要创建呢?分两种情况: 
    
        //1. 如果已提交的任务数小于当前线程数,表明还有空闲线程,无需创建新线程
        if (parent.getSubmittedCount()<=(parent.getPoolSize())) 
            return super.offer(o);
    
        //2. 如果已提交的任务数大于当前线程数,线程不够用了,返回false去创建新线程
        if (parent.getPoolSize()<parent.getMaximumPoolSize()) 
            return false;
    
        //默认情况下总是把任务添加到任务队列
        return super.offer(o);
    }

4.Tomcat的异步servlet


  • 线程池的线程,当处理任务耗时太长导致线程长时间阻塞,最终导致线程池出现大面积线程阻塞,导致线程池可用线程数量减少--会影响其他业务的执行,这时可以考虑使用异步servlet;
  • 在Servlet里用额外的线程池来处理耗时任务,并且不会销毁Request和Response对象,也不会把响应信息发到浏览器,tomcat 线程池的线程放回池子去处理其他请求。
  • 当耗时任务处理完成,会重新将准备就绪的request和response交给tomcat的线程池再去处理,响应给客户端。
    • 调用Processor的 processSocketEvent方法,并且传入了操作码OPEN_READ,Tomcat的线程池会在进行处理,响应最终的数据返回给客户端。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值