HTTP 与 SOCKS5 代理到底差在哪?用 Node.js 跑一次连接链路对比
摘要:HTTP 代理和 SOCKS5 代理的差别不只是一段地址的协议前缀。它们处理连接的层次、目标域名的解析位置、认证失败的表现和适用协议都不同。本文用 curl 与 Node.js 做一组可复现对照,并整理一套排错顺序。
同一个 HTTPS 接口,直连可以返回,切到 HTTP 代理后出现 407,换成 SOCKS5 又报 ENOTFOUND。如果只把这三种错误统称为"代理不可用",排查很容易走偏。
先给结论:
- HTTP 代理理解 HTTP 语义;访问 HTTPS 目标时,客户端通常先发送
CONNECT host:443建立隧道,再在隧道里完成 TLS 握手。 - SOCKS5 更接近通用的会话转发协议,不局限于 HTTP 请求。
- SOCKS5 使用目标主机名时,域名可以在客户端解析,也可以交给代理端解析;具体取决于客户端配置。
- 判断一种代理是否"适合",至少要同时看目标协议、DNS 解析位置、认证方式和客户端库支持,不能只比较一次请求的总耗时。
下面不比较供应商,也不讨论抽象的"速度快不快",只看一条请求真正经过了什么。
先画清两条连接路径
以访问 https://example.com/ 为例,HTTP 代理的常见路径是:
text
Node.js 客户端
│
│ TCP 连接到代理
▼
HTTP 代理
│
│ CONNECT example.com:443
│ ← 200 Connection Established
▼
到目标站的 TCP 隧道
│
│ TLS 握手 + HTTPS 请求
▼
example.com:443
这里有两个容易混淆的连接:客户端先连代理,代理再连目标站。407 Proxy Authentication Required 出现在代理认证阶段,还没有进入目标站业务。
SOCKS5 的路径不同:
text
Node.js 客户端
│
│ TCP 连接到 SOCKS5 代理
│ 协商认证方式
│ 请求连接 example.com:443
▼
SOCKS5 代理
│
│ 建立到目标站的 TCP 连接
▼
example.com:443
SOCKS5 不需要理解后续是不是 HTTP。隧道建立以后,里面可以传输 TLS 和 HTTPS,也可以承载其他基于 TCP 的协议。是否支持 UDP 转发,则要同时看协议能力、客户端库和代理服务端实现,不能看到 "SOCKS5" 就默认可用。
最小复现:先用 curl 把问题分层
不要一上来改业务代码。curl 的详细日志更适合确认连接到底停在哪一步。
1. HTTPS 经 HTTP 代理
bash
curl -v \
--proxy "http://YOUR_PROXY_HOST:YOUR_PROXY_PORT" \
--proxy-user "YOUR_PROXY_USER:YOUR_PROXY_PASSWORD" \
--connect-timeout 5 \
--max-time 15 \
"https://example.com/"
重点观察:
- 是否成功连接代理地址;
- 是否出现
CONNECT example.com:443; - 代理返回的是
200、407,还是直接断开; - CONNECT 成功后,TLS 握手是否继续;
- 最终 HTTP 状态码来自代理还是目标站。
不要把密码直接写进脚本仓库。上面的命令只是字段示意,实际运行可从安全的环境变量或凭据管理工具读取。
2. SOCKS5:本地解析目标域名
bash
curl -v \
--socks5 "YOUR_PROXY_HOST:YOUR_PROXY_PORT" \
--proxy-user "YOUR_PROXY_USER:YOUR_PROXY_PASSWORD" \
--connect-timeout 5 \
--max-time 15 \
"https://example.com/"
curl 官方手册明确区分了 --socks5 和 --socks5-hostname:前者由 curl 所在机器解析目标域名,然后把地址交给代理。
如果本地 DNS 无法解析目标域名,请求甚至还没进入目标连接阶段。这类失败不能简单归因于代理出口。
3. SOCKS5:代理端解析目标域名
bash
curl -v \
--socks5-hostname "YOUR_PROXY_HOST:YOUR_PROXY_PORT" \
--proxy-user "YOUR_PROXY_USER:YOUR_PROXY_PASSWORD" \
--connect-timeout 5 \
--max-time 15 \
"https://example.com/"
也可以使用等价的代理 URL:
bash
curl -v \
--proxy "socks5h://YOUR_PROXY_USER:YOUR_PROXY_PASSWORD@YOUR_PROXY_HOST:YOUR_PROXY_PORT" \
"https://example.com/"
socks5h 中的 h 可以记成 hostname:目标主机名交给代理端解析。注意这里只是改变目标域名的解析位置,代理服务器自身的域名仍然需要客户端能够找到。
用 curl 记录分阶段耗时
-v 适合看过程,--write-out 更适合做多次对照。下面的指标均是从请求开始累计到某一阶段的时间,并不是互相独立的耗时:
bash
curl --silent --show-error --output NUL \
--proxy "http://YOUR_PROXY_HOST:YOUR_PROXY_PORT" \
--connect-timeout 5 \
--max-time 15 \
--write-out "dns=%{time_namelookup}s connect=%{time_connect}s tls=%{time_appconnect}s ttfb=%{time_starttransfer}s total=%{time_total}s code=%{http_code}\n" \
"https://example.com/"
Windows PowerShell 中可使用 NUL;Linux 和 macOS 改为 /dev/null。
字段含义:
| 字段 | 表示什么 | 解读时的限制 |
|---|---|---|
time_namelookup |
curl 完成名称解析的累计时间 | 可能主要反映代理地址解析,不一定等于目标域名解析 |
time_connect |
完成到远端 TCP 连接的累计时间 | 使用代理时,远端通常先指代理 |
time_appconnect |
完成 TLS 等应用层握手的累计时间 | 包含此前 DNS、TCP、代理协商等时间 |
time_starttransfer |
收到首字节的累计时间 | 还包含服务端处理时间 |
time_total |
本次操作总时间 | 不能单独说明慢在哪一层 |
因此,tls - connect 可以作为观察差值,但不能在所有代理实现和连接复用场景中机械地等同于"纯 TLS 耗时"。需要严格压测时,还要固定 curl 版本、连接复用方式、目标接口和采样次数。
Node.js 中做同一组请求
下面使用 Node.js 20+,以及两个常用 Agent:
bash
npm install https-proxy-agent socks-proxy-agent
保存为 proxy-compare.mjs:
js
import https from 'node:https';
import { performance } from 'node:perf_hooks';
import { HttpsProxyAgent } from 'https-proxy-agent';
import { SocksProxyAgent } from 'socks-proxy-agent';
const target = process.env.TARGET_URL ?? 'https://example.com/';
const httpProxy = process.env.HTTP_PROXY_URL;
const socksProxy = process.env.SOCKS_PROXY_URL;
function requestOnce(label, agent) {
return new Promise((resolve) => {
const startedAt = performance.now();
const req = https.get(target, { agent, timeout: 10_000 }, (res) => {
let bytes = 0;
res.on('data', (chunk) => { bytes += chunk.length; });
res.on('end', () => resolve({
label,
ok: true,
status: res.statusCode,
bytes,
totalMs: Math.round((performance.now() - startedAt) * 10) / 10,
}));
});
req.on('timeout', () => req.destroy(new Error('request timeout')));
req.on('error', (error) => resolve({
label,
ok: false,
code: error.code ?? 'UNKNOWN',
message: error.message,
totalMs: Math.round((performance.now() - startedAt) * 10) / 10,
}));
});
}
const jobs = [];
if (httpProxy) {
jobs.push(requestOnce(
'http-connect',
new HttpsProxyAgent(httpProxy),
));
}
if (socksProxy) {
jobs.push(requestOnce(
'socks5',
new SocksProxyAgent(socksProxy),
));
}
if (jobs.length === 0) {
console.error('Set HTTP_PROXY_URL and/or SOCKS_PROXY_URL first.');
process.exitCode = 2;
} else {
console.table(await Promise.all(jobs));
}
PowerShell 运行示例:
powershell
$env:TARGET_URL = 'https://example.com/'
$env:HTTP_PROXY_URL = 'http://YOUR_PROXY_USER:YOUR_PROXY_PASSWORD@YOUR_PROXY_HOST:YOUR_PROXY_PORT'
$env:SOCKS_PROXY_URL = 'socks5h://YOUR_PROXY_USER:YOUR_PROXY_PASSWORD@YOUR_PROXY_HOST:YOUR_PROXY_PORT'
node .\proxy-compare.mjs
Linux/macOS 可写成:
bash
TARGET_URL='https://example.com/' \
HTTP_PROXY_URL='http://YOUR_PROXY_USER:YOUR_PROXY_PASSWORD@YOUR_PROXY_HOST:YOUR_PROXY_PORT' \
SOCKS_PROXY_URL='socks5h://YOUR_PROXY_USER:YOUR_PROXY_PASSWORD@YOUR_PROXY_HOST:YOUR_PROXY_PORT' \
node proxy-compare.mjs
下面是格式演示,不是真实线路测试结果:
text
(示例输出)
┌─────────┬────────────────┬──────┬────────┬───────┬─────────┐
│ (index) │ label │ ok │ status │ bytes │ totalMs │
├─────────┼────────────────┼──────┼────────┼───────┼─────────┤
│ 0 │ http-connect │ true │ 200 │ 1256 │ 418.7 │
│ 1 │ socks5 │ true │ 200 │ 1256 │ 463.2 │
└─────────┴────────────────┴──────┴────────┴───────┴─────────┘
这段 Node.js 代码只报告端到端耗时。它适合验证"相同目标和相同运行环境下,请求能否完成、返回什么状态、总共用了多久",不把内部阶段包装成精确测量。需要定位 DNS、TCP、TLS 和 TTFB 时,回到前面的 curl 输出更清楚。
三类常见错误,分别查什么
407 Proxy Authentication Required
这通常来自 HTTP 代理,表示代理已经收到请求,但认证信息缺失或不被接受。优先检查:
- 用户名与密码是否进行了正确的 URL 编码;
- 代理是否接受当前认证方式;
- 凭据是否绑定来源地址或使用范围;
- 代码是否真的把认证信息传给 Agent。
不要把目标 API 的 401 和代理的 407 混在一起。前者通常是目标服务认证,后者是代理认证。
ENOTFOUND 或 curl 退出码 6
这类错误与域名解析有关,但先要确认解析的是谁:
- 代理服务器域名解析失败:客户端连代理都做不到;
- SOCKS5 本地解析模式下目标域名失败:可尝试对照
socks5h; - 代理端解析目标域名失败:要检查代理侧 DNS,而不是继续修改本机 hosts。
ETIMEDOUT、curl 退出码 28
"超时"只是结果,不是原因。把它拆成:
- 连接代理超时;
- 代理连接目标超时;
- TLS 握手迟迟没有完成;
- 已建立连接,但首字节或响应体读取超时。
curl 的累计阶段时间和 -v 日志结合起来,通常比只看 Node.js 最终错误更有效。
HTTP 还是 SOCKS5:用任务条件判断
| 条件 | HTTP 代理 | SOCKS5 |
|---|---|---|
| 主要访问 HTTP/HTTPS API | 客户端生态成熟,通常容易接入 | 可以使用,但要确认库支持 |
| 需要代理理解或处理 HTTP | 更合适 | 不负责 HTTP 语义 |
| 需要承载非 HTTP 的 TCP 协议 | 能力取决于 CONNECT 目标与代理策略 | 通常更通用 |
| 需要明确控制目标 DNS 解析位置 | 取决于 HTTP 客户端和代理实现 | socks5/socks5h 通常更直观 |
| 排查代理认证 | 看 HTTP 状态和 Proxy-Authenticate | 看 SOCKS 协商或客户端错误 |
| 客户端只支持 HTTP Agent | 接入成本低 | 需要额外 Agent 或 transport |
实际选择时,顺序应该是:
- 客户端要访问什么协议;
- 当前库原生支持什么;
- 目标域名应该在哪里解析;
- 认证与凭据如何安全注入;
- 失败时能否拿到足够日志;
- 最后才比较同一测试条件下的性能。
几个容易踩的坑
第一,不要看到 HTTPS 目标就把代理 URL 也机械写成 https://。目标使用 HTTPS,不代表代理入口本身一定使用 HTTPS;代理提供方给出的协议和端口才是依据。
第二,不要把 socks5:// 与 socks5h:// 当成完全相同。至少在 curl 的语义里,前者本地解析目标域名,后者交给代理解析。
第三,不要用一次请求决定哪种协议更快。DNS 缓存、TLS 会话、连接复用、目标服务负载和代理节点状态都会影响结果。至少进行多轮采样,并记录失败率与分位数。
第四,不要在日志里完整打印代理 URL。URL 中可能包含用户名和密码,报错上报前需要脱敏。
复盘清单
遇到"代理请求失败"时,可以依次确认:
- 客户端能否解析并连接代理服务器;
- 代理协议与端口是否匹配;
- 认证失败发生在代理还是目标服务;
- HTTPS 经 HTTP 代理时,CONNECT 是否成功;
- SOCKS5 的目标域名是在本地还是代理端解析;
- 超时发生在连接、TLS、首字节还是响应读取阶段;
- 运行日志是否已去除凭据;
- 对比测试是否固定了目标、超时、并发和采样次数。
HTTP 与 SOCKS5 没有脱离场景的绝对优劣。真正值得保留的不是"哪一个更快"的单次结果,而是一套能把 DNS、连接、认证、握手和响应分开的复现方法。
参考资料
- curl 官方手册:
https://curl.se/docs/manpage.html - Node.js Performance Hooks:
https://nodejs.org/api/perf_hooks.html https-proxy-agent:https://github.com/TooTallNate/proxy-agents/tree/main/packages/https-proxy-agentsocks-proxy-agent:https://github.com/TooTallNate/proxy-agents/tree/main/packages/socks-proxy-agent