如何手写Tomcat框架
介绍
Tomcat,这只3脚猫,大学的时候就认识了,直到现在工作中,
也常会和它打交道。
这是一只神奇的猫,今天让我来抽象你,实现你!
软件架构说明
Tomcat是非常流行的Web Server,它还是一个满足Servlet规范的容器。
那么想一想,Tomcat和我们的Web应用是什么关系?
从感性上来说,我们一般需要把Web应用打成WAR包部署到Tomcat中,
在我们的Web应用中,我们要指明URL被哪个类的哪个方法所处理(不论是
原始的Servlet开发,还是现在流行的Spring MVC都必须指明)。
由于我们的Web应用是运行在Tomcat中,那么显然,请求必定是先到达Tomcat的。
Tomcat对于请求实际上会进行下面的处理:
第一:提供Socket服务
Tomcat的启动,必然是Socket服务,只不过它支持HTTP协议而已!
这里其实可以扩展思考下,Tomcat既然是基于Socket,
那么是基于BIO or NIO or AIO呢?
第二:进行请求的分发
要知道一个Tomcat可以为多个Web应用提供服务,那么很显然,
Tomcat可以把URL下发到不同的Web应用。
第三:需要把请求和响应封装成request/response
我们在Web应用这一层,可从来没有封装过request/response的,
我们都是直接使用的,这就是因为Tomcat已经为你做好了!
代码说明
https://gitee.com/ddfeiyu/framework-tomcat
工程截图
封装请求对象MyRequest
package dto;import java.io.IOException;import java.io.InputStream;/*** 封装请求对象** 这里,你可以清楚的看到,我们通过输入流,对HTTP协议进行解析,拿到了HTTP请求头的方法以及URL。*/public class MyRequest {private String url;private String method;public String getUrl() {return url;}public void setUrl(String url) {this.url = url;}public String getMethod() {return method;}public void setMethod(String method) {this.method = method;}public MyRequest(InputStream inputStream) throws IOException{String httpRequest = "";byte[] httpRequestBytes = new byte[1024];int len = 0 ;if ((len = inputStream.read(httpRequestBytes)) > 0){httpRequest = new String(httpRequestBytes, 0 ,len);}/*** 一个典型的http协议**GET /home/xman/data/tipspluslist?indextype=manht&_req_seqid=0x82203fe1000c4250&asyn=1&t=1624689601933&sid=34099_31660_34133_34072_33607_34106_34135_26350 HTTP/1.1Host: www.baidu.comConnection: keep-alivesec-ch-ua: " Not;A Brand";v="99", "Google Chrome";v="91", "Chromium";v="91"Accept: text/plain; q=0.01X-Requested-With: XMLHttpRequestsec-ch-ua-mobile: ?0User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36Sec-Fetch-Site: same-originSec-Fetch-Mode: corsSec-Fetch-Dest: emptyReferer: https://www.baidu.com/Accept-Encoding: gzip, deflate, brAccept-Language: zh-CN,zh;q=0.9Cookie: BIDUPSID=3D7**/// GET /home/xman/data/tipspluslist?indextype=manht&_req_seqid=0x82203fe1000c4250&asyn=1&t=1624689601933&sid=34099_31660_34133_34072_33607_34106_34135_26350 HTTP/1.1String httpHead = httpRequest.split("\n")[0];// method : GET// url : /home/xman/data/tipspluslist?indextype=manht&_req_seqid=0x82203fe1000c4250&asyn=1&t=1624689601933&sid=34099_31660_34133_34072_33607_34106_34135_26350url = httpHead.split("\\s")[1];method = httpHead.split("\\s")[0];System.out.println(this);}public String toString() {return "MyRequest{" +"url='" + url + '\'' +", method='" + method + '\'' +'}';}public static void main(String[] args) {String httpRequest = "GET /home/xman/data/tipspluslist?indextype=manht&_req_seqid=0x82203fe1000c4250&asyn=1&t=1624689601933&sid=34099_31660_34133_34072_33607_34106_34135_26350 HTTP/1.1\n" +" Host: www.baidu.com\n" +" Connection: keep-alive\n" +" sec-ch-ua: \" Not;A Brand\";v=\"99\", \"Google Chrome\";v=\"91\", \"Chromium\";v=\"91\"\n" +" Accept: text/plain; q=0.01\n" +" X-Requested-With: XMLHttpRequest\n" +" sec-ch-ua-mobile: ?0\n" +" User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36\n" +" Sec-Fetch-Site: same-origin\n" +" Sec-Fetch-Mode: cors\n" +" Sec-Fetch-Dest: empty\n" +" Referer: https://www.baidu.com/\n" +" Accept-Encoding: gzip, deflate, br\n" +" Accept-Language: zh-CN,zh;q=0.9\n" +" Cookie: BIDUPSID=3D7";// GET /home/xman/data/tipspluslist?indextype=manht&_req_seqid=0x82203fe1000c4250&asyn=1&t=1624689601933&sid=34099_31660_34133_34072_33607_34106_34135_26350 HTTP/1.1String httpHead = httpRequest.split("\n")[0];// 补充一个小的知识点: \s 可以匹配空格/*** 详解 "\\s+"正则表达式中\s匹配任何空白字符,包括空格、制表符、换页符等等, 等价于[ \f\n\r\t\v]\f -> 匹配一个换页\n -> 匹配一个换行符\r -> 匹配一个回车符\t -> 匹配一个制表符\v -> 匹配一个垂直制表符而“\s+”则表示匹配任意多个上面的字符。另因为反斜杠在Java里是转义字符,所以在Java里,我们要这么用“\\s+”.*/String url = httpHead.split("\\s")[1];String method = httpHead.split("\\s")[0];System.out.println("method : "+ method);System.out.println("url : "+ url);}}
封装响应对象MyResponse
基于HTTP协议的格式进行输出写入。
package dto;import java.io.IOException;import java.io.OutputStream;/*** 封装响应对象** 基于HTTP协议的格式进行输出写入。*/public class MyResponse {private OutputStream outputStream;public MyResponse(OutputStream outputStream){this.outputStream = outputStream;}public void write(String context) throws IOException {// 一个典型的http response header/*** HTTP/1.1 200 OKCache-Control: privateConnection: keep-aliveContent-Encoding: gzipContent-Type: text/html;charset=utf-8Date: Sat, 26 Jun 2021 06:40:01 GMTExpires: Sat, 26 Jun 2021 06:40:01 GMTServer: BWS/1.0Vary: Accept-EncodingContent-Length: 78*/// http response body/***{"errNo":"0","data": { "redDot": [ ], "newWord": [ ], "layer": [ ] }}*/StringBuilder httpResponse = new StringBuilder();httpResponse.append("HTTP/1.1 200 OK").append("\n").append("Content-Type: text/html;charset=utf-8").append("\n").append("\r\n").append("<html><body>").append(context).append("</html></body>");outputStream.write(httpResponse.toString().getBytes());outputStream.close();}}
封装MyServlet
Tomcat是满足Servlet规范的容器,那么自然Tomcat需要提供API。
这里你看到了Servlet常见的doGet/doPost/service方法。
package servlet;import dto.MyRequest;import dto.MyResponse;/**** Tomcat是满足Servlet规范的容器,那么自然Tomcat需要提供API。这里你看到了Servlet常见的doGet/doPost/service方法。*/public abstract class MyServlet {public abstract void doGet(MyRequest request, MyResponse response);public abstract void doPost(MyRequest request, MyResponse response);public void service(MyRequest request, MyResponse response){String method = request.getMethod().toUpperCase();if ("POST".equals(method)){doPost(request, response);}else if ("GET".equals(method)){doGet(request, response);}}}
自定义的2个servlet,用于测试
package servlet.impl;import dto.MyRequest;import dto.MyResponse;import servlet.MyServlet;import java.io.IOException;/*** 提供这2个具体的Servlet实现,只是为了后续的测试!**/public class HelloWorldServlet extends MyServlet {public void doGet(MyRequest request, MyResponse response) {try {response.write("get the world");} catch (IOException e) {e.printStackTrace();}}public void doPost(MyRequest request, MyResponse response) {try {response.write("post the world");} catch (IOException e) {e.printStackTrace();}}}
package servlet.impl;import dto.MyRequest;import dto.MyResponse;import servlet.MyServlet;import java.io.IOException;/*** 提供这2个具体的Servlet实现,只是为了后续的测试!**/public class StopTheWorldServlet extends MyServlet {public void doGet(MyRequest request, MyResponse response) {try {response.write("stop get the world");} catch (IOException e) {e.printStackTrace();}}public void doPost(MyRequest request, MyResponse response) {try {response.write("stop post the world");} catch (IOException e) {e.printStackTrace();}}}
定义ServletMappingConfig
我们在servlet开发中,不管是古老的servlet还是struts还是流行的
springmvc等等会在web.xml中或者注解的方式来指定哪个URL交给
哪个servlet进行处理。
package mapping;import java.util.ArrayList;import java.util.List;/*** 我们在servlet开发中,会在web.xml中通过和来进行指定哪个URL交给哪个servlet进行处理。*/public class ServletMappingConfig {public static List<ServletMapping> servletMappingList = new ArrayList<>();static {servletMappingList.add(new ServletMapping("HelloWorldServlet","/HelloWorld","servlet.impl.HelloWorldServlet"));servletMappingList.add(new ServletMapping("StopTheWorldServlet","/StopTheWorld","servlet.impl.StopTheWorldServlet"));}}
package mapping;public class ServletMapping {private String servletName;private String url;private String clazz;public ServletMapping(String servletName, String url, String clazz) {this.servletName = servletName;this.url = url;this.clazz = clazz;}public String getServletName() {return servletName;}public void setServletName(String servletName) {this.servletName = servletName;}public String getUrl() {return url;}public void setUrl(String url) {this.url = url;}public String getClazz() {return clazz;}public void setClazz(String clazz) {this.clazz = clazz;}}
定义MyTomcat
启动tomcat
重点关注 ServerSocket 、Socket
package bootstrap;import dto.MyRequest;import dto.MyResponse;import mapping.ServletMapping;import mapping.ServletMappingConfig;import servlet.MyServlet;import java.io.IOException;import java.io.InputStream;import java.io.OutputStream;import java.net.ServerSocket;import java.net.Socket;import java.util.HashMap;import java.util.List;import java.util.Map;/*** Tomcat的处理流程:把URL对应处理的Servlet关系形成,解析HTTP协议,封装请求/响应对象,利用反射实例化具体的Servlet进行处理即可。**/public class MyTomcat {private int port;private Map<String /*url*/, String /*clazz*/> urlServletMap = new HashMap<>();public MyTomcat(int port) {this.port = port;}public void start(){// 初始化url和servlet的mapping关系initServletMapping();ServerSocket serverSocket = null;try {serverSocket = new ServerSocket(port);System.out.println("MyTomcat is start...");while (true){Socket socket = serverSocket.accept();InputStream inputStream = socket.getInputStream();OutputStream outputStream = socket.getOutputStream();// 构造request responseMyRequest myRequest = new MyRequest(inputStream);MyResponse myResponse = new MyResponse(outputStream);// 请求分发doDispatch(myRequest, myResponse);socket.close();System.out.println("socket.close()");}}catch (IOException e){e.printStackTrace();}finally {if (serverSocket != null){try {serverSocket.close();System.out.println("serverSocket.close()");} catch (IOException e) {e.printStackTrace();}}}}private void initServletMapping(){List<ServletMapping> servletMappingList = ServletMappingConfig.servletMappingList;for (ServletMapping mapping:servletMappingList) {urlServletMap.put(mapping.getUrl(), mapping.getClazz());}}
请求分发doDispatch
private void doDispatch(MyRequest request, MyResponse response) throws IOException{System.out.println("-------doDispatch: request: "+request);String url = request.getUrl();String clazz = urlServletMap.get(url);if (url == null || url.length() == 0 || "/favicon.ico".equals(url)){response.write("404 : url is not exist");return;}if (clazz == null || clazz.length() == 0){response.write("404 : clazz is not exist");return;}// 反射try {Class<MyServlet> myServletClazz = (Class<MyServlet>)Class.forName(clazz);MyServlet myServlet = myServletClazz.newInstance();myServlet.service(request, response);} catch (InstantiationException e) {e.printStackTrace();} catch (IllegalAccessException e) {e.printStackTrace();} catch (ClassNotFoundException e) {e.printStackTrace();}}
测试TestMyTomcat
import bootstrap.MyTomcat;public class TestMyTomcat {public static void main(String[] args) {MyTomcat myTomcat = new MyTomcat(8081);myTomcat.start();}}
测试:
http://localhost:8081/
http://localhost:8081/HelloWorld
http://localhost:8081/StopTheWorld
安装教程
1. 测试类:TestMyTomcat
2. 打印日志:
Connected to the target VM, address: '127.0.0.1:2748', transport: 'socket'
MyTomcat is start...
MyRequest{url='/HelloWorld', method='GET'}
-------doDispatch: request: MyRequest{url='/HelloWorld', method='GET'}
socket.close()
MyRequest{url='/favicon.ico', method='GET'}
-------doDispatch: request: MyRequest{url='/favicon.ico', method='GET'}
socket.close()
MyRequest{url='/', method='GET'}
-------doDispatch: request: MyRequest{url='/', method='GET'}
socket.close()
MyRequest{url='/favicon.ico', method='GET'}
-------doDispatch: request: MyRequest{url='/favicon.ico', method='GET'}
socket.close()
MyRequest{url='/HelloWorld2', method='GET'}
-------doDispatch: request: MyRequest{url='/HelloWorld2', method='GET'}
socket.close()
MyRequest{url='/favicon.ico', method='GET'}
-------doDispatch: request: MyRequest{url='/favicon.ico', method='GET'}
socket.close()
MyRequest{url='/HelloWorld2', method='GET'}
-------doDispatch: request: MyRequest{url='/HelloWorld2', method='GET'}
socket.close()
MyRequest{url='/favicon.ico', method='GET'}
-------doDispatch: request: MyRequest{url='/favicon.ico', method='GET'}
socket.close()
MyRequest{url='/HelloWorld', method='GET'}
-------doDispatch: request: MyRequest{url='/HelloWorld', method='GET'}
socket.close()
MyRequest{url='/favicon.ico', method='GET'}
-------doDispatch: request: MyRequest{url='/favicon.ico', method='GET'}
socket.close()
MyRequest{url='/HelloWorld1', method='GET'}
-------doDispatch: request: MyRequest{url='/HelloWorld1', method='GET'}
socket.close()
MyRequest{url='/favicon.ico', method='GET'}
-------doDispatch: request: MyRequest{url='/favicon.ico', method='GET'}
socket.close()
MyRequest{url='/', method='GET'}
-------doDispatch: request: MyRequest{url='/', method='GET'}
socket.close()
MyRequest{url='/favicon.ico', method='GET'}
-------doDispatch: request: MyRequest{url='/favicon.ico', method='GET'}
socket.close()
MyRequest{url='/', method='GET'}
-------doDispatch: request: MyRequest{url='/', method='GET'}
socket.close()
MyRequest{url='/favicon.ico', method='GET'}
-------doDispatch: request: MyRequest{url='/favicon.ico', method='GET'}
socket.close()
MyRequest{url='/', method='GET'}
-------doDispatch: request: MyRequest{url='/', method='GET'}
socket.close()
MyRequest{url='/favicon.ico', method='GET'}
-------doDispatch: request: MyRequest{url='/favicon.ico', method='GET'}
socket.close()
MyRequest{url='/HelloWorld', method='GET'}
-------doDispatch: request: MyRequest{url='/HelloWorld', method='GET'}
socket.close()
MyRequest{url='/favicon.ico', method='GET'}
-------doDispatch: request: MyRequest{url='/favicon.ico', method='GET'}
socket.close()
MyRequest{url='/StopTheWorld', method='GET'}
-------doDispatch: request: MyRequest{url='/StopTheWorld', method='GET'}
socket.close()
MyRequest{url='/favicon.ico', method='GET'}
-------doDispatch: request: MyRequest{url='/favicon.ico', method='GET'}
socket.close()
MyRequest{url='/StopTheWorld', method='GET'}
-------doDispatch: request: MyRequest{url='/StopTheWorld', method='GET'}
socket.close()
MyRequest{url='/favicon.ico', method='GET'}
-------doDispatch: request: MyRequest{url='/favicon.ico', method='GET'}
socket.close()
MyRequest{url='/StopTheWorld', method='GET'}
-------doDispatch: request: MyRequest{url='/StopTheWorld', method='GET'}
socket.close()
MyRequest{url='/favicon.ico', method='GET'}
-------doDispatch: request: MyRequest{url='/favicon.ico', method='GET'}
socket.close()
MyRequest{url='/StopTheWorld', method='GET'}
-------doDispatch: request: MyRequest{url='/StopTheWorld', method='GET'}
socket.close()
MyRequest{url='/favicon.ico', method='GET'}
-------doDispatch: request: MyRequest{url='/favicon.ico', method='GET'}
socket.close()
MyRequest{url='/StopTheWorld2', method='GET'}
-------doDispatch: request: MyRequest{url='/StopTheWorld2', method='GET'}
socket.close()
MyRequest{url='/favicon.ico', method='GET'}
-------doDispatch: request: MyRequest{url='/favicon.ico', method='GET'}
socket.close()
MyRequest{url='/StopTheWorld', method='GET'}
-------doDispatch: request: MyRequest{url='/StopTheWorld', method='GET'}
socket.close()
MyRequest{url='/favicon.ico', method='GET'}
-------doDispatch: request: MyRequest{url='/favicon.ico', method='GET'}
socket.close()
MyRequest{url='/StopTheWorld', method='GET'}
-------doDispatch: request: MyRequest{url='/StopTheWorld', method='GET'}
socket.close()
MyRequest{url='/favicon.ico', method='GET'}
-------doDispatch: request: MyRequest{url='/favicon.ico', method='GET'}
socket.close()3. 运行截图:
