Tomcat剖析(一):一個簡單的Web服務器
- 1. Tomcat剖析(一):一個簡單的Web服務器
- 2. Tomcat剖析(二):一個簡單的Servlet服務器
- 3. Tomcat剖析(三):連接器(1)
- 4. Tomcat剖析(三):連接器(2)
- 5. Tomcat剖析(四):Tomcat默認連接器(1)
- 6. Tomcat剖析(四):Tomcat默認連接器(2)
- 7. Tomcat剖析(五):容器
第一部分:概述
這一節基於《深度剖析Tomcat》第一章:一個簡單的Web服務器 總結而成。
對Tomcat而言,如果直接對其源碼進行分析是困難的,所以這里被設計得足夠簡單使得你能理解一個 servlet 容器是如何工作的,沒有對Tomcat本身的連接器和容器進行分析,本節旨在明白服務器的整個流程大致是如何進行的。需要知道如何完善Web服務器,可以看下一節。
最好先到我的github上下載本書相關的代碼,同時去上網下載這本書。
文中詳細說明是從書中相關章節中copy下來的(因為書中講得很清楚,不用再進行更細致的講解) 注釋是為了便於理解自己添加或進行簡單翻譯的,其它部分也是自己寫的。
總的來說,一個簡單的Web服務器的流程是這樣的:
- Server創建一個serverSocket對象,等待Client發送請求
- Client發送請求后,Server獲取用戶socket,從而得到請求的輸入輸出流
- 從輸入輸出流中創建request和response對象
- 解析request,同時response設置靜態資源
- 關閉用戶socket
- 從request中獲取URI,判斷文件是否存在,如果不存在就響應404,如果是就關閉服務器,否則將對應的文件內容響應給瀏覽器寫入頁面
- 判斷URI是否為u/SHUTDOWN,如果不是則重新進入等待請求狀態,回到第2步,否則關閉服務器。
第二部分:代碼講解
HttpServer.java
package ex01.pyrmont;
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 {
// 獲取項目webroot目錄的實際物理路徑,判斷目標文件是否存在
public static final String WEB_ROOT = System.getProperty("user.dir")
+ File.separator + "webroot";
// 用於判斷是否需要關閉服務器,默認是false
private static final String SHUTDOWN_COMMAND = "/SHUTDOWN";
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
serverSocket = new ServerSocket(port, 1,
InetAddress.getByName("127.0.0.1"));
} catch (IOException e) {
e.printStackTrace();
System.exit(1);
}
//進入死循環,直到shutdown==/SHUTDOWN
while (!shutdown) {
Socket socket = null;
InputStream input = null;
OutputStream output = null;
try {
socket = serverSocket.accept();//Server一直等待直到Client發送請求
input = socket.getInputStream(); //接收請求后獲取輸入輸出流
output = socket.getOutputStream();
//創建request對象,傳入input參數用於獲取輸入流的參數
Request request = new Request(input);
request.parse(); //解析request對象,這一節只是獲取請求中請求行的URI
//創建response對象,傳入output對象用於獲取Writer對象將響應內容寫到瀏覽器
Response response = new Response(output);
//設置request,用於sendStaticResource()方法獲取URI判斷WEB_ROOT下是否存在目標文件
response.setRequest(request);
response.sendStaticResource();//如果請求不存在就發送404錯誤,否則寫入文件內容
//關閉用戶socket
socket.close();
//如果URI是/SHUTDOWN說明需要關閉服務器
shutdown = request.getUri().equals(SHUTDOWN_COMMAND);
} catch (Exception e) {
e.printStackTrace();
continue;
}
}
}
}
HttpServer.java詳細說明:
- await 方法首先創建一個 ServerSocket 實例然后進入一個 while 循環
- while 循環里邊的代碼運行到 ServletSocket 的 accept 方法停了下來,只會在 8080 端口接 收到一個 HTTP 請求的時候才返回:
- 接收到請求之后,await方法從accept方法返回的Socket實例中取得java.io.InputStream 和 java.io.OutputStream 對象
- await 方法接下去創建一個 Request 對象並且調用它的 parse 方法去解析 HTTP 請求的原始數據。
- 在這之后,創建一個 Response 對象,把 Request 對象設置給它,並調用它的 sendStaticResource 方法。
- 最后,await 關閉套接字並調用 Request 的 getUri 來檢測 HTTP 請求的 URI 是不是一個 shutdown 命令。假如是的話,shutdown 變量將被設置為 true 且程序會退出 while 循環。
如何發送請求:
- 為了請求一個靜態資源,在你的瀏覽器的地址欄或者網址框里邊敲入以下的 URL: http://machineName:port/staticResource
- 如果你要從一個不同的機器上發送請求到你的應用程序正在運行的機器上,machineName 應 該是正在運行應用程序的機器的名稱或者 IP 地址。假如你的瀏覽器在同一台機器上,你可以使 用 localhost 作為 machineName。端口是 8080,staticResource 是你需要請求的文件的名稱, 且必須位於 WEB_ROOT 里邊。舉例來說,假如你正在使用同一台計算機上測試應用程序,並且你想要調用 HttpServer 對 象去發送一個 index.html 文件,你可以使用一下的 URL: http://localhost:8080/index.html
- 要停止服務器,你可以在 web 瀏覽器的地址欄或者網址框里邊敲入預定義字符串,就在 URL 的 host:port 的后面,發送一個 shutdown 命令。 http://localhost:8080/SHUTDOWN
Request.java
解析請求流中的內容:本節僅僅獲取URI
package ex01.pyrmont;
import java.io.InputStream;
import java.io.IOException;
public class Request {
private InputStream input;
private String uri;
public Request(InputStream input) {
this.input = input;
}
// 解析input輸入流,這里只是獲取請求行的URI
// 實際的解析過程遠不止這些
public void parse() {
//下面是用最常見的read()方法獲取輸入流的內容,也是為什么要傳入輸入流的原因
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());
}
//獲取URI,通過對字符串進行簡單的查詢和切割獲得
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;
}
}
Request.java詳細說明:
- Request 類代表一個 HTTP 請求。從負責與客戶端通信的 Socket 中傳遞過來 InputStream 對象來構造這個類的一個實例。你調用 InputStream 對象其中一個 read 方法來獲 取 HTTP 請求的原始數據。
- parse 方法解析 HTTP 請求里邊的原始數據。這個方法沒有做很多事情。它唯一可用的信息 是通過調用HTTP請求的私有方法parseUri獲得的URI。parseUri方法在uri變量里邊存儲URI。 公共方法 getUri 被調用並返回 HTTP 請求的 URI。
- 為了理解parse和parseUri方法是怎樣工作的,你需要知道“超文本傳輸協議(HTTP)” 中 HTTP 請求的結構。在這一節中,我們僅僅關注 HTTP 請求的第一部分,請求行。請求行從 一個方法標記開始,接下去是請求的 URI 和協議版本,最后是用回車換行符(CRLF)結束。請求行 里邊的元素是通過一個空格來分隔的。例如,使用 GET 方法來請求 index.html 文件的請求行如下所示。 GET /index.html HTTP/1.1
- parse 方法從傳遞給 Requst 對象的套接字的 InputStream 中讀取整個字節流並在一個緩沖 區中存儲字節數組。然后它使用緩沖區字節數據的字節來填入一個 StringBuffer 對象,並且把 代表 StringBuffer 的字符串傳遞給 parseUri 方法。
- 然后 parseUri 方法從請求行里邊獲得 URI。
Response.java
對目標文件存在與否進行兩種不同的處理 如果存在就將文件的內容寫入瀏覽器,否則返回404頁面到瀏覽器 從這個類可以看出,這個類只是簡單的文件作為靜態資源,將文件的內容寫到瀏覽器中,沒有加載servlet的代碼
package ex01.pyrmont;
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 CopyOfResponse {
private static final int BUFFER_SIZE = 1024;
Request request;
OutputStream output;
public CopyOfResponse(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 {
//獲取URI對應磁盤下的文件對象,因為需要用到URI,所以傳入request參數
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) {
System.out.println(e.toString());
} finally {
if (fis != null)
fis.close();
}
}
}
Response.java詳細說明
- response對象是通過傳遞由套接字獲得的OutputStream對象給HttpServer類的await方法來構造的。
- Response 類有兩個公共方法:setRequest 和 sendStaticResource。setRequest 方法用來 傳遞一個 Request 對象給 Response 對象,sendStaticResource 方法是用來發送一個靜態資源,例如一個 HTML 文件。它首先通過傳遞上一級目錄的路徑和子路徑給 File 累的構造方法來實例化 java.io.File 類。 File file = new File(HttpServer.WEB_ROOT, request.getUri());
- 然后它檢查該文件是否存在。假如存在的話,通過傳遞 File 對象讓 sendStaticResource 構造一個 java.io.FileInputStream 對象。然后,它調用 FileInputStream 的 read 方法並把字節數組寫入 OutputStream 對象。請注意,這種情況下,靜態資源是作為原始數據發送給瀏覽器的。
- 假如文件並不存在,sendStaticResource 方法發送一個錯誤信息到瀏覽器。
第三部分:小結
這一節,我們大體知道了一個Web服務器的大致的整體流程,雖然其中有很多問題沒有考慮到,但是這里提供了一個很好的學習工具。
下一節會將這個最簡單的servlet容器演變為第二個稍微復雜的 servlet 容器
如果有什么疑問或錯誤,可以發表評論或者加我QQ:1096101803告知。
附
相應代碼可以在我的github上找到下載,拷貝到eclipse,然后打開對應包的代碼即可。
如發現編譯錯誤,可能是由於jdk不同版本對編譯的要求不同導致的,可以不管,供學習研究使用。
