手动实现 Tomcat 底层机制+ 自己设Servlet
目录
手动实现 Tomcat 底层机制+ 自己设Servlet
完成小案例
运行效果
此项目用maven至于怎么配置在下一篇文章
创建cal.html
CalServlet.java
web.xml
WebUtils
问题:
Tomcat 整体架构分析
测试分析:
抓包情况
手动实现 Tomcat 底层机制+ 自己设计 Servlet
分析+代码实现
● 分析示意图
代码实现
TomcatV1
问题分析:
需求分析/图解
● 分析示意图
代码实现
RequestHandler类
TomcatV2
问题分析:
分析+代码实现
● 分析示意图
WyxRequestHandler
wyxResponse
wyxRequest
wyxServlet接口
wyxHttpServlet
wyxCalServlet
WebUtils
wyxTomcatV3
手动实现 Tomcat 底层机制+ 自己设Servlet
完成小案例
运行效果
此项目用maven至于怎么配置在下一篇文章
创建cal.html
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><title>计算器</title>
</head><body><h1>计算器</h1><form action="/calServlet" method="get">num1:<input type="text" name="num1"><br />num2:<input type="text" name="num2"><br /><input type="submit" value="提交"></form>
</body></html>
CalServlet.java
public class CalServlet extends HttpServlet {protected void doPost(HttpServletRequest request,HttpServletResponse response) throws ServletException, IOException {//接收提交的数据进行计算//复制当前行 ctrl+alt+下光标String strNum1 = request.getParameter("num1");String strNum2 = request.getParameter("num2");//把strNum1 和 strNum2 转成 intint num1 = WebUtils.parseInt(strNum1, 0);int num2 = WebUtils.parseInt(strNum2, 0);int result = num1 + num2;response.setContentType("text/html;charset=utf-8");PrintWriter writer = response.getWriter();writer.print("<h1>" + num1 + " + " + num2 + " = " + result + "<h1>");writer.flush();writer.close();}protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {doPost(request, response);}
}
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" >
<web-app><display-name>Archetype Created Web Application</display-name><servlet><servlet-name>CalServlet</servlet-name><servlet-class>com.servlet.CalServlet</servlet-class></servlet><servlet-mapping><servlet-name>CalServlet</servlet-name><url-pattern>/calServlet</url-pattern></servlet-mapping>
</web-app>
WebUtils
public class WebUtils {/* 将一个字符串数字,转成 int, 如果转换失败,就返回传入 defaultVal* @param strNum* @param defaultVal* @return*/public static int parseInt(String strNum, int defaultVal) {try {return Integer.parseInt(strNum);} catch (NumberFormatException e) {System.out.println(strNum + " 格式不对,转换失败");}return defaultVal;}}
问题:
Tomcat 底层实现 和 调用到 Servlet 流程?
我们的目标: 不用 Tomcat, 不用系统提供的 Servlet, 模拟 Tomcat 底层实现并能调用我们自己设计的 Servle, 也能完成相同的功能
Tomcat 整体架构分析
● 说明: Tomcat 有三种运行模式(BIO, NIO, APR), 因为核心讲解的是 Tomcat 如何接
收客户端请求,解析请求, 调用 Servlet , 并返回结果的机制流程, 采用 BIO 线程模型来模拟.
测试分析:
浏览器 http://localhost:8080/cal.html 1.
浏览器输入 http://localhost:8080/cal.html
抓包情况
手动实现 Tomcat 底层机制+ 自己设计 Servlet
实现任务阶段
编写自己 Tomcat, 能给浏览器返回 Hi
基于 socket 开发服务端-流程
分析+代码实现
● 分析示意图
代码实现
TomcatV1
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;public class TomcatV1 {public static void main(String[] args) throws IOException {//1. 创建 ServerSocket, 在 8080 端口监听ServerSocket serverSocket = new ServerSocket(8080);System.out.println("=======mytomcat 在 8080 端口监听======");while (!serverSocket.isClosed()) {//等待浏览器/客户端的连接//如果有连接来,就创建一个 socket//这个 socket 就是服务端和浏览器端的连接/通道Socket socket = serverSocket.accept();//先接收浏览器发送的数据//inputStream 是字节流=> BufferedReader(字符流)InputStream inputStream = socket.getInputStream();BufferedReader bufferedReader =new BufferedReader(new InputStreamReader(inputStream, "utf-8"));String mes = null;System.out.println("=======接收到浏览器发送的数据=======");//循环的读取while ((mes = bufferedReader.readLine()) != null) {//判断 mes 的长度是否为 0if (mes.length() == 0) {break;//退出 while}System.out.println(mes);}//我们的 tomcat 会送-http 响应方式OutputStream outputStream = socket.getOutputStream();//构建一个 http 响应的头//\\r\\n 表示换行//http 响应体,需要前面有两个换行 \\r\\n\\r\\nString respHeader = "HTTP/1.1 200 OK\\r\\n" +"Content-Type: text/html;charset=utf-8\\r\\n\\r\\n";String resp = respHeader + "hi,";System.out.println("========我们的 tomcat 给浏览器会送的数据 ======");System.out.println(resp);outputStream.write(resp.getBytes());//将 resp 字符串以 byte[] 方式返回outputStream.flush();outputStream.close();inputStream.close();socket.close();}}
}
问题分析:
没有使用 BIO 线程模型,没有实现多线程,性能差
实现任务阶段 2- 使用 BIO 线程模型,支持多线程
BIO 线程模型介绍
需求分析/图解
- 需求分析如图, 浏览器请求 http://localhost:8080, 服务端返回 hi后台
- wtomcat 使用 BIO 线程模型,支持多线程=> 对前面的开发模式进行改造
- 分析+代码实现
● 分析示意图
代码实现
RequestHandler类
import java.io.*;
import java.net.Socket;public class RequestHandler implements Runnable {//定义Socketprivate Socket socket = null;public RequestHandler(Socket socket) {this.socket = socket;}@Overridepublic void run() {//这里我们可以对客户端/浏览器进行IO编程/交互try {InputStream inputStream = socket.getInputStream();//把inputStream -> BufferedReader -> 方便进行按行读取BufferedReader bufferedReader =new BufferedReader(new InputStreamReader(inputStream, "utf-8"));//不同的线程在和浏览器和客户端交互System.out.println("当前线程= " + Thread.currentThread().getName());System.out.println("=========tomcatv2 接收到的数据如下=========");String mes = null;while ((mes = bufferedReader.readLine()) != null) {//如果长度为0 ""if (mes.length() == 0) {break; //退出}System.out.println(mes);}//构建一下http响应头//返回的http的响应体和响应头之间有两个换行 \\r\\n\\r\\nString respHeader = "HTTP/1.1 200 OK\\r\\n" +"Content-Type: text/html;charset=utf-8\\r\\n\\r\\n";String resp = respHeader + "<h1>hi</h1>";System.out.println("========tomcatv2返回的数据是=========");System.out.println(resp);//返回数据给我们的浏览器/客户端-> 封装成http响应OutputStream outputStream = socket.getOutputStream();// resp.getBytes() 是把字符串转成字节数组outputStream.write(resp.getBytes());outputStream.flush();outputStream.close();inputStream.close();socket.close();} catch (IOException e) {e.printStackTrace();} finally {//最后一定确保socket要关闭if (socket != null) {try {socket.close();} catch (IOException e) {e.printStackTrace();}}}}
}
TomcatV2
public class TomcatV2 {public static void main(String[] args) throws IOException {//在8080端口监听ServerSocket serverSocket = new ServerSocket(8080);System.out.println("=======hsptomcatV2 在8080监听=======");//只要 serverSocket没有关闭,就一直等待浏览器/客户端的连接while (!serverSocket.isClosed()) {//1. 接收到浏览器的连接后,如果成功,就会得到socket//2. 这个socket 就是 服务器和 浏览器的数据通道Socket socket = serverSocket.accept();//3. 创建一个线程对象,并且把socket给该线程// 这个是java线程基础HspRequestHandler RequestHandler =new RequestHandler(socket);new Thread(RequestHandler).start();}}
}
问题分析:
Tomcat V2 只是简单返回结果,没有和 Servlet、web.xml 关联
实现任务阶段 3- 处理 Servlet
分析+代码实现
● 分析示意图
WyxRequestHandler
package com.wyxdu.tomcat.handler;import com.wyxdu.tomcat.WyxTomcatV3;
import com.wyxdu.tomcat.http.WyxRequest;
import com.wyxdu.tomcat.http.WyxResponse;import com.wyxdu.tomcat.servlet.WyxHttpServlet;
import com.wyxdu.tomcat.utils.WebUtils;import java.io.*;
import java.net.Socket;public class WyxRequestHandler implements Runnable {//定义Socketprivate Socket socket = null;public WyxRequestHandler(Socket socket) {this.socket = socket;}@Overridepublic void run() {//这里我们可以对客户端/浏览器进行IO编程/交互try {WyxRequest wyxRequest = new WyxRequest(socket.getInputStream());//这里我们可以同HspResponse对象,返回数据给浏览器/客户端WyxResponse wyxResponse = new WyxResponse(socket.getOutputStream());//1. 得到 uri => 就是 servletUrlMapping 的 url-patternString uri = wyxRequest.getUri();//=====================新增业务逻辑==========//(1) 判断uri是什么资源 => 工具方法//(2) 如果是静态资源,就读取该资源,并返回给浏览器 content-type text/html//(3) 因为目前并没有起到tomcat, 不是一个标准的web项目//(4) 把读取的静态资源放到 target/classes/cal.html//过滤,拦截 , 权限等待 => Handler.... => 分发if(WebUtils.isHtml(uri)) {//就是静态页面String content = WebUtils.readHtml(uri.substring(1));content = wyxResponse.respHeader + content;//得到outputstream , 返回信息(静态页面)给浏览器OutputStream outputStream = wyxResponse.getOutputStream();outputStream.write(content.getBytes());outputStream.flush();outputStream.close();socket.close();return;}//有了filter机制,可以理解再调用servlet之前,先匹配filter//1. 根据request对象封装的uri//2. 到 filterUrlMapping 去匹配//3. 如果匹配上就调用 filterMapping 对应的filer对象doFilter()//4. 如果没有匹配上,就直接走我们后的servlet/jsp/html.String servletName = WyxTomcatV3.servletUrlMapping.get(uri);if (servletName == null) {servletName = "";}//2. 通过uri->servletName->servlet的实例 , 真正的运行类型是其子类 WyxCalServletWyxHttpServlet wyxHttpServlet =WyxTomcatV3.servletMapping.get(servletName);//3. 调用service , 通过OOP的动态绑定机制,调用运行类型的 doGet/doPostif (wyxHttpServlet != null) {//得到wyxHttpServlet.service(wyxRequest, wyxResponse);} else {//没有这个servlet , 返回404的提示信息String resp = wyxResponse.respHeader + "<h1>404 Not Found</h1>";OutputStream outputStream = wyxResponse.getOutputStream();outputStream.write(resp.getBytes());outputStream.flush();outputStream.close();}socket.close();} catch (IOException e) {e.printStackTrace();} finally {//最后一定确保socket要关闭if (socket != null) {try {socket.close();} catch (IOException e) {e.printStackTrace();}}}}
}
wyxResponse
public class wyxResponse {private OutputStream outputStream = null;//写一个http的响应头 => 先死后活public static final String respHeader = "HTTP/1.1 200 OK\\r\\n" +"Content-Type: text/html;charset=utf-8\\r\\n\\r\\n";//在创建 wyxResponse 对象时,传入的outputStream是和Socket关联的public wyxResponse(OutputStream outputStream) {this.outputStream = outputStream;}//当我们需要给浏览器返回数据时,可以通过HspResponse 的输出流完成//public OutputStream getOutputStream() {return outputStream;}
}
wyxRequest
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.HashMap;/* 1. wyxRequest 作用是封装http请求的数据* get /WyxCalServlet?num1=10&num2=30* 2. 比如 method(get) 、 uri(/hspCalServlet) 、 还有参数列表 (num1=10&num2=30)* 3. wyxRequest 作用就等价原生的servlet 中的HttpServletRequest* 4. 一会走代码* 5. 这里考虑的是GET请求*/
public class wyxRequest {private String method;private String uri;//存放参数列表 参数名-参数值 => HashMapprivate HashMap<String, String> parametersMapping =new HashMap<>();private InputStream inputStream = null;//构造器=> 对http请求进行封装 => 可以写的代码封装成方法//inputStream 是和 对应http请求的socket关联public wyxRequest(InputStream inputStream) {this.inputStream = inputStream;//完成对http请求数据的封装..encapHttpRequest();}/* 将http请求的相关数据,进行封装,然后提供相关的方法,进行获取*/private void encapHttpRequest() {System.out.println("wyxRequest init()");try {//inputstream -> BufferedReaderBufferedReader bufferedReader =new BufferedReader(new InputStreamReader(inputStream, "utf-8"));//读取第一行/* GET /hspCalServlet?num1=10&num2=30 HTTP/1.1* Host: localhost:8080* User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:97.0) Gecko/20100101 Fi*/String requestLine = bufferedReader.readLine();//GET - /WyxCalServlet?num1=10&num2=30 - HTTP/1.1String[] requestLineArr = requestLine.split(" ");//得到methodmethod = requestLineArr[0];//解析得到 /WyxCalServlet//1. 先看看uri 有没有参数列表int index = requestLineArr[1].indexOf("?");if (index == -1) { //说明没有参数列表uri = requestLineArr[1];} else {//[0,index)uri = requestLineArr[1].substring(0, index);//获取参数列表->parametersMapping//parameters => num1=10&num2=30String parameters = requestLineArr[1].substring(index + 1);//num1=10 , num2=30 .... parametersPair= ["num1=10","num2=30" ]String[] parametersPair = parameters.split("&");//防止用户提交时 /hspCalServlet?if (null != parametersPair && !"".equals(parametersPair)) {//再次分割 parameterPair = num1=10for (String parameterPair : parametersPair) {//parameterVal ["num1", "10"]String[] parameterVal = parameterPair.split("=");if (parameterVal.length == 2) {//放入到 parametersMappingparametersMapping.put(parameterVal[0], parameterVal[1]);}}}}//这里不能关闭流 inputStream 和 socket关联//inputStream.close();} catch (Exception e) {e.printStackTrace();}}//request对象有一个特别重要方法public String getParameter(String name) {if (parametersMapping.containsKey(name)) {return parametersMapping.get(name);} else {return "";}}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;}@Overridepublic String toString() {return "wyxRequest{" +"method='" + method + '\\'' +", uri='" + uri + '\\'' +", parametersMapping=" + parametersMapping +'}';}
}
wyxServlet接口
public interface wyxServlet {void init() throws Exception;void service(wyxRequest request, wyxResponse response) throws IOException;void destroy();
}
wyxHttpServlet
public abstract class wyxHttpServlet implements wyxServlet {@Overridepublic void service(wyxRequest request, wyxResponse response) throws IOException {//说明 equalsIgnoreCase 比较字符串内容是相同,不区别大小写if("GET".equalsIgnoreCase(request.getMethod())) {//这里会有动态绑定this.doGet(request,response);} else if("POST".equalsIgnoreCase(request.getMethod())) {this.doPost(request,response);}}//这里我们使用的了模板设计模式 => java 基础的 抽象类专门讲过模板设计模式//让HspHttpServlet 子类 wyxCalServlet 实现public abstract void doGet(wyxRequest request, wyxResponse response);public abstract void doPost(wyxRequest request, wyxResponse response);
}
wyxCalServlet
public class wyxCalServlet extends wyxHttpServlet {@Overridepublic void doGet(wyxRequest request, wyxResponse response) {//java基础的 OOP 的动态绑定机制..//写业务代码,完成计算任务int num1 = WebUtils.parseInt(request.getParameter("num1"), 0);int num2 = WebUtils.parseInt(request.getParameter("num2"), 0);int sum = num1 + num2;//返回计算结果给浏览器//outputStream 和 当前的socket关联OutputStream outputStream = response.getOutputStream();String respMes = wyxResponse.respHeader+ "<h1>" + num1 + " + " + num2 + " = " + sum + " wyxTomcatV3 - 反射+xml创建</h1>";try {outputStream.write(respMes.getBytes());outputStream.flush();outputStream.close();} catch (IOException e) {e.printStackTrace();}}@Overridepublic void doPost(wyxRequest request, wyxResponse response) {this.doGet(request, response);}@Overridepublic void init() throws Exception {}@Overridepublic void destroy() {}
}
WebUtils
public class WebUtils {//将字符串转成数字方法public static int parseInt(String strNum, int defaultVal) {try {return Integer.parseInt(strNum);} catch (NumberFormatException e) {System.out.println(strNum + " 不能转成数字");}return defaultVal;}//判断uri是不是html文件public static boolean isHtml(String uri) {return uri.endsWith(".html");}//根据文件名来读取该文件->Stringpublic static String readHtml(String filename) {String path = com.wyxdu.utils.WebUtils.class.getResource("/").getPath();StringBuilder stringBuilder = new StringBuilder();try {BufferedReader bufferedReader = new BufferedReader(new FileReader(path + filename));String buf = "";while ((buf = bufferedReader.readLine()) != null) {stringBuilder.append(buf);}} catch (Exception e) {e.printStackTrace();}return stringBuilder.toString();}
}
wyxTomcatV3
package com.wyxdu.tomcat;import com.wyxdu.tomcat.handler.wyxRequestHandler;
import com.wyxdu.tomcat.servlet.wyxHttpServlet;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;import javax.servlet.Filter;
import javax.servlet.http.HttpSession;
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;/* 第3版的Tomcat, 实现通过xml+反射来初始化容器*/
public class wyxTomcatV3 {//1. 存放容器 servletMapping// -ConcurrentHashMap// -HashMap// key - value// ServletName 对应的实例public static final ConcurrentHashMap<String, wyxHttpServlet>servletMapping = new ConcurrentHashMap<>();//2容器 servletUrlMapping// -ConcurrentHashMap// -HashMap// key - value// url-pattern ServletNamepublic static final ConcurrentHashMap<String, String>servletUrlMapping = new ConcurrentHashMap<>();//你可以这里理解session, tomcat还维护一个容器public static final ConcurrentHashMap<String, HttpSession>sessionMapping = new ConcurrentHashMap<>();//你可以这里理解filter, tomcat还维护了filter的容器public static final ConcurrentHashMap<String, String>filterUrlMapping = new ConcurrentHashMap<>();public static final ConcurrentHashMap<String, Filter>filterMapping = new ConcurrentHashMap<>();public static void main(String[] args) {wyxTomcatV3 wyxTomcatV3 = new wyxTomcatV3();wyxTomcatV3.init();//启动wyxtomcat容器wyxTomcatV3.run();}//启动WyxTomcatV3容器public void run() {try {ServerSocket serverSocket = new ServerSocket(8080);System.out.println("=====wyxtomcatv3在8080监听======");while (!serverSocket.isClosed()) {Socket socket = serverSocket.accept();wyxRequestHandler wyxRequestHandler =new wyxRequestHandler(socket);new Thread(wyxRequestHandler).start();}} catch (IOException e) {e.printStackTrace();}}//直接对两个容器进行初始化public void init() {//读取web.xml => dom4j =>//得到web.xml文件的路径 => 拷贝一份.String path = wyxTomcatV3.class.getResource("/").getPath();//System.out.println("path= " + path);//使用dom4j技术完成读取SAXReader saxReader = new SAXReader();try {Document document = saxReader.read(new File(path + "web.xml"));System.out.println("document= " + document);//得到根元素Element rootElement = document.getRootElement();//得到根元素下面的所有元素List<Element> elements = rootElement.elements();//遍历并过滤到 servlet servlet-mappingfor (Element element : elements) {if ("servlet".equalsIgnoreCase(element.getName())) {//这是一个servlet配置//System.out.println("发现 servlet");//使用反射将该servlet实例放入到servletMappingElement servletName = element.element("servlet-name");Element servletClass = element.element("servlet-class");servletMapping.put(servletName.getText(),(wyxHttpServlet) Class.forName(servletClass.getText().trim()).newInstance());} else if ("servlet-mapping".equalsIgnoreCase(element.getName())) {//这是一个servlet-mapping//System.out.println("发现 servlet-mapping");Element servletName = element.element("servlet-name");Element urlPatter = element.element("url-pattern");servletUrlMapping.put(urlPatter.getText(), servletName.getText());}}} catch (Exception e) {e.printStackTrace();}//验证,这两个容器是否初始化成功System.out.println("servletMapping= " + servletMapping);System.out.println("servletUrlMapping= " + servletUrlMapping);}
}