بازگشت به وبلاگ

تنظیم پروکسی در Go (Golang): http.Client، SOCKS5 و چرخش IP برای پارسینگ و اتوماسیون

چگونه پروکسی سرور را در Go از طریق http.Client به درستی تنظیم کنیم - از پیکربندی پایه تا چرخش IP و کار با SOCKS5. راهنمای کامل با مثال‌های کد.

📅۲۰ مرداد ۱۴۰۵
```html

اگر شما یک پارسر می‌نویسید، جمع‌آوری داده‌ها را اتوماسیون می‌کنید یا API را از مناطق مختلف تست می‌کنید — دیر یا زود با مسدودیت‌های IP مواجه خواهید شد. Go یکی از محبوب‌ترین زبان‌ها برای نوشتن ابزارهای شبکه با عملکرد بالا است و تنظیم پروکسی در اینجا ویژگی‌های خاص خود را دارد. در این راهنما همه چیز را از پیکربندی پایه http.Client تا چرخش پیشرفته IP با پشتیبانی از SOCKS5 بررسی خواهیم کرد.

چرا به پروکسی در برنامه‌های Go نیاز داریم

Go به استانداردی برای نوشتن ابزارهای شبکه با بار بالا تبدیل شده است: پارسرها، خزنده‌ها، ربات‌های نظارت بر قیمت، ابزارهای اتوماسیون API. اینجا است که نیاز به پروکسی بیشتر از همه احساس می‌شود — و اینجا است که مهم است همه چیز را از ابتدا به درستی تنظیم کنید.

سناریوهای اصلی که بدون پروکسی نمی‌توان از آن‌ها عبور کرد:

  • پارس کردن بازارها — Wildberries، Ozon، AliExpress IP را پس از 50–200 درخواست از یک آدرس مسدود می‌کنند. بدون تغییر IP، پارسر متوقف می‌شود.
  • تست جغرافیایی — بررسی نتایج موتورهای جستجو، تبلیغات یا قیمت‌ها از کشورهای مختلف و مناطق.
  • دور زدن محدودیت نرخ — توزیع بار بین چندین IP به دور زدن محدودیت تعداد درخواست‌ها در دقیقه کمک می‌کند.
  • ناشناس بودن — پنهان کردن IP واقعی سرور هنگام کار با API‌های خارجی.
  • تست CDN و جغرافیایی — بررسی صحت ارائه محتوا برای مناطق مختلف.

کتابخانه استاندارد Go ابزارهای انعطاف‌پذیری برای کار با پروکسی ارائه می‌دهد — از طریق http.Transport و http.Client. هر روش را به تفصیل بررسی خواهیم کرد.

تنظیم پایه پروکسی HTTP از طریق http.Client

در Go پروکسی در سطح http.Transport — لایه انتقال HTTP-کلاینت تنظیم می‌شود. این کنترل کامل بر این که درخواست‌ها از چه پروکسی عبور می‌کنند را می‌دهد و به شما اجازه می‌دهد چندین کلاینت با پروکسی‌های مختلف به طور همزمان ایجاد کنید.

مثال کارکردی حداقلی از تنظیم پروکسی HTTP:

package main

import (
    "fmt"
    "io"
    "net/http"
    "net/url"
)

func main() {
    // آدرس سرور پروکسی را مشخص می‌کنیم
    proxyURL, err := url.Parse("http://123.45.67.89:8080")
    if err != nil {
        panic(err)
    }

    // ایجاد یک حمل و نقل با پروکسی
    transport := &http.Transport{
        Proxy: http.ProxyURL(proxyURL),
    }

    // ایجاد یک کلاینت با حمل و نقل ما
    client := &http.Client{
        Transport: transport,
    }

    // انجام درخواست از طریق پروکسی
    resp, err := client.Get("https://httpbin.org/ip")
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()

    body, _ := io.ReadAll(resp.Body)
    fmt.Println(string(body))
}

چه اتفاقی در این کد می‌افتد:

  1. url.Parse — رشته‌ای با آدرس پروکسی را به ساختار *url.URL تجزیه می‌کند
  2. http.Transport{Proxy: http.ProxyURL(proxyURL)} — یک حمل و نقل ایجاد می‌کند که تمام درخواست‌ها را از طریق پروکسی مشخص شده هدایت می‌کند
  3. http.Client{Transport: transport} — یک کلاینت با حمل و نقل ما ایجاد می‌کند

نکته مهم: اگر شما از http.DefaultClient استفاده می‌کنید (یعنی فقط http.Get() بدون کلاینت صریح)، پروکسی اعمال نخواهد شد. همیشه یک http.Client شخصی برای کار با پروکسی ایجاد کنید.

💡 نکته

یک تایم‌اوت به http.Client اضافه کنید: Timeout: 30 * time.Second. سرورهای پروکسی ممکن است کندتر از اتصال مستقیم پاسخ دهند و بدون تایم‌اوت، گوروتین‌ها به طور نامحدود متوقف خواهند شد.

پروکسی از طریق متغیرهای محیطی

Go از متغیرهای محیطی استاندارد برای پروکسی پشتیبانی می‌کند — HTTP_PROXY، HTTPS_PROXY و NO_PROXY. حمل و نقل استاندارد http.DefaultTransport به طور خودکار آن‌ها را از طریق تابع http.ProxyFromEnvironment می‌خواند.

تنظیم متغیرهای محیطی قبل از اجرای برنامه:

# در ترمینال (Linux/macOS)
export HTTP_PROXY="http://123.45.67.89:8080"
export HTTPS_PROXY="http://123.45.67.89:8080"
export NO_PROXY="localhost,127.0.0.1"

# اجرای برنامه
go run main.go

اگر می‌خواهید از ProxyFromEnvironment در حمل و نقل خود (برای مثال، با تنظیمات اضافی TLS) استفاده کنید، اینگونه عمل کنید:

transport := &http.Transport{
    Proxy: http.ProxyFromEnvironment,
    // تنظیمات اضافی...
    TLSHandshakeTimeout:   10 * time.Second,
    ResponseHeaderTimeout: 30 * time.Second,
}

client := &http.Client{
    Transport: transport,
    Timeout:   60 * time.Second,
}

این رویکرد برای محیط‌های تولید مناسب است، جایی که آدرس پروکسی از طریق پیکربندی استقرار (Docker، Kubernetes، systemd) تعیین می‌شود و نه به صورت هاردکد در کد. همچنین این کار تغییر پروکسی را بدون نیاز به بازترکیب برنامه ساده می‌کند.

اتصال پروکسی SOCKS5 در Golang

SOCKS5 پروتکلی بسیار عمومی‌تر نسبت به پروکسی HTTP است. این پروتکل در سطح پایین‌تری کار می‌کند و از هر گونه اتصال TCP/UDP پشتیبانی می‌کند، نه فقط HTTP/HTTPS. برای کار با پارسرها و اتوماسیون، SOCKS5 اغلب ترجیح داده می‌شود — به خصوص هنگام استفاده از پروکسی‌های مسکونی که بیشتر اوقات دقیقاً از این پروتکل ارائه می‌شوند.

برای کار با SOCKS5 در Go به بسته golang.org/x/net/proxy نیاز دارید:

go get golang.org/x/net/proxy

مثال اتصال پروکسی SOCKS5 بدون احراز هویت:

package main

import (
    "fmt"
    "io"
    "net/http"
    "golang.org/x/net/proxy"
)

func main() {
    // ایجاد یک دیالر SOCKS5
    dialer, err := proxy.SOCKS5("tcp", "123.45.67.89:1080", nil, proxy.Direct)
    if err != nil {
        panic(err)
    }

    // ایجاد یک حمل و نقل با دیالر سفارشی
    transport := &http.Transport{
        Dial: dialer.Dial,
    }

    client := &http.Client{
        Transport: transport,
    }

    resp, err := client.Get("https://httpbin.org/ip")
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()

    body, _ := io.ReadAll(resp.Body)
    fmt.Println(string(body))
}

از Go 1.20+ می‌توانید همچنین از پشتیبانی داخلی SOCKS5 از طریق url.Parse با پیشوند socks5:// استفاده کنید:

proxyURL, _ := url.Parse("socks5://123.45.67.89:1080")

transport := &http.Transport{
    Proxy: http.ProxyURL(proxyURL),
}

client := &http.Client{Transport: transport}

این نحو ساده‌تر است و به وابستگی‌های اضافی نیاز ندارد. اما برای سناریوهای پیچیده‌تر (برای مثال، SOCKS5 با احراز هویت در نسخه‌های قدیمی‌تر Go) بسته golang.org/x/net/proxy همچنان معتبر است.

احراز هویت: نام کاربری و رمز عبور در پروکسی

بیشتر ارائه‌دهندگان پروکسی تجاری از احراز هویت با نام کاربری و رمز عبور استفاده می‌کنند. دو روش برای ارسال اطلاعات کاربری در Go وجود دارد.

روش 1: در URL گنجانده شود

// فرمت: protocol://username:password@host:port
proxyURL, err := url.Parse("http://myuser:[email protected]:8080")
if err != nil {
    panic(err)
}

transport := &http.Transport{
    Proxy: http.ProxyURL(proxyURL),
}

client := &http.Client{
    Transport: transport,
    Timeout:   30 * time.Second,
}

روش 2: از طریق ساختار url.URL (بیشتر ایمن)

proxyURL := &url.URL{
    Scheme: "http",
    Host:   "proxy.example.com:8080",
    User:   url.UserPassword("myuser", "mypassword"),
}

transport := &http.Transport{
    Proxy: http.ProxyURL(proxyURL),
}

client := &http.Client{
    Transport: transport,
    Timeout:   30 * time.Second,
}

روش دوم ترجیح داده می‌شود — زیرا به درستی کاراکترهای خاص در رمز عبور (برای مثال، @، #، :) را که ممکن است تجزیه URL در گزینه اول را خراب کند، مدیریت می‌کند.

برای SOCKS5 با احراز هویت از طریق بسته golang.org/x/net/proxy:

auth := &proxy.Auth{
    User:     "myuser",
    Password: "mypassword",
}

dialer, err := proxy.SOCKS5("tcp", "proxy.example.com:1080", auth, proxy.Direct)
if err != nil {
    panic(err)
}

transport := &http.Transport{
    Dial: dialer.Dial,
}

client := &http.Client{
    Transport: transport,
    Timeout:   30 * time.Second,
}

⚠️ امنیت

هرگز نام کاربری و رمز عبور پروکسی را به صورت هاردکد در کد قرار ندهید. از متغیرهای محیطی یا فایل‌های پیکربندی استفاده کنید که در مخزن قرار نمی‌گیرند. .env را به .gitignore اضافه کنید.

چرخش پروکسی: تغییر IP بین درخواست‌ها

یک پروکسی — برای تست خوب است، اما برای پارس واقعی به چرخش IP نیاز دارید. سایت‌ها فرکانس درخواست‌ها از یک آدرس را ردیابی می‌کنند و حتی یک پروکسی خوب نیز مسدود خواهد شد اگر از طریق آن هزاران درخواست متوالی ارسال کنید.

دو رویکرد برای چرخش در Go وجود دارد:

رویکرد 1: لیست پروکسی با round-robin

package main

import (
    "fmt"
    "io"
    "net/http"
    "net/url"
    "sync/atomic"
    "time"
)

type ProxyRotator struct {
    proxies []*url.URL
    counter uint64
}

func NewProxyRotator(proxyURLs []string) (*ProxyRotator, error) {
    proxies := make([]*url.URL, 0, len(proxyURLs))
    for _, p := range proxyURLs {
        u, err := url.Parse(p)
        if err != nil {
            return nil, err
        }
        proxies = append(proxies, u)
    }
    return &ProxyRotator{proxies: proxies}, nil
}

// GetClient پروکسی بعدی را به صورت گردشی برمی‌گرداند
func (r *ProxyRotator) GetClient() *http.Client {
    idx := atomic.AddUint64(&r.counter, 1) % uint64(len(r.proxies))
    proxyURL := r.proxies[idx]

    transport := &http.Transport{
        Proxy: http.ProxyURL(proxyURL),
    }

    return &http.Client{
        Transport: transport,
        Timeout:   30 * time.Second,
    }
}

func main() {
    proxyList := []string{
        "http://user:[email protected]:8080",
        "http://user:[email protected]:8080",
        "http://user:[email protected]:8080",
    }

    rotator, err := NewProxyRotator(proxyList)
    if err != nil {
        panic(err)
    }

    // هر درخواست از طریق پروکسی بعدی می‌رود
    for i := 0; i < 10; i++ {
        client := rotator.GetClient()
        resp, err := client.Get("https://httpbin.org/ip")
        if err != nil {
            fmt.Printf("خطای درخواست %d: %v\n", i, err)
            continue
        }
        body, _ := io.ReadAll(resp.Body)
        resp.Body.Close()
        fmt.Printf("درخواست %d: %s\n", i, string(body))
    }
}

رویکرد 2: انتخاب دینامیک پروکسی از طریق تابع

فیلد Proxy در http.Transport تابعی با امضای func(*http.Request) (*url.URL, error) را می‌پذیرد. این امکان را می‌دهد که پروکسی را به صورت دینامیک برای هر درخواست انتخاب کنید:

proxies := []*url.URL{
    mustParseURL("http://proxy1.example.com:8080"),
    mustParseURL("http://proxy2.example.com:8080"),
    mustParseURL("http://proxy3.example.com:8080"),
}

var counter uint64

transport := &http.Transport{
    Proxy: func(req *http.Request) (*url.URL, error) {
        // پروکسی را بر اساس درخواست انتخاب می‌کند (round-robin)
        idx := atomic.AddUint64(&counter, 1) % uint64(len(proxies))
        return proxies[idx], nil
    },
}

client := &http.Client{
    Transport: transport,
    Timeout:   30 * time.Second,
}

func mustParseURL(s string) *url.URL {
    u, err := url.Parse(s)
    if err != nil {
        panic(err)
    }
    return u
}

رویکرد دوم زیباتر است — یک کلاینت به طور خودکار پروکسی را برای هر درخواست چرخش می‌دهد. این به ویژه در درخواست‌های همزمان از طریق گوروتین‌ها راحت است.

کدام نوع پروکسی را برای پروژه Go انتخاب کنیم

انتخاب نوع پروکسی به طور مستقیم بر موفقیت پروژه شما تأثیر می‌گذارد. برای وظایف مختلف انواع مختلفی مناسب هستند — بیایید گزینه‌های اصلی را بررسی کنیم:

نوع پروکسی سرعت ناشناس بودن بهترین برای
مرکز داده ⚡ بالا متوسط پارس کردن داده‌های باز، تست API، تست بار
مسکونی 🔄 متوسط بالا پارس کردن بازارها (Wildberries، Ozon)، دور زدن حفاظت ضد ربات
موبایل 🔄 متوسط حداکثر کار با شبکه‌های اجتماعی، سرویس‌های با حفاظت ضد ربات سخت

برای پارس کردن بازارها (Wildberries، Ozon، Avito): از پروکسی‌های مسکونی استفاده کنید. این پلتفرم‌ها به طور فعال از سیستم‌های شناسایی ربات‌ها (Cloudflare، راه‌حل‌های داخلی) استفاده می‌کنند و به راحتی IP‌های مراکز داده را مسدود می‌کنند. IP‌های مسکونی مانند کاربران خانگی عادی به نظر می‌رسند.

برای تست API و تست بار: پروکسی‌های مراکز داده — انتخاب بهینه. سرعت بالا، اتصال پایدار، تأخیر قابل پیش‌بینی.

برای تست جغرافیایی: پروکسی‌های مسکونی با انتخاب کشور/شهر. این امکان را می‌دهد که بررسی کنید سرویس یا تبلیغات شما برای کاربران از منطقه خاص چگونه به نظر می‌رسد.

اشتباهات رایج و بهترین شیوه‌ها

در سال‌های کار با پروکسی در پروژه‌های Go، لیستی از اشتباهات رایج که توسعه را کند کرده و قابلیت اطمینان پارسرها را کاهش می‌دهند، شکل گرفته است. هر یک را بررسی خواهیم کرد.

اشتباه 1: استفاده مجدد از حمل و نقل بدون بازنشانی اتصالات

http.Transport اتصالات TCP را کش می‌کند (keep-alive). اگر شما برای هر درخواست یک حمل و نقل جدید ایجاد کنید، این مزیت را از دست می‌دهید. اما اگر از یک حمل و نقل با پروکسی ثابت در هنگام چرخش استفاده کنید — درخواست‌ها ممکن است از طریق اتصال قدیمی بروند و پروکسی جدید را دور بزنند.

راه حل: هنگام تغییر پروکسی، transport.CloseIdleConnections() را فراخوانی کنید یا از رویکرد تابعی با انتخاب دینامیک پروکسی (که در بخش چرخش توضیح داده شده است) استفاده کنید.

اشتباه 2: عدم پردازش خطاهای پروکسی

پروکسی‌ها ممکن است در دسترس نباشند، بارگذاری شده باشند یا توسط سایت هدف مسدود شوند. بدون منطق retry، پارسر شما در اولین خطا متوقف خواهد شد.

func fetchWithRetry(client *http.Client, targetURL string, maxRetries int) ([]byte, error) {
    var lastErr error
    for i := 0; i < maxRetries; i++ {
        resp, err := client.Get(targetURL)
        if err != nil {
            lastErr = err
            time.Sleep(time.Duration(i+1) * time.Second) // تأخیر نمایی
            continue
        }
        defer resp.Body.Close()

        if resp.StatusCode == 429 || resp.StatusCode == 403 {
            lastErr = fmt.Errorf("مسدود شده: وضعیت %d", resp.StatusCode)
            time.Sleep(time.Duration(i+1) * 2 * time.Second)
            continue
        }

        return io.ReadAll(resp.Body)
    }
    return nil, fmt.Errorf("تمام تلاش‌ها به پایان رسید: %w", lastErr)
}

اشتباه 3: نادیده گرفتن تأیید TLS

برخی از توسعه‌دهندگان تأیید گواهی‌های TLS را برای پروکسی غیرفعال می‌کنند (InsecureSkipVerify: true). این یک آسیب‌پذیری برای حملات MITM ایجاد می‌کند. اگر سرور پروکسی از گواهی خودامضا شده استفاده کند — آن را به مجموعه معتبر اضافه کنید، نه اینکه تأیید را به طور کامل غیرفعال کنید.

اشتباه 4: هدرهای یکسان برای تمام درخواست‌ها

حتی با چرخش پروکسی، ممکن است شما بر اساس الگوی هدرها مسدود شوید. User-Agent را همراه با پروکسی تغییر دهید و هدرهای واقع‌گرایانه مرورگر را اضافه کنید:

userAgents := []string{
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36",
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 Chrome/119.0.0.0 Safari/537.36",
    "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/118.0.0.0 Safari/537.36",
}

req, _ := http.NewRequest("GET", targetURL, nil)
req.Header.Set("User-Agent", userAgents[rand.Intn(len(userAgents))])
req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
req.Header.Set("Accept-Language", "ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7")
req.Header.Set("Accept-Encoding", "gzip, deflate, br")

چک لیست یک کلاینت پروکسی مطمئن در Go

  • ✅ تایم‌اوت در کلاینت (Timeout: 30 * time.Second)
  • ✅ منطق retry با تأخیر نمایی
  • ✅ چرخش پروکسی در هنگام خطاهای 403/429
  • ✅ چرخش User-Agent همراه با پروکسی
  • ✅ ثبت خطاها با ذکر پروکسی استفاده شده
  • ✅ اطلاعات کاربری از طریق متغیرهای محیطی، نه هاردکد
  • ✅ بستن resp.Body از طریق defer
  • ✅ محدود کردن همزمانی از طریق سمفور یا worker pool

مثال یک worker pool کامل با چرخش پروکسی

package main

import (
    "fmt"
    "io"
    "net/http"
    "net/url"
    "sync"
    "sync/atomic"
    "time"
)

type Scraper struct {
    proxies []*url.URL
    counter uint64
    workers int
}

func (s *Scraper) getClient() *http.Client {
    idx := atomic.AddUint64(&s.counter, 1) % uint64(len(s.proxies))
    return &http.Client{
        Transport: &http.Transport{
            Proxy: http.ProxyURL(s.proxies[idx]),
        },
        Timeout: 30 * time.Second,
    }
}

func (s *Scraper) Scrape(urls []string) map[string]string {
    results := make(map[string]string)
    var mu sync.Mutex
    var wg sync.WaitGroup

    sem := make(chan struct{}, s.workers) // سمفور برای محدود کردن همزمانی

    for _, u := range urls {
        wg.Add(1)
        go func(targetURL string) {
            defer wg.Done()
            sem <- struct{}{}
            defer func() { <-sem }()

            client := s.getClient()
            resp, err := client.Get(targetURL)
            if err != nil {
                fmt.Printf("خطا: %v\n", err)
                return
            }
            defer resp.Body.Close()

            body, _ := io.ReadAll(resp.Body)

            mu.Lock()
            results[targetURL] = string(body)
            mu.Unlock()
        }(u)
    }

    wg.Wait()
    return results
}

نتیجه‌گیری

تنظیم پروکسی در Go — وظیفه‌ای است که با چند خط کد حل می‌شود، اما نیاز به درک جزئیات دارد: استفاده صحیح از http.Transport، پردازش صحیح احراز هویت، چرخش هوشمند IP و پردازش خطاها. کتابخانه استاندارد همه چیز لازم برای پروکسی HTTP را فراهم می‌کند و بسته golang.org/x/net/proxy نیاز به SOCKS5 را برطرف می‌کند.

نکات کلیدی از مقاله:

  • همیشه یک http.Client شخصی ایجاد کنید — http.DefaultClient به طور مستقیم از پروکسی پشتیبانی نمی‌کند
  • از رویکرد تابعی (Proxy: func(r *Request) (*url.URL, error)) برای چرخش دینامیک استفاده کنید
  • اطلاعات کاربری را در متغیرهای محیطی ذخیره کنید، نه در کد
  • منطق retry را اضافه کنید — پروکسی‌ها 100% قابل اعتماد نیستند
  • User-Agent را همراه با پروکسی تغییر دهید تا حداکثر کارایی را داشته باشید

اگر شما یک پارسر برای بازارهایی مانند Wildberries یا Ozon می‌نویسید، توصیه می‌کنیم از پروکسی‌های مسکونی استفاده کنید — آن‌ها IP‌های واقعی کاربران خانگی دارند و به طور قابل توجهی کمتر تحت مسدودیت‌های سیستم‌های ضد ربات قرار می‌گیرند. برای سناریوهای با بار بالا که هزاران درخواست در دقیقه دارند، بهترین انتخاب پروکسی‌های مراکز داده هستند — آن‌ها حداکثر سرعت و ثبات اتصال را فراهم می‌کنند.

```