自定义 TCP 协议可以在代理允许向目标发起 CONNECT 时穿过 HTTP 代理。应用协议本身不必变成 HTTP:HTTP 建立隧道,随后应用在隧道内交换自己的字节。

OpenJDK 25 在 Socket(Proxy) 中提供了 HTTP 代理路径。它的实现会完成CONNECT 交换,并返回一个已连接的 socket 供应用使用。

Proxy route = new Proxy(
    Proxy.Type.HTTP,
    new InetSocketAddress("proxy.example", 8080));

try (Socket socket = new Socket(route)) {
    socket.setSoTimeout(10000);
    socket.connect(
        InetSocketAddress.createUnresolved("tcp-service.example", 8443),
        5000);
    InputStream input = socket.getInputStream();
    OutputStream output = socket.getOutputStream();
}

这段代码使用了 java.net 和 java.io 中的类。请替换两个端点,并在离开 try 块之前完成协议交换。对于允许的公共 TCP 目标,要先核对该目标端口是否允许 CONNECT,然后才从 HTTP 代理列表中选择 HTTP 候选。代理地址是 Java 首先连接的地方;传给 connect 的地址则指明代理应当到达的服务。

未解析的目标会在该实现的 CONNECT 请求中保留其主机名。而已解析的 InetSocketAddress 则会向该请求提供解析后的 IP 地址。当代理必须解析服务名称或应用基于主机名的策略时,这一区别很重要。

成功连接之后剩下什么

得到的流承载的是隧道中服务的数据。它们不会暴露一个需要你的应用读取并丢弃的独立 HTTP 响应。再加一次 CONNECT 交换,就会在 JDK 已经建立隧道之后把 HTTP 文本发给该服务。

示例设置了连接超时和 socket 读取超时;它们是彼此独立的控制,而不是应用会话的单一总期限。

CONNECT 权限仍然属于代理。转发普通 Web 请求的端点可能会拒绝该服务的端口,或在打开隧道之前要求身份验证。请把这些失败与“接受隧道但随后拒绝你的应用协议”的服务器区分开。

如果该协议需要 TLS,请通过已连接的 socket,以服务预期的主机名和常规证书校验来协商 TLS。仅靠 CONNECT 并不会加密数据流。对于真正的 HTTP API,HTTP 客户端仍是更方便的接口,因为它还会处理 HTTP 消息。

相关文章