前言
作為后端開發(fā)人員,在實(shí)際的工作中我們會(huì)非常高頻地使用到web服務(wù)器。而tomcat作為web服務(wù)器領(lǐng)域中舉足輕重的一個(gè)web框架痹筛,又是不能不學(xué)習(xí)和了解的。
2004年出版的tomcat書籍《How tomcat works》用非常直觀和直白的方式來講述了tomcat的框架結(jié)構(gòu)和工作原理廓鞠,樓主正是參考此書來深入學(xué)習(xí)tomcat的帚稠。想對(duì)tomcat進(jìn)行深入了解的同學(xué)也可以閱讀此書,肯定會(huì)受益匪淺床佳。
【注意】:
本系列文章我們將分析tomcat-8.5.35版本的代碼滋早,可通過apache tomcat gitub網(wǎng)站下載,https://github.com/apache/tomcat85/archive/TOMCAT_8_5_35.zip
tomcat相關(guān)的源碼地址如下:
上面我們提到砌们,tomcat其實(shí)是一個(gè)web框架馆衔,那么其內(nèi)部是怎么實(shí)現(xiàn)的呢?如果不用tomcat我們能自己實(shí)現(xiàn)一個(gè)web服務(wù)器嗎怨绣?
首先角溃,tomcat內(nèi)部的實(shí)現(xiàn)是非常復(fù)雜的,也有非常多的各類組件篮撑,我們在后續(xù)章節(jié)會(huì)深入地了解减细。
其次,本章我們將自己實(shí)現(xiàn)一個(gè)web服務(wù)器的赢笨。
下面我們就自己來實(shí)現(xiàn)一個(gè)看看未蝌。(【注】:參考了《How tomcat works》這本書)
http協(xié)議簡介
http是一種協(xié)議(超文本傳輸協(xié)議),允許web服務(wù)器和瀏覽器通過Internet來發(fā)送和接受數(shù)據(jù)茧妒,是一種請(qǐng)求/響應(yīng)協(xié)議萧吠。http底層使用TCP來進(jìn)行通信。目前桐筏,http已經(jīng)迭代到了2.x版本纸型,從最初的0.9、1.0梅忌、1.1到現(xiàn)在的2.x狰腌,每個(gè)迭代都加了很多功能。
在http中牧氮,始終都是客戶端發(fā)起一個(gè)請(qǐng)求琼腔,服務(wù)器接受到請(qǐng)求之后,然后處理邏輯踱葛,處理完成之后再發(fā)送響應(yīng)數(shù)據(jù)丹莲,客戶端收到響應(yīng)數(shù)據(jù)光坝,然后請(qǐng)求結(jié)束。在這個(gè)過程中甥材,客戶端和服務(wù)器都可以對(duì)建立的連接進(jìn)行中斷操作教馆。比如可以通過瀏覽器的停止按鈕。
http協(xié)議-請(qǐng)求
一個(gè)http協(xié)議的請(qǐng)求包含三部分:
- 方法 URI 協(xié)議/版本
- 請(qǐng)求的頭部
- 主體內(nèi)容
舉個(gè)例子
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
數(shù)據(jù)的第一行包括:方法擂达、URI、協(xié)議和版本胶滋。在這個(gè)例子里板鬓,方法為POST
,URI為/examples/default.jsp
究恤,協(xié)議為HTTP/1.1
俭令,協(xié)議版本號(hào)為1.1
。他們之間通過空格來分離部宿。
請(qǐng)求頭部從第二行開始抄腔,使用英文冒號(hào)(:)來分離鍵和值。
請(qǐng)求頭部和主體內(nèi)容之間通過空行來分離理张,例子中的請(qǐng)求體為表單數(shù)據(jù)赫蛇。
http協(xié)議-響應(yīng)
類似于http協(xié)議的請(qǐng)求,響應(yīng)也包含三個(gè)部分雾叭。
- 協(xié)議 狀態(tài) 狀態(tài)描述
- 響應(yīng)的頭部
- 主體內(nèi)容
舉個(gè)例子
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 OK
表示協(xié)議、狀態(tài)和狀態(tài)描述织狐。
之后表示響應(yīng)頭部暂幼。
響應(yīng)頭部和主體內(nèi)容之間使用空行來分離。
Socket
Socket移迫,又叫套接字旺嬉,是網(wǎng)絡(luò)連接的一個(gè)端點(diǎn)(end point)。套接字允許應(yīng)用程序從網(wǎng)絡(luò)中讀取和寫入數(shù)據(jù)厨埋。兩個(gè)不同計(jì)算機(jī)的不同進(jìn)程之間可以通過連接來發(fā)送和接受數(shù)據(jù)邪媳。A應(yīng)用要向B應(yīng)用發(fā)送數(shù)據(jù),A應(yīng)用需要知道B應(yīng)用所在的IP地址和B應(yīng)用開放的套接字端口荡陷。java里面使用java.net.Socket
來表示一個(gè)套接字悲酷。
java.net.Socket
最常用的一個(gè)構(gòu)造方法為:public Socket(String host, int port);
,host表示主機(jī)名或ip地址亲善,port表示套接字端口设易。我們來看一個(gè)例子:
Socket socket = new Socket("127.0.0.1", "8080");
OutputStream os = socket.getOutputStream();
boolean autoflush = true;
PrintWriter out = new PrintWriter( socket.getOutputStream(), autoflush);
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputstream()));
// send an HTTP request to the web server
out.println("GET /index.jsp HTTP/1.1");
out.println("Host: localhost:8080");
out.println("Connection: Close");
out.println();
// read the response
boolean loop = true;
StringBuffer sb = new StringBuffer(8096);
while (loop) {
if (in.ready()) {
int i=0;
while (i != -1) {
i = in.read();
sb.append((char) i);
}
loop = false;
}
Thread.currentThread().sleep(50L);
}
這兒通過socket.getOutputStream()
來發(fā)送數(shù)據(jù),使用socket.getInputstream()
來讀取數(shù)據(jù)蛹头。
ServerSocket
Socket表示一個(gè)客戶端套接字顿肺,任何時(shí)候如果你想發(fā)送或接受數(shù)據(jù)戏溺,都需要構(gòu)造創(chuàng)建一個(gè)Socket。現(xiàn)在假如我們需要一個(gè)服務(wù)器端的應(yīng)用程序屠尊,我們需要額外考慮更多的東西旷祸。因?yàn)榉?wù)器需要隨時(shí)待命,它不清楚什么時(shí)候一個(gè)客戶端會(huì)連接到它讼昆。在java里面托享,我們可以通過java.net.ServerSocket
來表示一個(gè)服務(wù)器套接字。
ServerSocket和Socket不同浸赫,它需要等待來自客戶端的連接闰围。一旦有客戶端和其建立了連接,ServerSocket需要?jiǎng)?chuàng)建一個(gè)Socket來和客戶端進(jìn)行通信既峡。
ServerSocket有很多的構(gòu)造方法羡榴,我們拿其中的一個(gè)來舉例子。
public ServerSocket(int port, int backlog, InetAddress bindAddr) throws IOException;
new ServerSocket(8080, 1, InetAddress.getByName("127.0.0.1"));
- port表示端口
- backlog表示隊(duì)列的長度
- bindAddr表示地址
我們這兒還是看一個(gè)例子运敢。
- HttpServer表示一個(gè)服務(wù)器端入口校仑,提供了一個(gè)main方法,并一直在8080端口等待传惠,直到客戶端建立一個(gè)連接迄沫。這時(shí),服務(wù)器通過生成一個(gè)Socket來對(duì)此連接進(jìn)行處理卦方。
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;
}
}
}
}
- Request對(duì)象主要完成幾件事情
- 解析請(qǐng)求數(shù)據(jù)
- 解析uri(請(qǐng)求數(shù)據(jù)第一行)
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;
}
}
- Response主要是向客戶端發(fā)送文件內(nèi)容(如果請(qǐng)求的uri指向的文件存在)邢滑。
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();
}
}
}
總結(jié)
在看了上面的例子之后,我們驚奇地發(fā)現(xiàn)愿汰,在Java里面實(shí)現(xiàn)一個(gè)web服務(wù)器真TM容易困后,代碼也非常簡單和清晰!
既然我們能很簡單地實(shí)現(xiàn)web服務(wù)器衬廷,為啥我們還需要tomcat呢摇予?它又給我們帶來了哪些組件和特性呢,它又是怎么組裝這些組件的呢吗跋,后續(xù)章節(jié)我們將逐層扒開tomcat的各件漂亮的“大衣”侧戴。
這是我們后面將要分析的內(nèi)容,讓我們拭目以待跌宛!