摘要:原書中主要內(nèi)容是一步一步實現(xiàn)一個類似于的容器。圖一協(xié)議處于協(xié)議棧的應(yīng)用層,傳遞的內(nèi)容是報文,報文就相當于語言中的短語和句子用來表明意圖。類表示一次客戶端請求解析請求待實現(xiàn)解析待實現(xiàn)類表示返回值發(fā)送靜態(tài)頁面的相應(yīng)報文待實現(xiàn)。
前言
最近在讀《How Tomcat Works》,收獲頗豐,在編寫書中示例的過程中也踩了不少坑。不知你有沒有體會,編程就一門是“不試不知道,一試嚇一跳”的實踐藝術(shù)。所以我將將自己的實踐過程記錄下來并附上自己的思想過程編撰成文,望能拋磚引玉,引起大家思考。
原書中主要內(nèi)容是一步一步實現(xiàn)一個類似于Tomcat的Servlet容器。有點再造輪子的感覺,我也會根據(jù)書中章節(jié)并按照自己理解分步成文。
本文描述了一個簡單的Web服務(wù)器的實現(xiàn),這個服務(wù)器能接收瀏覽器請求,訪問本地的靜態(tài)HTML文件,如果文件不存在返回404頁面。這個瀏覽器只是一個示例,重點讓你了解Http請求到響應(yīng)過程的大致處理方法,對于細節(jié)沒有過多涉及。
基礎(chǔ)知識閱讀本文需要你先了解一下基礎(chǔ)知識:
Http協(xié)議。
Socket網(wǎng)絡(luò)編程。
1. Http協(xié)議“協(xié)議”廣義上說就是計算機相互交流的語言。Http協(xié)議就是網(wǎng)絡(luò)上千千萬萬瀏覽器和服務(wù)器交流的語言,瀏覽器通過Http協(xié)議向服務(wù)器發(fā)送請求,服務(wù)器通過同樣的協(xié)議回復(fù)瀏覽器。
【圖一】
Http協(xié)議處于TCP/IP協(xié)議棧的應(yīng)用層,Http傳遞的內(nèi)容是Http報文,報文就相當于語言中的“短語”和“句子”用來表明意圖。報文由一行行簡單的字符串組成,方便人們讀寫。
報文包括三個部分:起始行(star line)、首部(heads)、主體(body)
報文分為兩類:請求報文(request message)、響應(yīng)報文(response message)
報文實例:
請求報文:
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為起始行,其他為首部,沒有主體部分。
響應(yīng)報文:
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為首部,其余的為主體。
通過觀察請求和返回報文我們發(fā)現(xiàn)兩個關(guān)鍵點:
報文起始行和首部由行分割的ASCII文本,Http協(xié)議規(guī)定每一行由回車符(ASCII碼13)和換行符(ASCII碼10)表示結(jié)束。
一個空白行將實體和首部區(qū)分開來,返回報文的主體的就是HTML語言,瀏覽器就是通過返回的主體內(nèi)容渲染HTML語言展示請求內(nèi)容的,當然除了HTML語言之外,主體還可以返回其他字符和二進制內(nèi)容。
2. Socket網(wǎng)絡(luò)編程Http協(xié)議不僅規(guī)定了傳輸?shù)膬?nèi)容,還規(guī)定了用什么來傳輸,一門語言不能光有文字和語法,還要有傳播通道,例如空氣就是聲音的傳輸通道。
Http協(xié)議將傳輸?shù)墓ぷ鹘挥蒚CP協(xié)議負責,TCP協(xié)議位于TCP/IP協(xié)議棧的傳輸層,是很多上層應(yīng)用協(xié)議的傳輸方式。
TCP協(xié)議是面向連接的、保障型傳輸協(xié)議,一旦建立起TCP連接,客戶端和服務(wù)器端之間的報文交換就不會丟失、不會被破壞也不會在接收時錯序。
TCP協(xié)議一般由操作系統(tǒng)底層實現(xiàn),在Java中抽象為Socket接口供大家使用。
用代碼說話基礎(chǔ)知識介紹的差不多了,如果大家感興趣可以參考相應(yīng)的書籍。接下來讓我們用代碼說話。
一、 看似很簡單如果是只返回靜態(tài)Html,應(yīng)該很簡單吧。簡簡單單想了一下流程,初始化服務(wù)器——等待連接——解析請求——返回數(shù)據(jù)——關(guān)閉連接,搞定,大功告成。
1. 建個服務(wù)器骨架吧/** * 簡單的Web服務(wù)器 */ public class HttpServer { //定義一個資源存放路徑,用來存放靜態(tài)資源, public static final File WEB_ROOT = new File("d:webRoot"); public static void main(String[] args) { //創(chuàng)建服務(wù)器對象 HttpServer httpServer=new HttpServer(); //等待客戶端請求 httpServer.await(); } public void await() { try (ServerSocket serverSocket = new ServerSocket(8080)) { //創(chuàng)建socket嵌套字,監(jiān)聽8080端口。 serverProcess(serverSocket); } catch (IOException e) { e.printStackTrace(); } } private void serverProcess(ServerSocket serverSocket) { while (true) { //循環(huán)等待客戶端請求。 try (Socket socket = serverSocket.accept()) { InputStream input = socket.getInputStream(); OutputStream output = socket.getOutputStream(); //未完待續(xù)。。。 } catch (IOException e) { e.printStackTrace(); } catch (Exception e){ e.printStackTrace(); } } } }
非常簡單的Socket服務(wù)器骨架就這樣建好了,我們就可以接受客戶端請求了,這里需要注意的是每一個通過serverSocket.accept()從客戶端獲取socket處理完后都會被close。
2. 抽象一下“請求”和“響應(yīng)”有了服務(wù)器,接下來我們需要接收請求、處理請求、將處理結(jié)果返回給客戶端。根據(jù)領(lǐng)域驅(qū)動原則,我們將名詞抽象為類,動詞抽象為類的行為也就是方法。
Request類:
/** * 表示一次客戶端請求 */ public class Request { private InputStream input; private String uri; public Request(InputStream input) { this.input = input; } /** * 解析請求 */ public void parse() { //待實現(xiàn) } /** * 解析URL * @param requestString * @return */ private String parseUri(String requestString) { //待實現(xiàn) return null; } public String getUri() { return uri; } }
Response類:
/** * 表示返回值 */ public class Response { private OutputStream output; public Response1(OutputStream output) { this.output = output; } /** * 發(fā)送靜態(tài)頁面的相應(yīng)報文 * @throws IOException */ public void sendStaticResource() throws IOException { //待實現(xiàn)。 } }3. 實現(xiàn)Request和Response中的方法。
類和方法已經(jīng)定義的差不多了,現(xiàn)在我們來實現(xiàn)。
Request類:
/** * 表示請求值 */ 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類:
/** * 表示返回值 */ 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]; //讀取訪問地址請求的文件 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()); }
注:原書代碼沒有返回響應(yīng)頭部,測試發(fā)現(xiàn)瀏覽器不能識別這樣的響應(yīng)報文。
4. 補全服務(wù)器方法。public class HttpServer { //定義一個資源存放路徑,用來存放靜態(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. 見證奇跡的時候到了,運行一下。
首先在D:/webRoot文件夾建立index.html文件
寫入:
hello world!
啟動HttpService,在瀏覽器輸入http://localhost:8080/index.html,但你心心念的等待熟悉的“hello world!”頁面的時候,你會等的花兒都謝了。
二、 問題在哪里? 1. 調(diào)試吧,少年頁面并沒有顯示,問題出在哪里?進入debug調(diào)試模式,發(fā)現(xiàn)方法阻塞在while((i = input.read(buffer))!=-1)語句上,以往我們讀取輸入流的方法都這樣寫也沒有問題,為什么到了Socket就阻塞了呢?原因其實很簡單,客戶打開了一個socket的輸出流向服務(wù)器發(fā)送消息,服務(wù)器端通過socket的輸入流讀取消息,但是服務(wù)器并不知道客戶端消息的結(jié)尾,只要socket不關(guān)閉,服務(wù)器一旦讀取了所有可用內(nèi)容,read方法就要一直阻塞等待新的可用內(nèi)容(超期時間之后也能返回),而此時的客戶端也一直在等待服務(wù)器的返回,相互等待,死鎖了??磥肀镜匚募骱途W(wǎng)絡(luò)流處理方式不同。
【圖二】
翻看書中示例代碼是這樣寫的:
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書中一次性讀取了2048長度的字節(jié)數(shù)組,無論請求內(nèi)容是否結(jié)束都不會再去讀第二遍,避免讀取時遇到不可用情況造成的阻塞。
但是這依然有兩個問題:如果字符請求內(nèi)容大于2048長度字節(jié)數(shù)組的內(nèi)容,請求內(nèi)容讀取不全。
如果瀏覽器創(chuàng)建一個socket但是并不寫入任何內(nèi)容,服務(wù)器首次read的時候仍會被阻塞,不讀取不知道有沒有內(nèi)容,一旦發(fā)現(xiàn)沒有可用內(nèi)容就被阻塞了。(測試中Chrome就會發(fā)送空socket)
問題2還好,有可能瀏覽器通過發(fā)送空socket維持長連接,需要根據(jù)http協(xié)議決定如何關(guān)閉socket。但是對于問題1就比較嚴重了,雖然我們的示例代碼只需要讀取起始行從中取出URL地址訪問本地靜態(tài)資源,但是一個web服務(wù)器服務(wù)讀取所有請求內(nèi)容確實有點說不過去了。這個問題后續(xù)還需要解決。
2. 再試試,有沒有奇跡出現(xiàn)替換上面的代碼,再次重復(fù)剛剛的流程,好了,瀏覽器終于出現(xiàn)“hello world!”,見證奇跡。
三、 你以為這樣就完了?終于,人生中第一個web服務(wù)器就這樣誕生了!當我難掩激動的用各個瀏覽器測試的時候,又發(fā)現(xiàn)的一個問題,一旦我用Chrome訪問一次,再用其他瀏覽器訪問就會卡死。哎,好吧,沒完了。
1. 繼續(xù)debug經(jīng)過debug發(fā)現(xiàn),Chrome每次發(fā)送一次socket并收到服務(wù)器相應(yīng)之后,都會發(fā)送一個新的空socket,socket沒有寫入任何內(nèi)容,此時服務(wù)器就會阻塞在對這個空socket的讀取中。直到瀏覽器再次向服務(wù)器發(fā)送請求,才會向這個空socket寫入內(nèi)容,服務(wù)器阻塞才會結(jié)束,然后繼續(xù)重復(fù)以上的處理過程,只要Chrome瀏覽器發(fā)送一次請求,服務(wù)器就會阻塞與空socket的讀取,無法為其他瀏覽器服務(wù)。
【圖三】
2. 飯要一口一口吃除了上面提到的兩個問題還有其他問題,比如socket關(guān)閉時機問題,響應(yīng)主體文字編碼問(現(xiàn)在都是英文還好,中文就會出現(xiàn)亂碼)等等。畢竟http協(xié)議也是比較復(fù)雜的,有很多規(guī)則需要實現(xiàn)。但是本文的內(nèi)容就先到這了,我們實現(xiàn)了完成一個簡單服務(wù)器的目標。
后記本文到此結(jié)束了,參照《How Tomcat Works》第一章內(nèi)容,加上自己的理解和實踐,原書中沒有涉及我調(diào)試中拋出的兩個問題,關(guān)于這兩個問題我會在以后的文章中解決。其實讀書的的時候覺得很簡單,也沒有想到真正寫代碼的時候出現(xiàn)這些問題,所以希望大家讀書過程中多實踐,可以加深理解。作為專欄的第一篇文章,寫的格外用心,但是也難免出現(xiàn)紕漏,望大家指摘。
源碼文中源碼地址:https://github.com/TmTse/tiny...
參考《深入剖析Tomcat》
《Http權(quán)威指南》
《TCP/IP詳解卷1:協(xié)議》
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/76860.html
摘要:注本文使用規(guī)范是規(guī)范中的一個接口,我們可以自己實現(xiàn)這個接口在方法中實現(xiàn)自己的業(yè)務(wù)邏輯。我們只是實現(xiàn)一個簡單的容器示例,所以和其他方法留待以后實現(xiàn)。運行一下實現(xiàn)首先編寫一個自己的實現(xiàn)類。 前言 經(jīng)過上一篇文章《一步一步實現(xiàn)Tomcat——實現(xiàn)一個簡單的Web服務(wù)器》,我們實現(xiàn)了一個簡單的Web服務(wù)器,可以響應(yīng)瀏覽器請求顯示靜態(tài)Html頁面,本文更進一步,實現(xiàn)一個Servlet容器,我們不...
摘要:一步一步實現(xiàn)程序信息管理系統(tǒng)一步一步實現(xiàn)程序信息管理系統(tǒng)在程序中特別是信息管理系統(tǒng),登陸功能必須有而且特別重要。每一個學習程序開發(fā)或以后工作中,都會遇到實現(xiàn)登陸功能的需求。本篇記錄一下登陸功能的前端界面的實現(xiàn)。一步一步實現(xiàn)web程序信息管理系統(tǒng) 在web程序中特別是信息管理系統(tǒng),登陸功能必須有而且特別重要。每一個學習程序開發(fā)或以后工作中,都會遇到實現(xiàn)登陸功能的需求。而登陸功能最終提供給客戶或...
摘要:環(huán)境配置運行環(huán)境安裝配置數(shù)據(jù)庫下載安裝下載地址牢記安裝過程中設(shè)置的用戶的密碼安裝選擇版本的安裝配置數(shù)據(jù)庫驅(qū)動教程前提開發(fā)環(huán)境參考環(huán)境配置文檔基礎(chǔ)知識基本語法協(xié)議基礎(chǔ)知識只需了解請求即可基礎(chǔ)的等。 **寒假的時候老師讓寫個簡單的JavaEE教程給學弟or學妹看,于是寫了下面的內(nèi)容。發(fā)表到這個地方以防丟失。。。因為寫的時候用的是word,直接復(fù)制過來格式有點亂。。。所以不要在意細節(jié)了。。...
閱讀 2154·2023-04-26 00:23
閱讀 828·2021-09-08 09:45
閱讀 2448·2019-08-28 18:20
閱讀 2555·2019-08-26 13:51
閱讀 1608·2019-08-26 10:32
閱讀 1405·2019-08-26 10:24
閱讀 2042·2019-08-26 10:23
閱讀 2210·2019-08-23 18:10