如何手写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);
}
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 {
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 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. 运行截图: