1

前言

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

基础知识

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

1. Java Servlet

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

注:本文使用Servlet 2.3规范

2. javax.servlet.Servlet

是Servlet规范中的一个接口,我们可以自己实现这个接口在service方法中实现自己的业务逻辑。
service方法签名如下:

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

3. javax.servlet.ServletRequest

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

4. javax.servlet.ServletResponse

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

用代码说话

clipboard.png

【图一】

如图所示,一个简单的Servlet容器处理流程非常简单,我们只需要在上篇文章中代码基础上稍加改动,就可以实现我们想要的功能。

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

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》


JerryTse
768 声望126 粉丝