一步一步實現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》

相關文章
相關標籤/搜索