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.Clientbağlamın iptalini kendi iç kontrol noktalarında denetleyip isteği bu noktada durduruyor. errors.Isile denetlenen istek hatası, bağlamın taşıdığı sabit hatanın (context.Canceledya dacontext.DeadlineExceeded) sarmalanmış hâli.- Bir bağlantının yeniden kullanılıp kullanılmayacağı
httptrace.GotConnInfo.Reusedalanı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.