İçeriğe geç
academia.sh

Ders 02 / 12

HTTP İstemcisi

Önceki ders sınırı alıcı taraftan ölçtü; bu ders aynı sınırı gönderen taraftan ölçüyor. Zaman aşımı bir süreyle değil, iptal edilmiş bir bağlamla kuruluyor. Bağlantının yeniden kullanılıp kullanılmadığı yanıt gövdesinin kapatılıp kapatılmadığına bağlı: gövde okunup kapatılınca sonraki istek aynı bağlantıyı kullanıyor, kapatılmayınca kullanmıyor. Sınırlayıcı ölçüm: yanıt gövdesi bir okuyucudur, bir değer değil — bir kez okunduktan sonra ikinci okuma boş dönüyor.

İçindekiler

Önceki ders sınırı alıcı taraftan ölçtü: bir isteğin sunucuya ne getirdiğini, ve sunucunun gövdeye ne kadar körü körüne güvendiğini. Bu ders aynı sınırı gönderen taraftan soruyor. Bir istemci bir isteği gönderirken üç sorumluluğu üstleniyor: isteğin makul bir sürede sonuçlanmasını sağlamak, aynı sunucuyla konuşan bağlantıları yeniden kullanmak, ve aldığı yanıtın gövdesini kapatmak. Bu üçü birbirinden bağımsız görünse de, aşağıdaki ölçüm ikisinin (bağlantı yeniden kullanımı ile gövde kapatma) aslında aynı kararın iki yüzü olduğunu gösteriyor.

Bu ders de bir önceki gibi net/http/httptest kullanıyor; ama bu kez httptest.NewRecorder değil, httptest.NewServer — istemcinin gerçekten bir bağlantı kurup kapatabileceği, bellekte çalışan bir sunucu. Sunucunun adresi (sunucu.URL) yalnızca istek kurmak için kullanılıyor, hiçbir ölçüm satırında basılmıyor; ölçülen şey adresin kendisi değil, istemcinin bu adrese karşı nasıl davrandığı. Bu farkın kendisi ölçümün bir parçası: önceki ders sunucuyu hiç ağa bağlamadan koşturabiliyordu, çünkü ölçtüğü şey yalnızca isteğin ve yanıtın içeriğiydi; bu ders istemcinin bağlantı yönetimini ölçtüğü için gerçek (ama hiçbir yerde basılmayan) bir yerel bağlantıya ihtiyaç duyuyor.

Üç sorumluluk sırayla ele alınıyor. İlki, bir isteğin sonsuza kadar beklemeyeceğinin garantisi. İkincisi, art arda gelen isteklerin her birinde yeni bir bağlantı kurmanın maliyetinden kaçınmak. Üçüncüsü, alınan yanıtın gövdesini bırakmadan kapatmak — ve aşağıdaki ölçüm bu üçüncü sorumluluğun aslında ikinciyi mümkün kılan şart olduğunu gösteriyor.

Zaman Aşımı: Süre Değil, İptal Edilmiş Bir Bağlam

Önceki kursun bağlam paketi dersinde kurulan kalıp burada aynen sürüyor: bir zaman aşımı time.Sleep ya da gerçek bir süre ölçümüyle değil, önceden iptal edilmiş bir bağlamla kuruluyor. Bu, dersin çıktısının koşumdan koşuma değişmemesini sağlıyor — gerçek bir zaman aşımı denenseydi, isteğin ne kadar sürede başarısız olacağı makineye ve yüke bağlı kalırdı.

// sunucucekirdek.go — istemci olcumlerinin ortak sunucu govdesi
package main

import (
	"fmt"
	"net/http"
)

// yaniticiIsleyici, her istege sabit bir metin govde donduren bir isleyicidir.
// Govde kucuk tutuluyor ki okunup kapatilmasi olcumu etkilemesin.
func yaniticiIsleyici(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "text/plain")
	fmt.Fprint(w, "adet=7 birim=kg")
}

// olcumSatiri, sinirdaki tip bilgisinin uc sonucundan birini (korunuyor / yeniden /
// kayboluyor) bir kanitla birlikte yazdirir.
func olcumSatiri(parca, sinif, kanit string) {
	fmt.Printf("%-30s %-10s %s\n", parca, sinif, kanit)
}
// zamanasimi.go — zaman asimi sureyle degil, iptal edilmis bir baglamla kuruluyor
package main

import (
	"context"
	"errors"
	"fmt"
	"net/http"
	"net/http/httptest"
)

func main() {
	fmt.Println("-- zaman asimi: sureyle degil, iptal edilmis bir baglamla --")

	sunucu := httptest.NewServer(http.HandlerFunc(yaniticiIsleyici))
	defer sunucu.Close()

	baglam, iptalEt := context.WithCancel(context.Background())
	iptalEt() // istek daha baslamadan baglam zaten iptal edildi

	istek, _ := http.NewRequestWithContext(baglam, http.MethodGet, sunucu.URL, nil)
	_, hata := http.DefaultClient.Do(istek)

	fmt.Printf("istek hatasi var mi=%t\n", hata != nil)
	fmt.Printf("hata baglam iptaliyle mi eslesiyor=%t\n", errors.Is(hata, context.Canceled))
}
-- zaman asimi: sureyle degil, iptal edilmis bir baglamla --
istek hatasi var mi=true
hata baglam iptaliyle mi eslesiyor=true

AG7. iptalEt() çağrısı, isteğin gönderilmesinden önce çalışıyor; bu sıralama bilinçli, çünkü ölçümün amacı zamanlamayla ilgili hiçbir tartışmaya yer bırakmamak. Bağlam zaten iptal edilmiş durumdayken http.DefaultClient.Do çağrılıyor, ve istemci isteği hiç kabloya koymadan hemen hata döndürüyor. errors.Is(hata, context.Canceled) denetimi true çıkıyor — dönen hata, ağ katmanının kendi ürettiği bir hata değil, bağlamın taşıdığı sabit hatanın sarmalanmış hâli. Bir gerçek zaman aşımı (context.WithTimeout) kurulsaydı aynı mekanizma işlerdi, yalnızca hata context.Canceled yerine context.DeadlineExceeded olurdu; ikisi de aynı ctx.Err() yolundan geliyor.

AG8. Bu ölçüm, önceki kursun kapanışında bırakılan soruyu bu sınırda yeniden soruyor: bir goroutine’i durduran şey neydi — kendi kod yolundaki bir kontrol noktası. http.Client da aynı ilkeyi izliyor: bağlamın iptalini kendi iç döngüsünde (bağlantı kurulurken, yanıt beklerken) denetliyor ve bu kontrol noktasına ulaştığında durup hatayı döndürüyor. Fark, bu kez kontrol noktasının kaynak kodda görünmemesi — istemcinin kendisi standart kitaplığın bir parçası, ama davranış aynı: bağlam bir zorlama aracı değil, bir haber kaynağı, ve http.Client bu haberi dinleyen taraflardan biri. Bu tutarlılık, standart kitaplığın kendi iç işlevlerini de aynı ilkeye tabi tutmasından geliyor; bağlam paketi bir kez öğrenildiğinde, sınırın hangi tarafında olursa olsun aynı biçimde davranıyor.

Bu ölçümde gerçek bir süre hiç kullanılmadı; context.WithCancel ile iptalEt()in hemen çağrılması, isteğin başarısız olacağını kesin kılıyor, “belki zamanında yetişir belki yetişmez” belirsizliğini tamamen ortadan kaldırıyor. Gerçek bir context.WithTimeout kullanılsaydı, ölçümün sonucu doğrulayıcının çalıştığı makinenin hızına bağlı kalırdı — aynı kaynak kimi koşumda zaman aşımına uğrar, kimi koşumda sunucu yeterince hızlı yanıt verdiği için uğramazdı. Önceden iptal edilmiş bir bağlam bu belirsizliği koşum zamanından tasarım zamanına taşıyor.

Bağlantı Yeniden Kullanımı: Gövdeyi Kapatmanın Bedeli

Bir TCP bağlantısı kurmak ucuz bir işlem değil; Go’nun http.Transport‘u bu yüzden kullanılmış bağlantıları bir bağlantı havuzunda tutup sonraki isteklerde yeniden kullanmaya çalışıyor. Bağlantı havuzu kavramının kendisi Veri Erişim Katmanı kursunda kuruldu; burada ölçülen şey Go’nun standart kitaplığındaki http.Transport’un bu havuzu hangi koşulda işlettiği. Bu yeniden kullanım koşulsuz değil: bir yanıtın gövdesi tamamen okunup kapatılmadan, altındaki bağlantı havuza geri dönmüyor. Bu, önceki bölümdeki zaman aşımından bağımsız bir sorumluluk — istek başarıyla tamamlansa, durum kodu 200 dönse bile, gövdeyi görmezden gelmek bağlantıyı kaybettirebiliyor.

Aşağıdaki ölçüm bunu net/http/httptrace ile gözlemliyor: httptrace.ClientTracein GotConn alanı, her istek için elde edilen bağlantının yeniden kullanılmış olup olmadığını (Reused alanı, bir bool) bildiriyor. Bu paket de standart kitaplığın bir parçası ve hiçbir gerçek adres ya da bağlantı noktası açığa çıkarmıyor, yalnızca bir mantıksal bayrak veriyor.

Bir önceki bölüm zaman aşımını ölçerken http.DefaultClienti kullanmıştı, çünkü orada ölçülen şey bağlam iptaliydi ve hangi istemcinin kullanıldığı sonucu değiştirmiyordu. Bu bölüm kendi istemcilerini açıkça kuruyor, çünkü burada ölçülen şey tam olarak istemcinin taşıdığı bağlantı havuzu — http.DefaultClienti kullanmak, bu ölçümü kursun başka bir dersinde çalışan kodun bıraktığı bağlantılarla karışabilir hâle getirirdi. Bu, bir HTTP istemcisinin görünmeyen bir durum (açık bağlantılar) taşıdığının ilk işareti: iki farklı *http.Client değeri, aynı sunucuya karşı bile olsa, birbirinden tamamen farklı bir geçmiş biriktirebiliyor.

// baglantitekrar.go — govdenin kapatilmasi baglantinin havuza donup donmedigini belirliyor
package main

import (
	"context"
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"net/http/httptrace"
)

// izleyerekIste, bir istegi verilen istemciyle gonderir; baglantinin yeniden mi
// kullanildigini httptrace ile ogrenip govdeyi istege gore kapatir.
func izleyerekIste(istemci *http.Client, url string, govdeyiKapat bool) (yenidenKullanildi bool) {
	baglam := httptrace.WithClientTrace(context.Background(), &httptrace.ClientTrace{
		GotConn: func(bilgi httptrace.GotConnInfo) { yenidenKullanildi = bilgi.Reused },
	})
	istek, _ := http.NewRequestWithContext(baglam, http.MethodGet, url, nil)
	yanit, hata := istemci.Do(istek)
	if hata != nil {
		return false
	}
	if govdeyiKapat {
		io.ReadAll(yanit.Body)
		yanit.Body.Close()
	}
	return yenidenKullanildi
}

func main() {
	fmt.Println("-- baglanti yeniden kullanimi: govde kapatildiginda ve kapatilmadiginda --")

	sunucu := httptest.NewServer(http.HandlerFunc(yaniticiIsleyici))
	defer sunucu.Close()

	// Her istemci kendi Transport'unu tasiyor; boylece baglanti havuzlari birbirinden
	// bagimsiz oluyor ve bir istemcideki kapatilmamis govde otekini etkilemiyor.
	istemciA := &http.Client{Transport: &http.Transport{}}
	ilkA := izleyerekIste(istemciA, sunucu.URL, true)
	ikinciA := izleyerekIste(istemciA, sunucu.URL, true)
	fmt.Printf("govde her seferinde kapatildi: ilk=%t ikinci=%t\n", ilkA, ikinciA)

	istemciB := &http.Client{Transport: &http.Transport{}}
	izleyerekIste(istemciB, sunucu.URL, false)
	ikinciB := izleyerekIste(istemciB, sunucu.URL, true)
	fmt.Printf("ilk istegin govdesi hic kapatilmadi: ikinci=%t\n", ikinciB)
}
-- baglanti yeniden kullanimi: govde kapatildiginda ve kapatilmadiginda --
govde her seferinde kapatildi: ilk=false ikinci=true
ilk istegin govdesi hic kapatilmadi: ikinci=false

AG9. istemciA ile yapılan iki istekte gövde her seferinde okunup kapatılıyor. İlk istekte henüz açık bir bağlantı olmadığı için Reused=false çıkması beklenen bir sonuç; asıl ölçülen şey ikinci istek. Gövde ilk istekte tam okunup kapatıldığı için bağlantı havuza geri dönmüş oluyor, ve ikinci istek Reused=true ile aynı bağlantıyı devralıyor. Bu, istemci tarafının üçüncü sorumluluğunun (gövde kapatma) neden yalnızca “temizlik” değil, doğrudan başarım kararı olduğunu gösteriyor.

istemciA ve istemciBnin her biri kendi *http.Transport değerini taşıyor; bu bilinçli bir seçim. Transport alanı boş bırakılsaydı, ikisi de standart kitaplığın paylaşılan varsayılan aktarımını kullanırdı ve iki istemcinin bağlantı havuzları aslında aynı havuz olurdu — istemciBnin kapatılmamış gövdesi, adı farklı olsa bile istemciAnın sonraki isteğini de etkileyebilirdi. Ayrı birer Transport vermek, iki senaryoyu birbirinden yalıtıyor: ölçülen etki gerçekten istemciBnin kendi kapatılmamış gövdesinden geliyor, başka bir isteğin yan etkisinden değil.

AG10. istemciB ile yapılan ilk istekte gövde hiç okunmuyor, hiç kapatılmıyor. İkinci istek aynı istemciyle, gövdesi düzgün kapatılarak gönderiliyor, ama sonuç yine Reused=false. Sebep, ilk isteğin bağlantısının hâlâ meşgul sayılması: Transport, gövdesi okunup kapatılmamış bir yanıtın altındaki bağlantıyı asla havuza koymuyor, çünkü o bağlantının ne zaman serbest kalacağını bilmiyor. İkinci istek bu yüzden yeni bir bağlantı açmak zorunda kalıyor — birinci isteğin kapatılmamış gövdesi, kendisinden sonraki isteği de etkiliyor.

Bu etkinin ölçekle büyüdüğü açık: art arda yüzlerce istek gönderen, ama gövdelerinin bir kısmını kapatmayı unutan bir program, her kapatılmamış gövde için bir bağlantıyı sonsuza kadar meşgul tutuyor. Programın kendisi çökmüyor, hata da vermiyor — yalnızca istemcinin açık bağlantı sayısı zamanla birikiyor, ve bu birikim gövdenin kendisi kadar sessiz kalıyor.

AG11. Bu iki ölçüm birlikte okunduğunda ortaya çıkan kural net: bağlantı yeniden kullanımı kimin işi sorusuna tek bir yanıt veriyor — çağıranın işi. Standart kitaplık gövdeyi otomatik olarak okuyup kapatmıyor; Body.Close() çağrılmazsa, hatta çağrılsa bile gövde önce tam okunmazsa, bağlantı sızıyor ya da en azından yeniden kullanılamıyor. Bu, önceki dersteki ara katman zincirinde görülen ilkeyle aynı yerde duruyor: standart kitaplık sınırın bu tarafında bir mekanizma sunuyor, ama mekanizmayı doğru kullanmak yazan tarafın sorumluluğunda kalıyor.

Bu kural, tip sisteminin sınırdaki rolüne de bir örnek ekliyor: yanit.Bodynin tipi (io.ReadCloser) kendisi hiçbir yerde “beni tam okumadan kapatma” demiyor. Derleyici, gövdesi okunmadan bırakılan bir yanıtı reddetmiyor — kaynak sorunsuz derleniyor, sorunsuz çalışıyor, yalnızca bağlantı havuzu sessizce küçülüyor. Bu tür bir sözleşme (bir kaynağın kapatılmadan önce tüketilmesi gerektiği) tipin kendisinde değil, belgesinde ve yazan tarafın alışkanlığında yaşıyor.

Sınırlayıcı Ölçüm: Gövde Bir Okuyucudur, Bir Değer Değil

Önceki iki ölçüm gövdenin kapatılmasının neden önemli olduğunu gösterdi. Bu ölçüm gövdenin ne olduğunu netleştiriyor: yanit.Body, io.ReadCloser arayüzünü gerçekleştiren bir akış, isteğin gövdesi gibi bir kez tüketiliyor ve tüketildikten sonra geriye hiçbir şey kalmıyor. Bu, dilin önceki kurslarında görülen kopyalanabilir değerlerden (bir dilim, bir yapı) temelden farklı bir davranış: bir dilimi iki değişkene atayıp ikisini de ayrı ayrı okumak mümkündü, çünkü dilim bir değerdi. Bir okuyucu değer değil, bir konum taşıyor — okundukça o konum ilerliyor ve geriye dönmüyor.

// govdeokuma.go — yanit govdesi bir okuyucudur, bir deger degil
package main

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

func main() {
	fmt.Println("-- yanit govdesi bir okuyucudur, iki kez okunamaz --")

	sunucu := httptest.NewServer(http.HandlerFunc(yaniticiIsleyici))
	defer sunucu.Close()

	yanit, _ := http.Get(sunucu.URL)
	defer yanit.Body.Close()

	ilkOkuma, _ := io.ReadAll(yanit.Body)
	ikinciOkuma, ikinciHata := io.ReadAll(yanit.Body)

	olcumSatiri("yanit govdesi (istemcide okunan)", "kayboluyor",
		fmt.Sprintf("tip=%T ilk-uzunluk=%d", ilkOkuma, len(ilkOkuma)))
	fmt.Printf("ikinci okuma uzunlugu=%d hata=%v\n", len(ikinciOkuma), ikinciHata)
}
-- yanit govdesi bir okuyucudur, iki kez okunamaz --
yanit govdesi (istemcide okunan) kayboluyor tip=[]uint8 ilk-uzunluk=15
ikinci okuma uzunlugu=0 hata=<nil>

AG12. İlk io.ReadAll çağrısı gövdeyi baştan sona okuyup on beş baytlık bir dilim döndürüyor — sınırın bu tarafına ulaşan şey yine []byte, önceki derste görülen aynı sınıf: kayboluyor. İkinci io.ReadAll çağrısı ise 0 uzunlukta bir sonuç ve nil bir hata döndürüyor; okunacak hiçbir şey kalmadığı için okuma işlemi anında bitiyor, ama bu bir hata sayılmıyor. Gövde bir dilim ya da bir dizgi olsaydı, aynı değeri istendiği kadar okumak mümkün olurdu; bir okuyucu olduğu için tüketildiği an geride yalnızca boşluk kalıyor.

AG13. Bu davranışın pratik sonucu: bir yanıt gövdesi ancak bir kez ayrıştırılabilir. Bir işlev gövdeyi hem günlüğe yazmak hem de bir hedefe çözmek istiyorsa, gövdeyi önce bir arabelleğe (bytes.Buffer gibi) kopyalayıp iki farklı okumayı o arabellek üzerinden yapmak zorunda — kaynağın kendisine ikinci kez dönmek mümkün değil. Bu sınırlama, bir önceki bölümdeki bağlantı yeniden kullanımıyla da bağlantılı: gövdeyi bir arabelleğe kopyalamak, onu aynı zamanda tam olarak tüketmek anlamına geliyor, dolayısıyla bu kopyalama işlemi hem ikinci bir okumayı mümkün kılıyor hem de altındaki bağlantının havuza dönmesini sağlıyor — aynı io.ReadAll çağrısı iki ayrı sorunu birden çözüyor.

Kursun on üç sınır geçişinin tam dağılımı (1 korunuyor, 5 yeniden kuruluyor, 7 kayboluyor) sıradaki derste, JSON Dizileştirme’de kuruluyor; bu ders yalnız istemci tarafının üç sorumluluğunu ve gövdenin okuyucu doğasını ölçtü.

Özet

  • Zaman aşımı bir süre yerine önceden iptal edilmiş bir bağlamla kuruluyor; http.Client bağlamın iptalini kendi iç kontrol noktalarında denetleyip isteği bu noktada durduruyor.
  • errors.Is ile denetlenen istek hatası, bağlamın taşıdığı sabit hatanın (context.Canceled ya da context.DeadlineExceeded) sarmalanmış hâli.
  • Bir bağlantının yeniden kullanılıp kullanılmayacağı httptrace.GotConnInfo.Reused alanıyla ölçülüyor; gövde tam okunup kapatılmazsa bağlantı havuza dönmüyor.
  • Gövdesi kapatılmamış bir isteğin etkisi kendisinden sonraki isteğe de yansıyor: bir önceki bağlantı meşgul sayıldığı için yeni bir bağlantı açılıyor.
  • Yanıt gövdesi bir okuyucudur, bir değer değil: bir kez tam okunduktan sonra ikinci okuma boş dönüyor, hata vermeden.
  • Bu ders sınırı gönderen taraftan ölçtü; kursun on üç sınır geçişinin tam dağılımı henüz burada kurulmadı, sıradaki ders kuruyor.

Sonraki Adım

İki ders de sınırın taşıyıcısını ölçtü — istek ve yanıt bayt ve metin taşıyor, gövde bir kez okunabiliyor. Bu iki dersin ikisinde de gövdenin içeriği hiç önemli değildi; "adet=3" ya da "adet=7 birim=kg" yerine rastgele herhangi bir metin de aynı sonuçları verirdi, çünkü ölçülen şey gövdenin davranışıydı, taşıdığı veri değil. Ama hiçbiri şu soruyu henüz sormadı: gönderen taraf bir yapıyı gönderirken karşı tarafın onu geri kurabilmesi için ne bırakıyor? Sıradaki ders bu soruyu JSON ile ölçüyor ve kursun bağlayıcı sayısını üretiyor: bir yapının alan adları yapı etiketleriyle karşıya taşınıyor, ama bu taşımanın sınırları var — alıcıda karşılığı olmayan bir alan sessizce düşüyor, ve “geri kuruldu” görünmesi her zaman “doğru kuruldu” anlamına gelmiyor.

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat