2019-02-25 · Develop

原来写个 Tomcat 这么简单

今天看了一篇文章 写一个迷你版的Tomcat 让我对 Tomcat 有了一个不一样的理解,下面是这篇文章中代码的实现过程。

为了方便开发,我们使用 Maven 来管理项目,引入 Lombok 进行极简开发。使用 logback 进行日志记录。

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.16.18</version>
</dependency>
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.2.3</version>
</dependency>

解析 HTTP 协议,封装 Request/Response

@Slf4j @Getter @Setter @ToString
public class Request {

    private String url;
    private String method;

    public Request(InputStream in) throws IOException {
        String httpRequest = "";
        byte[] httpRequestBytes = new byte[1024];
        int length;
        if ((length = in.read(httpRequestBytes)) > 0) {
            httpRequest = new String(httpRequestBytes, 0, length);
        }

//        HTTP请求协议
//        GET /favicon.ico HTTP/1.1
//        Accept: */*
//        Accept-Encoding:gzip,deflate
//        User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0 rv:11.0) like Gecko
//        Host: localhost:8080
//        Connection: Keep-Alive

        String httpHead = httpRequest.split("\n")[0];
        url = httpHead.split("\\s")[1];
        method = httpHead.split("\\s")[0];
        log.info(this.toString());
    }
}

通过输入流,对 HTTP 进行解析拿到 HTTP 请求头的方法和 URL。下面是基于 HTTP 协议格式进行输出。

public class Response {

    private OutputStream out;

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

    public void write(String content) throws IOException {
//        HTTP相应协议
//        HTTP/1.1 200 OK
//        Content-Type:text/html
//
//        <html><body></body></html>

        String httpResponse = "HTTP/1.1 200 OK\n" +
                "Content-Type:text/html\n" +
                "\r\n" +
                "<html><body>" +
                content +
                "</body></html>";
        out.write(httpResponse.getBytes());
        out.close();
    }
}

Servlet

public abstract class Servlet {

    abstract void doGet(Request request, Response response);

    abstract void doPost(Request request, Response response);

    public void service(Request request, Response response) {
        if (request.getMethod().equalsIgnoreCase("POST")) {
            doPost(request, response);
        } else if (request.getMethod().equalsIgnoreCase("GET")) {
            doGet(request, response);
        }
    }
}

有了上面的 Servlet 后,再构造一个在内存中保存 Servlet 信息的 Map。

@Getter @Setter @NoArgsConstructor @AllArgsConstructor
public class ServletMapping {

    private String servletName;
    private String url;
    private String clazz;
}

测试模拟代码

由于是个极简的 Tomcat,所以没有实现读取 Web.xml 解析 Servlet 信息的部分,这部分就直接加载到内存中来模拟 Tomcat 的后续功能。

@Slf4j
public class FindGirlServlet extends Servlet {

    @Override
    void doGet(Request request, Response response) {
        try {
            response.write("get girl...");
        } catch (IOException e) {
            log.error("Find Girl Error On Get.", e);
        }
    }

    @Override
    void doPost(Request request, Response response) {
        try {
            response.write("post girl...");
        } catch (IOException e) {
            log.error("Find Girl Error On Post.", e);
        }
    }
}
@Slf4j
public class HelloWorldServlet extends Servlet {

    @Override
    void doGet(Request request, Response response) {
        try {
            response.write("get world...");
        } catch (IOException e) {
            log.error("Hello World Error On Get.", e);
        }
    }

    @Override
    void doPost(Request request, Response response) {
        try {
            response.write("post world...");
        } catch (IOException e) {
            log.error("Hello World Error On Post.", e);
        }
    }
}

上面两个是模拟的 Servlet 功能。下面把解析 Web.xml 也省略掉

public class ServletMappingConfig {

    public static List<ServletMapping> servletMappingList = new ArrayList<>();

    static {
        servletMappingList.add(new ServletMapping("findGirl", "/girl", "com.example.tomcat.FindGirlServlet"));
        servletMappingList.add(new ServletMapping("helloWorld", "/world", "com.example.tomcat.HelloWorldServlet"));
    }
}

Tomcat

最后是最重要的 Tomcat 部分

@Slf4j @NoArgsConstructor
public class Tomcat {

    private int port = 8080;

    private Map<String, String> urlServletMap = new HashMap<>();

    public Tomcat(int port) {
        this.port = port;
    }

    public void start() {

        // 初始化 URL 与对应处理的 Servlet 的关系
        initServletMapping();

        ServerSocket serverSocket;

        try {
            serverSocket = new ServerSocket(port);
            log.info("Tomcat is start... {}", port);

            while (true) {
                Socket socket = serverSocket.accept();
                InputStream in = socket.getInputStream();
                OutputStream out = socket.getOutputStream();

                Request request = new Request(in);
                Response response = new Response(out);

                // 请求分发
                dispatch(request, response);

                socket.close();
            }
        } catch (IOException e) {
            log.error("Tomcat start error.", e);
        }
    }

    private void dispatch(Request request, Response response) {
        String clazz = urlServletMap.get(request.getUrl());

        // 反射
        try {
            Class<Servlet> servletClass = (Class<Servlet>) Class.forName(clazz);
            Servlet servlet = servletClass.newInstance();
            servlet.service(request, response);
        } catch (Exception e) {
            log.error("Not find Mapping to process {}.{}", request.getUrl(), request.getMethod());
        }
    }

    private void initServletMapping() {
        for (ServletMapping servletMapping : ServletMappingConfig.servletMappingList) {
            urlServletMap.put(servletMapping.getUrl(), servletMapping.getClazz());
        }
    }
}

其中 start 方法进行初始化 Servlet 资源,然后使用输入输出流构建成 Requst/Response 再在 dispatch 中通过反射去分发请求。

测试

最后启动 Tomcat 进行,并访问浏览器 http://localhost:8080/girl 进行校验

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