手写Tomcat
所有新知识都应该本着:是什么,为什么,怎么做,这三步来执行。接下来是我对tomcat的一些新认识,对于这个项目应该有的大体掌握。
1.手写Tomcat前期准备
1.Tomcat是什么
- tomcat是一个开源的轻量级web应用服务器
- 也是个servlet容器
2.web应用服务器的设计思想
- web应用服务器的核心是对Http协议的实现。那么tomcat服务器的核心也是对http协议的实现。
3.Tomcat主要做什么
- 接受请求
- 解析请求数据
- 处理请求
- 发送响应 tomcat可以找到部署的应用,对应的资源,返回给浏览器。也就是说tomcat可以解析我们的java应用,并且能够调用我们的java程序(Servlet java)。
4.Tomcat实现思路
- 收集部署的servlet
- 读取解析http请求
- 匹配servlet
- 执行servlet
- 响应数据回浏览器
5.Tomcat两大核心容器及八大组件
-
container--engine,host,context,wrapper
-
connector--endpoiont,processor,protocalhandler,adapter
6.http协议基本原理
http协议是基于Tcp/Ip协议的应用层协议,不涉及数据包(packet)的传输,关注数据传输的格式是否规范。 所有类型的格式,均可采用Http传输。例如:图片,html网页,视频mp4,pdf,zip.
-
协议:约定的规则
-
网络协议:约定一种如何在网络上进行数据传输的规则
-
http协议:hyper text transform protocal
-
超文本传输协议 约定如何在网络上进行传输超文本的
-
http协议给予请求响应模式,分为请求部分以及响应部分
-
请求部分:
请求行:方法名 url 协议以及版本号
请求头:kv 作用:告诉服务器浏览器的一些信息,对本次请求的一些描述
请求体:Get无请求体,post有请求体
-
响应部分:
响应行:协议以及版本号 状态码 状态描述信息
其中状态码:200-OK 客户端请求成功
404-Not Found 请求资源不存在
响应头:kv 作用:告诉客户端服务端的一些信息,对本次响应的一些描述
响应体:服务端响应回客户端的部分,比如是html页面
http协议的基本工作流程
-
用户通过浏览器访问网址,或点击链接。接着,浏览器获取发送请求事件
-
浏览器向服务端发出TCP连接请求
-
服务程序接受浏览器连接请求,经过TCP三次握手建立连接
-
浏览器将请求数据打包【HTTP协议格式数据包】
-
浏览器将该数据包推入网络。数据包经过网络传输,最终达到端服务程序。
-
服务端拿到数据包后,以HTTP协议格式解包,获取到客户端的意图。
-
得知客户端意图后进行处理。比如:提供静态文件或者调用服务端程序获得动态结果。
-
服务器将响应结果(可能是HTML或图片)打包【HTTP协议格式数据包】
-
服务器将响应数据包推入网络,数据包经过网络传输最终到达浏览器
-
浏览器拿到数据包后,以HTTP协议的格式解包,然后解析数据,假设这里的数据是HTML。浏览器将HTML文件展示在页面上
5.socket连接
使用Tcp/Ip和UDP协议在客户端和服务端之间进行通信的技术,是网络编程的基础。
首先要先了解,浏览器与服务器之间是如何进行数据传输的?
1.浏览器与服务器之间基于B/S架构。
2.当我们在浏览器上输入网址在敲回车后,实际是向服务端发请求(浏览器底层去创建一个socket对象, 去连接服务端的serversocket)
3.服务端会创建一个serversocket对象监听本机的端口,一旦监听到客户端的请求就会接受,数据以字节流的方式流到socket连接上。
4.与此同时服务端也会创建一个socket对象,通过这个socket对象getInputStream()和OutputStream()方法来获取输入流输出流对象
5.通过输入流来读取客户端的数据,解析这些数据,通过输出流响应到客户端
6.servlet
-
Servlet(Server Applet)全称Java Servlet,是Java编写的服务器端程序
-
从广义上讲,Servlet指任何实现了Servlet接口的类
-
功能:在于交互式的浏览修改数据,生成动态web内容。
-
Servlet运行在java的应用服务器上,从实现上讲Servlet可以响应任何类型的请求,但绝大多数情况Servlet只用来扩展基于http协议的web服务器。可以理解成Servlet是java中的一个接口。
7.tomcat如何设计http服务器的
浏览器发给服务端的是一个HTTP格式的请求,HTTP服务器收到这个请求后,需要调用服务端程序来处理,所谓的服务端程序就是你写的Java类,一般来说不同的请求需要由不同的Java类来处理。
右边主要是为了解耦:http服务器不直接调用具体业务类而是把请求交给容器去处理,容器调用servlet接口调用业务类。
8.Servlet容器工作流程
web服务器的架构设计基本原理基于Servlet规范实现
当客户请求某个资源时:
HTTP服务器会用一个ServletRequest对象把客户的请求信息封装起来,然后调用Servlet容器的service()方法
Servlet容器根据请求的URL和Servlet的映射关系,找到相应的Servlet
- 如果Servlet还没有被加载,就用反射机制创建Servlet对象,并调用Servlet的init方法来完成初始化,接着调用Servlet的service方法来处理请求
- 如果Servlet已经加载,直接调用Servlet的service方法来处理请求
请求处理完毕,Servlet容器把ServletResponse对象返回给HTTP服务器,HTTP服务器会把响应发送给客户端
2.tomcat整体架构设计
设计架构之前我们应该先了解需求。tomcat两个核心需求:
- 接收客户端浏览器发送请求,处理socket连接,将字节流与request和response对象进行转化
- 加载管理servlet,以及处理request请求
3.容器及组件
container:Servlet容器 负责内部处理
想一个问题,Tomcat接收到数据并解析为HttpServletRequest对象后,就直接把请求交给Servlet了吗?
并不是,Tomcat还有其他考虑。
比如:1.假如,现在有一段逻辑,想让多个Servlet共用,就像切面一下,我们希望在请求被这些Servlet处理之前,能先执行公共逻辑.大家会有点懵,没关系,请直接往下面看,在Tomcat中存在四大Servlet容器:
1.Engine:表示Catalina的Servlet引擎,一个Service只能有一个engine,但是一个Engine可以包含多个Host
2.Host:一个Host表示一个虚拟主机,或者一个站点,可以给每个tomcat配置多个虚拟主机地址,而一个虚拟主机下面可以包含多个context。
3.Context:一个Context就是一个web应用,一个web应用下面可以包含多个wrapper
4.Wrapper:一个Wrapper表示一个Servlet的包装
这四个Servlet容器是具有层次关系的:
一个Engine下可以有多个Host
一个Host下可以有多个Context
一个Context下可以有多个Wrapper,
一个Wrapper下可以有多个Servlet实例对象。
Tomcat接收到某个请求后,首先会判断该请求的域名,根据域名找到对应的Host对象,Host对象再根据请求信息找到请求所要访问的应用,也就是找到一个Context对象,Context对象拿到请求后,会根据请求信息找到对应的Servlet,那么Wrapper是什么?
我们在定义一个Servlet时,如果额外实现了SingleThreadModel接口,那么就表示该Servlet是单线程模式
1.定义Servlet时如果没有实现SingleThreadModel接口,那么在Tomcat中只会产生该Servlet的一个实例对象,如果多个请求同时访问该Servlet,那么这多个请 求线程访问的是同一个Servlet对象,所以是并发不安全的
2.定义Servlet时如果实现了SingleThreadModel接口,那么在Tomcat中可能会产生多个该Servlet的实例对象,多个请求同时访问该Servlet,那么每个请求线程 会有一个单独的Servlet对象,所以是并发安全的
所以,我们发现,我们定义的某个Servlet,在Tomcat中可能会存在多个该类型的实例对象,所以Tomcat需要再抽象出来一层,这一层就是Wrapper,一个 Wrapper对应一个Servlet类型,Wrapper中有一个集合,用来存储该Wrapper对应的Servlet类型的实例对象。
所以一个Context表示一个应用,如果一个应用中定义了10个Servlet,那么Context下面就有10个Wrapper对象,而每个Wrapper中可能又会存在多个Servlet对象。
还有一点,在这个四个容器内部,有一个组件叫做Pipeline,也就管道,每个管道中可以设置多个Valve,也就是阀门。
管道与阀门的作用是,每个容器在接收到请求时会先把请求交给容器中的每个阀门处理,所有阀门都处理完了之后,在会将请求交给下层容器,通过这种机制,就解决了上面所提到的假设。
1.Engine:可以处理Tomcat所接收到所有请求,不管这些请求是请求哪个应用或哪个Servlet的。
3.Context:可以处理某个应用的所有请求
2.Host:可以处理某个特定域名的所有请求
4.Wrapper:可以处理某个Servlet的所有请求
值得一说的是:Wrapper还会负责调用Servlet对象的service()方法。到此,Tomcat接收到字节流并解析为HttpServetRequest对象之后,HttpServletRequest对象是如何流转的给大家分析完了
总结一下就是:
2.connector coyote连接器
- coyote是tomcat服务器提供的供浏览器访问的外部接口;
- 客户端通过coyote与服务器建立连接,发送请求,接收响应
一次请求对应一次service连接
接下来再来给大家分析一下,Tomcat是如何将字节流解析为HttpServletRequest对象的,这个问题其实不难,只要想到这些字节流是谁发过来的?比如浏览器。而浏览器在发送数据时,会先将数据按HTTP协议的格式进行包装,再把HTTP数据包通过TCP协议发送出去,Tomcat就是从TCP协议中接收到数据的,只是从TCP协议中接收的数据是字节流,接下来要做的就是同样按照HTTP协议进行解析,比如解析出请求行、请求头等,从而就可以生成 HttpServletRequest对象。不过,整个解析过程还是比较复杂的,包括Tomcat底层的NIO模型、BIO模型、线程模型的实现也是比较复杂的
相关链接www.yuque.com/renyong-jmo…
这就是浏览器与服务器之间进行通信的大体流程,现在我们主要来看一下手写实现tomcat具体流程
4.手写Tomcat流程
1.先写一个tomcat启动类
2.启动完成后,需要接收到网络请求,这里面要用到socket.
输出结果
遇到了bio阻塞,请求还没有拿到响应,浏览器还在等,当第二次访问的时候服务器已经不运行了,接收不到socket连接。
解决方案:加上while true 主线程处理完第一个socket连接之后,继续接收下一个socket连接
但是现在socket连接是串行的,如何去提高并发量?
解决方案:引入线程池,扔给线程池里面的线程并发处理socket连接
3.处理socket连接,⽐如从Socket连接上读取数据,或者向Socket连接上写⼊数据(返回数据给浏览器)。
处理socket连接就是从socket连接上面读数据,执行业务逻辑,将结果写回到socket连接上。对于tomcat而言就是读取http请求,执行servlet,将响应的结果返回到socket连接上.
从socket连接上读数据,socket连接上读到的都是字节流,通过字节数组的方式去解析请求行(包括请求方法,请求路径,协议以及版本号),请求头以及请求体。
回顾下http格式:
浏览器发送数据时就会遵守这个格式来发,所以对于Tomcat⽽⾔,就需要按这个格式来解析,⽐如逐个字节判断,如果遇到⼀个字节对应的是空格,那么之前的字节对应的就是请求⽅法,⽐如:
解析完之后我们就可以构造出来一个request对象了,这就是我们接收到的请求.构造一个request类
构造这个request对象是最终要传给servlet,所以要实现HttpServletRequest接口,这里要实现的方法过多,我们需要在创建一个AbstractHttpServletRequest类去实现HttpServletRequest接口。然后request对象继承AbstractHttpServletRequest
出现问题:
解决方案:注入java servlet-api依赖 blog.csdn.net/weixin_4734…
4.解析完请求后接下来我们就可以把这个request对象传递给某个Servlet了。实际上根据请求找Servlet也是⼀个复杂的流程后续会说。 先⾃⼰定义⼀个Servlet,根据servlet的service方法去决定调用doget还是dopost方法,获取请求方法。根据servlet规范发现还需要去传一个response对象,我们需要根据请求去构造响应对象。
5.根据请求构造响应对象,response里面根据每个请求去构造对应的响应信息,响应行,响应头
6.生成response对象,最终也要传给servlet,所以要实现HttpServletRequest接口,这里要实现的方法过多,我们需要在创建一个AbstractHttpServletResponse类去实现HttpServletResponse接口。然后response对象继承AbstractHttpServletResponse.
7.解析完执行servlet
8.servlet在service方法中service方法进行判断用doGet还是doPost向输出流里面写响应体数据
9.因为socket连接上都是以字节流的方式进行输入输出,所以我们在绑定一个ResponseServletOutputStream对象,他的write方法只需要将响应体数据写到字节数组里面就好了
10.service方法执行之后才将数据真正写到socket连接里面,就可以响应数据了。
11.再次启动tomcat,就能接收请求,执行Servlet,并响应请求。
5.Tomcat部署应用
大体流程
现在说一下根据请求去找servlet,嗯确实,有点复杂。
tomcat处理请求时候,需要根据请求找到servlet对象,执行service方法。
要先找出有哪些servlet对象
那在接收请求之前,需要知道tomcat中有哪些Servlet对象。tomcat启动过程中要做的就是收集tomcat中部署那些servlet。
1.Tomcat在启动时会在固定目录下去检查部署那些应用,并且看每个应用下有哪些servlet。
我们直接在tomcat⼯程⽬录下新建⼀个webapps⽬录,表示该⽬录下就是⽤来部署应⽤的,hello⽂件夹就表示⼀个应⽤,classes⽂件就表示这个应⽤下的class⽂件,其中就包括了servlet。
编译到我们的项目中
3.规定好⽬录后,Tomcat在启动时就要去进⾏应⽤部署的逻辑了:
1.找到webapps⽂件下部署了哪些应⽤
输出hello文件夹
2.拿到hello文件夹下面的classes目录
2.遍历底下的每一个文件夹,拿到该目录下面的所有文件
3.在遍历文件的时候,找到File对象,利用反射得到它对应的class对象,这就需要进⾏类的加载,在加载类之前,我们需要得到File对象对应的包格式(全限定名)
4这样我们就能拿到对应class⽂件的类全限定名,然后就可以利⽤类加载器去加载了,注意此时⽤AppClassLoader去加载这个类是加载不到的,因为webapps⽬录并不在classpath中,在Tomcat中是通过⾃定义类加载器来进⾏加载的。
5.这样就能拿到对应的class对象了,接下来就可以判断这个类是不是⼀个servlet了。
6.如果是就解析WebServlet注解得到对应的urlPatterns,有了urlPatterns那么我们就可以把映射关系进⾏保存,
7.当然我们不⽤把所有应⽤中的映射关系都保存在⼀起,最好是按应⽤隔开,所以我们定义⼀个Context表示⼀个应⽤:
8.找到了所有应⽤和Servlet之后,就可以在处理请求时进行匹配了。
9.如果匹配不到,我们可以定义⼀个DefaultServlet。
6.在学习的过程中感觉很有帮助的博客链接🔗
tomcat大框:t.csdn.cn/qykcJ
juejin.cn/post/684490… juejin.cn/post/713063… blog.csdn.net/qq_45547688…
❗️www.yuque.com/renyong-jmo… ❗️www.yuque.com/renyong-jmo…
7.我的收获
计算机网络,jvm,网络IO,线程池
8.涉及到的知识点以及扩展知识点
对于这些知识点我也进行了初步的总结 BIO,NIO,AIO juejin.cn/post/721876… OSI七层参考模型:juejin.cn/post/721884… Tcp/Ip,Udp
类加载
9.过程中遇到的难点,痛点以及解决方案
bio阻塞--while true
程序串行--引入线程池
继承HttpServletRequest找不到HttpServletRequest类--注入java servlet.api依赖
servlet里面写的doget,tomcat里面写的service。未搞清doget和service的原理及区别,抓包时请求没过来。
tomcat类加载与jvm类加载原理的不同之处及原理分析。