大飛帶你深入理解Tomcat(二)

weixin_34391445發表於2018-07-03

作者:叩丁狼教育王一飛,高階講師。轉載請註明出處。

接上一篇,通過HttpServer,Request,Response個類的配合勉強可以處理瀏覽器發起的請求跟響應請求。但功能有點寒酸,只可以處理靜態網頁和404,本篇加入對servlet的支援,注意,僅僅是對servlet的簡單支援。

servlet回顧:
servlet是java web一個元件,是java動態網頁的基石,使用相對簡單,想深入學習的朋友可以騰訊課堂看任小龍老師傳送門:java大神之路java大神之路,這裡不累贅了,就回顧下servlet用法:

public class MyServlet implements Servlet{
    public MyServlet() {
        System.out.println("建立....");
    }
    public void init(ServletConfig config) throws ServletException {
        System.out.println("初始化....");
    }
    public void service(ServletRequest req, ServletResponse resp) 
                throws ServletException, IOException {
        System.out.println("服務....");
    }
    public void destroy() {
        System.out.println("銷燬....");
    }
    public ServletConfig getServletConfig() {
        return null;
    }
    public String getServletInfo() {
        return null;
    }
}

tomcat啟動後, 發起第一請求時,servlet執行順序
建立(構造器)----初始化(init)---[服務(service)] 迴圈----銷燬(destroy)
非第一次發起請求,直接呼叫serivce方法重複執行。
好,回顧到這,下面進入主題。
程式碼結構:

UML類圖(借用書中類圖):


11401799-ad29a55b1666b4b2.png
本篇程式碼類圖

相對上篇程式碼做改進:

0:建立一個常量類Consts, 持有專案中所有的靜態常量
/**
 * 常量類
 */
public class Consts {
    // tomcat專案絕對路徑, 所有web專案都丟在webapps目錄下
    public static final String WEB_ROOT = System.getProperty("user.dir") + File.separator + "webapps";
    
    //請求404響應內容
    public static final String RESPONSE_404_CONTENT = "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>";
    
    //請求響應成功響應頭
    public static final String RESPONSE_200_HEADER = "HTTP/1.1 200 OK\r\n" +
              "Content-Type: text/html\r\n" +
              "Content-Length: #{count}\r\n" +
              "\r\n";
}
1:Request遵循serlvet規範,實現ServletRequest介面
/**
 * 請求資訊封裝物件
 */
public class Request implements ServletRequest{
    // 瀏覽器socket連線的讀流
    private InputStream in;
    //請求行資訊資訊中的uri
    private String uri;
    public Request(InputStream in) {
        this.in = in;
    }
    // 解析瀏覽器發起的請求
    public void parseRequest() {
        // 暫時忽略檔案上傳的請求,假設都字元型請求
        byte[] buff = new byte[2048];
        StringBuffer sb = new StringBuffer(2048);
        int len = 0;
        //請求內容
        try {
            len = in.read(buff);
            sb.append(new String(buff, 0, len));
        } catch (IOException e) {
            e.printStackTrace();
        }
        System.out.print(sb.toString());
        //解析請求行中uri資訊
        uri = this.parseUri(sb.toString());
    }
    public String parseUri(String httpContent) {
        //傳入的內容解析第一行的請求行即可:
        //請求行格式:  請求方式   請求uri 協議版本     3個內容以空格隔開
        int beginIndex = httpContent.indexOf(" ");
        int endIndex;
        if(beginIndex > -1) {
            endIndex = httpContent.indexOf(" ", beginIndex + 1);
            if(endIndex > beginIndex) {
                return httpContent.substring(beginIndex, endIndex).trim();
            }
        }
        return null;
    }
    public String getUri() {
        return uri;
    }
    
    /**省略一堆目前暫時沒用到的ServletRequest需要實現的方法*/
    public Object getAttribute(String name) {
        return null;
    }
    //-------------------------------------------------

實現ServletRequest介面,需要重寫的方法一概不動,空實現。

2:Response遵循servlet規範,實現ServletResponse介面
/**
 * 處理響應請求物件
 */
public class Response implements ServletResponse{
    // 瀏覽器socket連線的寫流
    private OutputStream out;
    
    public Response(OutputStream out) {
        this.out = out;
    }
    //跳轉
    public void sendRedirect(String uri) {
        File webPage = new File(Consts.WEB_ROOT, uri);
        FileInputStream fis = null;
        StringBuffer sb = new StringBuffer();
        try {
            //找得到頁面是
            if(webPage.exists()&& webPage.isFile()) {
                fis = new FileInputStream(webPage);
                byte[] buff = new byte[2048];
                int len = 0;
                while( (len = fis.read(buff))!= -1) {
                    sb.append(new String(buff, 0, len));
                }
                String respHeader=Consts.RESPONSE_200_HEADER.replace("#{count}", sb.length()+"");
                System.out.println(respHeader + sb);
                out.write((respHeader + sb).getBytes());
                
            }else {
                 //頁面找不到時
                out.write(Consts.RESPONSE_404_CONTENT.getBytes());
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (fis != null) {
                    fis.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    
    //重寫getWriter方法
    public PrintWriter getWriter() throws IOException {
        PrintWriter writer = new PrintWriter(out, true);
                //設定響應頭,後續還會修改
        writer.println(Consts.RESPONSE_200_HEADER);
        return writer;
    }
    //--------------------------

僅僅重寫getWriter方法,其他方法空實現

3:響應處理分離:處理靜態請求使用staticSourceProcessor類,處理servlet使用ServletProcessor類,判斷依據uri中使用servlet字樣
/**
 * 用於響應靜態檔案請求
 */
public class StaticSourceProcessor {

    public void process(Request request, Response response) {
        response.sendRedircet(request.getUri());
    }
    
}

servlet類的處理有點麻煩,原因:servlet擺放在約定好的webapp目錄下,專案使用時,需要額外載入自定義的servlet的位元組碼到記憶體。

public class ServletProcessor {
    public void process(Request request, Response response) {
        String uri = request.getUri();
        //從uri中獲取serlvet名稱
        String servletName = uri.substring(uri.lastIndexOf("/")+1);
        try {
            //載入classpath路徑,預設使用webapps
            //此處設一個限制,約束自定義的serlvet必須沒有包名,沒有為什麼,demo就不要那麼多要求
            File classPath = new File(Consts.WEB_ROOT);
            //轉換成url能識別的路徑, 簡單講加上file協議
            String repository = (new URL("file", null, classPath.getCanonicalPath() + File.separator)).toString();
            URL url = new URL(repository);
            //建立一個url類載入器,用於載入上面指定serlvetName的servlet類
            URLClassLoader loader = new URLClassLoader(new URL[] {url});
            //通過反射建立servlet類物件
            Class myClass = loader.loadClass(servletName);
            Servlet servlet= (Servlet) myClass.newInstance();
            //使用servlet呼叫service方法,servlet處理完成
            servlet.service(request, response);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

注意:這裡做2個約定, 1:自定義的serlvet必須沒有包名,直接放在src目錄裡。 2:將編譯後的自定義位元組碼拷貝到webapps中等價於部署。

4:HttpServer類改造,實現響應分離
/**
 * 模擬tomcat的核心類
 */
public class HttpServer {
    // 模擬tomcat關閉命令
    private static final String SHUTDOWN_CMD = "/SHUTDOWN";
    private boolean shutdown = false;
    //持續監聽埠
    @SuppressWarnings("resource")
    public void accept() {
        ServerSocket serverSocket = null;
        try {
            // 啟動socket服務, 監聽8080埠,
            serverSocket =  new ServerSocket(8080, 1, InetAddress.getByName("127.0.0.1"));
        } catch (IOException e) {
            e.printStackTrace();
            throw new RuntimeException("啟動myTomcat伺服器失敗:" + e.getMessage(), e);
        }
        // 沒接收到關閉命令前一直監聽
        while (!shutdown) {
            Socket socket = null;
            InputStream in = null;
            OutputStream out = null;
            try {
                // 接收請求
                socket = serverSocket.accept();
                in = socket.getInputStream();
                out = socket.getOutputStream();
                // 將瀏覽器傳送的請求資訊封裝成請求物件
                Request request = new Request(in);
                request.parseRequest();
                // 將相應資訊封裝相應物件
                Response response = new Response(out);
                
                //實現約定:servlet請求路徑必須以/servlet開頭,以servlet簡單類名結束
                if(request.getUri().startsWith("/servlet")) {
                    ServletProcessor processor = new ServletProcessor();
                    processor.process(request, response);
                }else {
                    //此處簡單響應一個靜態資原始檔
                    StaticSourceProcessor processor = new StaticSourceProcessor();
                    processor.process(request, response);
                }
                socket.close();
                //如果是使用關閉命令,停止監聽退出
                shutdown = request.getUri().equals(SHUTDOWN_CMD);
            } catch (Exception e) {
                e.printStackTrace();
                continue;
            }
        }
    }
    public static void main(String[] args) {
        new HttpServer().accept();
    }
}
5:新增自定義的servlet類

HelloServlet:
在MyTomcat專案中加入servlet-api.jar依賴包,自定義HelloServlet實現Servlet介面,同時實現service方法,用於響應瀏覽器傳送的請求。

public class HelloServlet implements Servlet {
    public void init(ServletConfig arg0) throws ServletException {}
    //重寫service方法,響應請求
    public void service(ServletRequest req, ServletResponse resp)
            throws ServletException, IOException {
        PrintWriter writer = resp.getWriter();
        writer.println("hello, servlet....");
    }
    public void destroy() {}
    public ServletConfig getServletConfig() {return null;   }
    public String getServletInfo() {return null;}
}

再次強調, 這個類沒有包(package),編譯之後,應該將位元組碼放到webapps目錄下,否則報類找不到異常。

6:測試

執行HttpServer類,
在瀏覽器中輸入:http://localhost:8080/hello/index.html 進入靜態資源處理

11401799-97f14a74a5b5429e.png
靜態資源

在瀏覽器中輸入:http://localhost:8080/servlet/HelloServlet 進入靜態資源處理

11401799-f88a81fc69e0aee2.png
servlet

807144-3e07078bf1370c6c.jpeg
WechatIMG9.jpeg

相關文章