手寫一個(gè)web服務(wù)器 !
前言
作為后端開發(fā)人員,在實(shí)際的工作中我們會(huì)非常高頻地使用到web服務(wù)器。而tomcat作為web服務(wù)器領(lǐng)域中舉足輕重的一個(gè)web框架,又是不能不學(xué)習(xí)和了解的。
tomcat其實(shí)是一個(gè)web框架,那么其內(nèi)部是怎么實(shí)現(xiàn)的呢?如果不用tomcat我們能自己實(shí)現(xiàn)一個(gè)web服務(wù)器嗎?
首先,tomcat內(nèi)部的實(shí)現(xiàn)是非常復(fù)雜的,也有非常多的各類組件,我們?cè)诤罄m(xù)章節(jié)會(huì)深入地了解。其次,本章我們將自己實(shí)現(xiàn)一個(gè)web服務(wù)器的。
http協(xié)議簡(jiǎn)介
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
HTTP?Response?Example ?
Welcome?to?Brainy?Software
第一行,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"));
參數(shù)如下:
port 表示端口 backlog 表示隊(duì)列的長(zhǎng)度 bindAddr 表示地址
HttpServer
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??????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"?+
??????????"File?Not?Found
";
????????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ù)器真容易,代碼也非常簡(jiǎn)單和清晰!
題外話: 目前小哈正在個(gè)人博客(新搭建的網(wǎng)站,域名就是犬小哈的拼音)?www.quanxiaoha.com?上更新《Go語言教程》、《Gin Web框架教程》,畢竟Go自帶天然的并發(fā)優(yōu)勢(shì),后端的同學(xué)還是要學(xué)一下的,這個(gè)教程系列小哈會(huì)一直更新下去,歡迎小伙伴們?cè)L問哦~
END
有熱門推薦?
1.?扔掉okhttp、httpClient,來試試這款輕量級(jí)HTTP客戶端神器?
2.?SpringCloud中Zuul網(wǎng)關(guān)原理及其配置,看它就夠了!
最近面試BAT,整理一份面試資料《Java面試BATJ通關(guān)手冊(cè)》,覆蓋了Java核心技術(shù)、JVM、Java并發(fā)、SSM、微服務(wù)、數(shù)據(jù)庫、數(shù)據(jù)結(jié)構(gòu)等等。
獲取方式:點(diǎn)“在看”,關(guān)注公眾號(hào)并回復(fù)?Java?領(lǐng)取,更多內(nèi)容陸續(xù)奉上。
文章有幫助的話,在看,轉(zhuǎn)發(fā)吧。
謝謝支持喲 (*^__^*)

