一、前言
本文章是对tomcat底层逻辑和servlet规范进行手动实现。
目的是掌握tomcat作为一个Java服务是如何通过封装请求和响应,并通过配置文件进而去找到servlet的流程。可以更好地掌握web开发本质。为后面学习框架打下基础
技术要点:网络编程(socket编程)、IO、多线程、xml解析(Dom4j)、反射
学习前知识掌握:Java基础、Tomcat+Servlet基本使用
二、Tomcat
2.1、Tomcat本质
tomca本质t是一个Java服务,他的工作是帮我们封装浏览器的http请求数据,通过请求解析出web应用(servlet)并通过http响应返回。
那么tomcat和servlet是如何一起工作的呢?tomcat实现了servlet的一套规范 例如servlet的生命周期。tomcat很智能,当得知你访问的资源是动态的,就会交给servlet去实现;反之如果是静态资源(html、css、js)的话,则不会走servlet,而是直接返回静态资源(后面会使用nginx成为替换方案)
总结:tomcat是一个java服务,通过端口监听浏览器请求,并处理请求交给servlet(具体看2.1),之后再封装http响应返回浏览器
2.2、Tomcat工作流程
tomcat则是通过web.xml去解析并找到servlet的
<servlet>
<servlet-name>HelloServlet</servlet-name>
<!--包路径需要让tomcat进行反射得到-->
<servlet-class>com.org.HelloServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>HelloServlet</servlet-name>
<!--配置路径 tomcat才知道要将请求发送给哪个Servlet-->
<url-pattern>/hello</url-pattern>
</servlet-mapping>
如果是第一次请求(Tomcat):
- 查询 web.xml
- 看看请求的资源 /hello, 在web.xml 配置 urlpattern
- 如果找到 url-pattern,就会得到 servlet-name: HelloServlet
- Tomcat 维护了一个大的HashMap<id, Servlet>,查询该Hashmap看看有没有这个Servlet实例
- 如果没有查询到该serletname 对应的id,即没有这个Servlet实例
- 就根据servlet-name 去得到serlvet-classs: 类的全路径
- 使用反射技术,将 servlet实例化->init0),并放入到 Tomcat 维护的HashMap<id, Servlet>
如果是第2次(以后)请求(Tomcat):
- 查询 web.xml
- 看看请求的资源 /hello, 在web.xml 配置 url-pattern
- 如果找到 url-pattern, 就会得到 servlet-name: HelloServlet
- Tomcat 维护了一个大的HashMap<id,Servlet>,查询该Hashmap看看有没有这个Servlet实例
- 如果查询到,就直接调用该Servlet的service()
- 结果显示
2.3、Tomcat调用service定位到doXxx
Servlet
定义一套Servlet规范
package javax.servlet;
import java.io.IOException;
public interface Servlet {
void init(ServletConfig var1) throws ServletException;
ServletConfig getServletConfig();
void service(ServletRequest var1, ServletResponse var2) throws ServletException, IOException;
String getServletInfo();
void destroy();
}
当我们自己的Servlet通过继承HttpServlet进行开发会比直接继承Servlet开发更加的容易
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @author Csir
*/
@WebServlet(urlPatterns = {"/hi", "/hi2"})
public class HiServlet extends HttpServlet {
private int get = 0;
private int post = 0;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
get++;
System.out.println("get is = " + get);
System.out.println("post is = " + post);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
post++;
System.out.println("get is = " + get);
System.out.println("post is = " + post);
}
}
如何定位到doGet/doPost:
首先Tomcat通过web.xml定位到HiServlet,然后需要调用service方法,HiServlet没有重写,就会找到HttpServlet去调用:
第一个调用Servlet的init方法只会调用一次,接下来如果继续调用该Servlet每次都会创建新的HttpServletRequest 对象 和 HttpServletResponse 对象 传递给service,然后在通过doXxx方法动态绑回我们自己的实现的doXxx方法,并将req和resp作为参数传递
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String method = req.getMethod();
long lastModified;
if (method.equals("GET")) {
lastModified = this.getLastModified(req);
if (lastModified == -1L) {
this.doGet(req, resp); // 动态绑定 调用HiServlet的doGet方法
} else {
long ifModifiedSince;
try {
ifModifiedSince = req.getDateHeader("If-Modified-Since");
} catch (IllegalArgumentException var9) {
ifModifiedSince = -1L;
}
if (ifModifiedSince < lastModified / 1000L * 1000L) {
this.maybeSetLastModified(resp, lastModified);
this.doGet(req, resp);
} else {
resp.setStatus(304);
}
}
} else if (method.equals("HEAD")) {
lastModified = this.getLastModified(req);
this.maybeSetLastModified(resp, lastModified);
this.doHead(req, resp);
} else if (method.equals("POST")) {
this.doPost(req, resp);
} else if (method.equals("PUT")) {
this.doPut(req, resp);
} else if (method.equals("DELETE")) {
this.doDelete(req, resp);
} else if (method.equals("OPTIONS")) {
this.doOptions(req, resp);
} else if (method.equals("TRACE")) {
this.doTrace(req, resp);
} else {
String errMsg = lStrings.getString("http.method_not_implemented");
Object[] errArgs = new Object[]{method};
errMsg = MessageFormat.format(errMsg, errArgs);
resp.sendError(501, errMsg);
}
}
三、手写Tomcat
3.1、流程梳理
3.2、第一版本
- **ServerSocket:**在服务端监听指定端口,如果浏览器/客户端连接该端口,则建立连接,返回Socket对象
- **Socket:**表示 服务端和客户端/浏览器间的连接,通过Socket可以得到InputStream和OutputStream 流对象
我们创建了第一个版本MyTomcatV1,用于监听8080端口接收数据并响应数据回去
package com.org.tomcat;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
/**
* @author Csir
* @version 1.0
* 可以完成,接受浏览器请求,并返回信息
*/
public class MyTomcatV1 {
public static void main(String[] args) throws IOException {
// 创建ServerSocket 在8080监听
ServerSocket serverSocket = new ServerSocket(8080);
System.out.println("=====MyTomcatV1在8080端口监听=====");
while (!serverSocket.isClosed()) {
// 等待浏览器/客户端连接
// 如果由连接来 创建一个socket(服务器和浏览器的连接通道)
Socket socket = serverSocket.accept();
// 先接受浏览器发送的数据
InputStream inputStream = socket.getInputStream();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, "utf-8"));
String msg = null;
System.out.println("=====开始接收客户端请求=====");
while ((msg = bufferedReader.readLine()) != null) {
if (msg.length() == 0) {
break;
}
System.out.println(msg);
}
OutputStream outputStream = socket.getOutputStream();
// 构建一个http响应头
// \r\n 表示换行
String respHeader = "HTTP/1.1 200 OK\r\n" +
"Content-Type: text/html;charset=utf-8\r\n\r\n";
String respBody = "<h1>Hello,你好啊</h1>";
String resp = respHeader + respBody;
System.out.println("======我们给浏览器响应数据=====");
System.out.println(resp);
outputStream.write(resp.getBytes());
outputStream.flush();
outputStream.close();
inputStream.close();
socket.close();
}
}
}
3.3、第二版本
第一个版本只有一个单线程在提供服务,很显然这样子的效率是非常低的,我们希望在第二个版本添加进多线程的知识。
我们采用了BIO的方式:也就是当服务器接受socket连接后,每一个socket连接就会创建一个线程并由该线程去处理。MyTomcat只起到了转发作用
我们创建接受Sokcet的处理器MyHttpHandler,实现Runnable接口
package com.org.tomcat.handler;
import java.io.*;
import java.net.Socket;
/**
* @author Csir
* 1、MyHttpHandler 对象是一个线程对象
* 2、处理htpp请求
*/
public class MyHttpHandler implements Runnable{
Socket socket = null;
public MyHttpHandler(Socket socket){
this.socket = socket;
}
@Override
public void run() {
try {
InputStream inputStream = socket.getInputStream();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, "utf-8"));
// 不同线程在和客户端交互
System.out.println("当前线程名为:" + Thread.currentThread().getName());
System.out.println("=====MyHttpHandler开始接收请求数据=====");
String msg = null;
while ( (msg = bufferedReader.readLine()) != null ){
if (msg.length() <= 0) {
break;
}
System.out.println(msg);
}
OutputStream outputStream = socket.getOutputStream();
String respHeader = "HTTP/1.1 200 OK\r\n" +
"Content-Type: text/html;charset=utf-8\r\n\r\n";
String respBody = "<h1>Hello,你好啊</h1>";
String resp = respHeader + respBody;
System.out.println("=====MyHttpHandler开始响应数据=====");
System.out.println(resp);
outputStream.flush();
outputStream.close();
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}finally {
if (socket != null) {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
我们第二个版本MyTomcatV2,用于监听8080端口得到的socket创建线程去处理
package com.org.tomcat;
import com.org.tomcat.handler.MyHttpHandler;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
/**
* @author Csir
* @version 2.0
* BIO 实现多线程 但是BIO这里每次来一个请求就会创建的一个线程
* NIO则是多路复用 为多个请求服务 所以效率比NIO高
*/
public class MyTomcatV2 {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(8080);
System.out.println("=====MyTomcatV2在8080端口监听=====");
while (!serverSocket.isClosed()) {
Socket socket = serverSocket.accept();
new Thread(new MyHttpHandler(socket)).start();
}
}
}
3.4、第三版本
真正要引入Servlet规范
3.4.1、创建Servlet规范并测试
1、MyServlet接口:对标的是Servlet接口
package com.org.tomcat.servlet;
import com.org.tomcat.http.MyRequest;
import com.org.tomcat.http.MyResponse;
/**
* @author Csir
* 相当于 Servlet接口
*/
public interface MyServlet {
void init() throws Exception;
void service(MyRequest request, MyResponse response) throws Exception;
void destroy();
}
2、MyRequest:对标的是HttpServletRequest,用于封装浏览器发送的http请求
package com.org.tomcat.http;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.HashMap;
/**
* @author Csir
* 1、MyRequest 作用封装http请求的数据
* get /myUserServelt?username=zs&age=20
* 2、比如 method(get)、uri(/myUserServelt)、参数列表(username=zs&age=20)
* 3、MyRequest 作用等价原生的servlet 中的 HttpServletRequest
*/
public class MyRequest {
private InputStream inputStream;
private String method;
private String uri;
private HashMap<String, String> parametersMapping = new HashMap<>();
// 构造器
// InputStream 必须是和 http的socket关联的
public MyRequest(InputStream inputStream) {
this.inputStream = inputStream;
// 完成对http请求数据的封装
encapRequest();
}
/**
* 封装http数据 并提供get方法
*/
private void encapRequest(){
try {
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, "utf-8"));
// 读取请求行: GET /myUserServlet?username=zs&age=20 HTTP/1.1
String requestLine = bufferedReader.readLine();
String[] requestLineArr = requestLine.split(" ");
method = requestLineArr[0];
// 解析得到 /myUserServlet
// 1、先看看uri 有没有参数列表
int index = requestLineArr[1].indexOf("?");
if (index == -1) {
uri = requestLineArr[1];
}else {
uri = requestLineArr[1].substring(0, index);
// parameters => username=zs&age=20
String parameters = requestLineArr[1].substring(index + 1);
// parametersPair => [ "username=zs", "age=20" ]
String[] parametersPair = parameters.split("&");
if ( parametersPair != null && !"".equals(parametersPair) ){
for (String parameterPair : parametersPair) {
String[] parameterValue = parameterPair.split("=");
if (parameterValue.length == 2) {
parametersMapping.put(parameterValue[0], parameterValue[1]);
}
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
public String getMethod() {
return method;
}
public String getUri() {
return uri;
}
public String getParameter(String name) {
if (parametersMapping.containsKey(name)){
return parametersMapping.get(name);
}
else {
return null;
}
}
@Override
public String toString() {
return "MyRequest{" +
"method='" + method + '\'' +
", uri='" + uri + '\'' +
", parametersMapping=" + parametersMapping +
'}';
}
}
3、MyResponse:对标HttpServletResponse,提供与socket相关联的OutputStream可以用来获取并进行调用
package com.org.tomcat.http;
import java.io.OutputStream;
/**
* @author Csir
* 1、MyResponse 对象 封装 OutputStream(和socket关联)
* 2、即可以通过 MyResponse 对象 返回Http响应给浏览器/客户端
* 3、MyResponse对象 等价原生servlet的 HttpServletResponse
*/
public class MyResponse {
private OutputStream outputStream;
public static final String respHeader = "HTTP/1.1 200 OK\r\n" +
"Content-Type: text/html;charset=utf-8\r\n\r\n";
// 创建MyResponse对象,传入的outputStream和socket关联
public MyResponse(OutputStream outputStream){
this.outputStream = outputStream;
}
// 当我们需要给浏览器返回数据时,可以通过MyResponse的输出流完成
public OutputStream getOutputStream(){
return outputStream;
}
}
4、MyHttpServlet抽象类:对标HttpServlet和GenericServlet
package com.org.tomcat.servlet;
import com.org.tomcat.http.MyRequest;
import com.org.tomcat.http.MyResponse;
import java.io.IOException;
/**
* @author Csir
* 相当于 HttpServelt和 GenericServlet
*/
public abstract class MyHttpServlet implements MyServlet{
@Override
public void service(MyRequest request, MyResponse response) throws IOException {
if ("GET".equalsIgnoreCase(request.getMethod())) {
this.doGet(request, response);
}else if ("POST".equalsIgnoreCase(request.getMethod())) {
this.doPost(request, response);
}
}
// 使用模板设计模式 让子类实现方法
public abstract void doGet(MyRequest request, MyResponse response);
public abstract void doPost(MyRequest request, MyResponse response);
}
5、测试:改造MyHttpHandler
package com.org.tomcat.handler;
import com.org.tomcat.http.MyRequest;
import com.org.tomcat.http.MyResponse;
import java.io.*;
import java.net.Socket;
/**
* @author Csir
* 1、MyHttpHandler 对象是一个线程对象
* 2、处理htpp请求
*/
public class MyHttpHandler implements Runnable{
Socket socket = null;
public MyHttpHandler(Socket socket){
this.socket = socket;
}
@Override
public void run() {
try {
MyRequest request = new MyRequest(socket.getInputStream());
System.out.println("======MyHttpHandler获取请求数据=====");
System.out.println(request);
MyResponse response = new MyResponse(socket.getOutputStream());
String respBody = "<h1>Hello,第二个版本</h1>";
System.out.println("======MyHttpHandler给浏览器响应数据=====");
String resp = MyResponse.respHeader + respBody;
System.out.println(resp);
OutputStream outputStream = response.getOutputStream();
outputStream.write(resp.getBytes());
outputStream.flush();
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}finally {
if (socket != null) {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
3.4.2、引入自己写的Servlet并测试
package com.org.tomcat.servlet;
import com.org.tomcat.http.MyRequest;
import com.org.tomcat.http.MyResponse;
import java.io.IOException;
import java.io.OutputStream;
/**
* @author Csir
*/
public class MyTestServlet extends MyHttpServlet{
@Override
public void doGet(MyRequest request, MyResponse response) {
int num1 = Integer.parseInt(request.getParameter("num1"));
int num2 = Integer.parseInt(request.getParameter("num2"));
int sum = num1 + num2;
OutputStream outputStream = response.getOutputStream();
String respMsg = MyResponse.respHeader +
"<h1>" + num1 + " + " + num2 + " = " + sum + "</h1>";
try {
outputStream.write(respMsg.getBytes());
outputStream.flush();
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void doPost(MyRequest request, MyResponse response) {
doGet(request, response);
}
@Override
public void init() throws Exception {
}
@Override
public void destroy() {
}
}
在MyHttpHandler先硬编码创建MyTestServlet 后面使用反射获得
package com.org.tomcat.handler;
import com.org.tomcat.http.MyRequest;
import com.org.tomcat.http.MyResponse;
import com.org.tomcat.servlet.MyTestServlet;
import java.io.*;
import java.net.Socket;
/**
* @author Csir
* 1、MyHttpHandler 对象是一个线程对象
* 2、处理htpp请求
*/
public class MyHttpHandler implements Runnable{
Socket socket = null;
public MyHttpHandler(Socket socket){
this.socket = socket;
}
@Override
public void run() {
try {
MyRequest request = new MyRequest(socket.getInputStream());
System.out.println("======MyHttpHandler获取请求数据=====");
System.out.println(request);
MyResponse response = new MyResponse(socket.getOutputStream());
// 创建 MyTestServlet -》 一会使用反射获取
MyTestServlet myTestServlet = new MyTestServlet();
myTestServlet.doGet(request,response);
} catch (IOException e) {
e.printStackTrace();
}finally {
if (socket != null) {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
3.4.3、使用web.xml进行反射
1、编写web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<servlet>
<servlet-name>MyTestServlet</servlet-name>
<servlet-class>com.org.tomcat.servlet.MyTestServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>MyTestServlet</servlet-name>
<url-pattern>/test</url-pattern>
</servlet-mapping>
</web-app>
2、创建第三个版本的MyTomcatV3,维护两个Map对象,一个存放容器名和容器实例映射、一个存放uri路径和容器名映射,通过读取web.xml配置文件添加到两个Map对象
package com.org.tomcat;
import com.org.tomcat.handler.MyHttpHandler;
import com.org.tomcat.servlet.MyHttpServlet;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import java.io.File;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author Csir
* @version 3.0
* 实现 通过 xml+反射来初始化容器
*/
public class MyTomcatV3 {
// 1、存放容器 servletMapping
public static final ConcurrentHashMap<String, MyHttpServlet> servletMapping = new ConcurrentHashMap<>();
// 2、存放路径映射 servletUrlMapping
public static final ConcurrentHashMap<String, String> servletUrlMapping = new ConcurrentHashMap<>();
public static void main(String[] args) {
MyTomcatV3 myTomcatV3 = new MyTomcatV3();
myTomcatV3.init();
myTomcatV3.run();
}
public void run() {
try {
ServerSocket serverSocket = new ServerSocket(8080);
System.out.println("=====MyTomcatV3在8080端口监听=====");
while (!serverSocket.isClosed()) {
Socket socket = serverSocket.accept();
MyHttpHandler myHttpHandler = new MyHttpHandler(socket);
new Thread(myHttpHandler).start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
public void init(){
// 读取 web.xml => dom4j
// 得到的web.xml文件路径 => 拷贝一份到 D:/person/out/production/person/
String path = MyTomcatV3.class.getResource("/").getPath();
// 读取xml
SAXReader saxReader = new SAXReader();
try {
Document document = saxReader.read(new File(path + "web.xml"));
// 获取根元素
Element rootElement = document.getRootElement();
// 得到根目录下的所有元素
List<Element> elements = rootElement.elements();
// 遍历并过滤到 servlet servlet-mapping
for (Element element : elements) {
if ("servlet".equalsIgnoreCase(element.getName())) {
// 使用反射 将servlet实例放入 servletMapping
Element servletName = element.element("servlet-name");
Element servletClass = element.element("servlet-class");
servletMapping.put(
servletName.getText(),
(MyHttpServlet) Class.forName(servletClass.getText().trim()).newInstance()
);
} else if ("servlet-mapping".equalsIgnoreCase(element.getName())) {
Element servletName = element.element("servlet-name");
Element urlPattern = element.element("url-pattern");
servletUrlMapping.put(
urlPattern.getText(),
servletName.getText()
);
}
}
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("servletMapping" + servletMapping);
System.out.println("servletUrlMapping" + servletUrlMapping);
}
}
3、在MyHttpHandler通过反射获取容器
package com.org.tomcat.handler;
import com.org.tomcat.MyTomcatV3;
import com.org.tomcat.http.MyRequest;
import com.org.tomcat.http.MyResponse;
import com.org.tomcat.servlet.MyHttpServlet;
import java.io.*;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
/**
* @author Csir
* 1、MyHttpHandler 对象是一个线程对象
* 2、处理htpp请求
*/
public class MyHttpHandler implements Runnable{
Socket socket = null;
public MyHttpHandler(Socket socket){
this.socket = socket;
}
@Override
public void run() {
try {
MyRequest request = new MyRequest(socket.getInputStream());
MyResponse response = new MyResponse(socket.getOutputStream());
String uri = request.getUri(); // uri 即 url-pattern
String servletName = MyTomcatV3.servletUrlMapping.get(uri);
if (servletName != null) { // ConcurrentHashMap 取不到null
// 编译类型为 MyHttpServlet 运行类型为子类 MyTestServlet
MyHttpServlet myHttpServlet = MyTomcatV3.servletMapping.get(servletName);
if (myHttpServlet != null) {
myHttpServlet.service(request, response);
}
}else {
String respMsg = MyResponse.respHeader + "<h1>404 Not Found</h1>";
outputStream.write(respMsg.getBytes(StandardCharsets.UTF_8));
OutputStream outputStream = response.getOutputStream();
outputStream.flush();
outputStream.close();
}
} catch (IOException e) {
e.printStackTrace();
}finally {
if (socket != null) {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
4、测试,启动MyTomcatV3,浏览器发送路径 http://127.0.0.1:8080/test?num1=10&num2=20
如果返回下面 则成功
你可以自己再写一个乘法的的Servlet通过实现MyHttpServlet。 也完全可以实现的
最后,如果这篇文章对你有帮助的话,请点个赞~