目录
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,依次初始化 Engine,executor,MapperListener,Connector【Host、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,SharedClassLoader,WebAppClassLoader,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的线程池会在进行处理,响应最终的数据返回给客户端。