如何在 Java JDK HttpClient 中使用 socks 代理

Posted

技术标签:

【中文标题】如何在 Java JDK HttpClient 中使用 socks 代理【英文标题】:How to use a socks proxy with Java JDK HttpClient 【发布时间】:2021-11-17 20:22:02 【问题描述】:

注意:这不是https proxy with JDK11 client 的重复,后者是关于 HTTP 代理的。我的问题是关于 socks 代理的,它需要不同的解决方案。也不是 How can I use HttpClient-4.5 against a SOCKS proxy? 的复制品,它是关于 Apache HttpClient 而不是 JDK HttpClient。

如何通过java.net.http.HttpClient 使用 socks 5 代理?

我尝试了下面的代码,但它导致了以下异常:

Exception in thread "main" java.io.IOException: HTTP/1.1 header parser received no bytes
    at java.net.http/jdk.internal.net.http.HttpClientImpl.send(HttpClientImpl.java:586)

代码:

import java.io.IOException;
import java.net.*;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.Collections;
import java.util.List;

public class ProxyApp 
    public static void main(String[] args) throws Exception 
        run1();
    

    public static void run1() throws Exception 
        HttpClient client = HttpClient.newBuilder()
                .version(HttpClient.Version.HTTP_1_1)
                .proxy(ProxySelector.of(new InetSocketAddress("localhost", 8181)))
                .build();

        HttpRequest request = HttpRequest
                .newBuilder(new URI("http://example.org/"))
                .build();
        HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());

        String verificationText = "Example Domain";
        // Should print "true" otherwise the request failed
        System.out.println(response.body().contains(verificationText));
    

【问题讨论】:

【参考方案1】:

问题 1:JDK HttpClient 不支持 socks 代理

JDK HttpClient 不支持 socks 代理,只支持 HTTP(S) 代理,即使像 java.net.Proxy.Type.SOCKS 这样的类型确实存在。 Java JDK 中根本没有记录此问题,这会导致 Java 在您尝试使用 socks 代理时通过使用非代理连接默默地忽略它。

这个问题在 2018 年被作为一个 bug 报告给 OpenJDK,但尚未修复:https://bugs.openjdk.java.net/browse/JDK-8214516 负责这种无声无视的代码是: https://github.com/openjdk/jdk/blob/29e552c03a2825f9526330072668a1d63ac68fd4/src/java.net.http/share/classes/jdk/internal/net/http/HttpRequestImpl.java#L299

    private static Proxy retrieveProxy(ProxySelector ps, URI uri) 
        Proxy proxy = null;
        List<Proxy> pl = ps.select(uri);
        if (!pl.isEmpty()) 
            Proxy p = pl.get(0);
            if (p.type() == Proxy.Type.HTTP)
                proxy = p;
        
        return proxy;
    

因此,如果 ProxySelector 提供类型为 Proxy.Type.SOCKSProxy 作为第一个代理,那么它会被忽略,并返回 null,这会导致 HttpRequestImpl 根本不使用代理。

问题 2:API 不清楚

就算解决了缺少的JDK实现问题,那么也存在API不清晰的问题:

不明确的问题是由java.net.ProxySelector.of(InetSocketAddress) 引起的,它没有记录它创建了什么样的代理。代理可以是 Web (http) 代理或 socks 代理。 ProxySelector.of() 的源代码显示它创建了一个私有 StaticProxySelector 对象,该对象创建了一个 Web 代理(Proxy.Type.HTTP)而不是一个 socks 代理(Proxy.Type.SOCKS)。

所以解决方案(如果 JDK 内部代码将被修复)是创建自己的 ProxySelector。这显示在下面的解决方案中。

import java.io.IOException;
import java.net.*;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.Collections;
import java.util.List;

public class ProxyApp 
    public static void main(String[] args) throws Exception 
        run2();
    

    public static void run2() throws Exception 
        ProxySelector proxySelector = new ProxySelector() 
            private List<Proxy> proxies;

            
                Proxy proxy = new Proxy(Proxy.Type.SOCKS, new InetSocketAddress("localhost", 8181));
                proxies = Collections.singletonList(proxy);
            

            @Override
            public List<Proxy> select(URI uri) 
                return proxies;
            

            @Override
            public void connectFailed(URI uri, SocketAddress sa, IOException ioe) 
                throw new RuntimeException(ioe);
            
        ;

        HttpClient client = HttpClient.newBuilder()
                .version(HttpClient.Version.HTTP_1_1)
                .proxy(proxySelector)
                .build();

        HttpRequest request = HttpRequest
                .newBuilder(new URI("http://example.org/"))
                .build();
        HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());

        String verificationText = "Example Domain";
        // Should print "true" otherwise the request failed
        System.out.println(response.body().contains(verificationText));
    

【讨论】:

以上是关于如何在 Java JDK HttpClient 中使用 socks 代理的主要内容,如果未能解决你的问题,请参考以下文章

Java11 中找不到 jdk.incubator.httpclient 模块

JDK 之 HttpClient(jdk11)

Java爬虫技术之HttpClient学习笔记

HttpClient

如何实现java http长连接推送数据

使用eclipse阅读java源码