vlambda博客
学习文章列表

如何手写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.1 Host: www.baidu.com Connection: keep-alive sec-ch-ua: " Not;A Brand";v="99", "Google Chrome";v="91", "Chromium";v="91" Accept: text/plain; q=0.01 X-Requested-With: XMLHttpRequest sec-ch-ua-mobile: ?0 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 Sec-Fetch-Site: same-origin Sec-Fetch-Mode: cors Sec-Fetch-Dest: empty Referer: https://www.baidu.com/ Accept-Encoding: gzip, deflate, br Accept-Language: zh-CN,zh;q=0.9 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.1 String 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_26350 url = httpHead.split("\\s")[1]; method = httpHead.split("\\s")[0]; System.out.println(this); }
@Override 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.1 String 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 OK Cache-Control: private Connection: keep-alive Content-Encoding: gzip Content-Type: text/html;charset=utf-8 Date: Sat, 26 Jun 2021 06:40:01 GMT Expires: Sat, 26 Jun 2021 06:40:01 GMT Server: BWS/1.0 Vary: Accept-Encoding Content-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 {

@Override public void doGet(MyRequest request, MyResponse response) { try { response.write("get the world"); } catch (IOException e) { e.printStackTrace(); } }


@Override 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 {


@Override public void doGet(MyRequest request, MyResponse response) { try { response.write("stop get the world"); } catch (IOException e) { e.printStackTrace(); } }


@Override 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 response MyRequest 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. 运行截图:



如何手写Tomcat框架