目录
1.Tomcat详解
①接收请求:
②根据请求计算响应:
③返回响应:
2.Tomcat执行流程
2.1.Tomcat 初始化流程
2.2.Tomcat 处理请求流程
2.3.Servlet 的 service 方法的实现
在 Servlet 的代码中并没有写 main ⽅法,那么对应的 doGet 代码是如何被调⽤的呢? 响应⼜是如何返回给浏览器的?这就要从 Tomcat 说起了。
1.Tomcat详解
我们⾃⼰的实现是在 Tomcat 基础上运⾏的。
当浏览器给服务器发送请求的时候,Tomcat 作为 HTTP 服务器,就可以接收到这个请求。HTTP 协议作为⼀个应⽤层协议,需要底层协议栈来⽀持⼯作:
更详细的交互过程:
①接收请求:
- ⽤户在浏览器输⼊⼀个 URL,此时浏览器就会构造⼀个 HTTP 请求。
- 这个 HTTP 请求会经过⽹络协议栈逐层进⾏封装成⼆进制的 bit 流,最终通过物理层的硬件设备转换成光信号/电信号传输出去。
- 这些承载信息的光信号/电信号通过互联⽹上的⼀系列⽹络设备,最终到达⽬标主机(这个过程也需要⽹络层和数据链路层参与)。
- 服务器主机收到这些光信号/电信号,⼜会通过⽹络协议栈逐层进⾏分⽤,层层解析,最终还原成 HTTP 请求。并交给 Tomcat 进程进⾏处理(根据端⼝号确定进程)。
- Tomcat 通过 Socket 读取到这个请求(⼀个字符串),并按照 HTTP 请求的格式来解析这个请求,根据请求中的 Context Path 确定⼀个 webapp,再通过 Servlet Path 确定⼀个具体的类。再根据当前请求的⽅法 (GET/POST/...),决定调⽤这个类的 doGet 或者 doPost 等⽅法。此时我们的代码中的 doGet / doPost ⽅法的第⼀个参数 HttpServletRequest 就包含了这个 HTTP 请求的详细信息。
②根据请求计算响应:
- 在我们的 doGet / doPost ⽅法中,就执⾏到了我们⾃⼰的代码。我们⾃⼰的代码会根据请求中的⼀些信息,来给 HttpServletResponse 对象设置⼀些属性,例如状态码,header,body 等。
③返回响应:
- 我们的 doGet / doPost 执⾏完毕后,Tomcat 就会⾃动把 HttpServletResponse 这个我们刚设置好的对象转换成⼀个符合 HTTP 协议的字符串,通过 Socket 把这个响应发送出去。
- 此时响应数据在服务器的主机上通过⽹络协议栈层层封装,最终⼜得到⼀个⼆进制的 bit 流,通过物理层硬件设备转换成光信号/电信号传输出去。
- 这些承载信息的光信号/电信号通过互联⽹上的⼀系列⽹络设备,最终到达浏览器所在的主机(这个过 程也需要⽹络层和数据链路层参与)。
- 浏览器主机收到这些光信号/电信号,⼜会通过⽹络协议栈逐层进⾏分⽤,层层解析,最终还原成 HTTP 响应,并交给浏览器处理。
- 浏览器也通过 Socket 读到这个响应(⼀个字符串),按照 HTTP 响应的格式来解析这个响应,并且把body 中的数据按照⼀定的格式显示在浏览器的界⾯上。
2.Tomcat执行流程
下⾯的代码通过 "伪代码" 的形式描述了 Tomcat 的"初始化"/"处理请求"两部分核⼼逻辑。
所谓 "伪代码",并不是⼀些语法严谨,功能完备的代码,只是通过这种形式来⼤概表达某种逻辑。
2.1.Tomcat 初始化流程
class Tomcat {
// ⽤来存储所有的 Servlet 对象
private List<Servlet> instanceList = new ArrayList<>();
public static void main(String[] args) {
new Tomcat().start();
}
public void start() {
// 根据约定,读取 WEB-INF/web.xml 配置⽂件,并解析被 @WebServlet 注解修饰的类
// 假定这个数组⾥就包含了我们解析到的所有被 @WebServlet 注解修饰的类
Class<Servlet>[] allServletClasses = ...
// 这⾥要做的的是实例化出所有的 Servlet 对象出来
for (Class<Servlet> cls : allServletClasses) {
// 这⾥是利⽤ java 中的反射特性做的
// 实际上还得涉及⼀个类的加载问题,因为我们的类字节码⽂件,是按照约定的⽅式(全部在WEB-INF/classes ⽂件夹下)存放的,所以 tomcat 内部是实现了⼀个⾃定义的类加载器(ClassLoader)⽤来负责这部分⼯作
Servlet ins = cls.newInstance();
instanceList.add(ins);
}
//开始方法
// 调⽤每个 Servlet 对象的 init() ⽅法,这个⽅法在对象的⽣命中只会被调⽤这⼀次
for (Servlet ins : instanceList) {
ins.init();
}
// 利⽤我们之前学过的知识,启动⼀个 HTTP 服务器
// 并⽤线程池的⽅式分别处理每⼀个 Request
ServerSocket serverSocket = new ServerSocket(8080); //开启一个web服务并设置端口号,监测:若有人访问此ip+端口,是能感知到的
// 实际上 tomcat 不是⽤的固定线程池,这⾥只是为了说明情况
ExecuteService pool = Executors.newFixedThreadPool(100);
//多次拦截调用方法
while (true) { //死循环,一直等待别人去访问
Socket socket = ServerSocket.accept(); //如果没有人访问,就会阻塞到这行代码;如果有人访问,就会拿到请求的信息,往下执行
// 每个请求都是⽤⼀个线程独⽴⽀持,这⾥体现了我们 Servlet 是运⾏在多线程环境下的
pool.execute(new Runnable() {
doHttpRequest(socket); //包含了一系列method方法
});
}
//销毁方法
// 调⽤每个 Servlet 对象的 destroy() ⽅法,这个⽅法在对象的⽣命中只会被调⽤这⼀次
for (Servlet ins : instanceList) {
ins.destroy();
}
}
}
小结:
- Tomcat 的代码中内置了 main ⽅法,当我们启动 Tomcat 的时候,就是从 Tomcat 的 main ⽅法开始执⾏的。
- 被 @WebServlet 注解修饰的类会在 Tomcat 启动的时候就被获取到,并集中管理。
- Tomcat 通过反射这样的语法机制来创建被 @WebServlet 注解修饰的类的实例。
- 这些实例被创建完了之后,会点调⽤其中的 init ⽅法进⾏初始化。(这个⽅法是 HttpServlet ⾃带的,我们⾃⼰写的类可以重写 init)
- 这些实例被销毁之前,会调⽤其中的 destory ⽅法进⾏收尾⼯作。(这个⽅法是 HttpServlet ⾃带的,我 们⾃⼰写的类可以重写 destory)
- Tomcat 内部也是通过 Socket API 进⾏⽹络通信。
- Tomcat 为了能同时相应多个 HTTP 请求,采取了多线程的⽅式实现。因此 Servlet 是运⾏在多线程环境下的。
2.2.Tomcat 处理请求流程
class Tomcat {
void doHttpRequest(Socket socket) {
// 参照我们之前学习的 HTTP 服务器类似的原理,进⾏ HTTP 协议的请求解析,和响应构建
HttpServletRequest req = HttpServletRequest.parse(socket);
HttpServletRequest resp = HttpServletRequest.build(socket);
// 判断 URL 对应的⽂件是否可以直接在我们的根路径上找到对应的⽂件,如果找到,就是静态内容
// 直接使⽤我们学习过的 IO 进⾏内容输出
if (file.exists()) {
// 返回静态内容
return;
}
// ⾛到这⾥的逻辑都是动态内容了
// 根据我们在配置中说的,按照 URL -> servlet-name -> Servlet 对象的链条
// 最终找到要处理本次请求的 Servlet 对象
Servlet ins = findInstance(req.getURL());
// 调⽤ Servlet 对象的 service ⽅法
// 这⾥就会最终调⽤到我们⾃⼰写的 HttpServlet 的⼦类⾥的⽅法了
try {
ins.service(req, resp);
} catch (Exception e) {
// 返回 500 ⻚⾯,表示服务器内部错误
}
}
}
小结:
- Tomcat 从 Socket 中读到的 HTTP 请求是⼀个字符串,然后会按照 HTTP 协议的格式解析成⼀个 HttpServletRequest 对象。
- Tomcat 会根据 URL 中的 path 判定这个请求是请求⼀个静态资源还是动态资源,如果是静态资源,直接找到对应的⽂件,把⽂件的内容通过 Socket 返回。如果是动态资源,才会执⾏到 Servlet 的相关逻辑。
- Tomcat 会根据 URL 中的 Context Path 和 Servlet Path 确定要调⽤哪个 Servlet 实例的 service ⽅法。
- 通过 service ⽅法,就会进⼀步调⽤到我们之前写的 doGet 或者 doPost。
2.3.Servlet 的 service 方法的实现
class Servlet {
public void service(HttpServletRequest req, HttpServletResponse resp) {
String method = req.getMethod();
if (method.equals("GET")) {
doGet(req, resp);
} else if (method.equals("POST")) {
doPost(req, resp);
} else if (method.equals("PUT")) {
doPut(req, resp);
} else if (method.equals("DELETE")) {
doDelete(req, resp);
}
......
}
}
小结:
- Servlet 的 service ⽅法内部会根据当前请求的⽅法,决定调⽤其中的某个 doXXX ⽅法。
- 在调⽤ doXXX ⽅法的时候,就会触发多态机制,从⽽执⾏到我们⾃⼰写的⼦类中的 doXXX ⽅法。
理解此处的多态
- 我们⾃⼰写的 HelloServlet 类,继承⾃ HttpServlet 类,⽽ HttpServlet ⼜继承⾃ Servlet,相当于 HelloServlet 就是 Servlet 的⼦类。
- 接下来,在 Tomcat 启动阶段,Tomcat 已经根据注解的描述,创建了 HelloServlet 的实例,然后把这个实例放到了Servlet 数组中。
- 后⾯我们根据请求的 URL 从数组中获取到了该 HelloServlet 实例,但是我们是通过 Servlet ins 这 样的⽗类引⽤来获取到 HelloServlet 实例的。
- 最后,我们通过 ins.doGet() 这样的代码调⽤ doGet 的时候,正是 "⽗类引⽤指向⼦类对象",此时就会触发多态机制,从⽽调⽤到我们之前在 HelloServlet 中所实现的 doGet ⽅法。
等价代码:
Servlet ins = new HelloServlet(); ins.doGet(req, resp);
小结:
①Tomcat的main方法
启动Socket网络编程->得到所有请求
②Tomcat的doHttpRequest方法
url->@WebServlet类
Servlet->service(req, resp)
③Servlet的service方法
得到方法类型。