前言
上一篇隨筆【雜談】一個回車下去,瀏覽器做了什麼?講了瀏覽器的處理,這裡再用一個例子講解一下,也不算講解,算是梳理一下服務端處理瀏覽器請求的過程。當然實際過程要比這複雜多了。下文的例子,其實就是《How Tomcat Works》這本書的第一個例子,感興趣的可以去看這本書。不過書上的例子有問題,我下文中會提到。
注:此專案不需要用tomcat,純Java底層程式碼寫就可以了。
概述
程式有三個類HttpServer,Request,Response。
HttpServer => 負責監聽socket連線,建立Request、Response物件
Request => 用於獲取請求資訊的URI(利用Socket的InputStream),這裡URI就是靜態網頁檔案的相對路徑
Response => 用於傳送響應資料包(利用Request獲取請求資訊,利用OutputStream寫出資料)
程式包圖:
完整程式碼
由於貼完整程式碼都會使篇幅略顯過長,所以下面都摺疊起來了,看客可以逐個展開檢視。
HttpServer.java
package com.wze.ex01.pyrmont; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.InetAddress; import java.net.ServerSocket; import java.net.Socket; public class HttpServer { public static final String WEB_ROOT = System.getProperty("user.dir") + File.separator + "webroot"; private static final String SHUTDOWN_COMMAND = "/SHUTDOWN"; private boolean shutdown = false; public static void main(String[] args) { System.out.println(WEB_ROOT); HttpServer server = new HttpServer(); server.await(); } public void await() { ServerSocket serverSocket = null; int port = 8080; try { //之所以要繫結監聽的IP地址,是因為一個電腦可能有多個網路卡 serverSocket = new ServerSocket(port, 1, InetAddress.getByName("127.0.0.1")); } catch (IOException e) { e.printStackTrace(); //如果繫結失敗,那麼這個程式也就沒有執行下去的必要了。 System.exit(1); } while(!shutdown) { Socket socket = null; InputStream input = null; OutputStream output = null; try { //接收一個請求,處理完畢後關閉連線 socket = serverSocket.accept(); input = socket.getInputStream(); output = socket.getOutputStream(); Request request = new Request(input); request.parse(); Response response = new Response(output); response.setRequest(request); response.sendStaticResource(); socket.close(); shutdown = request.getUri().equals(SHUTDOWN_COMMAND); } catch (Exception e) { e.printStackTrace(); continue; } } } }
Request.java
package com.wze.ex01.pyrmont; import java.io.IOException; import java.io.InputStream; public class Request { private InputStream input; private String uri; public Request(InputStream input) { this.input = input; } public void parse() { //之所以是大小是2048,是因為請求行的大小一般就是2048 StringBuffer request = new StringBuffer(2048); int i; byte[] buffer = new byte[2048]; try { i = input.read(buffer); //讀入資料到buffer,並返回請求行的實際長度 } catch (IOException e) { e.printStackTrace(); i = -1; } for(int j = 0; j < i; j++) { request.append((char)buffer[j]); } System.out.println(request.toString()); uri = parseUri(request.toString()); //從請求行中把uri取出來 System.out.println(uri); } /** * 獲取請求行中的uri * * 請求行格式:Method URI Version * 用空格做分隔符 * @param requestString * @return */ private String parseUri(String requestString) { int index1, index2; index1 = requestString.indexOf(' '); if(index1 != -1) { index2 = requestString.indexOf(' ', index1+1); System.out.println(index1 + " " + index2); if(index2 > index1) return requestString.substring(index1 + 1, index2); } return null; } public String getUri() { return uri; } }
Response.java
package com.wze.ex01.pyrmont; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.OutputStream; 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()); System.out.println(file); if(file.exists()) { //如果檔案存在,則讀取到緩衝陣列,再利用socket的outputstream寫出資料 long contentLength = file.length(); String successMessage = "HTTP/1.1 200 success\r\n" + "Content-Type:text/html\r\n" + "Content-Length:"+contentLength +"\r\n" + "\r\n"; output.write(successMessage.getBytes()); fis = new FileInputStream(file); //每次最多讀寫1024位元組,直到全部讀完 int ch = fis.read(bytes, 0, BUFFER_SIZE); System.out.println(ch); while(ch != -1) { output.write(bytes, 0, ch); ch = fis.read(bytes, 0, BUFFER_SIZE); } } else { 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) { System.out.println(e.toString()); } finally { if(fis != null) fis.close(); } } }
執行效果
執行HttpServer的主方法,然後在瀏覽器位址列鍵入localhost:8080/index.html,你就可以在瀏覽器看見網頁內容了。到這一步就相當於實現了一個apache伺服器。
注意:index.html是你自己建立的,你隨便寫點內容。我是隻在body裡面寫了hello。
程式碼解析
Request物件中緩衝大小為什麼是2048?
因為大多數瀏覽器請求行最大長度就是2048位元組,所以讀取2048位元組,裡面必然完全包含了請求行的資料。這也是parameter傳參長度限制的原因,因為parameter在URI中,而URI又是組成請求行的元素之一。
注:HTTP請求報文的請求行由三部分組成,請求方法,URI,協議版本,且這三個引數用空格隔開。
前面說的例子有問題在哪裡?
上面的例子是正常的,不過書本里面少了一部分,那就是響應頭的編寫,如果沒有傳送響應頭給瀏覽器,它無法識別傳送給它的資料是什麼。
Content-Length在上文中起什麼作用?
細心的朋友會發現,我在響應頭中新增了Content-Length的頭資訊,指明瞭檔案的長度,也就是位元組數。有了這個頭資訊,瀏覽器就可以知道什麼時候資料接收完成。這跟瀏覽器的載入提示有關。
怎麼讓別人也能訪問到這個網頁?
如果你的電腦有公網IP的話,那你要做的只是把程式跑起來掛著,然後開放埠。開放埠是什麼意思?預設情況下,防火牆會為了安全,其他電腦是不能隨便訪問本機的埠(例外,80埠是預設開啟的)。開啟的方法就是進入防火牆設定進站規則,開放8080埠。
感悟
其實涉及到網路通訊,底層傳遞的就是一堆位元組,而"協議"從一個角度來說,其實就是雙方共同遵守的資料格式,它指明從哪裡到哪裡的位元組資料表示的是什麼,應用程式根據這些進行處理。想來,其實這些東西在上《計算機網路》的時候都講到了,只是當時沒有現在這種感覺吧。