基于传统I/O手写Tomcat

2022-02-11 09:28:29 浏览数 (1)

本文节选自《Netty 4核心原理》

我们知道,Tomcat是基于J2EE规范的Web容器,主要入口是web.xml文件。web.xml文件中主要配置Servlet、Filter、Listener等,而Servlet、Filter、Listener在J2EE中只是抽象的实现,具体业务逻辑由开发者来实现。本章内容,就以最常用的Servlet为例来详细展开。

1 环境准备

1.1 定义GPServlet抽象类

首先,我们创建GPServlet类。我们都知道GPServlet生命周期中最常用的方法是doGet()方法和doPost()方法,而doGet()方法和doPost()方法是service()方法的分支实现,看下面的简易版Servlet源码实现。

代码语言:javascript复制

package com.tom.tomcat.http;

public abstract class GPServlet {

    public void service(GPRequest request,GPResponse response) throws Exception{

        //由service()方法决定是调用doGet()还是调用doPost()
        if("GET".equalsIgnoreCase(request.getMethod())){
            doGet(request, response);
        }else{
            doPost(request, response);
        }

    }

    public abstract void doGet(GPRequest request,GPResponse response) throws Exception;

    public abstract void doPost(GPRequest request,GPResponse response) throws Exception;

}

从上面的代码中,我们看到,doGet()方法和doPost()方法中有两个参数GPRequest和GPResponse对象,这两个对象是由Web容器创建的,主要是对底层Socket的输入输出的封装。其中GPRequest是对Input的封装,GPResponse是对Output的封装。

1.2 创建用户业务代码

下面基于GPServlet来实现两个业务逻辑FirstServlet和SecondServlet。FirstServlet类的实现代码如下。

代码语言:javascript复制

package com.tom.tomcat.servlet;

import com.tom.tomcat.http.GPRequest;
import com.tom.tomcat.http.GPResponse;
import com.tom.tomcat.http.GPServlet;

public class FirstServlet extends GPServlet {

    public void doGet(GPRequest request, GPResponse response) throws Exception {
        this.doPost(request, response);
    }

    public void doPost(GPRequest request, GPResponse response) throws Exception {
        response.write("This is First Servlet");
    }

}
SecondServlet类的实现代码如下。
package com.tom.tomcat.servlet;

import com.tom.tomcat.http.GPRequest;
import com.tom.tomcat.http.GPResponse;
import com.tom.tomcat.http.GPServlet;

public class SecondServlet extends GPServlet {

    public void doGet(GPRequest request, GPResponse response) throws Exception {
        this.doPost(request, response);
    }

    public void doPost(GPRequest request, GPResponse response) throws Exception {
        response.write("This is Second Servlet");
    }

}

1.3 完成web.properties配置

为了简化操作,我们用web.properties文件代替web.xml文件,具体内容如下。

代码语言:javascript复制

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

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

上述代码分别给两个Servlet配置了/firstServlet.do和/secondServlet.do的URL映射。

2 基于传统I/O手写Tomcat

下面我们来看GPRequest和GPResponse的基本实现。

2.1 创建GPRequest对象

GPRequest主要就是对HTTP的请求头信息进行解析。我们从浏览器发送一个HTTP请求,如在浏览器地址栏中输入 http://localhost:8080 ,后台服务器获取的请求其实就是一串字符串,具体格式如下。

示例:

代码语言:javascript复制

GET / HTTP/1.1
Host: localhost:8080
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36
Accept: text/html,application/xhtml xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9

在GPRequest获得输入内容之后,对这一串满足HTTP的字符信息进行解析。我们来看GPRequest简单直接的代码实现。

代码语言:javascript复制

package com.tom.tomcat.http;

import java.io.InputStream;

/**
 * Created by Tom.
 */
public class GPRequest {

    private String method;
    private String url;

    public GPRequest(InputStream in){
        try {
            //获取HTTP内容
            String content = "";
            byte[] buff = new byte[1024];
            int len = 0;
            if ((len = in.read(buff)) > 0) {
                content = new String(buff,0,len);
            }

            String line = content.split("\n")[0];
            String [] arr = line.split("\s");

            this.method = arr[0];
            this.url = arr[1].split("\?")[0];
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    public String getUrl() {
        return url;
    }

    public String getMethod() {
        return method;
    }
}

在上面的代码中,GPRequest主要提供了getUrl()方法和getMethod()方法。输入流InputStream作为GPRequest的构造参数传入,在构造函数中,用字符串切割的方法提取请求方式和URL。

2.2 创建GPResponse对象

接下来看GPResponse的实现,与GPRequest的实现思路类似,就是按照HTTP规范从Output输出格式化的字符串,具体格式如下:

示例:

代码语言:javascript复制

HTTP/1.1 200 OK
Server: Tomcat
Location: http://localhost:8080
Connection: Keep-Alive
Content-Type: text/html;

This My Servlet

下面来看代码。

代码语言:javascript复制

package com.tom.tomcat.http;

import java.io.OutputStream;

/**
 * Created by Tom.
 */
public class GPResponse {
    private OutputStream out;
    public GPResponse(OutputStream out){
        this.out = out;
    }

    public void write(String s) throws Exception {
        //输出也要遵循HTTP
        //状态码为200
        StringBuilder sb = new StringBuilder();
                sb.append("HTTP/1.1 200 OKn")
                .append("Content-Type: text/html;n")
                .append("rn")
                .append(s);
        out.write(sb.toString().getBytes());
    }
}

上面的代码中,输出流OutputStream作为GPResponse的构造参数传入,主要提供了一个write()方法。通过write()方法按照HTTP规范输出字符串。

2.3 创建GPTomcat启动类

前面2.1和2.2两节只是对J2EE规范的再现,接下来就是真正Web容器的实现逻辑,分为三个阶段:初始化阶段、服务就绪阶段、接受请求阶段。第一阶段:初始化阶段,主要是完成对web.xml文件的解析。

代码语言:javascript复制

package com.tom.tomcat;

import com.tom.tomcat.http.GPRequest;
import com.tom.tomcat.http.GPResponse;
import com.tom.tomcat.http.GPServlet;

import java.io.FileInputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

/**
 * Created by Tom.
 */
public class GPTomcat {
    private int port = 8080;
    private ServerSocket server;
    private Map<String,GPServlet> servletMapping = new HashMap<String,GPServlet>();

    private Properties webxml = new Properties();


    private void init(){

        //加载web.xml文件,同时初始化ServletMapping对象
        try{
            String WEB_INF = this.getClass().getResource("/").getPath();
            FileInputStream fis = new FileInputStream(WEB_INF   "web.properties");

            webxml.load(fis);

            for (Object k : webxml.keySet()) {

                String key = k.toString();
                if(key.endsWith(".url")){
                    String servletName = key.replaceAll("\.url$", "");
                    String url = webxml.getProperty(key);
                    String className = webxml.getProperty(servletName   ".className");
                    //单实例,多线程
                    GPServlet obj = (GPServlet)Class.forName(className).newInstance();
                    servletMapping.put(url, obj);
                }

            }


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

    }

}

上面代码中,首先从WEB-INF读取web.properties文件并对其进行解析,然后将URL规则和GPServlet的对应关系保存到servletMapping中。第二阶段:服务就绪阶段,完成ServerSocket的准备工作。在GPTomcat类中增加start()方法。

代码语言:javascript复制

    public void start(){

        //1.加载配置文件,初始化ServletMapping
        init();

        try {
            server = new ServerSocket(this.port);

            System.out.println("GPTomcat已启动,监听的端口是:"   this.port);

            //2.等待用户请求,用一个死循环来等待用户请求
            while (true) {
                Socket client = server.accept();
                //3.HTTP请求,发送的数据就是字符串——有规律的字符串(HTTP)
                process(client);

            }

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

第三阶段:接受请求阶段,完成每一次请求的处理。在GPTomcat中增加process()方法的实现。

代码语言:javascript复制

    private void process(Socket client) throws Exception {

        InputStream is = client.getInputStream();
        OutputStream os = client.getOutputStream();

        //4.Request(InputStrean)/Response(OutputStrean)
        GPRequest request = new GPRequest(is);
        GPResponse response = new GPResponse(os);

        //5.从协议内容中获得URL,把相应的Servlet用反射进行实例化
        String url = request.getUrl();

        if(servletMapping.containsKey(url)){
            //6.调用实例化对象的service()方法,执行具体的逻辑doGet()/doPost()方法
            servletMapping.get(url).service(request,response);
        }else{
            response.write("404 - Not Found");
        }


        os.flush();
        os.close();

        is.close();
        client.close();
    }

每次客户端请求过来以后,从servletMapping中获取其对应的Servlet对象,同时实例化GPRequest和GPResponse对象,将GPRequest和GPResponse对象作为参数传入service()方法,最终执行业务逻辑。最后,增加main()方法。

代码语言:javascript复制

     public static void main(String[] args) {
        new GPTomcat().start();
    }

服务启动后,运行效果如下图所示。

0 人点赞