HTTP實際上是基於TCP的應用層協議,它在更高的層次封裝了TCP的使用細節,是網絡請求操作更為易用. TCP連接是因特網上基於流的可靠連接,它為HTTP提供了一條可靠的比特傳輸管道. 從TCP連接一端填入的字節會從另一端以原有的順序,正確地傳遞出來,如下圖所示.
TCP的數據是通過名為IP分組(或IP數據報)的小數據塊來發送的. 這樣的話,如下圖的HTTP協議所示,HTTP就是”HTTP over TCP over IP”這個”協議棧”中的最頂層了.
HTTP要傳送一條報文時,會以流的形式將報文數據的內容通過一條打開的TCP連接按序傳輸. TCP收到數據流之后,會將數據流分割成被稱作段的小數據塊,並將段封裝在IP分組中,通過因特網進行傳輸. 所有這些工作都是由TCP/IP軟件來處理的,程序員什么都看不到.
下面我們就模擬一個簡單的Web服務器來深度了解一下HTTP的報文格式以及HTTP協議與TCP協議之間的協作原理.

一個HTTP請求就是一個典型的C/S模式,服務端在監聽某個端口,客戶端向服務端的端口發起請求. 服務端解析請求,並且向客戶端返回結果. 下面我們就先看看這個簡單的Web服務端.
代碼如下:
public class SimpleHttpServer extends Thread { public static void main(String[] args) { new SimpleHttpServer().start(); } // 服務端Socket ServerSocket mSocket = null; public SimpleHttpServer() { try { mSocket = new ServerSocket(SocketTool.PORT); } catch (IOException e) { e.printStackTrace(); } if (mSocket == null) { throw new RuntimeException("服務器Socket初始化失敗"); } } @Override public void run() { try { while (true) { // 無限循環,進入等待連接狀態 System.out.println("等待連接中"); // 一旦接收到連接請求,構建一個線程來處理 new DeliverThread(mSocket.accept()).start(); } } catch (IOException e) { e.printStackTrace(); } } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
SimpleHttpServer繼承自Thread類,在構造函數中我們會創建一個監聽10086端口的服務端Socket,並且覆寫Thread的run函數,在該函數中開啟無限循環,在該循環中調用ServerSocket的accept()函數等待客戶端的連接,該函數會阻塞,知道有客戶端進行連接,接收連接之后會構造一個線程來處理該請求. 也就是說,SimpleHttpServer本身是一個子線程,它在后台等待客戶端的連接,一旦接收到連接又會創建一個線程處理該請求,避免阻塞SimpleHttpServer線程.
現在我們一步一步來分析連接處理線程DeliverThread的代碼:
static class DeliverThread extends Thread { Socket mClientSocket; // 輸入流 BufferedReader mInputStream; // 輸出流 PrintStream mOutputStream; // 請求方法,GET、POST等 String httpMethod; // 子路徑 String subPath; // 分隔符 String boundary; // 請求參數 Map<String, String> mParams = new HashMap<String, String>(); // 請求headers Map<String, String> mHeaders = new HashMap<String, String>(); // 是否已經解析完Header boolean isParseHeader = false; public DeliverThread(Socket socket) { mClientSocket = socket; } @Override public void run() { try { // 獲取輸入流 mInputStream = new BufferedReader(new InputStreamReader( mClientSocket.getInputStream())); // 獲取輸出流 mOutputStream = new PrintStream(mClientSocket.getOutputStream()); // 解析請求 parseRequest(); // 返回Response handleResponse(); } catch (IOException e) { e.printStackTrace(); } finally { // 關閉流和Socket IoUtils.closeQuickly(mInputStream); IoUtils.closeQuickly(mOutputStream); IoUtils.closeSocket(mClientSocket); } } //代碼省略 }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
DeliverThread也繼承自Thread,在run函數中主要封裝了如下步驟:
- 獲取客戶端Socket的輸入,輸出流用於讀寫數據;
- 解析請求參數;
- 處理,返回請求結果;
- 關閉輸入,輸出流,客戶端Socket.
上文我們說過TCP的數據操作是基於流的,因此得到客戶端Socket連接之后,我們首先獲取到它的輸入,輸出流. 其中我們可以從輸入流中獲取該請求的數據,而通過輸出流就可以將結果返回給該客戶端. 得到流之后我們首先解析該請求,根據它請求的路徑,header,參數等作出處理,最后將處理結果通過輸出流返回給客戶端. 最終關閉流和Socket.
在分析HTTP請求解析的代碼之前,我們再來回顧一下HTTP請求的報文格式,如下圖所示

下面我們看一下解析請求的具體實現,即parseRequest函數:
private void parseRequest() { String line; try { int lineNum = 0; // 從輸入流讀取客戶端發送過來的數據 while ((line = mInputStream.readLine()) != null) { //第一行為請求行 if (lineNum == 0) { parseRequestLine(line); } // 判斷是否是數據的結束行 if (isEnd(line)) { break; } // 解析header參數 if (lineNum != 0 && !isParseHeader) { parseHeaders(line); } // 解析請求參數 if (isParseHeader) { parseRequestParams(line); } lineNum++; } } catch (IOException e) { e.printStackTrace(); } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
在parseRequest函數中,我們按照數據的分步進行解析. 首先解析第一行的請求行數據,即當lineNum為0時調用parseRequestLine函數進行解析. 該函數的實現如下:
// 解析請求行 private void parseRequestLine(String lineOne) { String[] tempStrings = lineOne.split(" "); httpMethod = tempStrings[0]; subPath = tempStrings[1]; System.out.println("請求行,請求方式 : " + tempStrings[0] + ", 子路徑 : " + tempStrings[1] + ",HTTP版本 : " + tempStrings[2]); System.out.println(); }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
在上文的格式分析中我么你說過,請求行由3部分組成,即請求方式,請求子路徑,協議版本,它們之間通過空格來進行分割. 因此,在parseRequestLine中我們用空格分隔請求行字符串,得到的結果就是這3個值.
請求行后面緊跟着請求Header,因此,我們的下一步就是解析Header區域. 對應的函數為parseHeaders,代碼如下:
// 解析header,參數為每個header的字符串 private void parseHeaders(String headerLine) { // header區域的結束符 if (headerLine.equals("")) { isParseHeader = true; System.out.println("-----------> header解析完成\n"); return; } else if (headerLine.contains("boundary")) { boundary = parseSecondField(headerLine); System.out.println("分隔符 : " + boundary); } else { // 解析普通header參數 parseHeaderParam(headerLine); } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
每個header為一個獨立行,格式為參數名: 參數值,還有一種情況是參數名1: 參數值2;參數名2: 參數值2. 例如下面兩個header:
Content-Length: 1234 Content-Type: multipart/form-data; boundary=OCqxMF6-JxtxoMDHmoG5W5eY9MGRsTBp
- 1
- 2
- 1
- 2
第一個header參數名為Content-Length,值為1234. 第二個header在同一行內有兩個數據,分別為值為multipart/form-data的Content-Type,以及值為OCqxMF6-JxtxoMDHmoG5W5eY9MGRsTBp的boundary. header與請求參數之間通過一個空行分隔,因此,我們檢測到header數據為空時則認為是header參數的結束行.
當一個header行數據中含有boundary字段時,則調用parseSecondField函數解析,該函數實現如下:
// 解析header中的第二個參數 private String parseSecondField(String line) { String[] headerArray = line.split(";"); parseHeaderParam(headerArray[0]); if (headerArray.length > 1) { return headerArray[1].split("=")[1]; } return ""; }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
因為boundary參數在header格式的第二個參數的位置上,因此,這里通過分號進行分割,獲取數組第二個位置的數據,也就是boundary=OCqxMF6-JxtxoMDHmoG5W5eY9MGRsTBp,然后再進行解析.
普通的header則是參數名: 參數值的格式,我們通過parseHeaderParam函數進行解析,代碼如下:
// 解析單個header private void parseHeaderParam(String headerLine) { String[] keyvalue = headerLine.split(":"); mHeaders.put(keyvalue[0].trim(), keyvalue[1].trim()); System.out.println("header參數名 : " + keyvalue[0].trim() + ", 參數值 : " + keyvalue[1].trim()); }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 1
- 2
- 3
- 4
- 5
- 6
- 7
解析完header之后我們就開始解析請求參數了. 對於POST和PUT請求來說,它們的每個參數格式都是固定的,格式如下:
--boundary值
header-1: value-1 ... header-n: value-n 空行 參數值
- 1
- 2
- 3
- 4
- 5
- 6
- 1
- 2
- 3
- 4
- 5
- 6
由於在我們的例子中每個請求參數只有一個header字段,因此, 我們的每個參數的格式簡化為:
--boundary Content-Disposition: form-data; name="參數名" 空行 參數值
- 1
- 2
- 3
- 4
- 1
- 2
- 3
- 4
根據上述格式,我們再來看解析函數:
// 解析請求參數 private void parseRequestParams(String paramLine) throws IOException { if (paramLine.equals("--" + boundary)) { // 讀取Content-Disposition行 String ContentDisposition = mInputStream.readLine(); // 解析參數名 String paramName = parseSecondField(ContentDisposition); // 讀取參數header與參數值之間的空行 mInputStream.readLine(); // 讀取參數值 String paramValue = mInputStream.readLine(); mParams.put(paramName, paramValue); System.out.println("參數名 : " + paramName + ", 參數值 : " + paramValue); } } // 是否是結束行 private boolean isEnd(String line) { return line.equals("--" + boundary + "--"); }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
至此,整個請求的各個部分均已解析完成. 后面要做的就是根據用戶的請求返回結果. 在這里我們直接返回了一個固定的Response. 代碼如下:
// 返回結果 private void handleResponse() { //模擬處理耗時 sleep(); //向輸出流寫數據 mOutputStream.println("HTTP/1.1 200 OK"); mOutputStream.println("Content-Type: application/json"); mOutputStream.println(); mOutputStream.println("{\"stCode\":\"success\"}"); } private void sleep() { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
在handleResponse函數中,通過Socket的輸出流向客戶端寫入數據. 寫入的數據也遵循了響應報文的基本格式,格式如下:
響應行
header區域 空行 響應數據
- 1
- 2
- 3
- 4
- 1
- 2
- 3
- 4
向客戶端寫完數據后,我們就會關閉輸入,輸出流以及Socket,至此,整個請求,響應流程完畢.
服務端邏輯分析完成之后我們再來看看客戶端的實現. 從上述的分析以及平時的開發經驗我們知道,客戶端要做的就是主動向服務器發起HTTP請求,它們之間的通信通道就是TCP/IP,因此,也是基於Socket實現. 下面我們就模擬一個Http POST請求,代碼如下:
public class HttpPost { public String url; // 請求參數 private Map<String, String> mParamsMap = new HashMap<String, String>(); private static final int PORT = 10086; //客戶端Socket Socket mSocket; public HttpPost(String url) { this.url = url; } public void addParam(String key, String value) { mParamsMap.put(key, value); } public void execute() { try { // 創建Socket連接 mSocket = new Socket(this.url, PORT); PrintStream outputStream = new PrintStream(mSocket.getOutputStream()); BufferedReader inputStream = new BufferedReader(new InputStreamReader( mSocket.getInputStream())); final String boundary = "my_boundary_123"; // 寫入header writeHeader(boundary, outputStream); // 寫入參數 writeParams(boundary, outputStream); // 等待返回數據 waitResponse(inputStream); } catch (UnknownHostException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { if (mSocket != null) { try { mSocket.close(); } catch (IOException e) { e.printStackTrace(); } } } } /代碼省略 }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
HttpPost構造函數中傳入請求的URL地址,然后用戶可以調用addParam函數添加普通的文本參數,當用戶設置好參數之后就可以通過execute函數執行該請求. 在execute函數中客戶端首先創建Socket連接,目標地址就是用戶執行的URL以及端口. 連接成功之后客戶端就可以獲取到輸入,輸出流,通過輸出流客戶端可以向服務端發送數據,通過輸入流則可以獲取服務端返回的數據. 之后我們一次寫入header,請求參數,最后等待Response的返回.
在該示例中,我們將header固定作出如下設置,代碼如下:
private void writeHeader(String boundary, PrintStream outputStream) { outputStream.println("POST /api/login/ HTTP/1.1"); outputStream.println("content-length:123"); outputStream.println("Host:" + this.url + ":" + PORT); outputStream.println("Content-Type: multipart/form-data; boundary=" + boundary); outputStream.println("User-Agent:android"); outputStream.println(); }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
然后,我們將mParamsMap中的所有參數通過輸出流傳遞給服務端,代碼如下:
private void writeParams(String boundary, PrintStream outputStream) { Iterator<String> paramsKeySet = mParamsMap.keySet().iterator(); while (paramsKeySet.hasNext()) { String paramName = paramsKeySet.next(); outputStream.println("--" + boundary); outputStream.println("Content-Disposition: form-data; name=" + paramName); outputStream.println(); outputStream.println(mParamsMap.get(paramName)); } // 結束符 outputStream.println("--" + boundary + "--"); }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
每個參數都必須遵循特定的格式,在上文服務器解析參數時就是按照這里設定的格式進行. 格式如下:
--boundary Content-Disposition: form-data; name="參數名" 空行 參數值
- 1
- 2
- 3
- 4
- 1
- 2
- 3
- 4
當參數結束之后需要寫一個結束行,格式為:兩個斜杠加上boundary值再加上兩個斜杠. 此時請求數據就已經發送到服務端,此時我們等待服務器返回數據. 得到返回的數據之后將結果輸出到控制台. 代碼如下:
private void waitResponse(BufferedReader inputStream) throws IOException { System.out.println("請求結果: "); String responseLine = inputStream.readLine(); while (responseLine == null || !responseLine.contains("HTTP")) { responseLine = inputStream.readLine(); } //輸出Response while ((responseLine = inputStream.readLine()) != null) { System.out.println(responseLine); } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
此時,客戶端的流程也執行完畢.
下面,運行這個例子. 首先需要啟動服務器,代碼如下:
public static void main(String[] args) throws Exception { new SimpleHttpServer().start(); }
- 1
- 2
- 3
- 1
- 2
- 3
服務器啟動之后就會在后台等待客戶端發起連接,此時我們再啟動客戶端,設置參數之后執行一個Http POST請求:
HttpPost httpPost = new HttpPost("127.0.0.1"); // 設置兩個參數 httpPost.addParam("username", "mr.simple"); httpPost.addParam("pwd", "my_pwd123"); // 執行請求 httpPost.execute();
- 1
- 2
- 3
- 4
- 5
- 6
- 1
- 2
- 3
- 4
- 5
- 6
執行結果如下圖所示:
-
服務端接到請求

-
客戶端請求結果

本文中我們用一個簡單的示例模擬了Web服務器與客戶端你的交互過程. 整個示例就是在TCP智商封裝了一層HTTP,用戶通過HTTP相關的類進行操作,但是傳輸層依舊是通過TCP層. 客戶端與服務端之間開辟了一條雙向的Socket,通過輸入,輸出流向對方發送,獲取數據,而雙方都遵循了規定的HTTP協議,因此,數據的發送與解析都能夠順利進行. 通過HTTP層屏蔽了直接使用Socket的復雜細節,使得整個通信過程更加簡單,易用.
完整示例:
SocketSamples
