首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

基於 Socket 實作 HTTP 協定客戶端:從協定規範到代碼完整解析

     編輯:Java 相關知識

Java 內建 URLConnection 封裝 HTTP 客戶端,但源碼不公開,實現細節成謎。本文依據 HTTP 協定規範 (RFC 2616),使用 java.net.Socket 從零手刻 HTTP 客戶端,實作 GET/HEAD/POST 三大核心方法,深度解析報文格式與解析邏輯。

1. Socket 基礎

TCP/IP 協定族通訊透過 Socket 完成。java.net.Socket 連接主機後返回 I/O 流,實現協定間資訊交換。

2. HTTP 協定報文結構

請求報文 (Client → Server)

請求方法 URL HTTP版本
請求標頭 (Header)
**空行 (CRLF)**
實體主體 (Entity Body)
  • 請求方法:HTTP/1.1 定義 GET、HEAD、POST、PUT、DELETE、OPTIONS、TRACE
  • 標頭:描述客戶端能力、內容型別、驗證資訊等 (如 Accept, User-Agent, Content-Length)
  • 空行:標頭與主體分隔符 (關鍵!)
  • 實體:POST/PUT 等方法攜帶的資料

回應報文 (Server → Client)

HTTP版本 狀態碼 狀態說明
回應標頭
**空行**
實體主體 (HTML/JSON/圖片等)
  • 狀態碼:200 OK、404 Not Found、503 Service Unavailable 等

GET 請求範例

GET https://www.youhost.com HTTP/1.0
Accept: www/source; text/html; image/gif; image/jpeg; */*
User-Agent: myAgent

// 空行結束

回應範例:

HTTP/1.1 200 OK
Date: Tue, 14 Sep 1999 02:19:57 GMT
Server: Apache/1.2.6
Connection: close
Content-Type: text/html

<html><head>...</html>

3. HTTP 客戶端核心類實作

import java.net.*; import java.io.*; import java.util.Properties;

public class Http {
    protected Socket client;
    protected BufferedOutputStream sender;
    protected BufferedInputStream receiver;
    protected ByteArrayInputStream byteStream;
    protected URL target;
    private int responseCode = -1;
    private String responseMessage = "", serverVersion = "";
    private Properties header = new Properties();

    // 建構子與 GET/HEAD/POST 入口方法
    public void GET(String url) { /* 建立連線、發送 GET 報文、接收解析 */ }
    public void HEAD(String url) { /* 僅請求標頭,驗證資源是否更新 */ }
    public void POST(String url, String content) { /* 發送表單資料,含 Content-Type/Length */ }

    // 協定檢查
    protected void checkHTTP(String url) throws ProtocolException { /* 驗證 HTTP 協定與 URL 格式 */ }

    // 建立 Socket 連線
    protected void openServer(String host, int port) throws UnknownHostException, IOException {
        header.clear(); responseCode = -1; responseMessage = "";
        if (client != null) closeServer();
        InetAddress addr = InetAddress.getByName(host);
        client = new Socket(addr, port == -1 ? 80 : port);
        sender = new BufferedOutputStream(client.getOutputStream());
        receiver = new BufferedInputStream(client.getInputStream());
    }

    // 發送報文
    protected void sendMessage(String data) throws IOException {
        sender.write(data.getBytes(), 0, data.length());
        sender.flush();
    }

    // 核心:接收並解析回應
    protected void receiveMessage() throws IOException {
        byte[] data = new byte[1024];
        int count = 0, word = -1;

        // 1. 解析狀態行
        while ((word = receiver.read()) != -1) {
            if (word == '\r' || word == '\n') { /* 讀取換行符 */ break; }
            if (count == data.length) data = addCapacity(data);
            data[count++] = (byte) word;
        }
        String line = new String(data, 0, count);
        int sp = line.indexOf(' ');
        serverVersion = line.substring(0, sp);
        while (sp < line.length() && line.charAt(sp+1) == ' ') sp++;
        responseCode = Integer.parseInt(line.substring(sp+1, sp+4));
        responseMessage = line.substring(sp+4).trim();

        // 狀態碼異常處理
        switch (responseCode) {
            case 400: throw new IOException("錯誤請求");
            case 404: throw new FileNotFoundException(getURLFormat(target));
            case 503: throw new IOException("服務器不可用");
        }

        // 2. 解析標頭 (多行,以空行結束)
        count = 0;
        while (word != '\r' && word != '\n' && word > -1) {
            if (word == '\t') word = ' ';
            if (count == data.length) data = addCapacity(data);
            data[count++] = (byte) word;
            // 讀取完整一行 (處理折行)
            int symbol;
            while ((symbol = receiver.read()) > -1) {
                if (symbol == '\t') symbol = ' ';
                if (symbol == '\r' || symbol == '\n') {
                    word = receiver.read();
                    if (symbol == '\r' && word == '\n') word = receiver.read();
                    if (word == '\r' || word == '\n' || word > ' ') break;
                    symbol = ' ';
                }
                if (count == data.length) data = addCapacity(data);
                data[count++] = (byte) symbol;
            }
            String headerLine = new String(data, 0, count);
            int colon = headerLine.indexOf(':');
            String key = colon > 0 ? headerLine.substring(0, colon) : null;
            if (key != null) {
                colon++;
                while (colon < headerLine.length() && headerLine.charAt(colon) <= ' ') colon++;
                header.put(key, headerLine.substring(colon));
            }
            count = 0;
        }

        // 3. 讀取實體主體
        while ((word = receiver.read()) != -1) {
            if (count == data.length) data = addCapacity(data);
            data[count++] = (byte) word;
        }
        if (count > 0) byteStream = new ByteArrayInputStream(data, 0, count);
        closeServer();
    }

    // 輔助方法:動態擴容位元組陣列
    private byte[] addCapacity(byte[] arr) {
        byte[] n = new byte[arr.length + 1024];
        System.arraycopy(arr, 0, n, 0, arr.length);
        return n;
    }

    // Getter 方法:getResponseCode(), getHeaderValue(key), getInputStream() 等...
}

關鍵設計細節

  • 緩衝流:BufferedOutputStream/InputStream 減少系統呼叫
  • 動態緩衝區:addCapacity() 應對超大標頭/主體
  • 標頭折行處理:RFC 允許標頭值跨行 (以空白/Tab 開頭),解析邏輯含狀態機
  • 狀態碼異常轉換:400/404/503 轉為對應 Java 異常
  • HEAD 方法優勢:僅取標頭,驗證快取有效性極高效
  • POST 表單編碼:application/x-www-form-urlencoded + Content-Length

擴展方向

程式碼僅實作 GET/HEAD/POST,可擴充:

  1. 支援 HTTPS (需 SSLSocketFactory)
  2. 連線池與 Keep-Alive
  3. Cookie 自動管理
  4. 重導向 (3xx) 自動追蹤
  5. 多執行緒非阻塞 I/O (NIO)

手刻 HTTP 客戶端是理解網路協定、排查疑難雜症 (如標頭解析異常、編碼問題) 的最佳途徑。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved