Tomcat 系统架构与原理剖析
注意:浏览器访问服务器使⽤的是Http协议,Http是应⽤层协议,⽤于定义数据通信的格式,具体的数据传输使⽤的是TCP/IP协议
Tomcat 系统总体架构
Tomcat是⼀个Http服务器(能够接收并且处理http请求,所以tomcat是⼀个http服务器)我们使⽤浏览器向某⼀个⽹站发起请求,发出的是Http请求,那么在远程,Http服务器接收到这个请求之后,会调⽤具体的程序(Java类)进⾏处理,往往不同的请求由不同的Java类完成处理。
Tomcat 设计了两个核⼼组件连接器(Connector)和容器(Container)来完成 Tomcat 的两⼤核⼼功能。
连接器,负责对外交流: 处理Socket连接,负责⽹络字节流与Request和Response对象的转化;
容器,负责内部处理:加载和管理Servlet,以及具体处理Request请求;
Tomcat 连接器组件 Coyote
Coyote 简介
Coyote 是Tomcat 中连接器的组件名称 , 是对外的接⼝。客户端通过Coyote与服务器建⽴连接、发送请
求并接受响应 。
(1)Coyote 封装了底层的⽹络通信(Socket 请求及响应处理)
(2)Coyote 使Catalina 容器(容器组件)与具体的请求协议及IO操作⽅式完全解耦
(3)Coyote 将Socket 输⼊转换封装为 Request 对象,进⼀步封装后交由Catalina 容器进⾏处理,处
理请求完成后, Catalina 通过Coyote 提供的Response 对象将结果写⼊输出流
(4)Coyote 负责的是具体协议(应⽤层)和IO(传输层)相关内容
在 8.0 之前 ,Tomcat 默认采⽤的I/O⽅式为 BIO,之后改为 NIO。 ⽆论 NIO、NIO2 还是 APR, 在性能⽅⾯均优于以往的BIO。 如果采⽤APR, 甚⾄可以达到 Apache HTTP Server 的影响性能。
Tomcat 服务器核⼼配置详解
⼿写实现迷你版 Tomcat
Tomcat 源码构建及核⼼流程源码剖析
Tomcat 类加载机制剖析
Tomcat 对 Https 的⽀持及 Tomcat 性能优化策略
nginx 相关
Nginx基础回顾(Nginx是什么?能做什么事情(应⽤在什么场合)?常⽤命令是什么?)
正向代理
在浏览器中配置代理服务器的相关信息,通过代理服务器访问⽬标⽹站,代理服务器收
到⽬标⽹站的响应之后,会把响应信息返回给我们⾃⼰的浏览器客户端
反向代理
浏览器客户端发送请求到反向代理服务器(⽐如 Nginx),由反向代理服务器选择原始
服务器提供服务获取结果响应,最终再返回给客户端浏览器
负载均衡服务器
负载均衡,当⼀个请求到来的时候(结合上图),Nginx反向代理服务器根据请求去找到⼀个原始服务器来处理当前请求,那么这叫做反向代理。那么,如果⽬标服务器有多台(⽐如上图中的tomcat1,tomcat2,tomcat3...),找哪⼀个⽬标服务器来处理当前请求呢,这样⼀个寻找确定的过程就叫做负载均衡。
⽣活中也有很多这样的例⼦,⽐如,我们去银⾏,可以处理业务的窗⼝有多个,那么我们会被分配到哪个窗⼝呢到底,这样的⼀个过程就叫做负载均衡。
Nginx 核⼼配置⽂件解读
Nginx的核⼼配置⽂件 conf/nginx.conf
包含三块内容:全局块、events块、http块
全局块
从配置⽂件开始到 events 块之间的内容,此处的配置影响nginx服务器整体的运⾏,⽐如worker进
程的数量、错误⽇志的位置等
event 模块
events块主要影响 nginx 服务器与⽤户的⽹络连接,⽐如 worker_connections 1024,标识每个 workderprocess ⽀持的最⼤连接数为1024
http 模块
http块是配置最频繁的部分,虚拟主机的配置,监听端⼝的配置,请求转发、反向代理、负载均衡等
Nginx应⽤场景之反向代理
再部署⼀台tomcat,保持默认监听8081端⼝
修改nginx配置,并重新加载
这⾥主要就是多location的使⽤,这⾥的nginx中server/location就好⽐tomcat中的
Host/Context location 语法如下:
在nginx配置⽂件中,location主要有这⼏种形式(优先级由高到低):
- 精确匹配 location = /lagou { }
- 匹配路径的前缀 location ^~ /lagou { }
- 不区分⼤⼩写的正则匹配 location ~* /lagou { }
- 正则匹配 location ~ /lagou { }
- 普通路径前缀匹配 location /lagou { }
Nginx应⽤场景之负载均衡
第一步是定义 upstream, 起一个名字. 然后再使用 proxy_pass 即可.
Nginx负载均衡策略:
- 轮询
默认策略,每个请求按时间顺序逐⼀分配到不同的服务器,如果某⼀个服务器下线,能⾃动剔除
location /abc {
proxy_pass http://myServer/;
}
upstream myServer{
server 111.229.248.243:8080;
server 111.229.248.243:8082;
}
- 权重 weight
weight代表权重,默认每⼀个负载的服务器都为1,权重越⾼那么被分配的请求越多(⽤于服务器性能不均衡的场景)
upstream myServer{
server 111.229.248.243:8080 weight=1;
server 111.229.248.243:8082 weight=2;
}
- ip_hash
每个请求按照ip的hash结果分配,每⼀个客户端的请求会固定分配到同⼀个⽬标服务器处理,可以解决session问题
第五部分 Nginx应⽤场景之动静分离
upstream myServer{
ip_hash;
server 111.229.248.243:8080;
server 111.229.248.243:8082;
}
Nginx 应⽤场景之动静分离
动静分离就是讲动态资源和静态资源的请求处理分配到不同的服务器上,⽐较经典的组合就是
Nginx+Tomcat架构(Nginx处理静态资源请求,Tomcat处理动态资源请求)
Nginx 底层进程机制剖析
Nginx启动后,以daemon多进程⽅式在后台运⾏,包括⼀个Master进程和多个Worker进程,Master 进程是领导,是⽼⼤,Worker进程是⼲活的⼩弟。
master进程
主要是管理worker进程,⽐如:
接收外界信号向各worker进程发送信号(./nginx -s reload)
监控worker进程的运⾏状态,当worker进程异常退出后Master进程会⾃动重新启动新的worker进程等
worker进程
worker进程具体处理⽹络请求。多个worker进程之间是对等的,他们同等竞争来⾃客户端的请求,各进程互相之间是独⽴的。⼀个请求,只可能在⼀个worker进程中处理,⼀个worker进程,不可能处理其它进程的请求。worker进程的个数是可以设置的,⼀般设置与机器cpu核数⼀致。
Nginx进程模型示意图如下
以 ./nginx -s reload 来说明nginx信号处理这部分
1)master进程对配置⽂件进⾏语法检查
2)尝试配置(⽐如修改了监听端⼝,那就尝试分配新的监听端⼝)
3)尝试成功则使⽤新的配置,新建worker进程
4)新建成功,给旧的worker进程发送关闭消息
5)旧的worker进程收到信号会继续服务,直到把当前进程接收到的请求处理完毕后关闭
所以reload之后worker进程pid是发⽣了变化的
worker进程处理请求部分的说明
例如,我们监听9003端⼝,⼀个请求到来时,如果有多个worker进程,那么每个worker进程都有可能处理这个链接。
master进程创建之后,会建⽴好需要监听的的socket,然后从master进程再fork出多个worker进程。所以,所有worker进程的监听描述符listenfd在新连接到来时都变得可读。
nginx使⽤互斥锁来保证只有⼀个workder进程能够处理请求,拿到互斥锁的那个进程注册 listenfd 读事件,在读事件⾥调⽤accept接受该连接,然后解析、处理、返回客户端
nginx多进程模型好处
每个worker进程都是独⽴的,不需要加锁,节省开销
每个worker进程都是独⽴的,互不影响,⼀个异常结束,其他的照样能提供服务
多进程模型为reload热部署机制提供了⽀撑
其他
URL 编码和解码问题
http://www.ruanyifeng.com/blog/2010/02/url_encoding.html
作业
作业⼀(编程题):
开发Minicat V4.0,在已有Minicat基础上进⼀步扩展,模拟出webapps部署效果 磁盘上放置⼀个webapps⽬录,webapps中可以有多个项⽬,⽐如demo1,demo2,demo3... 具体的项⽬⽐如demo1中有serlvet(也即为:servlet是属于具体某⼀个项⽬的servlet),这样的话在 Minicat初始化配置加载,以及根据请求url查找对应serlvet时都需要进⼀步处理
!!!重要
备注:读取项目磁盘统一路径: ****appBase="/Users/webapps",并且提交自己的webapps以及访问路径****
作业⼆(简答题): 请详细描述Tomcat体系结构(图⽂并茂)
作业具体要求参考以下链接文档:
https://gitee.com/lagouedu/alltestfile/raw/master/tomcat/Tomcat%E4%BD%9C%E4%B8%9A%E5%A4%A7%E9%A2%98.pdf
作业资料说明:
1、提供资料:工程代码和自己的webapps以及访问路径、功能演示和原理讲解视频,简答题资料。
2、讲解内容包含:题目分析、实现思路、代码讲解。
3、效果视频验证:实现模拟tomcat多项目部署效果,访问多个项目获得动态返回的内容。
- 增加server.xml配置⽂件,server.xml保持基本结构即可,如下
<?xml version="1.0" encoding="utf-8" ?>
<Server>
<services>
<!--监听端口号-->
<Connector port="80"/>
<Engine>
<!--appBase 为部署目录-->
<Host name="localhost" appBase="C:/Users/hp/Desktop/第二阶段模块1/第二阶段模块1/code/myWebapps" />
</Engine>
</services>
</Server>
- 制作 demo1 和 demo2 目录
demo1/
index.html
edu/lagou/server/LagouServlet01.class
edu/lagou/server/LagouServlet02.class
web.xml
demo2/
index.html
edu/lagou/server/LagouServlet03.class
web.xml
- 在 Bootstrap 类中添加 loadAppBase 方法,可得到 port 和 appBase的值.
简易的 Mapper 类
public class Mapper {
/**
* 一级目录 以及 对应的动态资源
*/
private Map<String, Map<String, HttpServlet>> content2Wrapper = new HashMap<>();
/**
* 一级目录 以及 对应的相对路径
*/
private Map<String, String> resourcesMap = new HashMap<>();
public void putStaticResources(String key, String value) {
resourcesMap.put("/" + key, value);
}
public void putServletMap(String key, Map<String, HttpServlet> value) {
content2Wrapper.put("/" + key, value);
}
public Map<String, HttpServlet> getServletMap(String key) {
return content2Wrapper.get(key);
}
public String getStaticPath(String key) {
return resourcesMap.get(key);
}
}
自定义的 ClassLoader 类: 用于加载特定目录的 class
public class MyClassLoader extends ClassLoader {
public MyClassLoader(String basePath) {
this.basePath = basePath;
}
/**
* @description 解析类文件获得当前解析后的类
* @param fullClassName 档期类的完全限定名, 例如 edu.lagou.server.my.Person
* @return 使用类加载器后获取的类
* @throws Exception 解析错误
*/
public Class<?> transClassFile(String fullClassName) throws Exception {
byte[] classBytes = this.loadBinaryClassFile(fullClassName);
//主要通过父类来解析当前的class二进制文件
return super.defineClass(fullClassName, classBytes, 0, classBytes.length);
}
/**
* @description 读取并加载类文件获得byte数组返回
* @return byte[] 数组
* @throws Exception 读取失败
*/
private byte[] loadBinaryClassFile(String packageName) throws Exception {
String relativePath = packageName.replaceAll("\\.", "/");
String classFilePath = this.basePath + "/" +relativePath + ".class";// 设置当前class文件的路径
File classFile = new File(classFilePath);
if (!classFile.exists()) {
throw new FileNotFoundException(classFile.getAbsolutePath() + "文件不存在。。。。。。。。。。");
}
InputStream fis = null;
ByteArrayOutputStream bos = null;// 内存流
byte[] bytes = new byte[(int) classFile.length()]; // 设置缓冲区
byte[] readBytes = null;
try {
bos = new ByteArrayOutputStream();
// 开始实例化流,并加载流
fis = new FileInputStream(classFile);// 这里必须为文件的实际路径
while (( fis.read(bytes)) != -1) {
bos.write(bytes);
}
readBytes = bos.toByteArray();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (fis != null) {
fis.close();
}
if (bos != null) {
bos.close();
}
}
return readBytes;
}
private final String basePath;
}
创建 loadServerXML 方法,将 server.xml 加载到内存
private void loadServerXML() {
InputStream inputStream = Bootstrap.class.getClassLoader().getResourceAsStream("server.xml");
SAXReader saxReader = new SAXReader();
try {
Document document = saxReader.read(inputStream);
Element rootElement = document.getRootElement();
Node portNode = rootElement.selectSingleNode("/Server/services/Connector/@port");
this.port = Integer.parseInt(portNode.getStringValue());
System.out.println("配置 port = " + this.port);
Node appBaseNode = rootElement.selectSingleNode("/Server/services/Engine/Host/@appBase");
this.appBase = appBaseNode.getStringValue();
System.out.println("配置 appBase = " + this.appBase);
} catch (Exception e) {
e.printStackTrace();
}
}
加载 appBase 目录下各项目 以及 对应的 url-pattern和HttpServlet 动态资源 和 静态资源
private void loadAppBaseServlet() {
File file = new File(this.appBase);
for (File f : Objects.requireNonNull(file.listFiles())) {
if (f.isDirectory()) {
// 解析 web.xml 并加载对应的 HttpServlet 类
String webXmlFileName = f.getAbsolutePath() + "/" + "web.xml";
File webXmlFile = new File(webXmlFileName);
// 如果连 web.xml 不存在 则跳过该目录。
if (webXmlFile.exists()) {
String fileName = f.getName();
System.out.println(fileName);
System.out.println("--------------");
System.out.println("...加载该目录" + f.getPath());
System.out.println(webXmlFileName);
// 加载对应的 各项目父路径 和 对应的 url-patten与servlet动态资源的对应关系, 例如 /demo1 -> (/lagou -> LagouServlet对象)
try {
InputStream inputStream = new FileInputStream(webXmlFile);
Map<String, HttpServlet> stringHttpServletMap = this.loadServletFromInputStream(inputStream,
new MyClassLoader(f.getAbsolutePath()));
this.mapper.putServletMap(fileName, stringHttpServletMap);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
// 加载对应的 各项目父路径 和 对应静态资源父目录的对应关系, 例如 /demo1 -> appBase/demo1
this.mapper.putStaticResources(fileName, f.getAbsolutePath());
} else {
System.out.println("...跳过该目录" + f.getPath());
}
}
}
}
RequestProcessor 的 run 方法
// url
String url = request.getUrl();
// 截取两端/index.html / + index.html
// /demo1/lagou
String[] split = url.split("/");
String part1;
String part2;
if (split.length == 2) {
part1 = "/" + split[0];
part2 = "/" + split[1];
} else {
part1 = "/" + split[1];
part2 = "/" + split[2];
}
Map<String, HttpServlet> stringHttpServletMap = this.mapper.getServletMap(part1);
if (stringHttpServletMap != null) {
HttpServlet httpServlet = stringHttpServletMap.get(part2);
// process static resources
if (null == httpServlet) {
String filePath = this.mapper.getStaticPath(part1);
response.outputHtml(filePath, part2);
} else {
httpServlet.service(request, response);
}
} else {
// 404
response.outputHtml("NOT FOUND");
}
思路:Mapper类—>Host->Context->Wrapper->Servlet
开始测试
正面案例
http://localhost/index.html
http://localhost/lagou
http://localhost/demo1/index.html
http://localhost/demo1/lagou001
http://localhost/demo1/lagou002
http://localhost/demo2/index.html
http://localhost/demo2/lagou333
反面案例
http://localhost/demo1/abc.html
http://localhost/demo2/lagou003
其中遇到的问题
StaticResourceUtil#getAbsolutePath
如果包含特殊字符,需要进行一次 URL 解码工作
String resourcePath = StaticResourceUtil.class.getResource("/").getPath();
System.out.println(resourcePath);
// 如果包含特殊字符,需要进行一次 URL 解码工作
try {
resourcePath = java.net.URLDecoder.decode(resourcePath, "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
作业2 解答
Tomcat 连接器组件 Coyote
Coyote 是Tomcat 中连接器的组件名称 , 是对外的接⼝。客户端通过Coyote与服务器建⽴连接、发送请
求并接受响应 。
(1)Coyote 封装了底层的⽹络通信(Socket 请求及响应处理)
(2)Coyote 使Catalina 容器(容器组件)与具体的请求协议及IO操作⽅式完全解耦
(3)Coyote 将Socket 输⼊转换封装为 Request 对象,进⼀步封装后交由Catalina 容器进⾏处理,处
理请求完成后, Catalina 通过Coyote 提供的Response 对象将结果写⼊输出流
Coyote 负责的是具体协议(应⽤层)和IO
即一个由 Server->Service->Engine->Host->Context 组成的结构,从里层向外层分别是:
Server:服务器Tomcat的顶级元素,它包含了所有东西。
Service:一组 Engine(引擎) 的集合,包括线程池 Executor 和连接器 Connector 的定义。
Engine(引擎):一个 Engine代表一个完整的 Servlet 引擎,它接收来自Connector的请求,并决定传给哪个Host来处理。
Container(容器):Host、Context、Engine和Wraper都继承自Container接口,它们都是容器。
Connector(连接器):将Service和Container连接起来,注册到一个Service,把来自客户端的请求转发到Container。
Host:即虚拟主机,所谓的”一个虚拟主机”可简单理解为”一个网站”。
Context(上下文 ): 即 Web 应用程序,一个 Context 即对于一个 Web 应用程序。Context容器直接管理Servlet的运行,Servlet会被其给包装成一个StandardWrapper类去运行。
Wrapper负责管理一个Servlet的装载、初始化、执行以及资源回收,它是最底层容器。
Container包含以下结构
Engine
表示整个Catalina的Servlet引擎,⽤来管理多个虚拟站点,⼀个Service最多只能有⼀个Engine, 但是⼀个引擎可包含多个Host
Host
代表⼀个虚拟主机,或者说⼀个站点,可以给Tomcat配置多个虚拟主机地址,⽽⼀个虚拟主机下可 包含多个Context
Context
表示⼀个Web应⽤程序, ⼀个Web应⽤可包含多个Wrapper
Wrapper
表示⼀个Servlet,Wrapper