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