目录
一、Tomcat底层整体架构
1.简介 :
Tomcat 有三种运行模式 (BIO[阻塞], NIO[非阻塞], APR),这里采用 BIO 线程模型来模拟实现。
2.分析图 :
如下图所示 :
编辑
浏览器请求servlet资源后,Tomcat底层会通过Socket网络编程来接收请求,每次请求,都会创建一个新的线程去调用相应的Web资源,并返回。
3.基于Socket开发服务端的流程 :
如下图所示 :
编辑
通过获取的Socket对象,来获取Socket对象对应的字节输入流和字节输出流。
可以利用对象转换流将字节流转换为字符流对象(InputStreamReader实现了Reader抽象类),然后再通过BufferedReader的包装,将节点流转换成包装流(处理流)。
4.打通服务器端和客户端的数据通道 :
PS :
Maven配置Web应用,运行出现jakarta.servlet.ServletException:
因为tomcat10之后不是javax.servlet,而是jakarta.servlet,所以Web的依赖应该换成如下所示 : (pom.xml配置文件)
<!--jar包的依赖--> <dependency> <groupId>jakarta.servlet</groupId> <artifactId>jakarta.servlet-api</artifactId> <version>5.0.0</version> <scope>provided</scope> </dependency> <!--jsp的依赖--> <dependency> <groupId>jakarta.servlet.jsp</groupId> <artifactId>jakarta.servlet.jsp-api</artifactId> <version>3.0.0</version> <scope>provided</scope> </dependency>
MyTomcat类代码如下 : (服务端;自定义的Tomcat)
package tomcat; import java.io.*; import java.net.ServerSocket; import java.net.Socket; /** 服务器端 */ public class MyTomcat { public static void main(String[] args) throws IOException { ServerSocket serverSocket = new ServerSocket(8080); System.out.println("Tomcat在8080端口进行监听..."); System.out.println("--------------------------------------"); while (!serverSocket.isClosed()) { //获取Socket对象 Socket socket = serverSocket.accept(); //接收来自浏览器端的信息 InputStream inputStream = socket.getInputStream(); BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8")); String content = null; //局部变量在使用前必须赋初值。 while ((content = bufferedReader.readLine()) != null) { //判断content是否是空串儿 if (content.length() == 0) { break; } System.out.println(content); } //服务器向浏览器回送消息 OutputStream outputStream = socket.getOutputStream(); //设置一个HTTP响应包的响应头 // "/r/n"表示换行 String respHeader = "HTTP/1.1 200\r\n" + "Content-Type: text/html;charset=utf-8\r\n\r\n"; //设置HTTP响应的响应体 String respBody = respHeader + "<h1>你好!</h1>"; /* 注意这里不能使用字符包装流来写数据!!!。 */ outputStream.write(respBody.getBytes()); System.out.println("--------------------------------------"); System.out.println(respBody); //释放资源 outputStream.flush(); outputStream.close(); inputStream.close(); socket.close(); // serverSocket.close(); } } }
在浏览器地址栏访问本机8080端口。
login.html代码如下 :
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>login</title> <style> table, tr, td { border:2px cornflowerblue solid; border-collapse: collapse; padding: 10px; background-color: lightcyan } #tr01 { text-align: center } </style> </head> <body> <form action="/Cyan_Tomcat/login" methods="get"> <table> <tr> <th colspan="2">User Logging</th> </tr> <tr> <td>Username: </td> <td><input type="text" name="username"/></td> </tr> <tr> <td>Password: </td> <td><input type="password" name="password"/></td> </tr> <tr id="tr01"> <td><input type="submit" value="Submit"/></td> <td><input type="reset" value="Reset"/></td> </tr> </table> </form> </body> </html>
LoginServlet类代码如下 :
package servlet; import jakarta.servlet.ServletException; import jakarta.servlet.annotation.WebServlet; import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; /** * @author : Cyan_RA9 * @version : 21.0 */ @WebServlet(urlPatterns = {"/login"}) public class LoginServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { System.out.println("LoginServlet's doPost is invoked~"); resp.setContentType("text/html; charset=utf-8"); PrintWriter writer = resp.getWriter(); req.setCharacterEncoding("utf-8"); String username = req.getParameter("username"); String password = req.getParameter("password"); if ("Cyan".equals(username) && "123".equals(password)) { writer.print("<h1>登录成功!</h1>"); } else { writer.print("<h1>登录失败!请重新尝试!</h1>"); } } @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { doPost(req, resp); } }
运行效果 :
如果以Web工程配置好的Tomcat运行,就会按照LoginServlet类的代码逻辑来处理业务,如下图所示 : (GIF)
编辑
如果以自定义的Tomcat运行,就会以MyTomcat类中的代码逻辑来处理业务。
如下图所示 : (GIF)
编辑
二、多线程模型的实现
1.思路分析 :
当服务器端接收到浏览器端的HTTP请求后,启动一个新的线程,令该线程持有浏览器对应的Socket对象,完成线程和浏览器的对接。
可通过实现Runnable接口的方式定义线程类HttpRequestHandler,线程对象用于处理来自浏览器的HTTP请求。
2.处理HTTP请求 :
线程类HttpRequestHandler类代码如下 :
package tomcat.handler; import tomcat.http.CyanServletRequest; import tomcat.http.CyanServletResponse; import tomcat.servlet.CyanLoginServlet; import java.io.*; import java.net.Socket; public class HttpRequestHandler implements Runnable { private Socket socket; public HttpRequestHandler(Socket socket) { this.socket = socket; } @Override public void run() { try { //接收客户端的信息 InputStream inputStream = socket.getInputStream(); /* 以下代码已在CyanServletRequest类中实现 BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, "utf-8")); //BIO,每次请求都对应一个新的线程 System.out.println("当前线程 = " + Thread.currentThread().getName()); String content = null; //局部变量在使用前必须赋初值。 while ((content = bufferedReader.readLine()) != null) { //判断是否读到了空字符串 if (content.length() == 0) { break; } System.out.println(content); } */ //Die first and live second //获取客户端的信息(利用了CyanServletRequest中封装好的方法) //以下代码已在CyanLoginServlet中实现 /* CyanServletRequest cyanServletRequest = new CyanServletRequest(inputStream); String username = cyanServletRequest.getParameter("username"); String password = cyanServletRequest.getParameter("password"); System.out.println("username = " + username); System.out.println("password = " + password); System.out.println(cyanServletRequest);*/ //给客户端回送信息 //以下代码已在CyanLoginServlet类中实现。 /* CyanServletResponse cyanServletResponse = new CyanServletResponse(socket.getOutputStream()); String resp = CyanServletResponse.respHeader + "<h1>CyanServletResponse!</h1>"; OutputStream outputStream = cyanServletResponse.getOutputStream(); outputStream.write(resp.getBytes()); outputStream.flush(); outputStream.close();*/ CyanServletRequest cyanServletRequest = new CyanServletRequest(inputStream); CyanServletResponse cyanServletResponse = new CyanServletResponse(socket.getOutputStream()); CyanLoginServlet cyanLoginServlet = new CyanLoginServlet(); cyanLoginServlet.doPost(cyanServletRequest, cyanServletResponse); //释放资源 inputStream.close(); socket.close(); /* 以下代码已在CyanServletResponse类中实现 : String respHeader = "HTTP/1.1 200\r\n" + "Content-Type: text/html;charset=utf-8\r\n\r\n"; String respHttp = respHeader + "<h1>Cyan_RA9</h1>"; System.out.println("-----------------------------------------------"); System.out.println("回送的信息如下:(回显)"); System.out.println(respHttp); OutputStream outputStream = socket.getOutputStream(); outputStream.write(respHttp.getBytes()); //释放资源 outputStream.flush(); outputStream.close(); inputStream.close(); socket.close(); */ } catch (IOException e) { throw new RuntimeException(e); } finally { //确保Socket关闭 if (socket != null) { try { socket.close(); } catch (IOException e) { throw new RuntimeException(e); } } } } }
3.自定义Tomcat :
在MyTomcat_EX类中实现线程的分发。
MyTomcat_EX类代码如下 :
package tomcat; import tomcat.handler.HttpRequestHandler; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.net.ServerSocket; import java.net.Socket; /** * @author : Cyan_RA9 * @version : 21.0 */ public class MyTomcat_EX { public static void main(String[] args) throws IOException { ServerSocket serverSocket = new ServerSocket(8080); System.out.println("MyTomcat_EX在8080端口进行监听..."); while (!serverSocket.isClosed()) { Socket socket = serverSocket.accept(); HttpRequestHandler httpRequestHandler = new HttpRequestHandler(socket); Thread thread = new Thread(httpRequestHandler); thread.start(); } } }
运行效果 : (GIF)
编辑
三、自定义Servlet规范
1. HTTP请求和响应 :
1° CyanServletRequest
CyanServletRequest类的作用等同于原始的HttpServletRequest,该类用于封装HTTP请求中的数据,eg : method, URI, 以及表单数据的参数列表等。
CyanServletRequest类代码如下 :
package tomcat.http; import java.io.*; import java.util.HashMap; /** * @author : Cyan_RA9 * @version : 21.0 * @function : like the original HttpServletRequest. */ public class CyanServletRequest { private String method; private String URI; private HashMap<String, String> parametersMapping = new HashMap<>(); private InputStream inputStream; /* 此处传入的InputStream对象是和Socket关联的InputStream. */ public CyanServletRequest(InputStream inputStream) { this.inputStream = inputStream; //完成对HTTP请求数据的封装 this.init(); } private void init() { System.out.println("\nCyanServletRequest's init is invoked~"); try { //注意转换流的形参列表 BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, "utf-8")); //首先读取HTTP请求的请求行 /* eg : GET /Cyan/cyan.html HTTP/1.1 */ String requestLine = bufferedReader.readLine(); String[] requestLineArr = requestLine.split(" "); //获取method method = requestLineArr[0]; //获取URI int index = requestLineArr[1].indexOf("?"); if (index == -1) { //if判断成立,说明请求行中没有参数列表 URI = requestLineArr[1]; } else { URI = requestLineArr[1].substring(0, index); //获取参数列表 String parameters = requestLineArr[1].substring(index + 1); String[] parameterPairs = parameters.split("&"); //兼容性处理,防止?后啥都没有。 if (null != parameterPairs && !"".equals(parameterPairs)) { for (String parameterPair : parameterPairs) { String[] parameter = parameterPair.split("="); if (parameter.length == 2) { //判断是否为一对完整的"name=value". parametersMapping.put(parameter[0],parameter[1]); } } } } //!!! 直接关闭Socket关联的InputStream,会引起Socket的关闭。 //inputStream.close(); } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); } catch (IOException e) { throw new RuntimeException(e); } } public String getMethod() { return method; } public void setMethod(String method) { this.method = method; } public String getURI() { return URI; } public void setURI(String URI) { this.URI = URI; } //重要 public String getParameter(String name) { if (parametersMapping.containsKey(name)) { //注意此处API的使用! return parametersMapping.get(name); } else { return null; } } @Override public String toString() { return "CyanServletRequest{" + "method='" + method + '\'' + ", URI='" + URI + '\'' + ", parametersMapping=" + parametersMapping + '}'; } }
CyanServletRequest类测试,运行效果如下GIF :
编辑
2° CyanServletResponse
CyanServletResponse类的作用等同于原始的HttpServletResponse,用于封装HTTP响应的相关信息。
CyanServletResponse类代码如下 :
package tomcat.http; import java.io.OutputStream; /** * @author : Cyan_RA9 * @version : 21.0 * @function : like the original HttpServletResponse */ public class CyanServletResponse { private OutputStream outputStream; //设置一个HTTP响应头 public static final String respHeader = "HTTP/1.1 200\r\n" + "Content-Type: text/html;charset=utf-8\r\n\r\n"; //传入与Socket关联的OutputStream对象 public CyanServletResponse(OutputStream outputStream) { this.outputStream = outputStream; } public OutputStream getOutputStream() { return outputStream; } }
运行测试(如下GIF图):
编辑
2.Servlet规范 :
1° CyanServlet
CyanServlet仅保留原生Servlet的init, destroy, service方法,其中,service方法供将来CyanServlet的抽象实现类CyanHttpServlet去重写。注意,service方法的形参列表,要使用自定义的CyanServletRequest 和 CyanServletResponse.
CyanServlet接口,代码如下 :
package tomcat.servlet; import tomcat.http.CyanServletRequest; import tomcat.http.CyanServletResponse; import java.io.IOException; public interface CyanServlet { void init() throws Exception; void service(CyanServletRequest req, CyanServletResponse resp) throws IOException; void destroy(); }
2° CyanHttpServlet
CyanHttpServlet的作用,类似于原生的HttpServlet;在CyanHttpServlet中实现CyanServlet接口中的service方法,在service方法中,要对HTTP请求的method类型进行判断。
CyanHttpServlet抽象类代码如下 :
package tomcat.servlet; import tomcat.http.CyanServletRequest; import tomcat.http.CyanServletResponse; import java.io.IOException; public abstract class CyanHttpServlet implements CyanServlet{ @Override public void service(CyanServletRequest req, CyanServletResponse resp) throws IOException { //忽略大小写 if ("GET".equalsIgnoreCase(req.getMethod())) { this.deGet(req, resp); } else if ("POST".equalsIgnoreCase(req.getMethod())) { this.doPost(req, resp); } } //模板设计模式 public abstract void deGet(CyanServletRequest req, CyanServletResponse resp); public abstract void doPost(CyanServletRequest req, CyanServletResponse resp); }
3° CyanLoginServlet
CyanLoginServlet是一个简单的servlet实例,用于继承CyanHttpServlet抽象类,并实现CyanHttpServlet类中的doGet和doPost抽象方法。之后,在HttpRequestHandler线程类中先尝试直接调用CyanLoginServlet实例。
CyanLoginServlet代码如下 :
package tomcat.servlet; import tomcat.http.CyanServletRequest; import tomcat.http.CyanServletResponse; import java.io.IOException; import java.io.OutputStream; public class CyanLoginServlet extends CyanHttpServlet{ @Override public void deGet(CyanServletRequest req, CyanServletResponse resp) { doPost(req, resp); } @Override public void doPost(CyanServletRequest req, CyanServletResponse resp) { String username = req.getParameter("username"); String password = req.getParameter("password"); //获取与当前Socket相关联的OutputStream对象 OutputStream outputStream = resp.getOutputStream(); String respInfo = CyanServletResponse.respHeader + "<h1>username = " + username + "</h1><br/> " + "<h1>password = " + password + "</h1>" + "<br/>" + "<h3>Cyan_RA9</h3>"; try { outputStream.write(respInfo.getBytes()); outputStream.flush(); outputStream.close(); } catch (IOException e) { throw new RuntimeException(e); } } @Override public void init() throws Exception { } @Override public void destroy() { } }
此外,还需要更新HttpServletHandler类中的类型,将已封装好的代码注释掉,HttpServletHandler类已更新,在上文“多线程模型实现”的HTTP请求处理模块。
运行效果如下图所示 :
编辑
3.容器实现 :
1° 思路分析
Tomcat中维护有至少两个大的HashMap容器。以web.xml配置文件的方式为例,其中一个HashMap容器,key存放<url-pattern>,value存放<servlet-name>;另一个HashMap容器,key存放<servlet-name>,value存放通过<servlet-class>来反射生成的servlet实例。
2° web.xml配置文件
web.xml配置文件如下 :
<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd" > <!--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"--> <!--IDEA报错—— 因为这是我们自定义的servlet,IDEA无法识别;无所谓!继续用!--> <web-app> <display-name>Archetype Created Web Application</display-name> <servlet> <servlet-name>CyanLoginServlet</servlet-name> <servlet-class>tomcat.servlet.CyanLoginServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>CyanLoginServlet</servlet-name> <url-pattern>/cyanLogin</url-pattern> </servlet-mapping> </web-app>
3° 最终版自定义Tomcat
在MyTomcat_Pro类中定义两个CurrentHashMap对象;定义init方法完成对两个CurrentHashMap对象的初始化(使用Dom4J读取web.xml配置文件)。
首先,需要在Maven的pom.xml配置文件中,引入dom4j依赖,如下图所示 :
编辑
然后,将web.xml配置文件拷贝到/target/classes/目录下一份,如下图所示 :
编辑
MyTomcat_Pro代码如下 :
package tomcat; import org.dom4j.Document; import org.dom4j.Element; import org.dom4j.io.SAXReader; import tomcat.handler.HttpRequestHandler; import tomcat.servlet.CyanHttpServlet; import java.io.File; import java.io.IOException; import java.lang.reflect.Constructor; import java.net.ServerSocket; import java.net.Socket; import java.util.List; import java.util.concurrent.ConcurrentHashMap; /** * The final own custom Tomcat. */ public class MyTomcat_Pro { //Tomcat维护的第一个容器 //String --> <servlet-name> //CyanHttpServlet --> 可存放它的子类(即各种servlet实例) public static final ConcurrentHashMap<String, CyanHttpServlet> servletMapping = new ConcurrentHashMap<>(); //Tomcat维护的第二个容器 //String --> <url-patterns> //String --> <servlet-name> public static final ConcurrentHashMap<String, String> servletURLMapping = new ConcurrentHashMap<>(); public static void main(String[] args) { MyTomcat_Pro myTomcat_pro = new MyTomcat_Pro(); myTomcat_pro.init(); myTomcat_pro.run(); } public void run() { try { ServerSocket serverSocket = new ServerSocket(8080); System.out.println("MyTomcat_Pro在8080端口进行监听..."); while (!serverSocket.isClosed()) { Socket socket = serverSocket.accept(); Thread thread = new Thread(new HttpRequestHandler(socket)); thread.start(); } } catch (IOException e) { throw new RuntimeException(e); } } public void init() { String path = MyTomcat_Pro.class.getResource("/").getPath(); //path = "/javaProject/Servlet/Cyan_Tomcat/target/classes/" //使用Dom4J技术解析web.xml文件 SAXReader saxReader = new SAXReader(); try { //注意文件名 Document document = saxReader.read(new File(path + "web.xml")); System.out.println("document = " + document); //获取根元素<web-app> Element rootElement = document.getRootElement(); //得到根元素下面的所有子元素 List<Element> elements = rootElement.elements(); //遍历并判断 for (Element element : elements) { if ("servlet".equals(element.getName())) { Element servlet_name = element.element("servlet-name"); Element servlet_class = element.element("servlet-class"); //反射机制创建servlet实例 (注意getText()方法的使用!) Class<?> clazz = Class.forName(servlet_class.getText().trim()); Constructor<?> constructor = clazz.getConstructor(); CyanHttpServlet o = (CyanHttpServlet) constructor.newInstance(); servletMapping.put(servlet_name.getText(), o); } else if ("servlet-mapping".equals(element.getName())) { Element url_pattern = element.element("url-pattern"); Element servlet_name = element.element("servlet-name"); servletURLMapping.put(url_pattern.getText(), servlet_name.getText()); } } } catch (Exception e) { throw new RuntimeException(e); } } }
4° 最终版自定义线程类
package tomcat.handler; import tomcat.MyTomcat_Pro; import tomcat.http.CyanServletRequest; import tomcat.http.CyanServletResponse; import tomcat.servlet.CyanHttpServlet; import tomcat.servlet.CyanLoginServlet; import java.io.*; import java.net.Socket; public class HttpRequestHandler implements Runnable { private Socket socket; public HttpRequestHandler(Socket socket) { this.socket = socket; } @Override public void run() { try { //接收客户端的信息 CyanServletRequest cyanServletRequest = new CyanServletRequest(socket.getInputStream()); CyanServletResponse cyanServletResponse = new CyanServletResponse(socket.getOutputStream()); String uri = cyanServletRequest.getURI(); System.out.println("uri = " + uri); String servlet_name = MyTomcat_Pro.servletURLMapping.get(uri); /* 这里的servlet_name可能为空。 解决方式一 : 将CurrentHashMap替换为HashMap 解决方式二 : 增加一个是否为null的判断。 */ if (servlet_name == null) { servlet_name = ""; } //多态 --> 动态绑定 CyanHttpServlet cyanHttpServlet = MyTomcat_Pro.servletMapping.get(servlet_name); if (cyanHttpServlet != null) { //判断是否正常得到servlet实例 cyanHttpServlet.service(cyanServletRequest, cyanServletResponse); } else { //如果没有找到servlet,返回404 String resp = CyanServletResponse.respHeader + "<h1>404 Not Found!</h1>"; OutputStream outputStream = cyanServletResponse.getOutputStream(); outputStream.write(resp.getBytes()); outputStream.flush(); outputStream.close(); } //释放资源 socket.close(); } catch (IOException e) { throw new RuntimeException(e); } finally { //确保Socket关闭 if (socket != null) { try { socket.close(); } catch (IOException e) { throw new RuntimeException(e); } } } } }
5° 容器启动测试
如下图所示(GIF):
编辑