【Tomcat】第八篇:150代码手写Tomcat(基于BIO,超详细注释)

既然要手写tomcat,那么从哪里入手呢?我们都知道tomcat是web容器,所以如果理解了tomcat的作用,我们应该就知道如何设计它了:

  1. tomcat要负责接受http请求,所以需要一个 MYRequest
  2. tomcat要负责返回响应,所以需要一个 MYResponse
  3. tomcat要负责实例化Servlet,所以需要一个 MYServlet 规范

那这三者如何统一起来呢,或者说谁来处理映射关系呢?配置文件 + MYTomcat

1.MYRequest

Request 本质就是 InputStream,主要工作下:

  1. 读取 InputStream 的内容,并保存
  2. 对读取的结果进行解码,提取关键信息(请求方式,url)

所以,它会对外提供 getMethod 方法与 getUrl 方法

public class MYRequest {
    
    

    private String method; // 请求方式(get、post、delete、put)
    private String url; 

    public MYRequest(InputStream in) {
    
    
        try {
    
    
            // 1.content用来保存InputStream中的http请求信息
            String content = "";
            byte[] buff = new byte[1024];
            int len = 0;
            if ((len = in.read(buff)) > 0) {
    
    
                content = new String(buff, 0, len);
            }

            // 2.对http请求信息进行处理,得到Method与Url
            String line = content.split("\\n")[0];
            String[] arr = line.split("\\s");
            this.method = arr[0];
            this.url = arr[1].split("\\?")[0];

        } catch (IOException e) {
    
    
            e.printStackTrace();
        }
    }

    public String getUrl() {
    
    
        return this.url;
    }

    public String getMethod() {
    
    
        return this.method;
    }
}

2.MYResponse

Response本质就是OutputStream,主要工作是:

  1. 将sevlet处理结果编码成http协议格式
  2. 通过 OutputStream 写出

所以,它只需要对外提供 write 方法即可。

public class MYResponse {
    
    

    private OutputStream out;

    public MYResponse(OutputStream out) {
    
    
        this.out = out;
    }

    /**
     * 向浏览器写出内容
     */
    public void write(String s) throws IOException {
    
    
        StringBuilder sb = new StringBuilder();
        // 因为写出的内容要被http协议解析,所以要符合http协议规范,有其要求的响应头(主要是状态码和响应格式)
        sb.append("HTTP/1.1 200 OK\n") // 状态码
                .append("Content-Type: text/html;\n") // 响应格式
                .append("\r\n")
                .append(s);
        out.write(sb.toString().getBytes()); // 通过IO流写出
    }
}

3.MYServlet

MYServlet是一个抽象类,它的主要工作是:

  1. 提供Servlet规范,即每个处理业务逻辑的Servlet都要继承它,重写doPost和doGet这俩模板方法
  2. 完成请求方式与对应方法的映射,对外提供统一方法 service。这里其实采用了模板方法模式。
public abstract class MYServlet{
    
    

    // 注:这里的request与response都是Tomcat对象创建好然后传进来的
    public void service(MYRequest request, MYResponse response) throws IOException {
    
    
        if ("GET".equalsIgnoreCase(request.getMethod())) {
    
    
            doGet(request, response);
        } else{
    
    
            doPost(request, response);
        }
    }

    // 这里是模板方法模式,交给子类去具体实现
    protected abstract void doPost(MYRequest request, MYResponse response) throws IOException;
    protected abstract void doGet(MYRequest request, MYResponse response) throws IOException;
}

4.MYTomcat(核心)

MYTomcat主要做了件事:

  1. 初始化 tomcat:
    1. 加载web.properties文件,在这里其实相当于Tocmat中的web.xml
    2. 寻找url与servlet的映射关系,即对配置文件进行解析
    3. 将url与Servlet实例保存在Map中,到时可直接根据url获取到处理业务的servlet(单例模式)
  2. 启动 tomcat:
    1. 调用init,目的是得到servletMapping的映射关系
    2. 通过BIO创建socket的服务端,在指定端口开始监听
    3. 用一个死循环持续等待并处理用户请求,处理用户请求的具体逻辑是:
      1. 创建IO流,并包装成Request与Response
      2. 获取请求URL,寻找相应Servlet进行处理。如果能找到就调用 servlet 的 service 方法进行处理;找不到就写出404。
      3. 等处理完之后关闭本次连接的相关资源
public class MYTomcat {
    
    

    private int port = 8080;
    private ServerSocket server;
    // 用来保存路径与Servlet的映射关系(servlet单例模式)
    private Map<String, MYServlet> servletMapping = new HashMap<>();
    // web.properties,相当于tomcat的web.xml
    private Properties webxml = new Properties();
	
	/**
	*初始化tocmat
	*/
    private void init(){
    
    

        try{
    
    
        	// 1.加载web.properties文件
            String WEB_INF = this.getClass().getResource("/").getPath();
            FileInputStream fis = new FileInputStream(WEB_INF + "web.properties");
            webxml.load(fis);
			
			// 2.遍历配置文件,寻找url与servlet映射关系配置
            for (Object k : webxml.keySet()) {
    
    

                String key = k.toString();
                // 以url结尾的key就是要映射的路径,下面是两条配置示例:
    			// servlet.one.url=/firstServlet.do
				// servlet.one.className=com.xupt.yzh.tomcat.servlet.FirstServlet
                if(key.endsWith(".url")){
    
    
                	// 去掉.url就是servlet的name(servlet.one)
                    String servletName = key.replaceAll("\\.url$", "");
                    
					// 2.1 获取到url(/first.do)          
                    String url = webxml.getProperty(key);
					
                    // 2.2 获取对应servlet全类名(com.xupt.yzh.tomcat...FirstServlet),并通过反进行实例化
                    String className = webxml.getProperty(servletName + ".className");
                    // 注:这里是将所有Servlet都强转为MyServlet,所以一定要继承MyServlet
                    MYServlet obj = (MYServlet)Class.forName(className).newInstance();
					
					// 3.将url与servlet实例保存到servletMapping中(单例模式)
                    servletMapping.put(url, obj);
                }
            }
        }catch(Exception e){
    
    
            e.printStackTrace();
        }
    }

    /**
     * 启动tomcat
     */
    public void start() {
    
    
    	// 1.调用init,目的是得到servletMapping的映射关系
        init();

        try {
    
    
        	// 2.通过BIO创建socket的服务端,在指定端口开始监听
            server = new ServerSocket(this.port);
            System.out.println("MYTomcat已启动,监听的端口是" + this.port);

            // 3.用一个死循环持续等待并处理用户请求
            while (true) {
    
    
                Socket client = server.accept();
                // process是具体处理请求的逻辑,参数是当前连接的Socket
                process(client);
            }
        } catch (IOException e) {
    
    
            e.printStackTrace();
        }

    }

    /**
     * 具体处理请求
     * 1.创建IO流,并包装成Request与Response
     * 2.获取请求Url,取出对应Servlet进行处理
     * 
     */
    private void process(Socket client) throws IOException {
    
    
        // 1.获取IO流,并封装成Request与Response
        InputStream is = client.getInputStream();
        OutputStream os = client.getOutputStream();
        // 注:这里要明白,每次请求的Request和Response都是不同的(因为连接时的socket不同),他俩的作用域仅为当前会话
        MYRequest request = new MYRequest(is);
        MYResponse response = new MYResponse(os);

        // 获取请求URL,寻找相应Servlet进行处理
        String url = request.getUrl();
		// 2.判断改url是否有对应的Servlet实例
        if (servletMapping.containsKey(url)) {
    
    
            // 如果有,调用service方法进行处理
            servletMapping.get(url).service(request, response);
        } else {
    
    
        	// 如果没有,写出404
            response.write("404 - Not Found");
        }
		
		// 3.关闭本次连接相关资源
        os.flush();
        os.close();
        is.close();
        client.close();
    }
	
	/**
	* 启动入口
	*/
    public static void main(String[] args) {
    
    
        new MYTomcat().start();
    }
}

好了,到这里手写的所有代码都结束了,下面就测试一下吧。

成果演示

首先,我们写了一个FirstServlet,他继承了 MYServlet 并且重写了 doGet 与 doPost

public class FirstServlet extends MYServlet {
    
    
    @Override
    protected void doPost(MYRequest request, MYResponse response) throws IOException {
    
    
        response.write("this is FirstServlet!");
    }

    @Override
    protected void doGet(MYRequest request, MYResponse response) throws IOException {
    
    
        doPost(request, response);
    }
}

然后就是配置文件 web.properties 了,配置的具体内容如下,其实就是配置url与全类名的映射关系

servlet.one.url=/firstServlet.do
servlet.one.className=com.xupt.yzh.tomcat.servlet.FirstServlet

servlet.two.url=/secondServlet.do
servlet.two.className=com.xupt.yzh.tomcat.servlet.SecondServlet

项目的整体结构截图如下:
在这里插入图片描述


好了,到了最激动人心的时刻了,我们在浏览器上通过8080端口进行访问。

  1. 情况一:访问一个没有相应servlet的url,返回404
    在这里插入图片描述
  2. 情况二:访问FirstServlet的url,返回正确内容
    在这里插入图片描述

到这里这篇文件就结束了,BIO 是阻塞式IO,一次只能处理一个客户端的线程,显然性能是跟不上的。后面作者还会发出基于netty的手写tomcat。

猜你喜欢

转载自blog.csdn.net/weixin_43935927/article/details/108743213