摘要:原書(shū)中主要內(nèi)容是一步一步實(shí)現(xiàn)一個(gè)類(lèi)似于的容器。圖一協(xié)議處于協(xié)議棧的應(yīng)用層,傳遞的內(nèi)容是報(bào)文,報(bào)文就相當(dāng)于語(yǔ)言中的短語(yǔ)和句子用來(lái)表明意圖。類(lèi)表示一次客戶(hù)端請(qǐng)求解析請(qǐng)求待實(shí)現(xiàn)解析待實(shí)現(xiàn)類(lèi)表示返回值發(fā)送靜態(tài)頁(yè)面的相應(yīng)報(bào)文待實(shí)現(xiàn)。
前言
最近在讀《How Tomcat Works》,收獲頗豐,在編寫(xiě)書(shū)中示例的過(guò)程中也踩了不少坑。不知你有沒(méi)有體會(huì),編程就一門(mén)是“不試不知道,一試嚇一跳”的實(shí)踐藝術(shù)。所以我將將自己的實(shí)踐過(guò)程記錄下來(lái)并附上自己的思想過(guò)程編撰成文,望能拋磚引玉,引起大家思考。
原書(shū)中主要內(nèi)容是一步一步實(shí)現(xiàn)一個(gè)類(lèi)似于Tomcat的Servlet容器。有點(diǎn)再造輪子的感覺(jué),我也會(huì)根據(jù)書(shū)中章節(jié)并按照自己理解分步成文。
本文描述了一個(gè)簡(jiǎn)單的Web服務(wù)器的實(shí)現(xiàn),這個(gè)服務(wù)器能接收瀏覽器請(qǐng)求,訪問(wèn)本地的靜態(tài)HTML文件,如果文件不存在返回404頁(yè)面。這個(gè)瀏覽器只是一個(gè)示例,重點(diǎn)讓你了解Http請(qǐng)求到響應(yīng)過(guò)程的大致處理方法,對(duì)于細(xì)節(jié)沒(méi)有過(guò)多涉及。
基礎(chǔ)知識(shí)閱讀本文需要你先了解一下基礎(chǔ)知識(shí):
Http協(xié)議。
Socket網(wǎng)絡(luò)編程。
1. Http協(xié)議“協(xié)議”廣義上說(shuō)就是計(jì)算機(jī)相互交流的語(yǔ)言。Http協(xié)議就是網(wǎng)絡(luò)上千千萬(wàn)萬(wàn)瀏覽器和服務(wù)器交流的語(yǔ)言,瀏覽器通過(guò)Http協(xié)議向服務(wù)器發(fā)送請(qǐng)求,服務(wù)器通過(guò)同樣的協(xié)議回復(fù)瀏覽器。
【圖一】
Http協(xié)議處于TCP/IP協(xié)議棧的應(yīng)用層,Http傳遞的內(nèi)容是Http報(bào)文,報(bào)文就相當(dāng)于語(yǔ)言中的“短語(yǔ)”和“句子”用來(lái)表明意圖。報(bào)文由一行行簡(jiǎn)單的字符串組成,方便人們讀寫(xiě)。
報(bào)文包括三個(gè)部分:起始行(star line)、首部(heads)、主體(body)
報(bào)文分為兩類(lèi):請(qǐng)求報(bào)文(request message)、響應(yīng)報(bào)文(response message)
報(bào)文實(shí)例:
請(qǐng)求報(bào)文:
GET / HTTP/1.1 Host: www.baidu.com User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:61.0) Gecko/20100101 Firefox/61.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2 Accept-Encoding: gzip, deflate, br Cookie: BAIDUID=DF436E68F85BD96DE35AEA9DC97FB19D:FG=1; BIDUPSID=DF436E68F85BD96DE35AEA9DC97FB19D; PSTM=1535160357; BD_UPN=1352; delPer=0; BD_HOME=0; H_PS_PSSID=1442_21097_26350 Connection: keep-alive
GET / HTTP/1.1為起始行,其他為首部,沒(méi)有主體部分。
響應(yīng)報(bào)文:
HTTP/1.1 200 OK Bdpagetype: 1 Bdqid: 0xc317983b0005c39e Cache-Control: private Connection: Keep-Alive Content-Encoding: gzip Content-Type: text/html Cxy_all: baidu+3d05fe4a15be8fad069c0f37a523ec5e Date: Sun, 26 Aug 2018 06:39:25 GMT Expires: Sun, 26 Aug 2018 06:39:09 GMT Server: BWS/1.1 Set-Cookie: delPer=0; expires=Tue, 18-Aug-2048 06:39:09 GMT Set-Cookie: BDSVRTM=0; path=/ Set-Cookie: BD_HOME=0; path=/ Set-Cookie: H_PS_PSSID=1442_21097_26350; path=/; domain=.baidu.com Strict-Transport-Security: max-age=172800 Vary: Accept-Encoding X-Ua-Compatible: IE=Edge,chrome=1 Transfer-Encoding: chunked百度一下,你就知道 太多了,省略...
HTTP/1.1 200 OK為起始行,Bdpagetype: 1到Transfer-Encoding: chunked為首部,其余的為主體。
通過(guò)觀察請(qǐng)求和返回報(bào)文我們發(fā)現(xiàn)兩個(gè)關(guān)鍵點(diǎn):
報(bào)文起始行和首部由行分割的ASCII文本,Http協(xié)議規(guī)定每一行由回車(chē)符(ASCII碼13)和換行符(ASCII碼10)表示結(jié)束。
一個(gè)空白行將實(shí)體和首部區(qū)分開(kāi)來(lái),返回報(bào)文的主體的就是HTML語(yǔ)言,瀏覽器就是通過(guò)返回的主體內(nèi)容渲染HTML語(yǔ)言展示請(qǐng)求內(nèi)容的,當(dāng)然除了HTML語(yǔ)言之外,主體還可以返回其他字符和二進(jìn)制內(nèi)容。
2. Socket網(wǎng)絡(luò)編程Http協(xié)議不僅規(guī)定了傳輸?shù)膬?nèi)容,還規(guī)定了用什么來(lái)傳輸,一門(mén)語(yǔ)言不能光有文字和語(yǔ)法,還要有傳播通道,例如空氣就是聲音的傳輸通道。
Http協(xié)議將傳輸?shù)墓ぷ鹘挥蒚CP協(xié)議負(fù)責(zé),TCP協(xié)議位于TCP/IP協(xié)議棧的傳輸層,是很多上層應(yīng)用協(xié)議的傳輸方式。
TCP協(xié)議是面向連接的、保障型傳輸協(xié)議,一旦建立起TCP連接,客戶(hù)端和服務(wù)器端之間的報(bào)文交換就不會(huì)丟失、不會(huì)被破壞也不會(huì)在接收時(shí)錯(cuò)序。
TCP協(xié)議一般由操作系統(tǒng)底層實(shí)現(xiàn),在Java中抽象為Socket接口供大家使用。
用代碼說(shuō)話(huà)基礎(chǔ)知識(shí)介紹的差不多了,如果大家感興趣可以參考相應(yīng)的書(shū)籍。接下來(lái)讓我們用代碼說(shuō)話(huà)。
一、 看似很簡(jiǎn)單如果是只返回靜態(tài)Html,應(yīng)該很簡(jiǎn)單吧。簡(jiǎn)簡(jiǎn)單單想了一下流程,初始化服務(wù)器——等待連接——解析請(qǐng)求——返回?cái)?shù)據(jù)——關(guān)閉連接,搞定,大功告成。
1. 建個(gè)服務(wù)器骨架吧/** * 簡(jiǎn)單的Web服務(wù)器 */ public class HttpServer { //定義一個(gè)資源存放路徑,用來(lái)存放靜態(tài)資源, public static final File WEB_ROOT = new File("d:webRoot"); public static void main(String[] args) { //創(chuàng)建服務(wù)器對(duì)象 HttpServer httpServer=new HttpServer(); //等待客戶(hù)端請(qǐng)求 httpServer.await(); } public void await() { try (ServerSocket serverSocket = new ServerSocket(8080)) { //創(chuàng)建socket嵌套字,監(jiān)聽(tīng)8080端口。 serverProcess(serverSocket); } catch (IOException e) { e.printStackTrace(); } } private void serverProcess(ServerSocket serverSocket) { while (true) { //循環(huán)等待客戶(hù)端請(qǐng)求。 try (Socket socket = serverSocket.accept()) { InputStream input = socket.getInputStream(); OutputStream output = socket.getOutputStream(); //未完待續(xù)。。。 } catch (IOException e) { e.printStackTrace(); } catch (Exception e){ e.printStackTrace(); } } } }
非常簡(jiǎn)單的Socket服務(wù)器骨架就這樣建好了,我們就可以接受客戶(hù)端請(qǐng)求了,這里需要注意的是每一個(gè)通過(guò)serverSocket.accept()從客戶(hù)端獲取socket處理完后都會(huì)被close。
2. 抽象一下“請(qǐng)求”和“響應(yīng)”有了服務(wù)器,接下來(lái)我們需要接收請(qǐng)求、處理請(qǐng)求、將處理結(jié)果返回給客戶(hù)端。根據(jù)領(lǐng)域驅(qū)動(dòng)原則,我們將名詞抽象為類(lèi),動(dòng)詞抽象為類(lèi)的行為也就是方法。
Request類(lèi):
/** * 表示一次客戶(hù)端請(qǐng)求 */ public class Request { private InputStream input; private String uri; public Request(InputStream input) { this.input = input; } /** * 解析請(qǐng)求 */ public void parse() { //待實(shí)現(xiàn) } /** * 解析URL * @param requestString * @return */ private String parseUri(String requestString) { //待實(shí)現(xiàn) return null; } public String getUri() { return uri; } }
Response類(lèi):
/** * 表示返回值 */ public class Response { private OutputStream output; public Response1(OutputStream output) { this.output = output; } /** * 發(fā)送靜態(tài)頁(yè)面的相應(yīng)報(bào)文 * @throws IOException */ public void sendStaticResource() throws IOException { //待實(shí)現(xiàn)。 } }3. 實(shí)現(xiàn)Request和Response中的方法。
類(lèi)和方法已經(jīng)定義的差不多了,現(xiàn)在我們來(lái)實(shí)現(xiàn)。
Request類(lèi):
/** * 表示請(qǐng)求值 */ public class Request { private InputStream input; private String uri; public Request(InputStream input) { this.input = input; } public void parse() { StringBuffer request = new StringBuffer(2048); int i; byte[] buffer = new byte[2048]; try { while((i = input.read(buffer))!=-1){ for (int j=0; j index1) return requestString.substring(index1 + 1, index2); } return null; } public String getUri() { return uri; } }
Response類(lèi):
/** * 表示返回值 */ public class Response { private static final int BUFFER_SIZE = 1024; private Request request; private 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]; //讀取訪問(wèn)地址請(qǐng)求的文件 File file = new File(HttpServer.WEB_ROOT, request.getUri()); try (FileInputStream fis = new FileInputStream(file)){ if (file.exists()) { //如果文件存在 //添加相應(yīng)頭。 StringBuilder heads=new StringBuilder("HTTP/1.1 200 OK "); heads.append("Content-Type: text/html "); //頭部 StringBuilder body=new StringBuilder(); //讀取相應(yīng)主體 int len ; while ((len=fis.read(bytes, 0, BUFFER_SIZE)) != -1) { body.append(new String(bytes,0,len)); } //添加Content-Length heads.append(String.format("Content-Length: %d ",body.toString().getBytes().length)); heads.append(" "); output.write(heads.toString().getBytes()); output.write(body.toString().getBytes()); } else { response404(output); } }catch (FileNotFoundException e){ response404(output); } } private void response404(OutputStream output) throws IOException { StringBuilder response=new StringBuilder(); response.append("HTTP/1.1 404 File Not Found "); response.append("Content-Type: text/html "); response.append("Content-Length: 23 "); response.append(" "); response.append("File Not Found
"); output.write(response.toString().getBytes()); }
注:原書(shū)代碼沒(méi)有返回響應(yīng)頭部,測(cè)試發(fā)現(xiàn)瀏覽器不能識(shí)別這樣的響應(yīng)報(bào)文。
4. 補(bǔ)全服務(wù)器方法。public class HttpServer { //定義一個(gè)資源存放路徑,用來(lái)存放靜態(tài)資源, static final File WEB_ROOT = new File("d:webRoot"); public static void main(String[] args) { HttpServer httpServer=new HttpServer(); httpServer.await(); } public void await() { try (ServerSocket serverSocket = new ServerSocket(8080)) { serverProcess(serverSocket); } catch (IOException e) { e.printStackTrace(); } } private void serverProcess(ServerSocket serverSocket) { while (true) { try (Socket socket = serverSocket.accept()) { System.out.println(socket.hashCode()); InputStream input = socket.getInputStream(); OutputStream output = socket.getOutputStream(); Request request = new Request(input); request.parse(); Response response = new Response(output); response.setRequest(request); response.sendStaticResource(); } catch (IOException e) { e.printStackTrace(); } catch (Exception e){ e.printStackTrace(); } } } }5. 見(jiàn)證奇跡的時(shí)候到了,運(yùn)行一下。
首先在D:/webRoot文件夾建立index.html文件
寫(xiě)入:
hello world!
啟動(dòng)HttpService,在瀏覽器輸入http://localhost:8080/index.html,但你心心念的等待熟悉的“hello world!”頁(yè)面的時(shí)候,你會(huì)等的花兒都謝了。
二、 問(wèn)題在哪里? 1. 調(diào)試吧,少年頁(yè)面并沒(méi)有顯示,問(wèn)題出在哪里?進(jìn)入debug調(diào)試模式,發(fā)現(xiàn)方法阻塞在while((i = input.read(buffer))!=-1)語(yǔ)句上,以往我們讀取輸入流的方法都這樣寫(xiě)也沒(méi)有問(wèn)題,為什么到了Socket就阻塞了呢?原因其實(shí)很簡(jiǎn)單,客戶(hù)打開(kāi)了一個(gè)socket的輸出流向服務(wù)器發(fā)送消息,服務(wù)器端通過(guò)socket的輸入流讀取消息,但是服務(wù)器并不知道客戶(hù)端消息的結(jié)尾,只要socket不關(guān)閉,服務(wù)器一旦讀取了所有可用內(nèi)容,read方法就要一直阻塞等待新的可用內(nèi)容(超期時(shí)間之后也能返回),而此時(shí)的客戶(hù)端也一直在等待服務(wù)器的返回,相互等待,死鎖了??磥?lái)本地文件流和網(wǎng)絡(luò)流處理方式不同。
【圖二】
翻看書(shū)中示例代碼是這樣寫(xiě)的:
public void parse() { StringBuilder request = new StringBuilder(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書(shū)中一次性讀取了2048長(zhǎng)度的字節(jié)數(shù)組,無(wú)論請(qǐng)求內(nèi)容是否結(jié)束都不會(huì)再去讀第二遍,避免讀取時(shí)遇到不可用情況造成的阻塞。
但是這依然有兩個(gè)問(wèn)題:如果字符請(qǐng)求內(nèi)容大于2048長(zhǎng)度字節(jié)數(shù)組的內(nèi)容,請(qǐng)求內(nèi)容讀取不全。
如果瀏覽器創(chuàng)建一個(gè)socket但是并不寫(xiě)入任何內(nèi)容,服務(wù)器首次read的時(shí)候仍會(huì)被阻塞,不讀取不知道有沒(méi)有內(nèi)容,一旦發(fā)現(xiàn)沒(méi)有可用內(nèi)容就被阻塞了。(測(cè)試中Chrome就會(huì)發(fā)送空socket)
問(wèn)題2還好,有可能瀏覽器通過(guò)發(fā)送空socket維持長(zhǎng)連接,需要根據(jù)http協(xié)議決定如何關(guān)閉socket。但是對(duì)于問(wèn)題1就比較嚴(yán)重了,雖然我們的示例代碼只需要讀取起始行從中取出URL地址訪問(wèn)本地靜態(tài)資源,但是一個(gè)web服務(wù)器服務(wù)讀取所有請(qǐng)求內(nèi)容確實(shí)有點(diǎn)說(shuō)不過(guò)去了。這個(gè)問(wèn)題后續(xù)還需要解決。
2. 再試試,有沒(méi)有奇跡出現(xiàn)替換上面的代碼,再次重復(fù)剛剛的流程,好了,瀏覽器終于出現(xiàn)“hello world!”,見(jiàn)證奇跡。
三、 你以為這樣就完了?終于,人生中第一個(gè)web服務(wù)器就這樣誕生了!當(dāng)我難掩激動(dòng)的用各個(gè)瀏覽器測(cè)試的時(shí)候,又發(fā)現(xiàn)的一個(gè)問(wèn)題,一旦我用Chrome訪問(wèn)一次,再用其他瀏覽器訪問(wèn)就會(huì)卡死。哎,好吧,沒(méi)完了。
1. 繼續(xù)debug經(jīng)過(guò)debug發(fā)現(xiàn),Chrome每次發(fā)送一次socket并收到服務(wù)器相應(yīng)之后,都會(huì)發(fā)送一個(gè)新的空socket,socket沒(méi)有寫(xiě)入任何內(nèi)容,此時(shí)服務(wù)器就會(huì)阻塞在對(duì)這個(gè)空socket的讀取中。直到瀏覽器再次向服務(wù)器發(fā)送請(qǐng)求,才會(huì)向這個(gè)空socket寫(xiě)入內(nèi)容,服務(wù)器阻塞才會(huì)結(jié)束,然后繼續(xù)重復(fù)以上的處理過(guò)程,只要Chrome瀏覽器發(fā)送一次請(qǐng)求,服務(wù)器就會(huì)阻塞與空socket的讀取,無(wú)法為其他瀏覽器服務(wù)。
【圖三】
2. 飯要一口一口吃除了上面提到的兩個(gè)問(wèn)題還有其他問(wèn)題,比如socket關(guān)閉時(shí)機(jī)問(wèn)題,響應(yīng)主體文字編碼問(wèn)(現(xiàn)在都是英文還好,中文就會(huì)出現(xiàn)亂碼)等等。畢竟http協(xié)議也是比較復(fù)雜的,有很多規(guī)則需要實(shí)現(xiàn)。但是本文的內(nèi)容就先到這了,我們實(shí)現(xiàn)了完成一個(gè)簡(jiǎn)單服務(wù)器的目標(biāo)。
后記本文到此結(jié)束了,參照《How Tomcat Works》第一章內(nèi)容,加上自己的理解和實(shí)踐,原書(shū)中沒(méi)有涉及我調(diào)試中拋出的兩個(gè)問(wèn)題,關(guān)于這兩個(gè)問(wèn)題我會(huì)在以后的文章中解決。其實(shí)讀書(shū)的的時(shí)候覺(jué)得很簡(jiǎn)單,也沒(méi)有想到真正寫(xiě)代碼的時(shí)候出現(xiàn)這些問(wèn)題,所以希望大家讀書(shū)過(guò)程中多實(shí)踐,可以加深理解。作為專(zhuān)欄的第一篇文章,寫(xiě)的格外用心,但是也難免出現(xiàn)紕漏,望大家指摘。
源碼文中源碼地址:https://github.com/TmTse/tiny...
參考《深入剖析Tomcat》
《Http權(quán)威指南》
《TCP/IP詳解卷1:協(xié)議》
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://m.hztianpu.com/yun/76860.html
摘要:注本文使用規(guī)范是規(guī)范中的一個(gè)接口,我們可以自己實(shí)現(xiàn)這個(gè)接口在方法中實(shí)現(xiàn)自己的業(yè)務(wù)邏輯。我們只是實(shí)現(xiàn)一個(gè)簡(jiǎn)單的容器示例,所以和其他方法留待以后實(shí)現(xiàn)。運(yùn)行一下實(shí)現(xiàn)首先編寫(xiě)一個(gè)自己的實(shí)現(xiàn)類(lèi)。 前言 經(jīng)過(guò)上一篇文章《一步一步實(shí)現(xiàn)Tomcat——實(shí)現(xiàn)一個(gè)簡(jiǎn)單的Web服務(wù)器》,我們實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的Web服務(wù)器,可以響應(yīng)瀏覽器請(qǐng)求顯示靜態(tài)Html頁(yè)面,本文更進(jìn)一步,實(shí)現(xiàn)一個(gè)Servlet容器,我們不...
摘要:一步一步實(shí)現(xiàn)程序信息管理系統(tǒng)一步一步實(shí)現(xiàn)程序信息管理系統(tǒng)在程序中特別是信息管理系統(tǒng),登陸功能必須有而且特別重要。每一個(gè)學(xué)習(xí)程序開(kāi)發(fā)或以后工作中,都會(huì)遇到實(shí)現(xiàn)登陸功能的需求。本篇記錄一下登陸功能的前端界面的實(shí)現(xiàn)。一步一步實(shí)現(xiàn)web程序信息管理系統(tǒng) 在web程序中特別是信息管理系統(tǒng),登陸功能必須有而且特別重要。每一個(gè)學(xué)習(xí)程序開(kāi)發(fā)或以后工作中,都會(huì)遇到實(shí)現(xiàn)登陸功能的需求。而登陸功能最終提供給客戶(hù)或...
摘要:環(huán)境配置運(yùn)行環(huán)境安裝配置數(shù)據(jù)庫(kù)下載安裝下載地址牢記安裝過(guò)程中設(shè)置的用戶(hù)的密碼安裝選擇版本的安裝配置數(shù)據(jù)庫(kù)驅(qū)動(dòng)教程前提開(kāi)發(fā)環(huán)境參考環(huán)境配置文檔基礎(chǔ)知識(shí)基本語(yǔ)法協(xié)議基礎(chǔ)知識(shí)只需了解請(qǐng)求即可基礎(chǔ)的等。 **寒假的時(shí)候老師讓寫(xiě)個(gè)簡(jiǎn)單的JavaEE教程給學(xué)弟or學(xué)妹看,于是寫(xiě)了下面的內(nèi)容。發(fā)表到這個(gè)地方以防丟失。。。因?yàn)閷?xiě)的時(shí)候用的是word,直接復(fù)制過(guò)來(lái)格式有點(diǎn)亂。。。所以不要在意細(xì)節(jié)了。。...
閱讀 2245·2023-04-26 00:23
閱讀 922·2021-09-08 09:45
閱讀 2511·2019-08-28 18:20
閱讀 2644·2019-08-26 13:51
閱讀 1675·2019-08-26 10:32
閱讀 1463·2019-08-26 10:24
閱讀 2099·2019-08-26 10:23
閱讀 2268·2019-08-23 18:10