معظم الأدلة حول البروكسي في Node.js توصي بإضافة https-proxy-agent، socks-proxy-agent وعدد قليل من الحزم الأخرى. في الواقع، بدءًا من إصدار Node.js 18+، يحتوي عميل HTTP المدمج undici بالفعل على ProxyAgent، الذي يغطي معظم مهام تحليل واختبار API عبر البروكسي. في هذه المقالة، سنستعرض كيفية إعداد نفق HTTP/HTTPS عبر undici، تنظيم تدوير IP بين الطلبات وماذا تفعل إذا كنت بحاجة إلى SOCKS5، ولا ترغب في إضافة اعتماد إضافي.
لماذا undici بدلاً من axios و request
undici هو عميل HTTP/1.1 و HTTP/2، كتبته فريق Node.js ومنذ الإصدار 18 أصبح جزءًا من fetch العالمي. ليس لديه تبعيات انتقالية، وهو أسرع من axios في معظم اختبارات الأداء على تجميع الاتصالات، وما هو مهم لموضوعنا هو أن لديه وحدة أصلية للعمل مع البروكسي عبر نفق HTTP CONNECT.
عند تحليل Wildberries أو Ozon أو اختبار الإعلانات الإبداعية من مناطق جغرافية مختلفة، كل حزمة إضافية في node_modules هي نقطة فشل محتملة عند تحديث Node.js. يأتي undici مع وقت التشغيل، لذا تحصل على تنفيذ حديث لـ TLS و HTTP/2 و keep-alive دون خطر أن تتعطل مكتبة مهجورة في إصدار رئيسي جديد.
التثبيت بسيط للغاية:
npm install undici
إذا كان لديك Node.js 20+، فإن جزءًا من API متاح عالميًا دون الحاجة إلى استيراد، ولكن لا يزال يتعين استيراد ProxyAgent لأنه لا يتم تصديره إلى fetch العالمي بشكل افتراضي.
الإعداد الأساسي لـ ProxyAgent
يبدو المثال العملي الأدنى لطلب عبر البروكسي كما يلي:
import { ProxyAgent, fetch } from 'undici';
const proxyAgent = new ProxyAgent('http://proxy.example.com:8080');
const response = await fetch('https://httpbin.org/ip', {
dispatcher: proxyAgent,
});
const data = await response.json();
console.log(data);
النقطة الرئيسية هي المعامل dispatcher. هذه ليست خيار agent، كما في الوحدة القديمة http، بل هي بالضبط مُدير undici، الذي يعترض الاتصال قبل مصافحة TLS ويقيم نفق عبر طريقة CONNECT إلى المضيف المستهدف. بالنسبة لطلبات HTTPS عبر البروكسي، هذا أمر حاسم: البروكسي HTTP العادي لا يرى محتوى حركة المرور المشفرة، بل يقوم فقط بتمرير البايتات داخل نفق TCP.
إذا كنت تعمل مع undici.request على مستوى منخفض بدلاً من fetch، فإن الصيغة متشابهة:
import { request, ProxyAgent } from 'undici';
const proxyAgent = new ProxyAgent('http://proxy.example.com:8080');
const { statusCode, body } = await request('https://api.example.com/data', {
dispatcher: proxyAgent,
});
console.log(statusCode);
for await (const chunk of body) {
console.log(chunk.toString());
}
بالنسبة لمحللات الإنتاج التي تقوم بمئات الطلبات في الدقيقة، من المهم عدم إنشاء ProxyAgent جديد لكل طلب — فهذا يقتل تجميع الاتصالات. قم بإنشاء مثيل للوكيل مرة واحدة على مجموعة الاتصالات وأعد استخدامه بين الاستدعاءات.
مصادقة البروكسي باستخدام اسم المستخدم وكلمة المرور
معظم مزودي البروكسي المدفوعين يوفرون الوصول من خلال زوج من اسم المستخدم وكلمة المرور، وليس من خلال القائمة البيضاء لـ IP. في undici، يتم تمرير بيانات الاعتماد مباشرة في عنوان URL للبروكسي:
import { ProxyAgent, fetch } from 'undici';
const login = 'user123';
const password = 'pass456';
const proxyUrl = `http://${login}:${password}@gate.proxycove.com:9000`;
const proxyAgent = new ProxyAgent(proxyUrl);
const response = await fetch('https://httpbin.org/ip', {
dispatcher: proxyAgent,
});
console.log(await response.json());
لاحظ: إذا كان اسم المستخدم أو كلمة المرور يحتويان على رموز خاصة (@، :، /)، يجب عليك تمريرها عبر encodeURIComponent، وإلا فإن محلل URL داخل undici سيقوم بتحليل سلسلة الاتصال بشكل غير صحيح وستحصل على خطأ مصادقة 407 بدلاً من رسالة مفهومة.
بالنسبة للمهام التي تتطلب استقرار الجلسة — مثل المصادقة على سوق مرتبط بـ IP واحد خلال فترة الجلسة — من الأفضل استخدام بروكسي سكنية مع جلسة ثابتة: IP لا يتغير لفترة محددة، مما يعني أن الخدمة لا ترى "القفزات" بين المناطق الجغرافية ولا تتطلب إعادة CAPTCHA في كل طلب.
تدوير البروكسي بين الطلبات
إذا كان لديك قائمة من N بروكسي وتحتاج إلى توزيع الطلبات بينها، فإن أبسط خيار هو round-robin بدون مكتبات خارجية لقوائم الانتظار:
import { ProxyAgent, fetch } from 'undici';
const proxyList = [
'http://user:[email protected]:8000',
'http://user:[email protected]:8000',
'http://user:[email protected]:8000',
];
let currentIndex = 0;
function getNextAgent() {
const proxyUrl = proxyList[currentIndex];
currentIndex = (currentIndex + 1) % proxyList.length;
return new ProxyAgent(proxyUrl);
}
async function fetchWithRotation(url) {
const agent = getNextAgent();
return fetch(url, { dispatcher: agent });
}
for (let i = 0; i < 10; i++) {
const res = await fetchWithRotation('https://httpbin.org/ip');
console.log(await res.json());
}
يعمل هذا النهج بشكل جيد لتحليل بطاقات المنتجات أو مراقبة الأسعار، حيث يكون كل طلب لاحق مستقل منطقيًا عن الطلب السابق. بالنسبة للمهام التي تتطلب سرعة عالية في تغيير IP لكل طلب دون إدارة يدوية للقائمة، من الأسهل استخدام بروكسي مع تدوير تلقائي من جانب المزود — حينها يتقلص كل الكود أعلاه إلى عنوان URL ثابت واحد للبوابة، ويتولى بروكسي مركز البيانات أو مجموعة سكنية مع تدوير حسب المؤقت.
إذا كنت بحاجة إلى تدوير غير دائري فحسب، بل وزني — على سبيل المثال، إعطاء الأولوية للبروكسي الأسرع — أضف الوزن إلى هيكل البيانات:
const weightedProxies = [
{ url: 'http://user:[email protected]:8000', weight: 5 },
{ url: 'http://user:[email protected]:8000', weight: 1 },
];
function pickWeighted(list) {
const total = list.reduce((sum, p) => sum + p.weight, 0);
let rnd = Math.random() * total;
for (const p of list) {
if (rnd < p.weight) return p.url;
rnd -= p.weight;
}
return list[0].url;
}
التحقق من صحة البروكسي واستبعاد IP "الميت"
تتدهور مجموعة البروكسي مع مرور الوقت: يتم حظر بعض IP من قبل الموقع المستهدف، وبعضها يتوقف عن الاستجابة. لتجنب توجيه الطلبات عبر عناوين "ميتة"، أضف فحص صحة مع مهلة عبر AbortController المدمج:
import { ProxyAgent, fetch } from 'undici';
async function isProxyAlive(proxyUrl, timeoutMs = 5000) {
const agent = new ProxyAgent(proxyUrl);
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
const res = await fetch('https://httpbin.org/ip', {
dispatcher: agent,
signal: controller.signal,
});
return res.status === 200;
} catch {
return false;
} finally {
clearTimeout(timer);
}
}
async function filterAliveProxies(proxyList) {
const checks = await Promise.all(
proxyList.map(async (url) => ({ url, alive: await isProxyAlive(url) }))
);
return checks.filter((c) => c.alive).map((c) => c.url);
}
قم بتشغيل هذا الفحص كل بضع دقائق باستخدام مؤقت في الخلفية وقم بتحديث مجموعة البروكسي النشطة، وليس القائمة الثابتة بالكامل. هذا مهم بشكل خاص إذا كنت تحتفظ ببروكسي خاص بك على IP رخيصة من مراكز البيانات — حيث تكون نسبة الفشل أعلى من تلك الخاصة بمجموعات المزودين المدارة.
SOCKS5 بدون مكتبات خارجية
ملاحظة هامة: يعمل ProxyAgent المدمج في undici عبر نفق HTTP CONNECT ويفهم "من الصندوق" فقط بروكسي HTTP/HTTPS. بروتوكول SOCKS5 يعمل بشكل مختلف — إنه مصافحة ثنائية على مستوى TCP، و undici لا تحلله بشكل أصلي. إذا كنت بحاجة ماسة إلى SOCKS5 دون تثبيت socks-proxy-agent، يمكنك تنفيذ موصل بسيط يدويًا عبر وحدة net وخيار connect في Agent مخصص:
import net from 'node:net';
import { Agent, fetch } from 'undici';
function socksHandshake(socket, targetHost, targetPort) {
return new Promise((resolve, reject) => {
// الخطوة 1: التحية، طريقة بدون مصادقة
socket.write(Buffer.from([0x05, 0x01, 0x00]));
socket.once('data', (greeting) => {
if (greeting[1] !== 0x00) {
return reject(new Error('SOCKS5: طريقة المصادقة غير مدعومة'));
}
const hostBuf = Buffer.from(targetHost, 'utf8');
const request = Buffer.concat([
Buffer.from([0x05, 0x01, 0x00, 0x03, hostBuf.length]),
hostBuf,
Buffer.from([targetPort >> 8, targetPort & 0xff]),
]);
socket.write(request);
socket.once('data', (reply) => {
if (reply[1] !== 0x00) {
return reject(new Error('SOCKS5: الاتصال مرفوض'));
}
resolve(socket);
});
});
});
}
const socksAgent = new Agent({
connect: (options, callback) => {
const socket = net.connect(1080, 'socks-proxy.example.com', async () => {
try {
await socksHandshake(socket, options.hostname, 443);
callback(null, socket);
} catch (err) {
callback(err, null);
}
});
socket.on('error', (err) => callback(err, null));
},
});
const response = await fetch('https://httpbin.org/ip', { dispatcher: socksAgent });
console.log(await response.json());
هذه مصافحة مبسطة بدون مصادقة عبر اسم المستخدم وكلمة المرور (الطريقة 0x02) وبدون دعم IPv6 — من الأفضل إضافة فرع للمصادقة ومعالجة أنواع العناوين المختلفة للإنتاج. لكن لفهم الآلية — هذا يكفي: SOCKS5 لا يتطلب TLS داخل النفق أثناء مرحلة المصافحة، على عكس HTTP CONNECT، لذلك الكود أقصر مما يبدو للوهلة الأولى.
في الممارسة العملية، غالبًا ما يتم استخدام SOCKS5 ليس لتحليل صفحات الويب، ولكن لتمرير حركة مرور التطبيقات المحمولة ومتصفحات مكافحة الكشف مثل Dolphin Anty أو AdsPower، حيث يكون البروتوكول مطلوبًا للعمل على مستوى المقابس، وليس طلبات HTTP. إذا كانت مهمتك هي حركة مرور HTTP/HTTPS في Node.js، فعادةً ما يكون ProxyAgent المدمج كافيًا، ولا حاجة للعبث مع موصل SOCKS5 اليدوي.
معالجة المهلات ومنطق إعادة المحاولة
تقوم البروكسي بقطع الاتصالات بشكل متكرر أكثر من الاتصال المباشر — وهذا أمر طبيعي بالنسبة للشبكات السكنية، حيث يمكن أن يذهب العقدة إلى وضع عدم الاتصال في أي لحظة. تحتاج إلى تغليف إعادة المحاولة مع تأخير أسي:
import { ProxyAgent, fetch } from 'undici';
async function fetchWithRetry(url, proxyList, maxRetries = 3) {
let lastError;
for (let attempt = 0; attempt < maxRetries; attempt++) {
const proxyUrl = proxyList[attempt % proxyList.length];
const agent = new ProxyAgent(proxyUrl);
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 8000);
try {
const res = await fetch(url, { dispatcher: agent, signal: controller.signal });
clearTimeout(timer);
if (res.status >= 500) throw new Error(`خطأ في الخادم: ${res.status}`);
return res;
} catch (err) {
clearTimeout(timer);
lastError = err;
const delay = 500 * 2 ** attempt;
await new Promise((r) => setTimeout(r, delay));
}
}
throw new Error(`تم استنفاد جميع المحاولات: ${lastError?.message}`);
}
هذا التغليف يحل أيضًا مشكلة التدوير عند حدوث الأخطاء: إذا تعطل البروكسي الحالي، فإن المحاولة التالية تذهب تلقائيًا إلى IP آخر من القائمة، بدلاً من الاستمرار في الاتصال بنفس العقدة الميتة.
مقارنة undici ProxyAgent و https-proxy-agent و socks-proxy-agent
| الأداة | التبعيات | دعم SOCKS5 | HTTP/2 | السرعة |
|---|---|---|---|---|
| undici ProxyAgent | لا (مضمن في Node 18+) | لا من الصندوق | نعم | عالية |
| https-proxy-agent | 1-2 حزم | لا | لا | متوسطة |
| socks-proxy-agent | 3-4 حزم | نعم | لا | متوسطة |
بالنسبة لتحليل HTTP/HTTPS النقي مع الحد الأدنى من التبعيات، فإن undici ProxyAgent يتفوق بفضل تكامله وملاءمته مع HTTP/2. بالنسبة لـ SOCKS5 في الإنتاج، من الأسهل استخدام socks-proxy-agent الموثوق بدلاً من دعم مصافحة مكتوبة يدويًا — التنفيذ اليدوي من القسم أعلاه جيد للتعلم وحالات الحافة النادرة، ولكن ليس للإنتاج المزدحم مع آلاف الاتصالات في الساعة.
حالات عملية: التحليل والاختبار الجغرافي
مراقبة الأسعار على الأسواق. سكربت على undici مع تدوير البروكسي ومنطق إعادة المحاولة من الأقسام أعلاه مثالي لتجاوز حدود المعدل على Wildberries و Ozon: كل طلب لاحق لبطاقة منتج يذهب من IP جديد، وفحص الصحة يستبعد العناوين المحظورة من المجموعة تلقائيًا.
اختبار الإعلانات الإبداعية حسب الجغرافيا. إذا كنت بحاجة إلى التحقق من كيفية تقديم المحتوى لمستخدم من دولة معينة لإعلانات Google أو إعلانات Facebook، يكفي تغيير عنوان URL للبروكسي في ProxyAgent إلى عقدة المنطقة المطلوبة — لا حاجة لرفع متصفح إذا كان الاختبار يتم عبر API أو استجابة HTML من الخادم.
التحقق من توفر المحتوى في دول مختلفة. بالنسبة للمهام التي يقدم فيها الموقع محتوى مختلفًا اعتمادًا على IP، فإن الاتصالات السكنية تبدو للخادم المستهدف غير مميزة عن المستخدم العادي، مما يقلل من خطر الحظر مقارنة بنطاقات مراكز البيانات. هذا السيناريو مهم بشكل خاص بالنسبة لـ بروكسي المحمول، إذا كان الخدمة المستهدفة تتحقق من نوع الشبكة وتحظر نطاقات مراكز البيانات الواضحة.
التحقق الجماعي من نقاط نهاية API. عند اختبار الضغط على API الخاص بك عبر مجموعة بروكسي خارجية، من المهم مراقبة تجميع الاتصالات — يقوم undici بشكل افتراضي بإعادة استخدام اتصالات TCP داخل ProxyAgent واحد، مما يوفر وقتًا ملحوظًا على مصافحات TLS عند إجراء مئات الطلبات المتتالية إلى نفس المضيف.
الخاتمة
يغطي undici ProxyAgent المدمج معظم المهام المتعلقة ببروكسي HTTP/HTTPS في Node.js دون الحاجة إلى إضافة حزم خارجية: المصادقة عبر URL، النفق عبر CONNECT، التوافق مع HTTP/2 وتجميع الاتصالات تعمل على الفور. من السهل تجميع التدوير وفحص الصحة للبروكسي باستخدام الوعود والموقتات النقية، ويمكن تنفيذ SOCKS5 يدويًا عند الحاجة عبر موصل connect مخصص، على الرغم من أنه من المنطقي في الإنتاج استخدام مكتبة جاهزة مخصصة لـ SOCKS.
إذا كان سكربتك على undici مخصصًا لتحليل الأسواق أو مراقبة الأسعار، فانتبه إلى البروكسي السكنية — فهي أكثر مقاومة لأنظمة مكافحة الروبوتات من IP مراكز البيانات التقليدية، وغالبًا ما تتطلب حلول CAPTCHA إضافية أقل عند إجراء طلبات جماعية إلى Wildberries أو Ozon أو Avito.