一步一步实现Tomcat之二——实现一个简单的Servlet容器

前言

通过上一篇文章《一步一步实现Tomcat——实现一个简单的Web服务器》,咱们实现了一个简单的Web服务器,能够响应浏览器请求显示静态Html页面,本文更进一步,实现一个Servlet容器,咱们不仅能响应静态页面请求,还能响应Servlet请求,虽然如今咱们只能在本身的Servlet中打印出“Hello World!”,可是咱们离Tomcat服务器更近了一步。html

基础知识

相信你们应该对Java EE编程比较熟悉,故在此只简单的描述一下基本概念。java

1. Java Servlet

Java Servlet 是运行在 Web 服务器或应用服务器上的程序,也能够说是一组规范,只要按照规范实现本身的类,就能够在相应的Servlet服务器(Tomcat、Jetty等)中运行,响应浏览器请求,动态生成内容。git

注:本文使用Servlet 2.3规范程序员

2. javax.servlet.Servlet

是Servlet规范中的一个接口,咱们能够本身实现这个接口在service方法中实现本身的业务逻辑。
service方法签名以下:github

public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException;

3. javax.servlet.ServletRequest

表示一次请求的接口,由服务器生成相应的实现类并传送给上面的service(ServletRequest req, ServletResponse res)使用,用户在实现本身的Servlet类是可使用传入的ServletRequest实现类中的各类方法,如请求地址,获取请求参数,获取cookie等。web

4. javax.servlet.ServletResponse

表示一次相应的接口,由服务器生成相应的实现类并传送给上面的service(ServletRequest req, ServletResponse res)使用,用户在实现本身的Servlet类是可使用传入的ServletRequest实现类中的各类方法,如设置http相应头部,向浏览器打印数据,跳转页面等。apache

用代码说话

clipboard.png

【图一】编程

如图所示,一个简单的Servlet容器处理流程很是简单,咱们只须要在上篇文章中代码基础上稍加改动,就能够实现咱们想要的功能。segmentfault

接收http请求工做咱们已经知道如何实现了,咱们先从后两项工做开始。api

1. 实现ServletRequest和ServletResponse类

上篇文章咱们也抽象了一个Request和Response类,可是这两类并无继承ServletRequestServletResponse接口,因此Servlet没法使用,因此咱们须要分别继承相应的接口。

1. 新Request类

原来Request中的方法都没有变化,由于实现了ServletRequest接口,因此必须实现接口中定义的方法,可是如今咱们还无需具体实现,大多都是返回null或留白。

/**
 * 表示请求值
 */
public class Request implements ServletRequest {

    private InputStream input;
    private String uri;

//    private String request;

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

    public void parse() {
        StringBuilder request = new StringBuilder(2048);
        int i;
        byte[] buffer = new byte[2048];
        try {
            i = input.read(buffer);
        }
        catch (IOException e) {
            e.printStackTrace();
            i = -1;
        }
        for (int j=0; j<i; j++) {
            request.append((char) buffer[j]);
        }

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

    private String parseUri(String requestString) {
        int index1, index2;
        index1 = requestString.indexOf(' ');
        if (index1 != -1) {
            index2 = requestString.indexOf(' ', index1 + 1);
            if (index2 > index1)
                return requestString.substring(index1 + 1, index2);
        }
        return null;
    }

    public String getUri() {
        return uri;
    }

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

    @Override
    public Enumeration getAttributeNames() {
        return null;
    }

    @Override
    public String getCharacterEncoding() {
        return null;
    }
    //其余方法省略...
 
}

2. 新Response类

同新Request类同样,新Response类也保留了原来的方法只是实现了ServletResponse接口,除了getWriter()方法由于稍后要用而实现了,其余ServletResponse接口方法均返回null或留白。

/**
 * 表示返回值
 */
public class Response implements ServletResponse {
    private static final int BUFFER_SIZE = 1024;
    private Request request;
    private OutputStream output;

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

    public void setRequest(Request request) {
        this.request = request;
    }

    public void sendStaticResource() throws IOException {
        byte[] bytes = new byte[BUFFER_SIZE];
        //读取访问地址请求的文件
        File file = new File(Constants.WEB_ROOT, request.getUri());
        try (FileInputStream fis = new FileInputStream(file)){
            if (file.exists()) {
                //若是文件存在
                //添加相应头。
                StringBuilder heads=new StringBuilder("HTTP/1.1 200 OK\r\n");
                heads.append("Content-Type: text/html\r\n");
                //头部
                StringBuilder body=new StringBuilder();
                //读取响应主体
                int len ;
                while ((len=fis.read(bytes, 0, BUFFER_SIZE)) != -1) {
                    body.append(new String(bytes,0,len));
                }
                //添加Content-Length
                heads.append(String.format("Content-Length: %d\n",body.toString().getBytes().length));
                heads.append("\r\n");
                output.write(heads.toString().getBytes());
                output.write(body.toString().getBytes());
            } else {
                response404(output);
            }
        }catch (FileNotFoundException e){
            response404(output);
        }
    }


    private void response404(OutputStream output) throws IOException {
        StringBuilder response=new StringBuilder();
        response.append("HTTP/1.1 404 File Not Found\r\n");
        response.append("Content-Type: text/html\r\n");
        response.append("Content-Length: 23\r\n");
        response.append("\r\n");
        response.append("<h1>File Not Found</h1>");
        output.write(response.toString().getBytes());
    }

    @Override
    public PrintWriter getWriter() throws IOException {
        return new PrintWriter(output,true);
    }
    @Override
    public String getCharacterEncoding() {
        return null;
    }
    //省略其余方法。
}

这里须要注意是new PrintWriter(output,true)方法,阅读方法注释,摘录以下内容:

autoFlush – A boolean; if true, the println, printf, or format methods will flush the output buffer

也就是说调用print方法不会输出到浏览器页面。原书中说这是一个问题须要解决。

我又阅读了Servlet API文档getWriter()相关内容(传送门),摘录以下内容:

Returns a PrintWriter object that can send character text to the client. The PrintWriter uses the character encoding returned by getCharacterEncoding(). If the response's character encoding has not been specified as described in getCharacterEncoding (i.e., the method just returns the default value ISO-8859-1), getWriter updates it to ISO-8859-1.

Calling flush() on the PrintWriter commits the response.

我理解此方法返回的PrintWriter是须要调用flush()才会刷新,因此我对全部的打印方法println();printf();print()等是否须要每次都自动刷新产生了疑惑,姑且先到这,看书中后面的处理可否能答疑解惑。

咱们只是实现一个简单的Servlet容器示例,因此ServletRequestServletResponse其余方法留待之后实现。

2. 运行用户的Servlet

上篇文章咱们直接读取静态Html文件,而后将内容直接返回给浏览器,其实处理Servlet也差很少,只不过咱们面对的class文件,咱们须要利用ClassLoader将类加载进虚拟机,而后利用反射原理生成Servlet类的对象,而后就能够调用相应service()方法,运行编写Servlet类程序员的代码了。

1. 处理Servlet的方法

/**
 * Servlet的处理类
 */
public class ServletProcessor {

    /**
     * Servlet处理方法。
     *
     * @param request
     * @param response
     */
    public void process(Request request, Response response) {
        //解析Servlet类名
        String uri = request.getUri();
        String servletName = uri.substring(uri.lastIndexOf("/") + 1);
        URLClassLoader loader = null;

        try {
            // create a URLClassLoader
            URL[] urls = new URL[1];
            URLStreamHandler streamHandler = null;
            File classPath = new File(Constants.WEB_ROOT);
            //类加载器加载路径
            String repository = (new URL("file", null, classPath.getCanonicalPath() + File.separator)).toString() ;
            urls[0] = new URL(null, repository, streamHandler);
            loader = new URLClassLoader(urls);
        }
        catch (IOException e) {
            throw new IllegalStateException(e);
        }
        Class clazz = null;
        try {
            //加载Servlet类
            clazz = loader.loadClass(servletName);
        }
        catch (ClassNotFoundException e) {
            throw new IllegalStateException(e);
        }
        try {
            //初始化Servlet类
            Servlet servlet = (Servlet) clazz.newInstance();
            //写入响应头部,不然浏览器没法解析。
            PrintWriter writer=response.getWriter();
            writer.print("HTTP/1.1 200 OK\r\n");
            writer.print("Content-Type: text/html\r\n");
            writer.print("\r\n");
            //print方法不会自动刷新。
            writer.flush();
            //调用Servlet类中service方法。
            servlet.service(request,response);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

注意这这三行代码,书中原始代码没有相应逻辑。

writer.print("HTTP/1.1 200 OK\r\n");
 writer.print("Content-Type: text/html\r\n");
 writer.print("\r\n");

和上篇文章同样,也须要加响应头部,不然浏览器没法解析,不过这个添加头部的方法十分不简陋,之后咱们会优雅的实现。

2. 有没有发现“坏味道”

注意这行代码:servlet.service(request,response);咱们将Request类和Response类直接传入了service方法,若是熟悉这个容器的程序员就能够在本身的Servlet使用这两个内部类和他的方法。

public class HelloWorldServlet implements Servlet {
    @Override
    public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
        Request request=(Request)req;
        Response response=(Response)res;
        request.parse();
    }
}

parse()方法并非ServletRequest接口方法,咱们不想暴露给程序员,但也不能parse()改为private由于容器中其余类也须要使用。
Tomcat用了一个很是巧妙的外观模式(Facade)解决了这个问题。

3. RequestResponse的外观模式

既然是由于RequestResponse向上转换类型后传输出现了问题,咱们就从这两个类入手改造,引入RequestFacadeResponseFacade两个类,这两个类和RequestResponse同样须要实现ServletRequestServletResponse接口。

- RequestFacade类
clipboard.png

【图二】

public class RequestFacade implements ServletRequest {

  private ServletRequest request = null;

  public RequestFacade(Request request) {
    this.request = request;
  }

  //实现ServletRequest中方法
  public Object getAttribute(String attribute) {
    return request.getAttribute(attribute);
  }

  public Enumeration getAttributeNames() {
    return request.getAttributeNames();
  }

  public String getRealPath(String path) {
    return request.getRealPath(path);
  }
  //其余方法省略...

- ResponseFacade类

clipboard.png

【图三】

public class ResponseFacade implements ServletResponse {

  private ServletResponse response;
  public ResponseFacade(Response response) {
    this.response = response;
  }
  //实现ServletResponse 中方法
  public void flushBuffer() throws IOException {
    response.flushBuffer();
  }

  public int getBufferSize() {
    return response.getBufferSize();
  }

  public String getCharacterEncoding() {
    return response.getCharacterEncoding();
  }
  //其余方法省略...

}

经过观察两个外观类,其实他们什么也没有作,全部的接口实现方法都是调用内部的ServletRequestServletResponse的具体实现类来处理的。咱们能够这样改造咱们上面ServletProcessor类中的代码

RequestFacade requestFacade = new RequestFacade(request);
ResponseFacade responseFacade = new ResponseFacade(response);
servlet.service( requestFacade, responseFacade);

传入Servlet实现类中service方法的参数变成了RequestFacadeResponseFacade类型,程序员就不能再代码中使用类型转换转换为RequestResponse类型,因此RequestFacadeResponseFacade避免了原来RequestResponse类不但愿对外可见的方法的暴露。

注:
1.其实从RequestFacadeResponseFacade实现和类图上更像是代理模式,可是此处使用场景确实起到了对外提供统一接口的做用,因此从功能上讲,叫外观模式也无可或非。
2.即便采用了外观类,程序员依然能够在Servlet中使用反射获取到外观类中private属性的内部类型,可是和强制转型相同,程序员应该按照Servlet协议编写程序,不然除非清楚本身目的,否则我想不到这样作的意义。

3. 处理浏览器请求

public class HttpServer {
    private static final String SHUTDOWN_COMMAND = "shutdown";
    private boolean shutdown = false;

    public static void main(String[] args) {
        HttpServer httpServer=new HttpServer();
        httpServer.await();
    }

    public void await() {
        try (ServerSocket serverSocket = new ServerSocket(8080)) {
            serverProcess(serverSocket);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void serverProcess(ServerSocket serverSocket) {
        while (!shutdown) {
            try (Socket socket = serverSocket.accept()) {
//                System.out.println(socket.hashCode());
                InputStream input = socket.getInputStream();
                OutputStream output = socket.getOutputStream();
                //建立Request对象
                Request request = new Request(input);
                request.parse();
                //建立Response对象
                Response response = new Response(output);
                response.setRequest(request);
                if (request.getUri().startsWith("/servlet/")) {
                    //若是地址以/servlet开头就做为Servlet处理
                    ServletProcessor processor = new ServletProcessor();
                    processor.process(request, response);
                }else {
                    //不然做为静态资源使用
                    StaticResourceProcessor processor = new StaticResourceProcessor();
                    processor.process(request, response);
                }
                shutdown = request.getUri().equals(SHUTDOWN_COMMAND);
            } catch (IOException e) {
                e.printStackTrace();
            } catch (Exception e){
                e.printStackTrace();
            }
        }
    }
}

和上篇文章中处理用户请求相似,咱们保留了处理处理静态资源的能力(StaticResourceProcessor具体实现见源码),又增长了处理Servlet的功能。

4. 运行一下

1. 实现HelloWorldServlet

首先编写一个本身的Servlet实现类。

public class HelloWorldServlet implements Servlet {
    @Override
    public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
        res.getWriter().println("<h1>Hello World!</h1>");
    }
   //其余方法省略
}

注意,这个HelloWorldServlet不在任何package下,由于加载的时候就是用请求地址携带的类名加载,若是添加了包名,反射的时候会加载失败,之后咱们会修复这个问题。

编译这个类,将编译好的class文件放入D:\webRoot文件夹(代码中定义的路径)。

2. 用浏览器发送请求

在浏览器地址栏输入http://localhost:8080/servlet/HelloWorldServlet,浏览器会打印出Hello World!。

后记

至此咱们实现了一个简单的Servlet容器,虽然咱们的功能很是简陋,可是经过两篇文章的讲解,你们应该能理解一个浏览器请求是如何通过服务器处理最终返回能够显示页面的大体流程。是否是颇有成就感,简单的几行代码就能演示咱们平常使用的Tomcat服务器的基本功能。不过咱们只看到了冰山一角,从此的文章会逐步一览全貌。

源码

文中源码地址:https://github.com/TmTse/tiny...

参考

《深刻剖析Tomcat》

相关文章
相关标签/搜索