深入理解 Tomcat(三)Tomcat 底層實現原理
轉載自:https://blog.csdn.net/qq_38182963/article/details/78660777
又是一個週末,這篇文章將從一個簡單的例子來理解tomcat的底層設計;
本文將介紹 Java Web 伺服器是如何執行的, Web 伺服器也稱為超文字傳輸協議( HyperText Transfer Protocol, HTTP)伺服器, 因為它使用 Http 與其客戶端(通常是 Web 瀏覽器)進行通訊, 基於 Java 的 Web 伺服器會使用兩個重要的類: java.net.Socket 類和 java.net.ServerSocket 類, 並通過傳送 Http 訊息進行通訊. 我們先花一些篇幅介紹 Http 協議(如果同學們熟悉HTTP協議可直接跳過)和這兩個類, 然後寫一個簡單的 Web 伺服器.
HTTP
Http : Http 允許 Web 伺服器和瀏覽器通過 Internet 傳送並接受資料, 是一種基於”請求—響應”的協議, 客戶端請求一個檔案, 伺服器端對該請求進行響應. Http 使用可靠的 tcp 連線, tcp 協議預設使用 tcp 80埠, http協議的第一個版本是 http/0.9, 後來被 http/1.0取代, 隨後 http/1.0又被http/1.1取代, http/1.1 定義域 RFC(Request for Comment, 請求註解)2616中.
如果各位對 Http1.1 有更多興趣, 請閱讀 RFC 2616.
在 Http 中, 總是由客戶端通過建立連線併發送 http 請求來初始化一個事務的. Web 伺服器端並不負責聯絡客戶端或建立一個到客戶端的回撥連線.客戶端或伺服器端可提前關閉連線, 例如, 當使用 Web 瀏覽器瀏覽網頁時, 可以單擊瀏覽器上的 stop 按鈕來停止下載檔案, 這樣就有效的關閉了一個 Web 伺服器的 http 連線.
HTTP 請求
一個 HTTP 請求包含以下三部分:
* 請求方法—-統一資源識別符號(Uniform Resource Identifier, URI)——協議/版本
* 請求頭
* 實體
下面是一個 HTTP 請求的例子:
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; Windows 98 )
Content-Length: 33 Content-Type: application/x-www-form-urlencoded Accept-Encoding: gzip, deflate
lastName=Franks&firstName=Michael
方法—統一資源識別符號(URI)—協議/版本出現在請求的第一行。
POST /examples/default.jsp HTTP/1.1
這裡 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 請求的第四部分。
在前面一個 HTTP 請求中,主體內容只不過是下面一行:
lastName=Franks&firstName=Michael
實體內容在一個典型的 HTTP 請求中可以很容易的變得更長。
HTTP 響應
類似於 HTTP 請求,一個 HTTP 響應也包括三個組成部分:
* 方法—統一資源識別符號(URI)—協議/版本
* 響應的頭部
* 主體內容
下面是一個 HTTP 響應的例子:
HTTP/1.1 200 OK
Server: Microsoft-IIS/4.0
Date: Mon, 5 Jan 2004 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 Brainy Software
</body>
</html>
響應頭部的第一行類似於請求頭部的第一行。第一行告訴你該協議使用 HTTP 1.1,請求成 功(200=成功),表示一切都執行良好。
響應頭部和請求頭部類似,也包括很多有用的資訊。響應的主體內容是響應本身的 HTML 內 容。頭部和主體內容通過 CRLF 分隔開來。
Socket 類
套接字是網路連線的一個端點。套接字使得一個應用可以從網路中讀取和寫入資料。放在兩 個不同計算機上的兩個應用可以通過連線傳送和接受位元組流。為了從你的應用傳送一條資訊到另 一個應用,你需要知道另一個應用的 IP 地址和套接字埠。在 Java 裡邊,套接字指的是java.net.Socket
類。
要建立一個套接字,你可以使用 Socket 類眾多構造方法中的一個。其中一個接收主機名稱 和埠號:
public Socket (java.lang.String host, int port)
在這裡主機是指遠端機器名稱或者 IP 地址,埠是指遠端應用的埠號。例如,要連線 yahoo.com 的 80 埠,你需要構造以下的 Socket 物件:
new Socket ("yahoo.com", 80);
一旦你成功建立了一個 Socket 類的例項,你可以使用它來發送和接受位元組流。要傳送位元組 流,你首先必須呼叫Socket類的getOutputStream方法來獲取一個java.io.OutputStream物件。 要 發 送 文 本 到 一 個 遠 程 應 用 , 你 經 常 要 從 返 回 的 OutputStream 對 象 中 構 造 一 個 java.io.PrintWriter 物件。要從連線的另一端接受位元組流,你可以呼叫 Socket 類的 getInputStream 方法用來返回一個 java.io.InputStream 物件。
ServerSocket 類
Socket 類代表一個客戶端套接字,即任何時候你想連線到一個遠端伺服器應用的時候你構 造的套接字,現在,假如你想實施一個伺服器應用,例如一個 HTTP 伺服器或者 FTP 伺服器,你 需要一種不同的做法。這是因為你的伺服器必須隨時待命,因為它不知道一個客戶端應用什麼時 候會嘗試去連線它。為了讓你的應用能隨時待命,你需要使用 java.net.ServerSocket 類。這是 伺服器套接字的實現。
ServerSocket 和 Socket 不同,伺服器套接字的角色是等待來自客戶端的連線請求。一旦服 務器套接字獲得一個連線請求,它建立一個 Socket 例項來與客戶端進行通訊。
要建立一個伺服器套接字,你需要使用 ServerSocket 類提供的四個構造方法中的一個。你 需要指定 IP 地址和伺服器套接字將要進行監聽的埠號。通常,IP 地址將會是 127.0.0.1,也 就是說,伺服器套接字將會監聽本地機器。伺服器套接字正在監聽的 IP 地址被稱為是繫結地址。 伺服器套接字的另一個重要的屬性是 backlog,這是伺服器套接字開始拒絕傳入的請求之前,傳 入的連線請求的最大佇列長度。
其中一個 ServerSocket 類的構造方法如下所示:
java
public ServerSocket(int port, int backLog, InetAddress bindingAddress);
應用程式
如果同學們下載過我們在第一篇文章提供的原始碼(How Tomcat Works)的話, 我們可以看一看我們的目錄:
我們的 web 伺服器應用程式放在 cxs01.pyrmont(編譯的時候因為錯誤改名字了,也就懶得改回來了) 包裡邊,由三個類組成:
* HttpServer
* Request
* Response
這個應用程式的入口點(靜態 main 方法)可以在 HttpServer 類裡邊找到。main 方法建立了 一個 HttpServer 的例項並呼叫了它的 await 方法。await 方法,顧名思義就是在一個指定的端 口上等待 HTTP 請求,處理它們併發送響應返回客戶端。它一直等待直至接收到 shutdown 命令。
應用程式不能做什麼,除了傳送靜態資源,例如放在一個特定目錄的 HTML 檔案和影象檔案。 它也在控制檯上顯示傳入的 HTTP 請求的位元組流。不過,它不給瀏覽器傳送任何的頭部例如日期 或者 cookies。
下面我們來看看我們今天的重點,這三個類,也就是tomcat的雛形程式碼
HttpServer 類
HttpServer 類代表一個 web 伺服器,也就是程式的入口,看程式碼:
public class HttpServer {
public static final String WEB_ROOT =
System.getProperty("user.dir") + File.separator + "webroot";
// 關閉命令
private static final String SHUTDOWN_COMMAND = “/SHUTDOWN”;
// 是否關閉
private boolean shutdown = false;
public static void main(String[] args) {
HttpServer server = new HttpServer();
server.await();
}
main 方法中建立了一個HttpServer物件,並呼叫了該物件的await方法。看名字,該方法應該是等待http請求之類的東東。我們來看看方法內部:
public void await() {
ServerSocket serverSocket = null;
int port = 8080;
try {
// 建立一個socket伺服器
serverSocket = new ServerSocket(port, 1, InetAddress.getByName("127.0.0.1"));
}
catch (IOException e) {
e.printStackTrace();
System.exit(1);
}
<span class="hljs-comment">// 迴圈等待http請求</span>
<span class="hljs-keyword">while</span> (!shutdown) {
Socket socket = <span class="hljs-keyword">null</span>;
InputStream input = <span class="hljs-keyword">null</span>;
OutputStream output = <span class="hljs-keyword">null</span>;
<span class="hljs-keyword">try</span> {
<span class="hljs-comment">// 阻塞等待http請求</span>
socket = serverSocket.accept();
input = socket.getInputStream();
output = socket.getOutputStream();
<span class="hljs-comment">// 建立一個Request物件用於解析http請求內容</span>
Request request = <span class="hljs-keyword">new</span> Request(input);
request.parse();
<span class="hljs-comment">// 建立一個Response 物件,用於傳送靜態文字</span>
Response response = <span class="hljs-keyword">new</span> Response(output);
response.setRequest(request);
response.sendStaticResource();
<span class="hljs-comment">// 關閉流</span>
socket.close();
<span class="hljs-comment">//檢查URI中是否有關閉命令</span>
shutdown = request.getUri().equals(SHUTDOWN_COMMAND);
}
<span class="hljs-keyword">catch</span> (Exception e) {
e.printStackTrace();
<span class="hljs-keyword">continue</span>;
}
}
}
我們看到,該方法建立了一個Socket伺服器,並迴圈阻塞監聽http請求,當有http請求到來時, 該方法便建立一個Request物件,構造引數是socket獲取的輸入流物件, 用於讀取客戶端請求的資料並解析。 然後再建立一個Response物件,構造引數是socket的輸出流物件, 並含有一個Request物件的成員變數。Response物件用於將靜態頁面傳送給瀏覽器或者是其他的客戶端。最後, 該方法校驗請求中是否含有關閉命令的字串,如果有,就停止伺服器的執行。
這就是一個簡單的伺服器, 當我第一次看到的時候,心想: 真TMD簡單啊。原來沒那麼複雜嘛。我想同學們心裡想的跟我也一樣吧。so, 不論多麼龐大的程式碼,底層原理都是很簡單的,只要我們學好了基礎,學習起來就會輕鬆很多。
廢話不多說,我們繼續看看Request 是如何解析Http請求的吧。
Request 類
類結構圖如下:
Request 類代表一個 HTTP 請求。從負責與客戶端通訊的 Socket 中傳遞過來 InputStream 物件來構造這個類的一個例項。你呼叫 InputStream 物件其中一個 read 方法來獲 取 HTTP 請求的原始資料。其中最主要的方法就是parse 和 parseUri ,他們用於逐個解析每個從客戶端傳遞過來的位元組,我們先看parse方法:
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
uri = parseUri(request.toString());
}
我們也看到該方法是十分的簡單, 建立一個StringBuffer 物件,然後從流中讀取位元組,然後迴圈將位元組轉成字元寫入到Stringbuffer物件中。最後傳入到parseUri方法中進行解析。
我們再看看parseUri方法, 這個方法中,我們前面學習的關於HTTP的知識會起作用:
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;
}
該方法從請求行裡邊獲得 URI。parseUri 方法搜尋請求裡邊的第一個和第二個空格並從中獲取 URI。
為什麼是第一個空格和第二個空格之間的內容呢?我們看看前面的Http請求的例子:
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; Windows 98)
Content-Length: 33 Content-Type: application/x-www-form-urlencoded Accept-Encoding: gzip, deflate
lastName=Franks&firstName=Michael
我們看第一行:
POST 和 HTTP/1.1之間的就是我們需要的URI。so, 我們只需要將中間那段字串擷取就OK了。
我們總結一下Request類,這個類其實就是解析HTTP 訊息頭內容的,先將流中資料轉成位元組,然後將轉成字元,最後將字元解析,得到自己感興趣的內容。奏是這麼簡單。好了,我們再看看Response類。看看他是怎麼實現的。
Response類
我們先看看這個類的結構圖:
Response 代表了Http請求中的一個響應。我們關注其中的 sendStaticResource 方法,看名字,該方法應該是傳送靜態資源給客戶端。我們看看程式碼:
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 {
// 沒有檔案返回404
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();
}
}
可以看到,該方法也非常的簡單, sendStaticResource 方法是用來發送一個靜態資源,例如一個 HTML 檔案。它首先通過傳遞 上一級目錄的路徑和子路徑給 File 累的構造方法來例項化 java.io.File 類。
然後它檢查該檔案是否存在。假如存在的話,通過傳遞 File 物件讓 sendStaticResource 構造一個 java.io.FileInputStream 物件。然後,它呼叫 FileInputStream 的 read 方法並把字 節陣列寫入 OutputStream 物件。請注意,這種情況下,靜態資源是作為原始資料傳送給瀏覽器 的。
假如檔案並不存在,sendStaticResource 方法傳送一個錯誤資訊到瀏覽器
執行程式,啟動HttpServer mian方法,使用Edge瀏覽器在位址列敲入:http://localhost:8080/index.html
返回:
表示檔案存在, 再看看我們的後臺控制檯:
如期列印了http請求頭中的內容。並且下面還請求了一張圖片。
總結
至此,我們已經知道了一個簡單的Web伺服器是如何工作的。破除了我們之前的疑惑,實際上tomcat底層就是這麼實現的,可能關於阻塞IO和非阻塞NIO會有區別,但總體上還是這個思路,然後其餘的元件都是針對優化效能,提高擴充套件性來設計新的架構。所以,我們明白了底層設計,再去學習他的設計,就不會那麼迷茫。從而感到洩氣。畢竟每個夜晚,我們孤獨的學習,不想徒勞無功。
好了,本文結束!!! good luck !!!