前言:
無論是之前所在實習單位小到一個三五個人做的項目,還是如今一個在做的百人以上的產品,一直都能看到tomcat的身影。工作中經常遇到的操作就是啟動和關閉tomcat服務,或者修改了摸個java文件,編譯該文件,將生成的class文件塞到tomcat目錄下相應的jar包中去,以使其生效,但是也可以熱部署,不需要這么繁瑣的操作。
總之,一直以來都是習慣了tomcat的存在,沒有深究tomcat的運行機制和原理,上一次對於tomcat源碼的躍躍欲試還是去年的事兒了——《探秘Tomcat(一)——Myeclipse中導入Tomcat源碼》。在這篇文章中,我下載了tomcat6版本的代碼,並將其導入到eclipse中,時隔一年多,原來的項目還在,但是為顯誠意,我還是重頭做了一遍導入tomcat源碼到eclipse的操作。
對於tomcat的崇敬之情讓我決定深入其中,一探究竟。於是我找到了網上大家首推的教材——《深入剖析tomcat》,書比較老,但是很權威,不影響原理性東西的理解。目前已經看到第二章。
讀過或者了解該書的應該都知道,這不是一本上來就直接告訴你tomcat的設計思想,用到的什么設計模式或者源碼中某一行有什么匠心獨運的地方。該書采用一個循序漸進的方式從一個簡單的不能再簡單的servlet容器開始,之后慢慢豐富,添加功能模塊,最終形成我們想知道的tomcat的模樣。
背景知識:
- HTTP請求:
請求方法——統一資源標識符URI——協議/版本
請求頭
實體
比如這里我們可以看到請求的方法Request Method是GET, Request URL為http://tech.qq.com/a/20160604/007535.htm,並且分別有Request和下面要講的Response的請求頭信息,如Content-Type等。
- HTTP共支持7中請求方法
GET,POST,HEAD,OPTIONS,PUT,DELETE,TRACE
- HTTP響應
協議——狀態碼——描述
響應頭
響應實體段
- Socket
socket在應用程序中用於從網絡中讀取數據,實現不同計算機之間的通訊,實現一個socket需要知道對應應用程序的ip地址和端口號。
首先創建該socket類的實例,有了該實例后,就可以使用它實現發送或接收字節流。如果想要發送字節流,需要調用socket類的getOutputStream來獲取一個java.io.OutputStream對象;要發送文本到遠程應用程序,需要使用返回的OutputStream對象創建一個java.io.PrintWriter對象;要從連接的另一端接收字節流,需要調用Socket類的getInputStream方法,其會返回一個java.io.InputStream對象。
- ServerSocket類
有了客戶端的socket可以發送請求,如果沒有服務端來響應,發送的請求也是肉包子打狗,ServerSocket就是充當服務端的角色,serversocket出於隨時待命的狀態,一旦有客戶端發出請求,serversocket就要給出響應,從而實現與客戶端的通信。
請求響應模型
有了以上的背景知識,我們就可以實現一個簡單到爆的通訊模型,新建一個socket客戶端通訊類,用於發送和接收數據,還需要創建一個服務端的ServerSocket用於監聽和響應客戶端的請求。
主要包含以下三個類:
HttpServer:模擬一個Web服務器
package myTest; import java.net.Socket; import java.net.ServerSocket; import java.net.InetAddress; import java.io.InputStream; import java.io.OutputStream; import java.io.IOException; import java.io.File; public class HttpServer { /** WEB_ROOT is the directory where our HTML and other files reside. * For this package, WEB_ROOT is the "webroot" directory under the working * directory. * The working directory is the location in the file system * from where the java command was invoked. */ public static final String WEB_ROOT = System.getProperty("user.dir") + File.separator + "webroot"; // shutdown command private static final String SHUTDOWN_COMMAND = "/SHUTDOWN"; // the shutdown command received private boolean shutdown = false; public static void main(String[] args) { HttpServer server = new HttpServer(); server.await(); } public void await() { ServerSocket serverSocket = null; int port = 8080; try { serverSocket = new ServerSocket(port, 1, InetAddress.getByName("127.0.0.1")); } catch (IOException e) { e.printStackTrace(); System.exit(1); } // Loop waiting for a request while (!shutdown) { Socket socket = null; InputStream input = null; OutputStream output = null; try { socket = serverSocket.accept(); input = socket.getInputStream(); output = socket.getOutputStream(); // create Request object and parse Request request = new Request(input); request.parse(); // create Response object Response response = new Response(output); response.setRequest(request); response.sendStaticResource(); // Close the socket socket.close(); //check if the previous URI is a shutdown command shutdown = request.getUri().equals(SHUTDOWN_COMMAND); } catch (Exception e) { e.printStackTrace(); continue; } } } }
從代碼可以看出:
- 類中分別創建了socket和serversocket;
- 定義了一個WEB_ROOT目錄,其中存放了對應請求的相應結果文件;
- 定義了一個關閉命令,通過在瀏覽器中輸入類似.../SHUTDOWN來關閉Web服務器
- 創建了一個await方法,一直監聽127.0.0.1的8080端口,如果有請求產生(比如在瀏覽器中輸入一個請求地址),則會進入await方法並執行serverSocket的accept方法。
Request類:
模擬一個HTTP請求。
package myTest; import java.io.InputStream; import java.io.IOException; public class Request { private InputStream input; private String uri; public Request(InputStream input) { this.input = input; } public void parse() { // Read a set of characters from the socket StringBuffer request = new StringBuffer(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]); } System.out.print(request.toString()); 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; } }
從代碼中可以發現:
- 可以實現傳遞InputStream對象,在處理與客戶端通訊的Socket對象中獲取;
- 調用InputStream對象的read來獲取HTTP請求的原始數據;
- parse方法用於解析HTTP請求中的原始數據(原始數據由上面的getInputStream中獲得);
- parseUri作為一個私有方法被parse調用,用於解析HTTP請求的URI
Response類:
模擬HTTP的響應。
package myTest; import java.io.OutputStream; import java.io.IOException; import java.io.FileInputStream; import java.io.File; /* HTTP Response = Status-Line *(( general-header | response-header | entity-header ) CRLF) CRLF [ message-body ] Status-Line = HTTP-Version SP Status-Code SP Reason-Phrase CRLF */ public class Response { private static final int BUFFER_SIZE = 1024; Request request; 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]; FileInputStream fis = null; try { File file = new File(HttpServer.WEB_ROOT, request.getUri()); if (file.exists()) { fis = new FileInputStream(file); int ch = fis.read(bytes, 0, BUFFER_SIZE); while (ch!=-1) { output.write(bytes, 0, ch); ch = fis.read(bytes, 0, BUFFER_SIZE); } } else { // file not found String errorMessage = "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>"; output.write(errorMessage.getBytes()); } } catch (Exception e) { // thrown if cannot instantiate a File object System.out.println(e.toString() ); } finally { if (fis!=null) fis.close(); } } }
從代碼可以發現:
- 和Request類似,這里通過接受OutputStream構造了Response對象;
- setRequest方法用於接收Request對象,因為在Response中需要用到Request的getUri方法;
- sendStaticResource方法主要用於處理請求的響應,如這里發送一個靜態資源html作為請求的結果
至此, 本篇主要提到:
- 一些基本概念如http請求、http響應、socket等;
- 對於一個超級簡陋的web服務器有了基本的認識;
- 明確了客戶端和服務端各自的角色和職責。
如果您覺得閱讀本文對您有幫助,請點一下“推薦”按鈕,您的“推薦”將是我最大的寫作動力!如果您想持續關注我的文章,請掃描二維碼,關注JackieZheng的微信公眾號,我會將我的文章推送給您,並和您一起分享我日常閱讀過的優質文章。
友情贊助
如果你覺得博主的文章對你那么一點小幫助,恰巧你又有想打賞博主的小沖動,那么事不宜遲,趕緊掃一掃,小額地贊助下,攢個奶粉錢,也是讓博主有動力繼續努力,寫出更好的文章^^。
1. 支付寶 2. 微信