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

從零構建 Java 輕量級 HTTP 代理伺服器:核心原理與完整實作

     編輯:Java 相關知識

前言

HTTP 代理伺服器是網路中介,轉發客戶端請求至目標伺服器並回傳響應。本文基於 Java 原生 Socket API,實現一個支援多埠監聽、非阻塞 I/O、連接管道轉發的輕量級代理,核心代碼約 300 行,適合學習網路編程與代理原理。

架構設計

  • 主伺服器:ServerSocket 監聽指定埠,接受客戶端連接。
  • 會話處理:每個連接啟動一個 HTTPSession 執行緒,解析 HTTP 請求頭中的 Host 欄位確定目標地址。
  • 雙向管道:建立到目標伺服器的 Socket,將客戶端請求轉發,再將響應原路返回,實現透明代理。

核心代碼實現

package com.example.proxy;

import java.io.*;
import java.net.*;

public class HttpProxy {
    private final ServerSocket serverSocket;
    private final Thread acceptorThread;
    private static long threadCount = 0;

    public HttpProxy(int port) throws IOException {
        this.serverSocket = new ServerSocket(port);
        this.acceptorThread = new Thread(() -> {
            try {
                while (!Thread.interrupted()) {
                    Socket client = serverSocket.accept();
                    new HTTPSession(client).start();
                }
            } catch (IOException ignored) {}
        });
        acceptorThread.setDaemon(true);
        acceptorThread.start();
        System.out.println("代理伺服器啟動於埠口: " + port);
    }

    public void stop() throws IOException, InterruptedException {
        serverSocket.close();
        acceptorThread.join();
    }

    // 內部類:處理單個 HTTP 會話
    private class HTTPSession extends Thread {
        private final Socket clientSocket;

        HTTPSession(Socket socket) {
            this.clientSocket = socket;
            setDaemon(true);
        }

        @Override
        public void run() {
            threadCount++;
            try (InputStream in = clientSocket.getInputStream();
                 OutputStream out = clientSocket.getOutputStream()) {

                // 1. 讀取請求頭(直到雙 CRLF)
                byte[] buffer = new byte[8192];
                int totalRead = 0, headerEnd = 0;
                while ((headerEnd = findHeaderEnd(buffer, totalRead)) == 0) {
                    int read = in.read(buffer, totalRead, buffer.length - totalRead);
                    if (read <= 0) return;
                    totalRead += read;
                }

                // 2. 解析 Host 頭
                String header = new String(buffer, 0, headerEnd, "ISO-8859-1");
                String hostHeader = extractHost(header);
                if (hostHeader == null) {
                    out.write("HTTP/1.1 400 Bad Request\r\n\r\nMissing Host header".getBytes());
                    return;
                }
                Host target = parseHost(hostHeader);
                System.out.printf("轉發至: %s:%d%n", target.address, target.port);

                // 3. 建立到目標的連接並雙向轉發
                try (Socket targetSocket = new Socket(target.address, target.port);
                     OutputStream targetOut = targetSocket.getOutputStream();
                     InputStream targetIn = targetSocket.getInputStream()) {

                    // 先寫入已讀取的請求頭
                    targetOut.write(buffer, 0, totalRead);
                    targetOut.flush();

                    // 雙向管道:客戶端 -> 目標
                    pipe(in, targetOut);
                    // 目標 -> 客戶端 (在管道結束後處理剩餘響應)
                    pipe(targetIn, out);
                }
            } catch (IOException e) {
                System.err.println("會話異常: " + e.getMessage());
            } finally {
                threadCount--;
                closeQuietly(clientSocket);
            }
        }

        // 尋找 HTTP 頭結束位置 (\r\n\r\n)
        private int findHeaderEnd(byte[] buf, int len) {
            for (int i = 0; i + 3 < len; i++) {
                if (buf[i] == '\r' && buf[i+1] == '\n' && buf[i+2] == '\r' && buf[i+3] == '\n') {
                    return i + 4;
                }
            }
            return 0;
        }

        private String extractHost(String header) {
            for (String line : header.split("\r\n")) {
                if (line.toLowerCase().startsWith("host:")) return line.substring(5).trim();
            }
            return null;
        }

        private Host parseHost(String hostHeader) {
            // 格式: host:port 或 host (預設 80)
            int colon = hostHeader.indexOf(':');
            if (colon == -1) return new Host(hostHeader, 80);
            String addr = hostHeader.substring(0, colon);
            int port = Integer.parseInt(hostHeader.substring(colon + 1));
            return new Host(addr, port);
        }

        // 通用管道轉發
        private void pipe(InputStream src, OutputStream dst) throws IOException {
            byte[] buf = new byte[32 * 1024];
            int len;
            while ((len = src.read(buf)) != -1) {
                dst.write(buf, 0, len);
                dst.flush();
            }
        }

        private void closeQuietly(Socket s) {
            try { if (s != null) s.close(); } catch (IOException ignored) {}
        }

        private static class Host {
            final String address; final int port;
            Host(String address, int port) { this.address = address; this.port = port; }
        }
    }

    // 測試啟動
    public static void main(String[] args) throws Exception {
        new HttpProxy(8580);
        System.in.read(); // 阻塞主執行緒
    }
}

關鍵技術點解析

1. HTTP 請求頭解析

代理必須從請求中提取 Host 頭以確定目標伺服器。HTTP/1.1 強制要求 Host 頭,解析邏輯需處理「域名:埠」與「僅域名」兩種格式。

2. 連接管道與緩衝策略

使用 32KB 緩衝區進行流式轉發,避免將完整請求/響應載入記憶體,支援大文件傳輸。flush() 確保數據及時發送,降低延遲。

3. 執行緒模型

每連接一執行緒模型簡單直觀,適合中低併發場景。高併發下可升級為 NIO 選擇器模型(單執行緒多路複用)。

功能擴展方向

  • HTTPS 支援:需實現 CONNECT 方法建立隧道,涉及 SSL/TLS 握手與證書管理。
  • 請求/響應過濾:在管道中攔截修改 Header、Body(如廣告攔截、日誌記錄)。
  • 緩存機制:緩存 GET 響應,減少上游請求。
  • 認證授權:支援 Proxy-Authorization 頭驗證。

總結

本實現展示了 HTTP 代理的核心骨架:監聽 → 解析 Host → 建立上游連接 → 雙向管道轉發。代碼無第三方依賴,邏輯清晰,是理解應用層代理協議、Socket 編程、多執行緒併發的優秀教學案例。生產環境建議使用成熟組件(如 LittleProxy、Netty 實現的代理)。

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