又是一個(gè)周末,這篇文章將從一個(gè)簡(jiǎn)單的例子來(lái)理解tomcat的底層設(shè)計(jì);
本文將介紹 Java Web 服務(wù)器是如何運(yùn)行的, Web 服務(wù)器也稱(chēng)為超文本傳輸協(xié)議( HyperText Transfer Protocol, HTTP)服務(wù)器, 因?yàn)樗褂?Http 與其客戶(hù)端(通常是 Web 瀏覽器)進(jìn)行通信, 基于 Java 的 Web 服務(wù)器會(huì)使用兩個(gè)重要的類(lèi): java.net.Socket 類(lèi)和 java.net.ServerSocket 類(lèi), 并通過(guò)發(fā)送 Http 消息進(jìn)行通信. 我們先花一些篇幅介紹 Http 協(xié)議(如果同學(xué)們熟悉HTTP協(xié)議可直接跳過(guò))和這兩個(gè)類(lèi), 然后寫(xiě)一個(gè)簡(jiǎn)單的 Web 服務(wù)器.
HTTP
Http : Http 允許 Web 服務(wù)器和瀏覽器通過(guò) Internet 發(fā)送并接受數(shù)據(jù), 是一種基于"請(qǐng)求---響應(yīng)"的協(xié)議, 客戶(hù)端請(qǐng)求一個(gè)文件, 服務(wù)器端對(duì)該請(qǐng)求進(jìn)行響應(yīng). Http 使用可靠的 tcp 連接, tcp 協(xié)議默認(rèn)使用 tcp 80端口, http協(xié)議的第一個(gè)版本是 http/0.9, 后來(lái)被 http/1.0取代, 隨后 http/1.0又被http/1.1取代, http/1.1 定義域 RFC(Request for Comment, 請(qǐng)求注解)2616中.
如果各位對(duì) Http1.1 有更多興趣, 請(qǐng)閱讀 RFC 2616.
在 Http 中, 總是由客戶(hù)端通過(guò)建立連接并發(fā)送 http 請(qǐng)求來(lái)初始化一個(gè)事務(wù)的. Web 服務(wù)器端并不負(fù)責(zé)聯(lián)系客戶(hù)端或建立一個(gè)到客戶(hù)端的回調(diào)連接.客戶(hù)端或服務(wù)器端可提前關(guān)閉連接, 例如, 當(dāng)使用 Web 瀏覽器瀏覽網(wǎng)頁(yè)時(shí), 可以單擊瀏覽器上的 stop 按鈕來(lái)停止下載文件, 這樣就有效的關(guān)閉了一個(gè) Web 服務(wù)器的 http 連接.
HTTP 請(qǐng)求
一個(gè) HTTP 請(qǐng)求包含以下三部分:
- 請(qǐng)求方法----統(tǒng)一資源標(biāo)識(shí)符(Uniform Resource Identifier, URI)------協(xié)議/版本
- 請(qǐng)求頭
- 實(shí)體
下面是一個(gè) HTTP 請(qǐng)求的例子:
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
方法—統(tǒng)一資源標(biāo)識(shí)符(URI)—協(xié)議/版本出現(xiàn)在請(qǐng)求的第一行记餐。
POST /examples/default.jsp HTTP/1.1
這里 POST 是請(qǐng)求方法固惯,/examples/default.jsp
是 URI,而 HTTP/1.1 是協(xié)議/版本部分洛心。 每個(gè) HTTP 請(qǐng)求可以使用 HTTP 標(biāo)準(zhǔn)里邊提到的多種方法之一细燎。HTTP 1.1 支持 7 種類(lèi)型的請(qǐng) 求:GET, POST, HEAD, OPTIONS, PUT, DELETE 和 TRACE。GET 和 POST 在互聯(lián)網(wǎng)應(yīng)用里邊最普遍使用的皂甘。
URI 完全指明了一個(gè)互聯(lián)網(wǎng)資源玻驻。URI 通常是相對(duì)服務(wù)器的根目錄解釋的。因此偿枕,始終一斜 線(xiàn)/開(kāi)頭璧瞬。統(tǒng)一資源定位器(URL)其實(shí)是一種 URI(查看 http://www.ietf.org/rfc/rfc2396.txt)
來(lái)的。該協(xié)議版本代表了正在使用的 HTTP 協(xié)議的版本渐夸。
請(qǐng)求的頭部包含了關(guān)于客戶(hù)端環(huán)境和請(qǐng)求的主體內(nèi)容的有用信息嗤锉。例如它可能包括瀏覽器設(shè) 置的語(yǔ)言,主體內(nèi)容的長(zhǎng)度等等墓塌。每個(gè)頭部通過(guò)一個(gè)回車(chē)換行符(CRLF)來(lái)分隔的瘟忱。
對(duì)于 HTTP 請(qǐng)求格式來(lái)說(shuō),頭部和主體內(nèi)容之間有一個(gè)回車(chē)換行符(CRLF)是相當(dāng)重要的苫幢。CRLF 告訴HTTP服務(wù)器主體內(nèi)容是在什么地方開(kāi)始的访诱。在一些互聯(lián)網(wǎng)編程書(shū)籍中,CRLF還被認(rèn)為是HTTP 請(qǐng)求的第四部分韩肝。
在前面一個(gè) HTTP 請(qǐng)求中触菜,主體內(nèi)容只不過(guò)是下面一行:
lastName=Franks&firstName=Michael
實(shí)體內(nèi)容在一個(gè)典型的 HTTP 請(qǐng)求中可以很容易的變得更長(zhǎng)。
HTTP 響應(yīng)
類(lèi)似于 HTTP 請(qǐng)求哀峻,一個(gè) HTTP 響應(yīng)也包括三個(gè)組成部分:
- 方法—統(tǒng)一資源標(biāo)識(shí)符(URI)—協(xié)議/版本
- 響應(yīng)的頭部
- 主體內(nèi)容
下面是一個(gè) HTTP 響應(yīng)的例子:
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>
響應(yīng)頭部的第一行類(lèi)似于請(qǐng)求頭部的第一行涡相。第一行告訴你該協(xié)議使用 HTTP 1.1哲泊,請(qǐng)求成 功(200=成功),表示一切都運(yùn)行良好催蝗。
響應(yīng)頭部和請(qǐng)求頭部類(lèi)似械筛,也包括很多有用的信息抖所。響應(yīng)的主體內(nèi)容是響應(yīng)本身的 HTML 內(nèi) 容。頭部和主體內(nèi)容通過(guò) CRLF 分隔開(kāi)來(lái)。
Socket 類(lèi)
套接字是網(wǎng)絡(luò)連接的一個(gè)端點(diǎn)哄孤。套接字使得一個(gè)應(yīng)用可以從網(wǎng)絡(luò)中讀取和寫(xiě)入數(shù)據(jù)蜈项。放在兩 個(gè)不同計(jì)算機(jī)上的兩個(gè)應(yīng)用可以通過(guò)連接發(fā)送和接受字節(jié)流掌呜。為了從你的應(yīng)用發(fā)送一條信息到另 一個(gè)應(yīng)用毫捣,你需要知道另一個(gè)應(yīng)用的 IP 地址和套接字端口。在 Java 里邊遍尺,套接字指的是java.net.Socket
類(lèi)截酷。
要?jiǎng)?chuàng)建一個(gè)套接字,你可以使用 Socket 類(lèi)眾多構(gòu)造方法中的一個(gè)乾戏。其中一個(gè)接收主機(jī)名稱(chēng) 和端口號(hào):
public Socket (java.lang.String host, int port)
在這里主機(jī)是指遠(yuǎn)程機(jī)器名稱(chēng)或者 IP 地址迂苛,端口是指遠(yuǎn)程應(yīng)用的端口號(hào)。例如鼓择,要連接 yahoo.com 的 80 端口三幻,你需要構(gòu)造以下的 Socket 對(duì)象:
new Socket ("yahoo.com", 80);
一旦你成功創(chuàng)建了一個(gè) Socket 類(lèi)的實(shí)例,你可以使用它來(lái)發(fā)送和接受字節(jié)流呐能。要發(fā)送字節(jié) 流念搬,你首先必須調(diào)用Socket類(lèi)的getOutputStream方法來(lái)獲取一個(gè)java.io.OutputStream對(duì)象。 要 發(fā) 送 文 本 到 一 個(gè) 遠(yuǎn) 程 應(yīng) 用 摆出, 你 經(jīng) 常 要 從 返 回 的 OutputStream 對(duì) 象 中 構(gòu) 造 一 個(gè) java.io.PrintWriter 對(duì)象朗徊。要從連接的另一端接受字節(jié)流,你可以調(diào)用 Socket 類(lèi)的 getInputStream 方法用來(lái)返回一個(gè) java.io.InputStream 對(duì)象偎漫。
ServerSocket 類(lèi)
Socket 類(lèi)代表一個(gè)客戶(hù)端套接字爷恳,即任何時(shí)候你想連接到一個(gè)遠(yuǎn)程服務(wù)器應(yīng)用的時(shí)候你構(gòu) 造的套接字,現(xiàn)在象踊,假如你想實(shí)施一個(gè)服務(wù)器應(yīng)用温亲,例如一個(gè) HTTP 服務(wù)器或者 FTP 服務(wù)器,你 需要一種不同的做法杯矩。這是因?yàn)槟愕姆?wù)器必須隨時(shí)待命栈虚,因?yàn)樗恢酪粋€(gè)客戶(hù)端應(yīng)用什么時(shí) 候會(huì)嘗試去連接它。為了讓你的應(yīng)用能隨時(shí)待命菊碟,你需要使用 java.net.ServerSocket 類(lèi)节芥。這是 服務(wù)器套接字的實(shí)現(xiàn)。
ServerSocket 和 Socket 不同逆害,服務(wù)器套接字的角色是等待來(lái)自客戶(hù)端的連接請(qǐng)求头镊。一旦服 務(wù)器套接字獲得一個(gè)連接請(qǐng)求,它創(chuàng)建一個(gè) Socket 實(shí)例來(lái)與客戶(hù)端進(jìn)行通信魄幕。
要?jiǎng)?chuàng)建一個(gè)服務(wù)器套接字相艇,你需要使用 ServerSocket 類(lèi)提供的四個(gè)構(gòu)造方法中的一個(gè)。你 需要指定 IP 地址和服務(wù)器套接字將要進(jìn)行監(jiān)聽(tīng)的端口號(hào)纯陨。通常坛芽,IP 地址將會(huì)是 127.0.0.1,也 就是說(shuō)翼抠,服務(wù)器套接字將會(huì)監(jiān)聽(tīng)本地機(jī)器咙轩。服務(wù)器套接字正在監(jiān)聽(tīng)的 IP 地址被稱(chēng)為是綁定地址。 服務(wù)器套接字的另一個(gè)重要的屬性是 backlog阴颖,這是服務(wù)器套接字開(kāi)始拒絕傳入的請(qǐng)求之前活喊,傳 入的連接請(qǐng)求的最大隊(duì)列長(zhǎng)度。
其中一個(gè) ServerSocket 類(lèi)的構(gòu)造方法如下所示:
public ServerSocket(int port, int backLog, InetAddress bindingAddress);
應(yīng)用程序
如果同學(xué)們下載過(guò)我們?cè)诘谝黄恼绿峁┑脑创a(How Tomcat Works)的話(huà)量愧, 我們可以看一看我們的目錄:
我們的 web 服務(wù)器應(yīng)用程序放在 cxs01.pyrmont(編譯的時(shí)候因?yàn)殄e(cuò)誤改名字了钾菊,也就懶得改回來(lái)了) 包里邊,由三個(gè)類(lèi)組成:
- HttpServer
- Request
- Response
這個(gè)應(yīng)用程序的入口點(diǎn)(靜態(tài) main 方法)可以在 HttpServer 類(lèi)里邊找到偎肃。main 方法創(chuàng)建了 一個(gè) HttpServer 的實(shí)例并調(diào)用了它的 await 方法煞烫。await 方法,顧名思義就是在一個(gè)指定的端 口上等待 HTTP 請(qǐng)求,處理它們并發(fā)送響應(yīng)返回客戶(hù)端累颂。它一直等待直至接收到 shutdown 命令滞详。
應(yīng)用程序不能做什么,除了發(fā)送靜態(tài)資源紊馏,例如放在一個(gè)特定目錄的 HTML 文件和圖像文件料饥。 它也在控制臺(tái)上顯示傳入的 HTTP 請(qǐng)求的字節(jié)流。不過(guò)瘦棋,它不給瀏覽器發(fā)送任何的頭部例如日期 或者 cookies稀火。
下面我們來(lái)看看我們今天的重點(diǎn),這三個(gè)類(lèi)赌朋,也就是tomcat的雛形代碼
HttpServer 類(lèi)
HttpServer 類(lèi)代表一個(gè) web 服務(wù)器凰狞,也就是程序的入口,看代碼:
public class HttpServer {
public static final String WEB_ROOT =
System.getProperty("user.dir") + File.separator + "webroot";
// 關(guān)閉命令
private static final String SHUTDOWN_COMMAND = "/SHUTDOWN";
// 是否關(guān)閉
private boolean shutdown = false;
public static void main(String[] args) {
HttpServer server = new HttpServer();
server.await();
}
main 方法中創(chuàng)建了一個(gè)HttpServer對(duì)象沛慢,并調(diào)用了該對(duì)象的await方法赡若。看名字团甲,該方法應(yīng)該是等待http請(qǐng)求之類(lèi)的東東逾冬。我們來(lái)看看方法內(nèi)部:
public void await() {
ServerSocket serverSocket = null;
int port = 8080;
try {
// 創(chuàng)建一個(gè)socket服務(wù)器
serverSocket = new ServerSocket(port, 1, InetAddress.getByName("127.0.0.1"));
}
catch (IOException e) {
e.printStackTrace();
System.exit(1);
}
// 循環(huán)等待http請(qǐng)求
while (!shutdown) {
Socket socket = null;
InputStream input = null;
OutputStream output = null;
try {
// 阻塞等待http請(qǐng)求
socket = serverSocket.accept();
input = socket.getInputStream();
output = socket.getOutputStream();
// 創(chuàng)建一個(gè)Request對(duì)象用于解析http請(qǐng)求內(nèi)容
Request request = new Request(input);
request.parse();
// 創(chuàng)建一個(gè)Response 對(duì)象,用于發(fā)送靜態(tài)文本
Response response = new Response(output);
response.setRequest(request);
response.sendStaticResource();
// 關(guān)閉流
socket.close();
//檢查URI中是否有關(guān)閉命令
shutdown = request.getUri().equals(SHUTDOWN_COMMAND);
}
catch (Exception e) {
e.printStackTrace();
continue;
}
}
}
我們看到,該方法創(chuàng)建了一個(gè)Socket服務(wù)器身腻,并循環(huán)阻塞監(jiān)聽(tīng)http請(qǐng)求产还,當(dāng)有http請(qǐng)求到來(lái)時(shí), 該方法便創(chuàng)建一個(gè)Request對(duì)象嘀趟,構(gòu)造參數(shù)是socket獲取的輸入流對(duì)象脐区, 用于讀取客戶(hù)端請(qǐng)求的數(shù)據(jù)并解析。 然后再創(chuàng)建一個(gè)Response對(duì)象她按,構(gòu)造參數(shù)是socket的輸出流對(duì)象牛隅, 并含有一個(gè)Request對(duì)象的成員變量。Response對(duì)象用于將靜態(tài)頁(yè)面發(fā)送給瀏覽器或者是其他的客戶(hù)端酌泰。最后媒佣, 該方法校驗(yàn)請(qǐng)求中是否含有關(guān)閉命令的字符串,如果有陵刹,就停止服務(wù)器的運(yùn)行默伍。
這就是一個(gè)簡(jiǎn)單的服務(wù)器, 當(dāng)我第一次看到的時(shí)候授霸,心想: 真TMD簡(jiǎn)單啊巡验。原來(lái)沒(méi)那么復(fù)雜嘛。我想同學(xué)們心里想的跟我也一樣吧碘耳。so显设, 不論多么龐大的代碼,底層原理都是很簡(jiǎn)單的辛辨,只要我們學(xué)好了基礎(chǔ)捕捂,學(xué)習(xí)起來(lái)就會(huì)輕松很多。
廢話(huà)不多說(shuō)斗搞,我們繼續(xù)看看Request 是如何解析Http請(qǐng)求的吧指攒。
Request 類(lèi)
類(lèi)結(jié)構(gòu)圖如下:
Request 類(lèi)代表一個(gè) HTTP 請(qǐng)求。從負(fù)責(zé)與客戶(hù)端通信的 Socket 中傳遞過(guò)來(lái) InputStream 對(duì)象來(lái)構(gòu)造這個(gè)類(lèi)的一個(gè)實(shí)例僻焚。你調(diào)用 InputStream 對(duì)象其中一個(gè) read 方法來(lái)獲 取 HTTP 請(qǐng)求的原始數(shù)據(jù)允悦。其中最主要的方法就是parse 和 parseUri ,他們用于逐個(gè)解析每個(gè)從客戶(hù)端傳遞過(guò)來(lái)的字節(jié)虑啤,我們先看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 {
// 讀取流中內(nèi)容
i = input.read(buffer);
}
catch (IOException e) {
e.printStackTrace();
i = -1;
}
for (int j=0; j<i; j++) {
// 將每個(gè)字節(jié)轉(zhuǎn)換為字符
request.append((char) buffer[j]);
}
// 打印字符串
System.out.print(request.toString());
// 根據(jù)轉(zhuǎn)換出來(lái)的字符解析URI
uri = parseUri(request.toString());
}
我們也看到該方法是十分的簡(jiǎn)單隙弛, 創(chuàng)建一個(gè)StringBuffer 對(duì)象,然后從流中讀取字節(jié)狞山,然后循環(huán)將字節(jié)轉(zhuǎn)成字符寫(xiě)入到Stringbuffer對(duì)象中全闷。最后傳入到parseUri方法中進(jìn)行解析。
我們?cè)倏纯磒arseUri方法萍启, 這個(gè)方法中总珠,我們前面學(xué)習(xí)的關(guān)于HTTP的知識(shí)會(huì)起作用:
private String parseUri(String requestString) {
int index1, index2;
// 找到第一個(gè)空格
index1 = requestString.indexOf(' ');
if (index1 != -1) {
// 找到第二個(gè)空格
index2 = requestString.indexOf(' ', index1 + 1);
if (index2 > index1)
// 截取第一個(gè)空格到第二個(gè)空格之間的內(nèi)容
return requestString.substring(index1 + 1, index2);
}
return null;
}
該方法從請(qǐng)求行里邊獲得 URI屏鳍。parseUri 方法搜索請(qǐng)求里邊的第一個(gè)和第二個(gè)空格并從中獲取 URI。
為什么是第一個(gè)空格和第二個(gè)空格之間的內(nèi)容呢局服?我們看看前面的Http請(qǐng)求的例子:
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了腌逢。
我們總結(jié)一下Request類(lèi)降淮,這個(gè)類(lèi)其實(shí)就是解析HTTP 消息頭內(nèi)容的超埋,先將流中數(shù)據(jù)轉(zhuǎn)成字節(jié)搏讶,然后將轉(zhuǎn)成字符,最后將字符解析霍殴,得到自己感興趣的內(nèi)容媒惕。奏是這么簡(jiǎn)單。好了来庭,我們?cè)倏纯碦esponse類(lèi)妒蔚。看看他是怎么實(shí)現(xiàn)的月弛。
Response類(lèi)
我們先看看這個(gè)類(lèi)的結(jié)構(gòu)圖:
Response 代表了Http請(qǐng)求中的一個(gè)響應(yīng)肴盏。我們關(guān)注其中的 sendStaticResource 方法,看名字帽衙,該方法應(yīng)該是發(fā)送靜態(tài)資源給客戶(hù)端菜皂。我們看看代碼:
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 {
// 沒(méi)有文件返回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();
}
}
可以看到,該方法也非常的簡(jiǎn)單厉萝, sendStaticResource 方法是用來(lái)發(fā)送一個(gè)靜態(tài)資源恍飘,例如一個(gè) HTML 文件。它首先通過(guò)傳遞 上一級(jí)目錄的路徑和子路徑給 File 累的構(gòu)造方法來(lái)實(shí)例化 java.io.File 類(lèi)谴垫。
然后它檢查該文件是否存在章母。假如存在的話(huà),通過(guò)傳遞 File 對(duì)象讓 sendStaticResource 構(gòu)造一個(gè) java.io.FileInputStream 對(duì)象翩剪。然后乳怎,它調(diào)用 FileInputStream 的 read 方法并把字 節(jié)數(shù)組寫(xiě)入 OutputStream 對(duì)象。請(qǐng)注意前弯,這種情況下蚪缀,靜態(tài)資源是作為原始數(shù)據(jù)發(fā)送給瀏覽器 的。
假如文件并不存在博杖,sendStaticResource 方法發(fā)送一個(gè)錯(cuò)誤信息到瀏覽器
運(yùn)行程序椿胯,啟動(dòng)HttpServer mian方法,使用Edge瀏覽器在地址欄敲入:http://localhost:8080/index.html
返回:
表示文件存在剃根, 再看看我們的后臺(tái)控制臺(tái):
如期打印了http請(qǐng)求頭中的內(nèi)容哩盲。并且下面還請(qǐng)求了一張圖片。
總結(jié)
至此,我們已經(jīng)知道了一個(gè)簡(jiǎn)單的Web服務(wù)器是如何工作的廉油。破除了我們之前的疑惑惠险,實(shí)際上tomcat底層就是這么實(shí)現(xiàn)的,可能關(guān)于阻塞IO和非阻塞NIO會(huì)有區(qū)別抒线,但總體上還是這個(gè)思路班巩,然后其余的組件都是針對(duì)優(yōu)化性能,提高擴(kuò)展性來(lái)設(shè)計(jì)新的架構(gòu)嘶炭。所以抱慌,我們明白了底層設(shè)計(jì),再去學(xué)習(xí)他的設(shè)計(jì)眨猎,就不會(huì)那么迷茫抑进。從而感到泄氣。畢竟每個(gè)夜晚睡陪,我們孤獨(dú)的學(xué)習(xí)寺渗,不想徒勞無(wú)功。
好了兰迫,本文結(jié)束P攀狻!汁果! good luck N芯小!须鼎!