TOMCAT內核之旅–一個簡單的WEB服務器–學習心得(一)
標籤(空格分隔): web服務器
一、學習背景
本人是一名大三學生,開始以java學習爲主,後來學習了javaWEB,瞭解到了TOMCAT服務器,很好奇其內部是如何實現的,其與瀏覽器是如何聯繫起來的,帶着這一系列問題,我開始了TOMCAT的內核之旅。
二、知識支撐
本次學習藉助HOW TOMCAT WORKS一書,跟隨其思路,實現代碼。這一節我們主要是瞭解一個WEB服務器是如何工作的?
三、基礎知識
現在,我們就正式開始我們的學習。首先,我們知道,瀏覽器服務器這種B/S架構,其通信主要玩的就是協議!!!
什麼是協議?我們舉個簡單的例子, 如果沒有漢字,語法,文化這些限制,生活中我們還能正常溝通嗎?
這些規範就是協議!而瀏覽器,服務器之間要通信,就必須有自己的協議。
Web服務器也成爲超文本傳輸協議(HTTP)服務器,因爲它使用HTTP來跟客戶端進行通信的,這通常是個web瀏覽器。一個基於java的web服務器使用兩個重要的類:java.net.Socket和java.net.ServerSocket,並通過HTTP消息進行通信。
3.1 超文本傳輸協議(HTTP)
HTTP是一種協議,允許web服務器和瀏覽器通過互聯網進行來發送和接受數據。它是一種請求和響應協議。客戶端請求一個文件而服務器響應請求。HTTP使用可靠的TCP連接--TCP默認使用80端口。第一個HTTP版是HTTP/0.9,然後被HTTP/1.0所替代。正在取代HTTP/1.0的是當前版本HTTP/1.1,它定義於徵求意見文檔(RFC) 2616,可以從http://www.w3.org/Protocols/HTTP/1.1/rfc2616.pdf下載。
在HTTP中,始終都是客戶端通過建立連接和發送一個HTTP請求從而開啓一個事務。web服務器不需要聯繫客戶端或者對客戶端做一個回調連接。無論是客戶端或者服務器都可以提前終止連接。舉例來說,當你正在使用一個web瀏覽器的時候,可以通過點擊瀏覽器上的停止按鈕來停止一個文件的下載進程,從而有效的關閉與web服務器的HTTP連接。
3.2 HTTP請求
一個HTTP請求包括三個組成部分:
- 方法-統一資源標識符(URI)-協議/版本
- 請求的頭部
主題內容
下面我們看一個例子: POST /examples/default.jsp HTTP/1.1 Accept: text/plain; text/html Accept-Language: en-gb Connection: Keep-Alive Host: localhost User-Agent: Mozilla/4.0 (compatible; MSIE 4.01; Windows98) Content-Length: 33 Content-Type: application/x-www-form-urlencoded Accept-Encoding: gzip, deflate lastName=Franks&firstName=Michael 這裏POST是請求方法,/examples/default.jsp是URI,而HTTP/1.1是協議/版本部分。每個HTTP請求可以使用HTTP標準裏邊提到的多種方法之一。HTTP 1.1支持7種類型的請求:GET, POST,HEAD,OPTIONS, PUT,DELETE和TRACE。GET和POST在互聯網應用裏邊最普遍使用的。URI完全指明瞭一個互聯網資源。URI通常是相對服務器的根目錄解釋的。因此,始終一斜線/開頭。統一資源定位器(URL)其實是一種URI(查看http://www.ietf.org/rfc/rfc2396.txt)來的。該協議版本代表了正在使用的HTTP協議的版本。 請求的頭部包含了關於客戶端環境和請求的主體內容的有用信息。例如它可能包括瀏覽器設置的語言,主體內容的長度等等。每個頭部通過一個回車換行符(CRLF)來分隔的。 對於HTTP請求格式來說,頭部和主體內容之間有一個回車換行符(CRLF)是相當重要的。CRLF告訴HTTP服務器主體內容是在什麼地方開始的。在一些互聯網編程書籍中,CRLF還被認爲是HTTP請求的第四部分。
3.3 HTTP響應
類似於HTTP請求,一個HTTP響應也包括三個組成部分:
- 方法—統一資源標識符(URI)—協議/版本
- 響應的頭部
主體內容
下面我們看一個例子: HTTP/1.1 200 OK Server: Microsoft-IIS/4.0 Date: Mon, 5 Jan 2017 13:13:33 GMT Content-Type: text/html Last-Modified: Mon, 5 Jan 2004 13:13:12 GMT Content-Length: 112 <html> <head> <title>HTTP Response Example</title> </head> <body> Welcome to My CSDN </body> </html> 響應頭部的第一行類似於請求頭部的第一行。第一行告訴你該協議使用HTTP 1.1,請求成功(200=成功),表示一切都運行良好。 響應頭部和請求頭部類似,也包括很多有用的信息。響應的主體內容是響應本身的HTML內容。頭部和主體內容通過CRLF分隔開來。 現在你是不是對協議有了瀏覽器與服務器之間的通信有了一定的瞭解?那麼我們繼續我們的學習。
3.4 Socket類
套接字是網絡連接的一個端點。套接字使得一個應用可以從網絡中讀取和寫入數據。放在兩個不同計算機上的兩個應用可以通過連接發送和接受字節流。爲了從你的應用發送一條信息到另一個應用,你需要知道另一個應用的IP地址和套接字端口。在Java裏邊,套接字指的是java.net.Socket類。
一旦你成功創建了一個Socket類的實例,你可以使用它來發送和接受字節流。要發送字節流,你首先必須調用Socket類的getOutputStream方法來獲取一個java.io.OutputStream對象。要發送文本到一個遠程應用,你經常要從返回的OutputStream對象中構造一個java.io.PrintWriter對象。要從連接的另一端接受字節流,你可以調用Socket類的getInputStream方法用來返回一個java.io.InputStream對象。
3.4 ServerSocket類
Socket類代表一個客戶端套接字,即任何時候你想連接到一個遠程服務器應用的時候你構造的套接字,現在,假如你想實施一個服務器應用,例如一個HTTP服務器或者FTP服務器,你需要一種不同的做法。這是因爲你的服務器必須隨時待命,因爲它不知道一個客戶端應用什麼時候會嘗試去連接它。爲了讓你的應用能隨時待命,你需要使用java.net.ServerSocket類。這是服務器套接字的實現。
ServerSocket和Socket不同,服務器套接字的角色是等待來自客戶端的連接請求。一旦服務器套接字獲得一個連接請求,它創建一個Socket實例來與客戶端進行通信。
要創建一個服務器套接字,你需要使用ServerSocket類提供的四個構造方法中的一個。你需要指定IP地址和服務器套接字將要進行監聽的端口號。通常,IP地址將會是127.0.0.1,也就是說,服務器套接字將會監聽本地機器。服務器套接字正在監聽的IP地址被稱爲是綁定地址。服務器套接字的另一個重要的屬性是backlog,這是服務器套接字開始拒絕傳入的請求之前,傳入的連接請求的最大隊列長度。
有一定JAVA基礎的同學,我相信,這些基礎知識我們已經耳熟能詳。接下來,我們就藉助這些知識,完成一個最最最最最基礎的WEB服務器。
四、“活尿泥版”的WEB服務器
4.1 源代碼
我們的web服務器應用程序有三個類組成:
HttpServer類
package com.liu.tomcat.simpleTomcat; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.InetAddress; import java.net.ServerSocket; import java.net.Socket; import com.liu.tomcat.simpleTomcat.request.Request; import com.liu.tomcat.simpleTomcat.response.Response; 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 httpServer = new HttpServer(); System.out.println(httpServer.WEB_ROOT); httpServer.await(); } public void await() { ServerSocket serverSocket = null; int port = 8088; 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 inputStream = null; OutputStream outputStream = null; try { //這裏會產生阻塞,等待客戶端的請求 socket = serverSocket.accept(); inputStream = socket.getInputStream(); outputStream = socket.getOutputStream(); // create Request object and parse Request request = new Request(inputStream); request.parse(); // create Response object Response response = new Response(outputStream); 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 (IOException e) { e.printStackTrace(); continue; } } } }
Request類
package com.liu.tomcat.simpleTomcat.request; import java.io.IOException; import java.io.InputStream; public class Request { private InputStream inputStream; private String uri; public String getUri() { return uri; } public Request(InputStream inputStream) { this.inputStream = inputStream; } 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 = inputStream.read(buffer); } catch (IOException e) { e.printStackTrace(); i = -1; } for(int j = 0; j < i; j++) { request.append((char)buffer[j]); } System.out.println("request:\n" + 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) { System.out.println("resquest文件:" + requestString.substring(index1+1, index2)); return requestString.substring(index1+1, index2); } } return null; } }
Response類
package com.liu.tomcat.simpleTomcat.response; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.OutputStream; import com.liu.tomcat.simpleTomcat.HttpServer; import com.liu.tomcat.simpleTomcat.request.Request; /* 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 outputStream; public Response(OutputStream outputStream) { super(); this.outputStream = outputStream; } 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()) { String successMessage = "HTTP/1.1 200 OK\r\n" + "Content-Type: text/html\r\n" + "\r\n"; fis = new FileInputStream(file); //把文件裏的東西讀出來放到bytes字符數組裏 int ch = fis.read(bytes, 0, BUFFER_SIZE); outputStream.write(successMessage.getBytes()); //把bytes數組裏的東西放到要給客戶端回覆的流裏面 while(ch != -1) { outputStream.write(bytes, 0, ch); ch = fis.read(bytes, 0, BUFFER_SIZE); } } else { //file not find String errorMassage = "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>"; outputStream.write(errorMassage.getBytes()); } } catch (Exception e){ // thrown if cannot instantiate a File object System.out.println(e.toString() ); } finally { if(fis != null) { fis.close(); } } } }
4.2 源碼分析
這個應用程序的入口點(靜態main方法)可以在HttpServer類裏邊找到。main方法創建了一個HttpServer的實例並調用了它的await方法。await方法,顧名思義就是在一個指定的端口上等待HTTP請求,處理它們併發送響應返回客戶端。它一直等待直至接收到shutdown命令。
應用程序不能做什麼,除了發送靜態資源,例如放在一個特定目錄的HTML文件和圖像文件。它也在控制檯上顯示傳入的HTTP請求的字節流。不過,它不給瀏覽器發送任何的頭部例如日期或者cookies。
4.3 運行結果
首先我們必須將我們的WEB服務器打開,使其運行起來,這樣子我們在瀏覽器中訪問我們的WEB服務器,我們來看結果:
至此,本小結結束,累死了。目前已經凌晨1:25,不說了,去睡覺了;有問題請留言哈哈哈!!!