凌峰创科服务平台

Java如何搭建HTTP代理服务器?

核心原理

一个 HTTP 代理服务器的工作流程如下:

Java如何搭建HTTP代理服务器?-图1
(图片来源网络,侵删)
  1. 监听: 代理服务器在指定端口(8080)上监听来自客户端(如浏览器)的连接请求。
  2. 接收请求: 客户端通过 CONNECT 方法(用于 HTTPS)或直接发送 HTTP 请求(用于 HTTP)到代理服务器。
  3. 解析请求: 代理服务器解析客户端的请求,获取目标服务器的地址(主机名和端口)以及请求的详细信息(如方法、路径、头信息等)。
  4. 转发请求: 代理服务器作为客户端,向目标服务器发起一个新的请求,将原始请求的内容转发过去。
  5. 接收响应: 目标服务器处理请求后,将响应返回给代理服务器。
  6. 转发响应: 代理服务器将收到的响应原封不动地转发给最初的客户端。
  7. 连接关闭: 客户端和代理服务器之间的连接关闭。

对于 HTTPS,过程稍有不同:

  • 客户端首先向代理服务器发送一个 CONNECT example.com:443 HTTP/1.1 请求。
  • 代理服务器接收到后,不解析具体内容,而是直接与目标服务器(example.com 的 443 端口)建立一条 纯 TCP 隧道
  • 建立成功后,代理服务器返回 HTTP/1.1 200 Connection established
  • 之后,客户端和目标服务器之间的所有通信都通过这个隧道进行,代理服务器只是透明地转发数据包,无法解密 HTTPS 内容(除非配置了 SSL 证书进行中间人攻击)。

使用 Java 标准库 (Java 1.4+)

Java 的标准库提供了 java.net.ServerSocketjava.net.Socket,我们可以用它们来构建一个基础的、功能单一的 HTTP 代理,这个实现不会处理 HTTPS 的 CONNECT 方法,但能很好地理解代理的核心逻辑。

代码示例 (仅支持 HTTP):

import java.io.*;
import java.net.*;
public class SimpleHttpProxy {
    public static void main(String[] args) throws IOException {
        int port = 8080;
        try (ServerSocket serverSocket = new ServerSocket(port)) {
            System.out.println("代理服务器启动,监听端口: " + port);
            while (true) {
                // 1. 接受客户端连接
                Socket clientSocket = serverSocket.accept();
                System.out.println("新的客户端连接: " + clientSocket.getInetAddress());
                // 2. 为每个客户端连接创建一个新线程处理
                new Thread(new ProxyHandler(clientSocket)).start();
            }
        }
    }
    static class ProxyHandler implements Runnable {
        private final Socket clientSocket;
        public ProxyHandler(Socket clientSocket) {
            this.clientSocket = clientSocket;
        }
        @Override
        public void run() {
            try (
                // 客户端输入流 (来自浏览器的请求)
                BufferedReader clientReader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
                // 客户端输出流 (发送给浏览器的响应)
                OutputStream clientOutputStream = clientSocket.getOutputStream();
                // 目标服务器的 Socket
                Socket targetSocket = null;
                // 目标服务器的输入流 (来自目标服务器的响应)
                InputStream targetInputStream = null;
                // 目标服务器的输出流 (发送给目标服务器的请求)
                OutputStream targetOutputStream = null;
            ) {
                // 3. 解析客户端请求的第一行,获取目标主机和端口
                String requestLine = clientReader.readLine();
                if (requestLine == null) return;
                System.out.println("收到请求: " + requestLine);
                String[] parts = requestLine.split(" ");
                if (parts.length < 3) return;
                String method = parts[0];
                String url = parts[1];
                // 解析主机和端口
                String host;
                int port = 80; // 默认 HTTP 端口
                if (url.startsWith("http://")) {
                    url = url.substring(7);
                    int slashIndex = url.indexOf('/');
                    if (slashIndex != -1) {
                        host = url.substring(0, slashIndex);
                        url = url.substring(slashIndex);
                    } else {
                        host = url;
                        url = "/";
                    }
                } else {
                    // 处理相对路径,需要从 Host 头中获取主机名
                    // 这是一个简化版,实际代理需要解析 Host 头
                    host = "example.com"; // 示例,实际应从请求头中获取
                }
                if (host.contains(":")) {
                    String[] hostParts = host.split(":");
                    host = hostParts[0];
                    port = Integer.parseInt(hostParts[1]);
                }
                System.out.println("目标主机: " + host + ", 端口: " + port);
                // 4. 连接到目标服务器
                targetSocket = new Socket(host, port);
                targetInputStream = targetSocket.getInputStream();
                targetOutputStream = targetSocket.getOutputStream();
                // 5. 转发请求头和请求体
                // 先转发请求行
                targetOutputStream.write((method + " " + url + " HTTP/1.1\r\n").getBytes());
                // 转发其余请求头
                String line;
                while (!(line = clientReader.readLine()).isEmpty()) {
                    // 跳过 Host 头,因为我们已经在 URL 中指定了
                    if (line.toLowerCase().startsWith("host:")) {
                        targetOutputStream.write(("Host: " + host + "\r\n").getBytes());
                    } else {
                        targetOutputStream.write((line + "\r\n").getBytes());
                    }
                }
                targetOutputStream.write("\r\n".getBytes()); // 请求头结束
                targetOutputStream.flush();
                // 6. 转发响应
                byte[] buffer = new byte[4096];
                int bytesRead;
                while ((bytesRead = targetInputStream.read(buffer)) != -1) {
                    clientOutputStream.write(buffer, 0, bytesRead);
                }
                clientOutputStream.flush();
            } catch (IOException e) {
                System.err.println("处理请求时出错: " + e.getMessage());
            } finally {
                try {
                    clientSocket.close();
                } catch (IOException e) {
                    // ignore
                }
            }
        }
    }
}

如何测试:

Java如何搭建HTTP代理服务器?-图2
(图片来源网络,侵删)
  1. 运行上述 Java 程序。
  2. 在浏览器或操作系统的网络设置中,将代理服务器设置为 localhost:8080
  3. 访问一个 HTTP 网站(如 http://httpbin.org/get),你会在控制台看到代理服务器的日志输出。

使用第三方库 (推荐)

手动实现一个稳定、功能完善的代理服务器非常复杂,需要处理各种 HTTP/HTTPS 规范、连接池、性能优化、编码问题等,在生产环境中,强烈推荐使用成熟的第三方库。

推荐库:Netty

Netty 是一个高性能的异步事件驱动的网络应用框架,非常适合用来构建网络服务,包括代理服务器,它能极大地简化 NIO 编程。

添加 Maven 依赖:

<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
    <version>4.1.90.Final</version> <!-- 使用最新稳定版本 -->
</dependency>

代码示例 (支持 HTTP 和 HTTPS):

Java如何搭建HTTP代理服务器?-图3
(图片来源网络,侵删)

这个例子使用 Netty 的 HttpProxyHandler,它内部已经处理了大部分复杂的逻辑。

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.proxy.HttpProxyHandler;
import io.netty.handler.proxy.ProxyHandler;
public class NettyHttpProxyServer {
    private static final int PORT = 8080;
    public static void main(String[] args) throws Exception {
        // 1. 创建 EventLoopGroup
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            // 2. 创建 ServerBootstrap
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
             .channel(NioServerSocketChannel.class)
             .handler(new LoggingHandler(LogLevel.INFO))
             .childHandler(new ChannelInitializer<SocketChannel>() {
                 @Override
                 protected void initChannel(SocketChannel ch) throws Exception {
                     // Netty 的 HttpProxyHandler 会自动处理 CONNECT 请求并建立隧道
                     // 它也会处理普通的 HTTP 请求转发
                     ch.pipeline().addLast(new HttpProxyHandler(null)); // null 表示不进行认证
                 }
             });
            // 3. 绑定端口并启动
            ChannelFuture f = b.bind(PORT).sync();
            System.out.println("Netty HTTP 代理服务器已启动,监听端口: " + PORT);
            // 4. 等待服务器关闭
            f.channel().closeFuture().sync();
        } finally {
            // 5. 优雅关闭
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }
    }
}

为什么 Netty 更好?

  • 异步非阻塞: 性能极高,能处理大量并发连接。
  • 功能强大: 内置了对 HTTP/HTTPS 代理、WebSocket 等协议的支持。
  • 代码简洁: 相比于手动处理 Socket 和流,Netty 的 API 更清晰、更健壮。
  • 生态成熟: 社区活跃,文档丰富,问题容易解决。

完整实战:一个功能更丰富的代理服务器

让我们结合标准库和第三方库的思想,构建一个稍微复杂一点的代理服务器,它可以打印出请求和响应的摘要信息。

这个版本仍然使用标准库,因为它能更好地展示数据流转的过程。

目标功能:

  1. 支持基本的 HTTP 和 CONNECT 请求(HTTPS 隧道)。
  2. 打印出请求的 URL 和响应的状态码。
import java.io.*;
import java.net.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class AdvancedHttpProxy {
    private static final int PORT = 8080;
    private static final ExecutorService threadPool = Executors.newCachedThreadPool();
    public static void main(String[] args) throws IOException {
        try (ServerSocket serverSocket = new ServerSocket(PORT)) {
            System.out.println("高级代理服务器启动,监听端口: " + PORT);
            while (true) {
                Socket clientSocket = serverSocket.accept();
                threadPool.execute(new ProxyTask(clientSocket));
            }
        }
    }
    static class ProxyTask implements Runnable {
        private final Socket clientSocket;
        public ProxyTask(Socket clientSocket) {
            this.clientSocket = clientSocket;
        }
        @Override
        public void run() {
            try (
                BufferedReader clientReader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
                OutputStream clientOutputStream = clientSocket.getOutputStream();
            ) {
                // 1. 读取请求行
                String requestLine = clientReader.readLine();
                if (requestLine == null) return;
                System.out.println("\n--- 新请求 ---");
                System.out.println("请求行: " + requestLine);
                String[] parts = requestLine.split(" ");
                String method = parts[0];
                String url = parts[1];
                // 2. 处理 CONNECT 方法 (HTTPS)
                if ("CONNECT".equalsIgnoreCase(method)) {
                    handleHttpsTunnel(clientReader, clientOutputStream, url);
                    return;
                }
                // 3. 处理 HTTP 请求
                String host = extractHostFromHeaders(clientReader);
                if (host == null) {
                    sendError(clientOutputStream, 400, "Bad Request: Host header missing");
                    return;
                }
                System.out.println("目标主机: " + host);
                String targetUrl = "http://" + host + url;
                forwardHttpRequest(targetUrl, clientReader, clientOutputStream);
            } catch (IOException e) {
                System.err.println("处理任务时出错: " + e.getMessage());
            } finally {
                try {
                    clientSocket.close();
                } catch (IOException e) {
                    // ignore
                }
            }
        }
        private String extractHostFromHeaders(BufferedReader reader) throws IOException {
            String line;
            while ((line = reader.readLine()) != null && !line.isEmpty()) {
                if (line.toLowerCase().startsWith("host:")) {
                    return line.substring(5).trim();
                }
            }
            return null;
        }
        private void handleHttpsTunnel(BufferedReader clientReader, OutputStream clientOutputStream, String hostAndPort) throws IOException {
            System.out.println("建立 HTTPS 隧道到: " + hostAndPort);
            String[] parts = hostAndPort.split(":");
            String host = parts[0];
            int port = parts.length > 1 ? Integer.parseInt(parts[1]) : 443;
            try (Socket targetSocket = new Socket(host, port);
                 InputStream targetInputStream = targetSocket.getInputStream();
                 OutputStream targetOutputStream = targetSocket.getOutputStream()) {
                // 告诉客户端隧道建立成功
                clientOutputStream.write("HTTP/1.1 200 Connection established\r\n\r\n".getBytes());
                clientOutputStream.flush();
                // 双向转发数据
                new Thread(() -> pipeStreams(clientInputStream, targetOutputStream)).start();
                pipeStreams(targetInputStream, clientOutputStream);
            } catch (IOException e) {
                System.err.println("HTTPS 隧道建立失败: " + e.getMessage());
                sendError(clientOutputStream, 502, "Bad Gateway");
            }
        }
        private void pipeStreams(InputStream in, OutputStream out) {
            byte[] buffer = new byte[4096];
            int bytesRead;
            try {
                while ((bytesRead = in.read(buffer)) != -1) {
                    out.write(buffer, 0, bytesRead);
                    out.flush();
                }
            } catch (IOException e) {
                // 连接关闭是正常情况
            }
        }
        private void forwardHttpRequest(String targetUrl, BufferedReader clientReader, OutputStream clientOutputStream) throws IOException {
            URL url = new URL(targetUrl);
            try (InputStream targetInputStream = url.openStream()) {
                // 获取响应头
                HttpURLConnection connection = (HttpURLConnection) url.openConnection();
                int statusCode = connection.getResponseCode();
                System.out.println("响应状态码: " + statusCode);
                // 转发响应头
                String responseHeader = "HTTP/1.1 " + statusCode + " " + connection.getResponseMessage() + "\r\n";
                clientOutputStream.write(responseHeader.getBytes());
                for (String key : connection.getHeaderFields().keySet()) {
                    if (key != null) {
                        String value = connection.getHeaderField(key);
                        clientOutputStream.write((key + ": " + value + "\r\n").getBytes());
                    }
                }
                clientOutputStream.write("\r\n".getBytes()); // 响应头结束
                clientOutputStream.flush();
                // 转发响应体
                byte[] buffer = new byte[4096];
                int bytesRead;
                while ((bytesRead = targetInputStream.read(buffer)) != -1) {
                    clientOutputStream.write(buffer, 0, bytesRead);
                }
                clientOutputStream.flush();
            } catch (IOException e) {
                System.err.println("转发 HTTP 请求失败: " + e.getMessage());
                sendError(clientOutputStream, 502, "Bad Gateway");
            }
        }
        private void sendError(OutputStream out, int code, String message) throws IOException {
            String errorResponse = "HTTP/1.1 " + code + " " + message + "\r\n" +
                                   "Content-Type: text/plain\r\n" +
                                   "Connection: close\r\n" +
                                   "\r\n" +
                                   "Proxy Error: " + message;
            out.write(errorResponse.getBytes());
        }
    }
}
方法 优点 缺点 适用场景
标准库 - 无需依赖
- 原理清晰,适合学习
- 代码复杂,易出错
- 性能不高(同步阻塞)
- 功能有限
学习网络编程、快速原型验证
第三方库 (Netty) - 性能极高(异步NIO)
- 功能强大,稳定可靠
- 代码简洁,易于维护
- 需要引入外部依赖
- 学习曲线稍陡
生产环境、高性能需求、复杂网络服务

建议:

  • 如果你是初学者,想了解代理服务器的工作原理,可以从 标准库 的例子开始。
  • 如果你要构建一个真正可用的、高性能的代理服务器,请直接使用 Netty 这样的成熟框架,它能让你站在巨人的肩膀上,避免重复造轮子和处理棘手的底层问题。
分享:
扫描分享到社交APP
上一篇
下一篇