Java 与 PHP
Java 的 HttpClient、OkHttp、Apache HttpClient,以及 PHP 的 cURL 和 Guzzle 接入方式。
两种语言各有一个共同的坑:Java 的 HttpClient 默认不会把凭据发给代理,PHP 的 CURLOPT_PROXY 需要显式指定代理类型。下面都处理好了。
Java 11+ HttpClient
标准库自带,但代理认证需要一个 Authenticator,而且要打开一个系统属性:
import java.net.Authenticator;
import java.net.InetSocketAddress;
import java.net.PasswordAuthentication;
import java.net.ProxySelector;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
public class NextProxyExample {
private static final String GATEWAY_HOST = "GATEWAY_HOST";
private static final int GATEWAY_PORT = 58971;
private static final String PROXY_USER =
"USERNAME-country-US-session-job1-time-30";
private static final String PROXY_PASS = "PASSWORD";
public static void main(String[] args) throws Exception {
// 没有这一行,JDK 会拒绝对代理发送 Basic 凭据,表现为持续 407
System.setProperty("jdk.http.auth.tunneling.disabledSchemes", "");
HttpClient client = HttpClient.newBuilder()
.proxy(ProxySelector.of(
new InetSocketAddress(GATEWAY_HOST, GATEWAY_PORT)))
.authenticator(new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(
PROXY_USER, PROXY_PASS.toCharArray());
}
})
.connectTimeout(Duration.ofSeconds(15))
.build();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.ipify.org?format=json"))
.timeout(Duration.ofSeconds(30))
.build();
HttpResponse<String> response =
client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode() + " " + response.body());
}
}
OkHttp
比标准库省事得多,凭据用 Authenticator 直接给:
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.util.concurrent.TimeUnit;
import okhttp3.Credentials;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
public class OkHttpExample {
static OkHttpClient buildClient(String proxyUser, String proxyPass) {
Proxy proxy = new Proxy(Proxy.Type.HTTP,
new InetSocketAddress("GATEWAY_HOST", 58971));
return new OkHttpClient.Builder()
.proxy(proxy)
.proxyAuthenticator((route, response) -> {
// 已经带过凭据还是 401/407 就别再重试,避免死循环
if (response.request().header("Proxy-Authorization") != null) {
return null;
}
return response.request().newBuilder()
.header("Proxy-Authorization",
Credentials.basic(proxyUser, proxyPass))
.build();
})
.connectTimeout(15, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build();
}
public static void main(String[] args) throws IOException {
OkHttpClient client = buildClient(
"USERNAME-country-US-session-job1-time-30", "PASSWORD");
Request request = new Request.Builder()
.url("https://api.ipify.org?format=json")
.build();
try (Response response = client.newCall(request).execute()) {
System.out.println(response.code() + " " + response.body().string());
}
}
}
OkHttp 的 SOCKS5
Proxy proxy = new Proxy(Proxy.Type.SOCKS,
new InetSocketAddress("GATEWAY_HOST", 58971));
// SOCKS5 的凭据走 java.net.Authenticator,不是 proxyAuthenticator
java.net.Authenticator.setDefault(new java.net.Authenticator() {
@Override
protected java.net.PasswordAuthentication getPasswordAuthentication() {
return new java.net.PasswordAuthentication(
"USERNAME-country-US", "PASSWORD".toCharArray());
}
});
OkHttpClient client = new OkHttpClient.Builder().proxy(proxy).build();
轮换 IP
OkHttp 的连接池会复用连接。想让每次都换 IP,要么每次新建客户端,要么调用 evictAll():
OkHttpClient client = buildClient("USERNAME-country-US", "PASSWORD");
for (int i = 0; i < 5; i++) {
Request request = new Request.Builder()
.url("https://api.ipify.org")
.build();
try (Response response = client.newCall(request).execute()) {
System.out.println(response.body().string());
}
// 清空连接池,下一次会建新连接从而重新选路
client.connectionPool().evictAll();
}
并发 + 每任务独立会话
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.stream.IntStream;
public class ConcurrentExample {
public static void main(String[] args) throws Exception {
try (ExecutorService pool = Executors.newFixedThreadPool(8)) {
List<?> futures = IntStream.range(0, 8)
.mapToObj(id -> pool.submit(() -> {
// session 值只允许 [A-Za-z0-9.-],用点号不用下划线
String user = "USERNAME-country-US-session-task."
+ id + "-time-30";
OkHttpClient client =
OkHttpExample.buildClient(user, "PASSWORD");
Request request = new Request.Builder()
.url("https://api.ipify.org")
.build();
try (Response response =
client.newCall(request).execute()) {
return id + " -> " + response.body().string();
}
}))
.toList();
for (Object future : futures) {
System.out.println(((java.util.concurrent.Future<?>) future).get());
}
}
}
}
Apache HttpClient 5
import org.apache.hc.client5.http.auth.AuthScope;
import org.apache.hc.client5.http.auth.UsernamePasswordCredentials;
import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.core5.http.HttpHost;
import org.apache.hc.core5.http.io.entity.EntityUtils;
public class ApacheExample {
public static void main(String[] args) throws Exception {
HttpHost proxy = new HttpHost("http", "GATEWAY_HOST", 58971);
BasicCredentialsProvider credentials = new BasicCredentialsProvider();
credentials.setCredentials(
new AuthScope(proxy),
new UsernamePasswordCredentials(
"USERNAME-country-US-session-job1-time-30",
"PASSWORD".toCharArray()));
try (CloseableHttpClient client = HttpClients.custom()
.setProxy(proxy)
.setDefaultCredentialsProvider(credentials)
.build()) {
HttpGet request = new HttpGet("https://api.ipify.org?format=json");
client.execute(request, response -> {
System.out.println(response.getCode() + " "
+ EntityUtils.toString(response.getEntity()));
return null;
});
}
}
}
PHP 的 cURL 扩展
<?php
declare(strict_types=1);
function proxyRequest(string $url, string $proxyUser, string $proxyPass): string
{
$handle = curl_init($url);
curl_setopt_array($handle, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_PROXY => 'GATEWAY_HOST:58971',
// 必须显式指定类型,否则 libcurl 的默认值可能不是你想的那个
CURLOPT_PROXYTYPE => CURLPROXY_HTTP,
CURLOPT_PROXYUSERPWD => $proxyUser . ':' . $proxyPass,
CURLOPT_CONNECTTIMEOUT => 15,
CURLOPT_TIMEOUT => 30,
]);
$body = curl_exec($handle);
if ($body === false) {
$error = curl_error($handle);
curl_close($handle);
throw new RuntimeException('请求失败: ' . $error);
}
$status = curl_getinfo($handle, CURLINFO_HTTP_CODE);
curl_close($handle);
if ($status !== 200) {
throw new RuntimeException('目标返回 HTTP ' . $status);
}
return (string) $body;
}
echo proxyRequest(
'https://api.ipify.org?format=json',
'USERNAME-country-US-session-job1-time-30',
'PASSWORD'
), PHP_EOL;
PHP 的 SOCKS5
curl_setopt_array($handle, [
CURLOPT_PROXY => 'GATEWAY_HOST:58971',
// 让代理解析域名,等价于 socks5h://
CURLOPT_PROXYTYPE => CURLPROXY_SOCKS5_HOSTNAME,
CURLOPT_PROXYUSERPWD => 'USERNAME-country-US:PASSWORD',
]);
Guzzle
composer require guzzlehttp/guzzle
<?php
declare(strict_types=1);
require 'vendor/autoload.php';
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
$proxyUser = 'USERNAME-country-US-session-job1-time-30';
$proxyPass = 'PASSWORD';
$proxy = sprintf('http://%s:%s@GATEWAY_HOST:58971',
rawurlencode($proxyUser), rawurlencode($proxyPass));
$client = new Client([
// http 和 https 都指到同一个明文代理,网关没有 TLS 入站
'proxy' => ['http' => $proxy, 'https' => $proxy],
'timeout' => 30,
'connect_timeout' => 15,
'http_errors' => false,
]);
try {
$response = $client->get('https://api.ipify.org?format=json');
echo $response->getStatusCode(), ' ', $response->getBody(), PHP_EOL;
} catch (RequestException $exception) {
echo '请求失败: ', $exception->getMessage(), PHP_EOL;
}
Guzzle 并发
<?php
use GuzzleHttp\Client;
use GuzzleHttp\Pool;
use GuzzleHttp\Psr7\Request;
$password = 'PASSWORD';
$requests = static function () use ($password): Generator {
for ($i = 0; $i < 8; $i++) {
// session 值只允许 [A-Za-z0-9.-]
$user = sprintf('USERNAME-country-US-session-task.%d-time-30', $i);
$proxy = sprintf('http://%s:%s@GATEWAY_HOST:58971',
rawurlencode($user), rawurlencode($password));
yield new Request('GET', 'https://api.ipify.org') => [
'proxy' => ['http' => $proxy, 'https' => $proxy],
];
}
};
// Guzzle 的 Pool 不支持 per-request 换代理,所以按会话分批建客户端
for ($i = 0; $i < 8; $i++) {
$user = sprintf('USERNAME-country-US-session-task.%d-time-30', $i);
$proxy = sprintf('http://%s:%s@GATEWAY_HOST:58971',
rawurlencode($user), rawurlencode($password));
$client = new Client(['proxy' => ['http' => $proxy, 'https' => $proxy]]);
echo $i, ' -> ', $client->get('https://api.ipify.org')->getBody(), PHP_EOL;
}
排错清单
| 语言 | 现象 | 原因 |
|---|---|---|
| Java | HTTPS 一直 407,Authenticator 没被调用 | 没设 jdk.http.auth.tunneling.disabledSchemes="" |
| Java | SOCKS5 凭据串了 | 全局 Authenticator 是进程级的,改用 HTTP 协议 |
| Java | IP 一直不变 | OkHttp 连接池复用,需要 evictAll() |
| PHP | 代理没生效 | 没设 CURLOPT_PROXYTYPE |
| PHP | 凭据被截断 | 密码放 URL 里没 rawurlencode |
| 通用 | 400 | invalid_proxy_parameters:参数格式、顺序或依赖错误 |
| 通用 | 402 | traffic_exhausted / account_quota_exhausted / quota_exhausted:流量或额度不足 |
| 通用 | 403 | account_disabled / account_expired / access_denied / target_denied:账号或策略拒绝 |
| 通用 | 429 | connection_limit:降低并发和建连频率 |
| 通用 | 502 | 出口连接失败 |
| 通用 | 503 | 暂无可用线路或服务暂不可用 |
| 通用 | 504 | 建立代理连接超时 |
HTTP / CONNECT 失败可读取 X-NextProxy-Error 与 JSON 的 error.code;目标网站返回的状态和 CONNECT 握手错误需分别处理。完整映射见 错误码对照。