لماذا تحتاج بروكسي HTTP في Rust؟
إذا كنت تبني بنية استخراج بيانات عالية الأداء في Rust، فستواجه حتماً حظر IPs أو قيود معدل الطلبات. البروكسي السكني (residential proxy) هو الحل — فهو يوزّع طلباتك عبر آلاف عناوين IP الحقيقية، ما يجعل كل طلب يبدو كأنه من مستخدم عادي. في هذا الدليل سنغطي كل ما تحتاجه من إعداد reqwest مع البروكسي، مروراً بـhyper منخفض المستوى، وصولاً إلى أنماط التزامن مع tokio وتجريدات تجمع البروكسي الدوار.
سنتعمق في مصادقة البروكسي، الجلسات اللاصقة، الاستهداف الجغرافي، TLS، أعلام الميزات، ومعالجة الأخطاء — كل ذلك مع أمثلة كود قابلة للتشغيل تستخدم ProxyHat كبوابة بروكسي.
إعداد reqwest مع بروكسي HTTP
reqwest هو عميل HTTP الأكثر شيوعاً في Rust. يدعم البروكسي مدمجاً عبر نوع Proxy. إليك كيف تبدأ مع بروكسي HTTP أساسي عبر ProxyHat:
use reqwest::{Client, Proxy};
use std::error::Error;
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
// بروكسي HTTP بسيط
let proxy = Proxy::all("http://user-country-US:pass@gate.proxyhat.com:8080")?;
let client = Client::builder()
.proxy(proxy)
.user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
.build()?;
let resp = client
.get("https://httpbin.org/ip")
.send()
.await?;
let body = resp.text().await?;
println!("IP from proxy: {}", body);
Ok(())
}
المصادقة والاستهداف الجغرافي والجلسات اللاصقة
مع ProxyHat، تُمرَّر أعلام الاستهداف الجغرافي والجلسة مباشرة في اسم المستخدم. هذا يتيح لك التحكم بسلاسة دون إعدادات إضافية:
use reqwest::{Client, Proxy};
use std::error::Error;
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
// استهداف جغرافي: ألمانيا، مدينة برلين
let geo_proxy = Proxy::all(
"http://user-country-DE-city-berlin:pass@gate.proxyhat.com:8080"
)?;
let geo_client = Client::builder()
.proxy(geo_proxy)
.build()?;
// جلسة لاصقة: نفس IP لعدة طلبات
let sticky_proxy = Proxy::all(
"http://user-session-myorder123-country-US:pass@gate.proxyhat.com:8080"
)?;
let sticky_client = Client::builder()
.proxy(sticky_proxy)
.cookie_store(true)
.build()?;
// طلبان بنفس الجلسة اللاصقة — نفس IP الخروج
let r1 = sticky_client.get("https://httpbin.org/ip").send().await?.text().await?;
let r2 = sticky_client.get("https://httpbin.org/ip").send().await?.text().await?;
println!("Sticky session r1: {}", r1);
println!("Sticky session r2: {}", r2);
Ok(())
}
نصيحة: استخدم الجلسات اللاصقة فقط عندما تحتاج نفس IP (مثل تسجيل الدخول متعدد الخطوات). للاستخراج العام، التدوير لكل طلب أفضل لأنه يوزع الحمل.
hyper منخفض المستوى مع proxy-connect لـ HTTPS
عندما تحتاج تحكماً أدق، hyper يمنحك وصولاً مباشراً إلى طبقة HTTP. لطلبات HTTPS عبر بروكسي HTTP، تحتاج إرسال طلب CONNECT ثم إنشاء نفق TLS عبر الاتصال الناتج:
use hyper::{Client, Uri, Request, body::Bytes};
use hyper::client::connect::HttpConnector;
use hyper_proxy::{Proxy, ProxyConnector, Intercept};
use hyper_rustls::HttpsConnector;
use std::error::Error;
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
// بناء موصل البروكسي فوق موصل HTTPS
let https = HttpsConnector::builder()
.with_webpki_roots()
.https_only(true)
.enable_http1()
.build();
let proxy_uri: Uri = "http://user-country-GB:pass@gate.proxyhat.com:8080"
.parse()?;
let proxy = Proxy::new(Intercept::All, proxy_uri);
let proxy_connector = ProxyConnector::from_proxy(https, proxy)?;
let client: Client<_, hyper::body::Body> = Client::builder()
.build(proxy_connector);
let target: Uri = "https://httpbin.org/ip".parse()?;
let resp = client.get(target).await?;
let body_bytes = hyper::body::to_bytes(resp.into_body()).await?;
println!("Response via hyper+proxy: {}", String::from_utf8_lossy(&body_bytes));
Ok(())
}
هذا النمط مفيد عندما تريد التحكم في تجمع الاتصالات، مهلات TLS، أو بناء عميل مخصص للاستخراج.
التزامن مع tokio و JoinSet
القوة الحقيقية لـ Rust تظهر في التزامن. tokio::task::JoinSet يتيح تشغيل مئات الطلبات المتزامنة مع إدارة دورة حياة كل مهمة:
use reqwest::{Client, Proxy};
use std::sync::Arc;
use tokio::task::JoinSet;
use std::error::Error;
const CONCURRENCY: usize = 50;
const TARGETS: &[&str] = &[
"https://httpbin.org/ip",
"https://httpbin.org/headers",
"https://httpbin.org/user-agent",
];
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let proxy = Proxy::all("http://user-country-US:pass@gate.proxyhat.com:8080")?;
let client = Arc::new(
Client::builder()
.proxy(proxy)
.pool_max_idle_per_host(CONCURRENCY)
.timeout(std::time::Duration::from_secs(30))
.build()?
);
let mut tasks: JoinSet<Result<String, reqwest::Error>> = JoinSet::new();
for url in TARGETS.iter().cycle().take(200) {
let client = Arc::clone(&client);
let url = url.to_string();
tasks.spawn(async move {
let resp = client.get(&url).send().await?;
resp.text().await
});
}
let mut success = 0u32;
let mut failures = 0u32;
while let Some(result) = tasks.join_next().await {
match result {
Ok(Ok(body)) => {
success += 1;
if success <= 5 {
println!("OK (first 5): {}...", &body[..body.len().min(80)]);
}
}
_ => failures += 1,
}
}
println!("Done: {} success, {} failures", success, failures);
Ok(())
}
الإنتاج أولاً: في بيئة الإنتاج، أضف حد معدل (rate limiter) مثل governor، وآلية إعادة المحاولة مع تراجع أسي (exponential backoff)، وتسجيل (logging) مهيأ.تجريد تجمع البروكسي الدوار
بدلاً من ترميز عنوان البروكسي ثابتاً، بنِ تجريداً يتيح التبديل بين عدة بروكسيات أو جلسات. إليك trait واضح مع تنفيذ ProxyHat:
use reqwest::{Client, Proxy};
use std::sync::Arc;
use tokio::sync::RwLock;
use std::error::Error;
/// تجريد تجمع البروكسي — نفّذه لأي مزود
#[async_trait::async_trait]
pub trait ProxyPool: Send + Sync {
async fn next_proxy_url(&self) -> String;
fn pool_name(&self) -> &str;
}
/// تنفيذ تجمع ProxyHat مع تدوير لكل طلب
pub struct ProxyHatPool {
username_base: String,
password: String,
counter: RwLock<u64>,
}
impl ProxyHatPool {
pub fn new(username_base: impl Into<String>, password: impl Into<String>) -> Self {
Self {
username_base: username_base.into(),
password: password.into(),
counter: RwLock::new(0),
}
}
}
#[async_trait::async_trait]
impl ProxyPool for ProxyHatPool {
async fn next_proxy_url(&self) -> String {
let mut count = self.counter.write().await;
*count += 1;
// كل طلب يحصل على جلسة فريدة → IP مختلف
let username = format!("{}-session-{}", self.username_base, count);
format!(
"http://{}:{}@gate.proxyhat.com:8080",
username, self.password
)
}
fn pool_name(&self) -> &str {
"proxyhat-residential"
}
}
/// عميل استخراج يستخدم التجريد
pub struct Scraper<P: ProxyPool> {
pool: Arc<P>,
}
impl<P: ProxyPool> Scraper<P> {
pub fn new(pool: Arc<P>) -> Self { Self { pool } }
pub async fn fetch(&self, url: &str) -> Result<String, reqwest::Error> {
let proxy_url = self.pool.next_proxy_url().await;
let proxy = Proxy::all(&proxy_url)?;
let client = Client::builder()
.proxy(proxy)
.timeout(std::time::Duration::from_secs(20))
.build()?;
client.get(url).send().await?.text().await
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let pool = Arc::new(ProxyHatPool::new("user-country-US", "pass"));
let scraper = Scraper::new(pool);
let body = scraper.fetch("https://httpbin.org/ip").await?;
println!("Rotated IP: {}", body);
Ok(())
}
هذا النمط يفصل منطق اختيار البروكسي عن منطق الطلب، ما يسهل التبديل بين مزودين أو إضافة منطق تدوير مخصص.
معالجة الأخطاء مع thiserror
في الاستخراج الإنتاجي، الأخطاء ليست استثنائية — بل هي القاعدة. شبّك أخطاء البروكسي وأخطاء HTTP وأخطاء TLS في نوع خطأ واحد واضح:
use thiserror::Error;
use reqwest::StatusCode;
#[derive(Debug, Error)]
pub enum ScrapeError {
#[error("Proxy connection failed: {0}")]
ProxyConnect(String),
#[error("HTTP {status} for {url}")]
Http { status: StatusCode, url: String },
#[error("Request timed out after {secs}s for {url}")]
Timeout { secs: u64, url: String },
#[error("TLS handshake failed: {0}")]
Tls(String),
#[error("Rate limited — retry after {retry_after}s")]
RateLimited { retry_after: u64 },
#[error("Body read error: {0}")]
Body(String),
}
impl From<reqwest::Error> for ScrapeError {
fn from(err: reqwest::Error) -> Self {
if err.is_timeout() {
ScrapeError::Timeout {
secs: 20,
url: err.url().map(|u| u.to_string()).unwrap_or_default(),
}
} else if err.is_connect() {
ScrapeError::ProxyConnect(err.to_string())
} else {
ScrapeError::Body(err.to_string())
}
}
}
/// إعادة محاولة مع تراجع أسي
pub async fn fetch_with_retry(
client: &reqwest::Client,
url: &str,
max_retries: u32,
) -> Result<String, ScrapeError> {
let mut attempt = 0u32;
loop {
match client.get(url).send().await {
Ok(resp) => {
let status = resp.status();
if status == StatusCode::TOO_MANY_REQUESTS {
let retry_after = resp
.headers()
.get("retry-after")
.and_then(|v| v.to_str().ok())
.and_then(|v| v.parse::<u64>().ok())
.unwrap_or(5);
if attempt >= max_retries {
return Err(ScrapeError::RateLimited { retry_after });
}
attempt += 1;
tokio::time::sleep(
std::time::Duration::from_secs(retry_after * attempt as u64)
).await;
continue;
}
if !status.is_success() {
return Err(ScrapeError::Http {
status,
url: url.to_string(),
});
}
return resp.text().await.map_err(ScrapeError::from);
}
Err(e) if attempt < max_retries => {
attempt += 1;
let delay = std::time::Duration::from_millis(200 * 2u64.pow(attempt));
eprintln!("Retry {}/{} after {:?}: {}", attempt, max_retries, delay, e);
tokio::time::sleep(delay).await;
continue;
}
Err(e) => return Err(ScrapeError::from(e)),
}
}
}
TLS: rustls مقابل native-tls
اختيار باقة TLS يؤثر على التوافق والأداء وملفات البصمة (fingerprinting). إليك المقارنة:
| المعيار | rustls | native-tls |
|---|---|---|
| التنفيذ | Rust نقي (ring / aws-lc) | مُشتق من OpenSSL / SChannel / Secure Transport |
| حجم البناء الثنائي | أصغر (لا رابط C) | أكبر (يعتمد على مكتبة النظام) |
| التوافق مع البصمة | بصمة TLS مختلفة عن المتصفح | أقرب لمتصفح النظام |
| التجميع المتقاطع | أسهل (لا تبعيات C) | يحتاج ربط OpenSSL متقاطع |
| دعم TLS 1.3 | مدمج | يعتمد على النظام |
| النضج | ناضج للاستخدام العام | ناضج جداً ومجرب |
للاستخراج العام، rustls أبسط في التجميع. إذا واجهت مواقع ترفض بصمة rustls، انتقل إلى native-tls. reqwest يدعم كليهما عبر أعلام الميزات:
# Cargo.toml — اختر واحدة فقط
# خيار A: rustls (أبسط تجميع)
[dependencies]
reqwest = { version = "0.12", default-features = false, features = [
"rustls-tls",
"proxy",
"charset",
"http2",
] }
# خيار B: native-tls (أفضل توافق بصمة)
[dependencies]
reqwest = { version = "0.12", default-features = false, features = [
"native-tls",
"proxy",
"charset",
"http2",
] }
# خيار C: اختيار وقت الترجمة عبر ميزة مخصصة
[features]
proxy-rustls = ["reqwest/rustls-tls", "reqwest/proxy"]
proxy-native = ["reqwest/native-tls", "reqwest/proxy"]
socks5 = ["reqwest/socks"] # لدعم SOCKS5
default = ["proxy-rustls"]
أعلام الميزات لتكوين البروكسي وقت الترجمة
يمكنك التحكم بدقة في ما يُدرج في البناء الثنائي. مثلاً، أضف دعم SOCKS5 فقط عند الحاجة:
// Cargo.toml
[features]
default = ["http-proxy"]
http-proxy = ["reqwest/proxy"]
socks5-proxy = ["reqwest/socks", "reqwest/proxy"]
all-proxies = ["http-proxy", "socks5-proxy"]
// src/proxy.rs
#[cfg(feature = "http-proxy")]
pub fn http_proxy_url() -> &'static str {
"http://user-country-US:pass@gate.proxyhat.com:8080"
}
#[cfg(feature = "socks5-proxy")]
pub fn socks5_proxy_url() -> &'static str {
"socks5://user-country-US:pass@gate.proxyhat.com:1080"
}
// بناء مع: cargo build --features socks5-proxy
// أو: cargo build --features all-proxies
هذا يقلل حجم البناء الثنائي ويمنع تبعيات غير ضرورية من التجميع.
نمط كامل: مستخرج إنتاجي مع التزامن والتدوير والأخطاء
لنجمّع كل ما تعلمناه في نمط واحد متكامل. هذا ما تستخدمه فعلياً في بيئة إنتاج:
use reqwest::{Client, Proxy, StatusCode};
use std::sync::Arc;
use std::time::Duration;
use tokio::task::JoinSet;
use tokio::sync::Semaphore;
// --- تجمع البروكسي ---
#[async_trait::async_trait]
pub trait ProxyPool: Send + Sync {
async fn next_proxy_url(&self) -> String;
}
pub struct ProxyHatRotating {
base_user: String,
password: String,
counter: std::sync::atomic::AtomicU64,
}
impl ProxyHatRotating {
pub fn new(base_user: &str, password: &str) -> Self {
Self {
base_user: base_user.to_string(),
password: password.to_string(),
counter: std::sync::atomic::AtomicU64::new(0),
}
}
}
#[async_trait::async_trait]
impl ProxyPool for ProxyHatRotating {
async fn next_proxy_url(&self) -> String {
let id = self.counter.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
format!(
"http://{}-session-{}:{}@gate.proxyhat.com:8080",
self.base_user, id, self.password
)
}
}
// --- إعادة المحاولة ---
pub async fn fetch_with_retry(
url: &str,
pool: &dyn ProxyPool,
max_retries: u32,
) -> Result<String, String> {
let mut attempt = 0u32;
loop {
let proxy_url = pool.next_proxy_url().await;
let proxy = Proxy::all(&proxy_url).map_err(|e| e.to_string())?;
let client = Client::builder()
.proxy(proxy)
.timeout(Duration::from_secs(20))
.build()
.map_err(|e| e.to_string())?;
match client.get(url).send().await {
Ok(resp) => {
let status = resp.status();
if status.is_success() {
return resp.text().await.map_err(|e| e.to_string());
}
if status == StatusCode::TOO_MANY_REQUESTS && attempt < max_retries {
attempt += 1;
tokio::time::sleep(Duration::from_secs(2u64.pow(attempt))).await;
continue;
}
return Err(format!("HTTP {} for {}", status, url));
}
Err(e) if attempt < max_retries => {
attempt += 1;
eprintln!("Retry {}/{}: {}", attempt, max_retries, e);
tokio::time::sleep(Duration::from_millis(300 * 2u64.pow(attempt))).await;
continue;
}
Err(e) => return Err(e.to_string()),
}
}
}
// --- المستخرج الرئيسي ---
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let pool = Arc::new(ProxyHatRotating::new("user-country-US", "pass"));
let urls = vec![
"https://httpbin.org/ip",
"https://httpbin.org/headers",
"https://httpbin.org/user-agent",
];
let semaphore = Arc::new(Semaphore::new(20)); // حد التزامن
let mut tasks: JoinSet<Result<String, String>> = JoinSet::new();
for url in urls.iter().cycle().take(100) {
let pool = Arc::clone(&pool);
let url = url.to_string();
let sem = Arc::clone(&semaphore);
tasks.spawn(async move {
let _permit = sem.acquire().await.unwrap();
fetch_with_retry(&url, &*pool, 3).await
});
}
let mut ok = 0u32;
let mut err = 0u32;
while let Some(res) = tasks.join_next().await {
match res {
Ok(Ok(body)) => {
ok += 1;
if ok <= 3 {
println!("✓ {}...", &body[..body.len().min(60)]);
}
}
_ => err += 1,
}
}
println!("Result: {} ok, {} errors", ok, err);
Ok(())
}
النقاط الرئيسية
- reqwest + Proxy هو أسرع طريق لبدء استخدام بروكسي HTTP في Rust — المصادقة والاستهداف الجغرافي يمرران عبر اسم المستخدم.
- hyper + proxy-connect للتحكم الدقيق في نفق TLS عبر بروكسي HTTP.
- JoinSet + Semaphore يمنحك تزامناً خاضعاً للرقابة دون إغراق البوابة أو الهدف.
- trait ProxyPool يفصل منطق اختيار البروكسي عن منطق الطلب — سهّل التبديل بين المزودين.
- thiserror يجعل أنواع الأخطاء واضحة وقابلة للمطابقة، مع إعادة محاولة بتراجع أسي.
- rustls أبسط تجميعاً؛ native-tls أقرب لبصمة المتصفح — اختر حسب احتياجك.
- أعلام الميزات تتيح تضمين أو استبعاد دعم البروكسي وSOCKS5 وقت التجميع.
الأسئلة الشائعة
كيف أستخدم بروكسي SOCKS5 في reqwest؟
فعّل ميزة socks في Cargo.toml واستخدم Proxy::all("socks5://user:pass@gate.proxyhat.com:1080"). منفذ SOCKS5 في ProxyHat هو 1080.
هل reqwest يدعم الجلسات اللاصقة (sticky sessions)؟
نعم — مرر user-session-YOURID في اسم المستخدم. طالما أن العميل يحتفظ بتجمع الاتصالات، ستستخدم الطلبات المتتالية نفس نفق البروكسي. مع ProxyHat، معرف الجلسة يضمن نفس IP الخروج.
ما الفرق بين rustls و native-tls للاستخراج؟
rustls أبسط في التجميع المتقاطع ولا يحتاج OpenSSL، لكن بصمة TLS تختلف عن المتصفح. native-tls يستخدم مكتبة النظام فبصمته أقرب للمتصفح، لكنه يحتاج ربطاً بـOpenSSL.
كيف أتعامل مع 429 Too Many Requests؟
نفّذ إعادة محاولة مع تراجع أسي كما في مثال fetch_with_retry. اقرأ رأس Retry-After إن وُجد. استخدم Semaphore لتحديد التزامن ومنع إغراق الهدف.
هل يمكنني تغيير البروكسي لكل طلب بنفس Client؟
لا — reqwest يربط البروكسي بالعميل وقت البناء. لتدوير البروكسي لكل طلب، أنشئ عميلاً جديداً لكل طلب (كما في نمط ProxyPool)، أو استخدم reqwest::ClientBuilder::proxy مع بروكسي مختلف لكل تجمع اتصالات.






