Tomcat源码之旅--最简单的Servlet容器实现

学习Tomcat源码是因为我之前写的《Spring之我见》系列文章,当学习到spring是如何启动的时候涉及到了tomcat从web.xml读取到ContextLoaderListener,从而初始化spring容器。换句话说,spring启动靠的是tomcat的帮助,这让我想先研究tomcat开始。

tomcat我参考的是《深入剖析Tomcat》,虽然介绍的tomcat比较老,还是tomcat4,但是学习思想是最重要的。而且这本书是教你怎么从零实现一个servlet容器,这种学习方法能够让我们知其然,并知其所以然。

最简单的servlet容器需要几个比较重要的组件

  • HttpServlet 等待并接受请求
  • Request 封装Request请求
  • Response 封装Response请求
  • ServletProcessor 具体处理Servlet请求
  • StaticResourceProcessor 具体处理静态页面

HttpServlet 容器的入口

我们先看 HttpServlet ,这段代码本质是ServerSocket 阻塞等待请求,如果拿到了就封装Request和Response,然后根据url分发给不同的处理器,处理完后继续轮询等待请求,直到遇到SHUTDOWN_COMMAND命令关闭程序。

import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.time.LocalDateTime;
import java.util.logging.Logger;

public class HttpServlet {

    private static final Logger log = Logger.getLogger(HttpServlet.class.getName());

//    请求接口
    private static final int port = 8088;
    private static final String SHUTDOWN_COMMAND = "/SHUTDOWN";
    private boolean shutdown = false;

    private void await() {
        ServerSocket serverSocket = null;

        try {
            serverSocket = new ServerSocket(HttpServlet.port, 1, InetAddress.getByName("127.0.0.1"));
        } catch (Exception e) {
            e.printStackTrace();
            System.exit(1);
        }

        while (true) {
            Socket socket;
            InputStream input;
            OutputStream output;

            try {
                System.out.println("等待指令。。。。" + LocalDateTime.now().toString());
//                从 socket拿到InputStream 和 OutputStream 并封装成 Request 和 Response
                socket = serverSocket.accept();
                input = socket.getInputStream();
                output = socket.getOutputStream();

                Request request = new Request(input);
                request.parse();
                Response response = new Response(output);

                if (SHUTDOWN_COMMAND.equals(request.getUri())) {
                    break;
                }

                response.setRequest(request);

//              根据url 分发任务给 servlet处理器(ServletProcessor) 或者 静态页面处理器 (StaticResourceProcessor)
                if (request.getUri().startsWith("/servlet/")) {
                    ServletProcessor servletProcessor = new ServletProcessor();
                    servletProcessor.process(request, response);
                } else {
                    StaticResourceProcessor staticResourceProcessor = new StaticResourceProcessor();
                    staticResourceProcessor.process(request, response);
                }
                socket.close();
            } catch (Exception e) {

            }
        }
    }

    public static void main(String[] args) {
        HttpServlet httpServlet = new HttpServlet();
        System.out.println("servlet容器启动成功");
        httpServlet.await();
    }
}

Socket的介绍我直接摘百度百科吧

网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个socket。
建立网络通信连接至少要一对端口号(socket)。socket本质是编程接口(API),对TCP/IP的封装,TCP/IP也要提供可供程序员做网络开发所用的接口,这就是Socket编程接口;HTTP是轿车,提供了封装或者显示数据的具体形式;Socket是发动机,提供了网络通信的能力。

对于我肤浅的理解。socket就是操作TCP/IP请求的api,给编程人员带来便利,从这点我们也知道,Tomcat的基础是Socket。

Request 与 Response

HttpServlet代码首先提到了Request 和 Response,封装的理由是为了做一些额外的操作,我们看一下Request代码

import javax.servlet.*;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.util.Enumeration;
import java.util.Locale;
import java.util.Map;
import java.util.logging.Logger;

public class Request implements ServletRequest {

    private static final int BUFFER_SIZE = 2048;
    private InputStream inputStream;
    private String uri;

    private static final Logger log = Logger.getLogger(Request.class.getName());

    /**
     * 解析请求url
     */
    public String parseUri(String requestUri) {
        int index1, index2;

        index1 = requestUri.indexOf(" ");
        if (index1 != -1) {
            index2 = requestUri.indexOf(" ", index1 + 1);
            if (index2 > index1) {
                return requestUri.substring(index1 + 1, index2);
            }
        }
        return null;
    }

    /**
     * 读取inputStream
     */
    public void parse() {
        StringBuffer request = new StringBuffer(2048);
        int i;
        byte[] buffer = new byte[BUFFER_SIZE];
        try {
            i = inputStream.read(buffer);
        } catch (Exception e) {
            log.severe(e.toString());
            i = -1;
        }

        for (int j = 0; j < i; j++) {
            request.append((char) buffer[j]);
        }

        uri = parseUri(request.toString());
    }

    public Request(InputStream inputStream) {
        this.inputStream = inputStream;
    }

    public String getUri() {
        return uri;
    }

    public void setUri(String uri) {
        this.uri = uri;
    }

    public Object getAttribute(String s) {
        return null;
    }

    public Enumeration<String> getAttributeNames() {
        return null;
    }

    public String getCharacterEncoding() {
        return null;
    }

    public void setCharacterEncoding(String s) throws UnsupportedEncodingException {

    }

    public int getContentLength() {
        return 0;
    }

    public long getContentLengthLong() {
        return 0;
    }

    public String getContentType() {
        return null;
    }

    public ServletInputStream getInputStream() throws IOException {
        return null;
    }

    public String getParameter(String s) {
        return null;
    }

    public Enumeration<String> getParameterNames() {
        return null;
    }

    public String[] getParameterValues(String s) {
        return new String[0];
    }

    public Map<String, String[]> getParameterMap() {
        return null;
    }

    public String getProtocol() {
        return null;
    }

    public String getScheme() {
        return null;
    }

    public String getServerName() {
        return null;
    }

    public int getServerPort() {
        return 0;
    }

    public BufferedReader getReader() throws IOException {
        return null;
    }

    public String getRemoteAddr() {
        return null;
    }

    public String getRemoteHost() {
        return null;
    }

    public void setAttribute(String s, Object o) {

    }

    public void removeAttribute(String s) {

    }

    public Locale getLocale() {
        return null;
    }

    public Enumeration<Locale> getLocales() {
        return null;
    }

    public boolean isSecure() {
        return false;
    }

    public RequestDispatcher getRequestDispatcher(String s) {
        return null;
    }

    public String getRealPath(String s) {
        return null;
    }

    public int getRemotePort() {
        return 0;
    }

    public String getLocalName() {
        return null;
    }

    public String getLocalAddr() {
        return null;
    }

    public int getLocalPort() {
        return 0;
    }

    public ServletContext getServletContext() {
        return null;
    }

    public AsyncContext startAsync() throws IllegalStateException {
        return null;
    }

    public AsyncContext startAsync(ServletRequest servletRequest, ServletResponse servletResponse) throws IllegalStateException {
        return null;
    }

    public boolean isAsyncStarted() {
        return false;
    }

    public boolean isAsyncSupported() {
        return false;
    }

    public AsyncContext getAsyncContext() {
        return null;
    }

    public DispatcherType getDispatcherType() {
        return null;
    }
}

Request实现了ServletRequest,大部分方法因为现在用不上所以没有实现,其中parse()方法从inputStream读取请求信息,读取内容范例如下

GET /servlet/TestServlet HTTP/1.1
Host: localhost:8088
Connection: keep-alive
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.162 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Cookie: cookie_mobile=15921307683; cookie_user_info="{\"firstName\":\"%E8%B4%BE%E7%A7%8B%E7%94%9F\",\"mobile\":\"15921307683\",\"email\":\"\"}"; cookie_user_org="{\"orgName\":\"%E5%BC%80%E5%8F%91%E7%BB%84\",\"orgCode\":\"006\"}"; sessionid=cc8798be-08f6-4051-936f-988a5c846ad7

而作为最简单的servlet容器,我们先不管Cookie啥信息,只看第一行。嗯,我们读到了是一个GET请求,请求的url是 /servlet/TestServlet ,http协议版本是HTTP/1.1 ,前面说到我们要根据url要分发请求,这就用上了parseUri()方法,它通过字符串处理直接提取出了 “/servlet/TestServlet” 并保存到uri变量。

Response也是同理,多了一个sendStaticResource方法,这个方法是由静态页面处理器(StaticResourceProcessor)直接调用的,用来返回html页面。这个方法现在还没用上,后面会再提。

    public void sendStaticResource() throws IOException {
        byte[] bytes = new byte[BUFFER_SIZE];
        FileInputStream fis = null;

        try {
            File file = new File(WEB_ROOT + "webapp", request.getUri());
            fis = new FileInputStream(file);

            outputStream.write(("HTTP/1.1 200 \r\n"
                    + "Content-Type: text/html\r\n" + "\r\n"
            ).getBytes());

            int ch = fis.read(bytes, 0, BUFFER_SIZE);
            while (ch != -1) {
                outputStream.write(bytes, 0, ch);
                ch = fis.read(bytes, 0, BUFFER_SIZE);
            }


        } catch (Exception e) {
            String errorMsg = "HTTP/1.1 404 File Not Found\r\n"
                    + "Content-Type: text/html\r\n" + "Content-Length: 23\r\n"
                    + "\r\n" + "<h1>File Not Found</h1>";
            outputStream.write(errorMsg.getBytes());
        } finally {
            outputStream.flush();
            outputStream.close();
            if (fis != null) {
                fis.close();
            }
        }
    }

ServletProcessor StaticResourceProcessor 处理的大脑

介绍完了Request和Response,开始进入核心处理类ServletProcessor

ServletProcessor只有一个process方法,接受Request和Response参数,这里面涉及类加载器的,大概流程是通过类加载器加载url中同名的类,然后调用service方法,比如请求url是“/servlet/TestServlet”,“/servlet”说明需要调用servlet处理器(ServletProcessor),然后 “TestServlet”说明调用同名的TestServlet类。

ServletProcessor:


import javax.servlet.Servlet;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.File;
import java.net.URL;
import java.net.URLClassLoader;
import java.net.URLStreamHandler;
import java.util.logging.Logger;

public class ServletProcessor {

    private static final Logger log = Logger.getLogger(HttpServlet.class.getName());

    public void process(Request request, Response response) {
        String uri = request.getUri();
        String servletName = uri.substring(uri.lastIndexOf("/") + 1);

        URLClassLoader loader = null;

        try {
            URL[] urls = new URL[1];
            URLStreamHandler streamHandler = null;
            File classPath = new File(Response.WEB_ROOT + "servlet" + File.separator);
            String repository = (new URL("file", null, classPath.getCanonicalPath() + File.separator)).toString();

            urls[0] = classPath.toURI().toURL();

            loader = new URLClassLoader(urls);
        } catch (Exception e) {
            log.severe(e.toString());
        }

        Class myClass = null;

        try {
            myClass = loader.loadClass("servlet." + servletName);
        } catch (Exception e) {
            log.severe(e.toString());
        }

        Servlet servlet;

        try {
            servlet = (Servlet) myClass.newInstance();

//            使用包装类,可以让Servlet开发者接触不到parseUri等方法
            RequestWrapper requestWrapper = new RequestWrapper(request);
            ResponseWrapper responseWrapper = new ResponseWrapper(response);

            servlet.service(requestWrapper, responseWrapper);
        } catch (Exception e) {
            log.severe(e.toString());
        }
    }
}

TestServlet:继承标准的Servlet接口,复写service方法,service方法会由ServletProcessor调用,到这一块对于学习了Servlet的人就很熟悉了。

package servlet;

import javax.servlet.*;
import java.io.IOException;
import java.io.PrintWriter;

public class TestServlet implements Servlet {

    @Override
    public void init(ServletConfig servletConfig) throws ServletException {
        System.out.println("TestServlet init");
    }

    @Override
    public ServletConfig getServletConfig() {
        return null;
    }

    @Override
    public void service(ServletRequest req, ServletResponse res) throws IOException {
        System.out.println("TestServlet service ");
        PrintWriter writer = res.getWriter();
        writer.println("HTTP/1.1 200 \r\n"
                + "Content-Type: text/html\r\n" + "\r\n" + "hello , it is TestServlet");
    }

    @Override
    public String getServletInfo() {
        return null;
    }

    @Override
    public void destroy() {
        System.out.println("TestServlet destroy");
    }
}

这里面有一点改进的地方在于:对于TestServlet 来说,是客户端程序员会编写的类,而ServletProcessor做了什么是不需要关心的,但是service会拿到ServletProcessor给的ServletRequest 和ServletResponse 变量,也就是Request和Response对象,而我们知道Request和Response有一些拓展方法比如 parseUri,这些是不允许 客户端程序员通过强转来调用的。 那如何保证安全性呢?

我们可以通过外观类来实现,我们设计一个外观类RequestWrapper ,它拥有一个Request对象,一般方法都直接调用Request的方法,而对于私密的方法不暴露在RequestWrapper 类中,达到调用安全的目的。

ResponseWrapper一样,不再贴出代码


import javax.servlet.*;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.Enumeration;
import java.util.Locale;
import java.util.Map;

public class RequestWrapper implements ServletRequest {

    private Request request;


    public RequestWrapper(Request request) {
        if (request == null) {
            throw new IllegalArgumentException("Request cannot be null");
        }
        this.request = request;
    }

    @Override
    public Object getAttribute(String name) {
        return request.getAttribute(name);
    }

    @Override
    public Enumeration<String> getAttributeNames() {
        return request.getAttributeNames();
    }

    @Override
    public String getCharacterEncoding() {
        return null;
    }

    @Override
    public void setCharacterEncoding(String env) throws UnsupportedEncodingException {

    }

    @Override
    public int getContentLength() {
        return 0;
    }

    @Override
    public long getContentLengthLong() {
        return 0;
    }

    @Override
    public String getContentType() {
        return null;
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        return null;
    }

    @Override
    public String getParameter(String name) {
        return null;
    }

    @Override
    public Enumeration<String> getParameterNames() {
        return null;
    }

    @Override
    public String[] getParameterValues(String name) {
        return new String[0];
    }

    @Override
    public Map<String, String[]> getParameterMap() {
        return null;
    }

    @Override
    public String getProtocol() {
        return null;
    }

    @Override
    public String getScheme() {
        return null;
    }

    @Override
    public String getServerName() {
        return null;
    }

    @Override
    public int getServerPort() {
        return 0;
    }

    @Override
    public BufferedReader getReader() throws IOException {
        return null;
    }

    @Override
    public String getRemoteAddr() {
        return null;
    }

    @Override
    public String getRemoteHost() {
        return null;
    }

    @Override
    public void setAttribute(String name, Object o) {

    }

    @Override
    public void removeAttribute(String name) {

    }

    @Override
    public Locale getLocale() {
        return null;
    }

    @Override
    public Enumeration<Locale> getLocales() {
        return null;
    }

    @Override
    public boolean isSecure() {
        return false;
    }

    @Override
    public RequestDispatcher getRequestDispatcher(String path) {
        return null;
    }

    @Override
    public String getRealPath(String path) {
        return null;
    }

    @Override
    public int getRemotePort() {
        return 0;
    }

    @Override
    public String getLocalName() {
        return null;
    }

    @Override
    public String getLocalAddr() {
        return null;
    }

    @Override
    public int getLocalPort() {
        return 0;
    }

    @Override
    public ServletContext getServletContext() {
        return null;
    }

    @Override
    public AsyncContext startAsync() throws IllegalStateException {
        return null;
    }

    @Override
    public AsyncContext startAsync(ServletRequest servletRequest, ServletResponse servletResponse) throws IllegalStateException {
        return null;
    }

    @Override
    public boolean isAsyncStarted() {
        return false;
    }

    @Override
    public boolean isAsyncSupported() {
        return false;
    }

    @Override
    public AsyncContext getAsyncContext() {
        return null;
    }

    @Override
    public DispatcherType getDispatcherType() {
        return null;
    }
}

而对于静态页面处理器就更简单了,只有一个方法,方法也只有一句话

sendStaticResource方法就是之前写在Response 的拓展方法。通过File读取servlet容器中预存的html页面,然后通过outputStream发出去。

/**
 * http://localhost:8080/index.html
 */
public class StaticResourceProcessor {

    public void process(Request request, Response response) {
        try {
            response.sendStaticResource();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

结语

大致的流程就是这些,这是最简单的servlet容器,本书会一层层深入,完整代码可以看我的项目SimpleServlet ,我会跟着看书的进度同时更新代码和文章

对应的分支是v1.1

https://github.com/lovejj1994/SimpleServlet
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值