Rehber · Güncelleme 2026-11-13
PHP cURL Proxy Kullanımı: curl_multi ve Guzzle ile Rotasyon
PHP cURL proxy kullanımı: CURLOPT_PROXY ve kimlik ayarı, handle yeniden kullanımında IP neden değişmez, curl_multi eşzamanlılığı, Guzzle Pool ve hata kodları.

PHP cURL proxy kullanımı: tek istekte dört seçenek
PHP cURL proxy kullanımı dört seçenekle başlar: CURLOPT_PROXY proxy adresini, CURLOPT_PROXYUSERPWD kimlik bilgisini, CURLOPT_PROXYTYPE protokolü, CURLOPT_HTTPPROXYTUNNEL ise düz HTTP isteklerinin de CONNECT tüneliyle geçip geçmeyeceğini belirler. Rotating ürünlerde proxy listesi yoktur; panelde yazan tek gateway adresi ve kullanıcı adı bu dört seçeneğe girilir, çıkış IP’sini gateway değiştirir. Gateway mantığı ayrı bir rehberde anlatılır; burada yalnız PHP tarafı vardır.
En kısa kurulum şudur: $ch = curl_init(“https://hedef.example/sayfa”); curl_setopt($ch, CURLOPT_PROXY, “http://gateway-adresi:port”); curl_setopt($ch, CURLOPT_PROXYUSERPWD, “kullanici:sifre”); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10); curl_setopt($ch, CURLOPT_TIMEOUT, 30); $body = curl_exec($ch). CURLOPT_PROXY değeri şema alır: http://, socks5:// ya da socks5h:// yazılabilir; şema yazılmazsa libcurl adresi HTTP proxy sayar. SOCKS5 kullanacaksanız socks5h tercih edin, çünkü h’li biçimde alan adı çözümlemesini proxy yapar; socks5’te çözümleme kendi makinenizde olur ve hedefin coğrafi DNS yanıtı proxy ülkesine değil size göre döner.
Kimlik bilgisini adrese gömmek de mümkündür: http://kullanici:sifre@gateway-adresi:port. Oturum parametresi taşıyan kullanıcı adlarında bu biçim daha az hata üretir, çünkü tek bir dize vardır. Şifrede : ya da @ geçiyorsa yüzde kodlaması gerekir; CURLOPT_PROXYUSERPWD ile ayrı vermek bu sorunu ortadan kaldırır. Kimlik yöntemi varsayılan olarak Basic’tir; CURLOPT_PROXYAUTH ile değiştirilebilir ama rotating gateway’lerde Basic yeterlidir.
Ortam değişkenleri PHP’yi de etkiler. libcurl http_proxy (yalnız küçük harf), HTTPS_PROXY, ALL_PROXY ve NO_PROXY değişkenlerini okur; sunucuda bunlardan biri tanımlıysa CURLOPT_PROXY vermediğiniz istekler oradan geçer. Ters durum da vardır: bir isteğin proxy’siz gitmesini istiyorsanız CURLOPT_PROXY’ye boş dize verin; libcurl boş dizeyi “proxy kullanma” sayar ve ortam değişkenini yok sayar. Kodda proxy ayarı yaptığınız hâlde IP değişmiyorsa ilk bakılacak yer bu iki kaynağın çakışıp çakışmadığıdır.
Handle yaşam döngüsü: curl_init her istekte mi, bir kez mi?
libcurl, açık bağlantıları handle’a bağlı bir önbellekte tutar. Aynı handle ile art arda beş istek attığınızda libcurl ilk istekte gateway’e açtığı TCP bağlantısını (HTTPS hedeflerde CONNECT tünelini) sonraki dört istek için yeniden kullanır. Rotating gateway çıkış IP’sini bağlantı başına atadığı için beş istek de aynı IP’den çıkar ve “rotasyon çalışmıyor” izlenimi doğar. Bu davranış hata değildir; bağlantı kurulumunu ödememek için tasarlanmıştır ve IP’nin değişmemesinin nedenleri ayrı bir rehberde toplanmıştır.
Her istekte yeni IP istiyorsanız iki yol vardır. Birincisi her istek için curl_init ile yeni handle açıp curl_close ile kapatmaktır; PHP 8’de handle nesne olduğu için değişken kapsam dışına çıkınca da kapanır. İkincisi aynı handle’ı tutup CURLOPT_FRESH_CONNECT ile her isteği yeni bağlantıyla başlatmak ve CURLOPT_FORBID_REUSE ile biten bağlantının önbelleğe girmesini engellemektir. İki yol da her istekte TCP ve TLS el sıkışmasını yeniden öder; gateway’e olan bacak kısaysa fark küçük, uzaksa belirgindir.
Aynı IP’de kalmak istiyorsanız tersini yapın: handle’ı açık tutun ve gateway’in oturum parametresini kullanıcı adına ekleyin. Oturum parametresinin sözdizimi ürüne göre panelde yazar; PHP tarafında yapılacak tek şey kullanıcı adı dizesini o parametreyle kurmaktır. Yalnız keep-alive’a güvenmek yetmez, çünkü hedef sunucu ya da gateway bağlantıyı kapattığında libcurl sessizce yeni bağlantı açar ve IP değişir. Oturum parametresi bu durumda da aynı çıkışı verir.
PHP-FPM altında çalışan bir web uygulamasında bu ayrım çoğu zaman görünmez: her HTTP isteği kendi PHP sürecinde başlar ve biter, handle de onunla birlikte ölür, bağlantı önbelleği bir sonraki ziyarete taşınmaz. Davranış asıl olarak uzun süren CLI betiklerinde, kuyruk tüketicilerinde ve cron işlerinde ortaya çıkar; scraping kodu da genelde orada çalışır. Kodunuzu test ederken php -r ile komut satırından denemek, FPM altında göremeyeceğiniz keep-alive etkisini görünür kılar.
curl_multi ile eşzamanlı istek: döngü ve bağlantı sınırı
Tek handle ile ardışık istek, gecikmeyi istek sayısıyla çarpar. curl_multi eşzamanlılık katmanıdır: birden çok handle tek bir multi handle’a eklenir, libcurl hepsini aynı anda yürütür. Resmî belgedeki döngü şudur: $mh = curl_multi_init(); her URL için $ch oluşturup proxy seçeneklerini verin ve curl_multi_add_handle($mh, $ch) ile ekleyin; sonra do { $status = curl_multi_exec($mh, $aktif); while (($bilgi = curl_multi_info_read($mh)) !== false) { if ($bilgi[“msg”] === CURLMSG_DONE) { $icerik = curl_multi_getcontent($bilgi[“handle”]); $sonuc = $bilgi[“result”]; curl_multi_remove_handle($mh, $bilgi[“handle”]); } } if ($aktif) { curl_multi_select($mh); } } while ($aktif); curl_multi_close($mh).
Üç ayrıntı sık atlanır. curl_multi_exec’in dönüş değeri yalnız multi yığınının kendi hatasını söyler; tek tek isteklerin sonucu curl_multi_info_read’in döndürdüğü dizideki result anahtarındadır ve curl_errno bu aşamada 0 döner. İkincisi, curl_multi_select bazı ortamlarda -1 dönebilir; döngü o durumda boş dönmesin diye küçük bir usleep eklemek yaygın uygulamadır. Üçüncüsü, curl_multi_getcontent ile içeriği handle’ı multi’den çıkarmadan önce almanız gerekir; aksi hâlde bilgi dizisinin işaret ettiği veri geçersizleşir.
Eşzamanlılığı bir çalışma havuzuyla kurun: yüz handle’ı aynı anda eklemek yerine başta en çok N handle ekleyin, biri bitince yenisini ekleyin. Üst sınırı libcurl tarafında da koyabilirsiniz: curl_multi_setopt($mh, CURLMOPT_MAX_TOTAL_CONNECTIONS, 20) toplam açık bağlantıyı, CURLMOPT_MAX_HOST_CONNECTIONS ise aynı sunucuya açılan bağlantıyı sınırlar. Proxy üzerinden giden her istek gateway adresine bağlandığı için ikinci seçenek burada hedef siteyi değil gateway’i sayar; yani toplam sınır gibi davranır.
Multi içinde de bağlantı önbelleği vardır ve rotasyon tartışması aynen geçerlidir: havuzdaki yirmi handle yirmi bağlantı açar, her bağlantı kendi çıkış IP’sini alır, ama biten handle’ın yerine eklenen yeni handle eski bağlantıyı devralabilir. Her işte farklı IP istiyorsanız yeni handle’lara CURLOPT_FRESH_CONNECT verin. GB bazlı ürünlerde eşzamanlılık kotayı değiştirmez, yalnız tükenme hızını artırır; kataloğa göre rotating IPv4 datacenter 10 GB için 6 USD, rotating IPv4 residential 10 GB için 12 USD’dir ve yirmi paralel istek on GB’yi tek istek kadar harcar, yalnız daha kısa sürede.
Guzzle ile proxy: seçenek, Pool ve yeniden deneme
Guzzle kullanıyorsanız cURL seçeneklerini doğrudan yazmazsınız; proxy adlı istek seçeneği vardır. Dize verirseniz bütün protokoller o proxy’den geçer: $client->request(“GET”, $url, [“proxy” => “http://kullanici:sifre@gateway-adresi:port”]). Dizi verirseniz http, https ve no anahtarlarıyla protokol başına proxy ve istisna listesi tanımlanır. Kimlik bilgisi belgedeki örnekte olduğu gibi adrese gömülür. Seçeneği istemciyi kurarken new Client([“proxy” => …]) biçiminde verirseniz her isteğe uygulanır; istek bazında vermek oturum parametresi değiştirmek için daha uygundur.
Belge iki ortam değişkeni ayrıntısı verir: Guzzle NO_PROXY değişkenini kendisi okur ve proxy dizisinin no anahtarını onunla doldurur; HTTP_PROXY ise yalnız CLI SAPI’de dikkate alınır. Yani komut satırında çalışan bir betikle web sunucusu altında çalışan aynı kod farklı proxy’den çıkabilir. Üretimde proxy’yi kod içindeki seçeneğe yazmak bu ikiliği kapatır. Zaman aşımı için connect_timeout (yalnız cURL handler’ında geçerli) ve timeout (toplam süre) saniye cinsinden verilir; varsayılan ikisi için de 0, yani sınırsızdır ve rotating gateway arkasında sınırsız bekleme kuyruğu kilitler.
cURL’de olup Guzzle seçeneğinde olmayan bir ayar gerekiyorsa curl anahtarıyla geçirilir: [“curl” => [CURLOPT_FRESH_CONNECT => true]] gibi. on_stats seçeneği her istek için TransferStats nesnesi verir; getTransferTime ile süreyi, getHandlerStats ile libcurl’ün ham sayaçlarını alırsınız ve rotasyon ya da gecikme ölçümü için ayrı kod yazmanız gerekmez.
Eşzamanlılık GuzzleHttpPool ile kurulur: $pool = new Pool($client, $istekler(), [“concurrency” => 10, “fulfilled” => function ($yanit, $indeks) { … }, “rejected” => function ($hata, $indeks) { … }]); $pool->promise()->wait(). İstekler bir generator’dan gelir, concurrency aynı anda uçuştaki istek sayısıdır. Pool altta curl_multi kullanır; yukarıdaki bağlantı önbelleği ve FRESH_CONNECT tartışması burada da geçerlidir. Yeniden deneme için Guzzle’ın Middleware::retry fonksiyonu bir karar kapanışı ve bekleme kapanışı alır; hangi hatanın yeniden deneneceği retry ve backoff rehberindeki tabloyla aynıdır, 429’da IP değiştirip sınırı aşmaya çalışmak yerine tempo düşürülür.
Hata, neden, çözüm: PHP’de görülen kodlar
cURL hataları curl_errno ve curl_error ile okunur; Guzzle aynı hataları ConnectException ve RequestException olarak sarar ve mesaj içinde cURL hata numarasını taşır. En sık görülenler şunlardır. CURLE_COULDNT_RESOLVE_PROXY (5): gateway alan adı çözülemedi; adres yanlış yazılmıştır ya da sunucunun DNS’i çalışmıyordur. CURLE_COULDNT_CONNECT (7): gateway’e TCP bağlantısı kurulamadı; port yanlıştır, güvenlik duvarı giden bağlantıyı kesiyordur ya da IP yetkilendirmesi gereken bir üründe sunucu IP’si panele eklenmemiştir.
CURLE_OPERATION_TIMEDOUT (28): CURLOPT_TIMEOUT doldu; hedef yavaş olabilir, ama rotating gateway arkasında bu kodun tipik nedeni bağlantı sınırının aşılması ve isteğin gateway’de kuyrukta beklemesidir. CURLE_RECV_ERROR (56) ve “Received HTTP code 407 from proxy after CONNECT” mesajı: kimlik bilgisi reddedildi. 407’yi doğrudan görmek için curl_getinfo($ch, CURLINFO_HTTP_CONNECTCODE) kullanın; bu değer CONNECT isteğine gateway’in verdiği yanıt kodudur ve hedefin verdiği CURLINFO_HTTP_CODE ile karışmaz. Yeni libcurl sürümlerinde proxy kaynaklı hatalar CURLE_PROXY (97) altında toplanır ve PHP 8.2 ile gelen CURLINFO_PROXY_ERROR ayrıntıyı verir.
CURLE_SSL_CONNECT_ERROR (35): tünel kuruldu ama hedefle TLS el sıkışması başarısız; proxy’nin kendisiyle ilgisi yoktur, hedefin sertifikası ya da sunucudaki eski OpenSSL ile ilgilidir. Hedeften gelen 403 ve 429 ise cURL hatası değildir; curl_exec gövdeyi döndürür, CURLINFO_HTTP_CODE kodu verir. Guzzle’da http_errors varsayılan olarak açıktır ve 4xx–5xx için ClientException ya da ServerException fırlatır; gövdeyi okumak için istisnadan getResponse çağrılır. 429’un ne anlama geldiği ve nasıl yavaşlanacağı 429 rehberinde anlatılır.
Teşhis sırası şöyle kurulur: önce proxy’siz tek istek (hedef ayakta mı), sonra proxy’li tek istek bir IP yankı adresine (kimlik ve çıkış doğru mu), sonra aynı istek hedefe (hedef gateway çıkışını kabul ediyor mu), en son eşzamanlı deneme (sınır nerede). Hata hangi adımda başlıyorsa neden o katmandadır; dört adımı birden değiştirip sonucu yorumlamak çoğu zaman yanlış yere götürür.
Rotasyon deneyi ve GB ölçümü: beş istek, iki ayar
Rotasyonun gerçekten çalıştığını on satırlık bir deneyle görün. Dönen IP’yi düz metin olarak veren bir adrese beş istek atın; birinci turda tek handle’ı yeniden kullanın, ikinci turda her istek için yeni handle açın ya da CURLOPT_FRESH_CONNECT verin. Her yanıtı curl_getinfo ile aldığınız total_time ve size_download değerleriyle birlikte yazdırın. Beklenen tablo şudur: birinci turda beş satırda aynı IP ve ilk satırdan sonra belirgin biçimde kısalan süre; ikinci turda farklı IP’ler ve her satırda bağlantı kurulumunu içeren daha uzun süre. İlk turda IP değişiyorsa gateway bağlantı başına değil istek başına rotasyon yapıyordur; ikinci turda değişmiyorsa oturum parametresi açık kalmıştır ya da ortam değişkeni devrededir.
GB ölçümü için size_download yetmez; gateway başlıkları da sayar. Yaklaşık bir ölçüm için yanıt gövdesine curl_getinfo’nun header_size değerini ekleyin ve her isteğin toplamını bir sayaçta tutun. Sayaç panel rakamıyla birebir tutmaz ama hangi URL’lerin kotayı yaktığını gösterir. Sıkıştırmayı açmayı unutmayın: CURLOPT_ENCODING’e boş dize verirseniz libcurl desteklediği bütün sıkıştırma biçimlerini Accept-Encoding’e yazar ve gövdeyi kendisi açar; metin ağırlıklı sayfalarda aktarılan bayt belirgin düşer. Bant genişliği azaltma rehberi diğer yöntemleri sıralar.
Port bazlı IPv6 ürünlerinde GB sayacı yoktur, ölçülecek şey eşzamanlılıktır: kataloğa göre rotating IPv6 datacenter ve residential paketleri 1, 5 ve 10 port olarak aylık 29, 80 ve 175 USD’dir ve her port kendi oturum ayarıyla çalışır. PHP tarafında her port ayrı bir proxy adresi demektir; curl_multi havuzunda handle’ları portlara dağıtmak, aynı porta yüz bağlantı yığmaktan daha dengeli çalışır. Hedefin IPv6 adresi olup olmadığı ise ayrı bir sorudur ve IPv4 mi IPv6 mi rehberinde ele alınır.
Canlıya almadan önce kontrol listesi
Proxy adresi şemayla yazıldı (http:// ya da socks5h://), kimlik bilgisi CURLOPT_PROXYUSERPWD ile ya da yüzde kodlanmış biçimde adreste. Sunucuda http_proxy, HTTPS_PROXY ve ALL_PROXY değişkenleri kontrol edildi; proxy’siz gitmesi gereken istekler CURLOPT_PROXY’ye boş dize alıyor. CURLOPT_CONNECTTIMEOUT ve CURLOPT_TIMEOUT (Guzzle’da connect_timeout ve timeout) açıkça verildi; sınırsız bekleme kalmadı.
Rotasyon modu bilinçli seçildi: her istekte yeni IP için yeni handle ya da FRESH_CONNECT, sabit IP için oturum parametresi; keep-alive’a tek başına güvenilmiyor. curl_multi ya da Pool eşzamanlılığı bir üst sınırla kuruldu ve hata oranı izlenerek küçükten büyütüldü. Hatalar curl_errno ve CURLINFO_HTTP_CONNECTCODE ile ayrıştırılıyor; 407 kimlik, 7 bağlantı, 28 sınır, 429 hedef tempo olarak ayrı ayrı loglanıyor. Beş istekli rotasyon deneyi CLI’de çalıştırıldı ve beklenen tabloyu verdi. Bayt sayacı ve Accept-Encoding açık; kota alarmı toplam tüketime bağlı.
Sık sorulan sorular
PHP’de proxy ayarı yaptım ama IP değişmiyor; neden? Üç nedenden biri: aynı handle bağlantıyı yeniden kullanıyor (yeni handle ya da CURLOPT_FRESH_CONNECT deneyin), kullanıcı adında oturum parametresi açık kalmış ya da sunucudaki http_proxy ortam değişkeni isteği başka bir yere yönlendiriyor. Beş istekli deney hangisi olduğunu gösterir.
curl_multi mi Guzzle Pool mu? Aynı altyapıdır; Pool, curl_multi’yi promise arayüzüyle sarar ve yeniden deneme, zaman aşımı, hata sınıfları gibi işleri hazır verir. Bağımlılık eklemek istemediğiniz küçük betiklerde curl_multi döngüsü yeter; uygulama kodunda Guzzle okunabilirliği artırır. İkisinde de bağlantı sınırını ve FRESH_CONNECT kararını siz verirsiniz.
SOCKS5 mi HTTP proxy mi kullanmalıyım? Rotating gateway iki protokolü de sunuyorsa HTTPS hedefler için fark küçüktür; ikisi de tünel açar. SOCKS5 seçerseniz socks5h şemasını kullanın ki alan adı proxy tarafında çözülsün. UDP ya da HTTP dışı trafik PHP scraping işinde zaten yoktur.
407 hatasını nasıl ayırt ederim? curl_getinfo ile CURLINFO_HTTP_CONNECTCODE okuyun; 407 dönüyorsa gateway kimlik bilgisini reddetmiştir. Şifrede özel karakter varsa yüzde kodlamasını, kullanıcı adındaki oturum parametresinin sözdizimini ve IP yetkilendirmesi gereken üründe sunucu IP’sinin panelde kayıtlı olduğunu kontrol edin.
Eşzamanlılığı kaç yapmalıyım? Ürün kataloğunda bağlantı sınırı yazmaz; destekten sorulur. Pratikte 5 ile başlayıp hata oranı ve toplam süreyi izleyerek artırın; 28 numaralı zaman aşımı ve dalga hâlinde 407 artışı sınırı aştığınızın işaretidir. Port bazlı IPv6 ürünlerinde bağlantıları portlara dağıtın.
