Ders 01 / 12
HTTP Sunucusu
Kursun açılış dersi. Bir isteğin taşıdığı yol, üstbilgi ve gövdenin üçünün de tip bilgisi taşımadığı `net/http/httptest` ile gerçek bağlantı noktası açmadan ölçülüyor; ara katman kalıbının bir işleyiciyi saran başka bir işleyici olduğu üç halkalı bir zincirle koşturuluyor. Sınırlayıcı ölçüm: içerik tipi üstbilgisi bir ipucudur, tip değil — yanlış bir ipucu sunucu tarafında hiçbir hata üretmiyor, hata yalnızca gövdeyi o ipucuna göre çözmeye çalışan tarafta ortaya çıkıyor.
İçindekiler
Önceki kursun kapanışı bir goroutine’in nasıl duracağını ölçtü ve durmanın çoğunlukla başka birinin yaptığı bir işe bağlı olduğunu gösterdi: bir kanal kapanmadan, bir bağlam iptal edilmeden, bekleyen goroutine kendi kararını veremiyordu. O ölçüm hep programın içindeydi — iki goroutine aynı ikili çalıştırılabilir dosyanın parçasıydı, aynı tip sistemine bağlıydı. Bu ders soruyu programın dışına taşıyor: bir değer ağa, dosyaya, şablona ya da veritabanına gittiğinde ne oluyor? Karşı taraf aynı derleyiciden geçmedi, aynı tip tanımlarını görmedi — bazen aynı dilde bile yazılmadı. Kursun tamamı bu tek soruyu on iki farklı sınırda soruyor: bir değer sınırı geçerken tip bilgisine ne oluyor? Bu ders sınırın en sık geçileni ile başlıyor: bir HTTP isteği.
Bir sunucunun ne iş yaptığı, kimin isteği ne zaman işlediği ve ara katmanların bir zincirde nasıl sıralandığı önceki kurslarda kavram olarak kuruldu. Burada tekrarlanmıyor; bu dersin sorduğu soru farklı — sunucunun görevi değil, sunucuya gelen ve sunucudan çıkan verinin tip sistemiyle ilişkisi. Bu ayrım kursun geri kalanında da sürüyor: her ders bir katmanın görevini değil, o katmanın sınırındaki verinin akıbetini ölçüyor.
İşleyicinin Aldığı Şey: Yol, Üstbilgi, Gövde
Bir istemci bir HTTP isteği gönderdiğinde, isteğin içinde ne olduğuna dair tek kaynak
kablodan geçen bayt dizisidir. Go’nun standart kitaplığı bu bayt dizisini ayrıştırıp bir
*http.Request değerine dönüştürüyor, ama dönüştürülen şey hâlâ üç ham parça: yol
(r.URL.Path, bir dizgi), üstbilgiler (r.Header, dizgiden dizgi dilimine bir sözlük)
ve gövde (r.Body, bayt akıtan bir okuyucu). İsteği gönderen tarafın kafasında yol bir
tamsayı kimlik, üstbilgi bir yapılandırılmış değer, gövde bir kayıt olabilir — ama sınırı
geçtikten sonra elde kalan yalnızca dizgi ve bayttır. Go’nun tip sistemi bu üçünün hiçbirine
“bu aslında bir tamsayıydı” ya da “bu aslında bir yapıydı” demiyor; söylemeye yetkisi de yok,
çünkü kablonun karşı ucunda hangi programın çalıştığını, o programın hangi tipleri
tanımladığını bilmiyor.
Aşağıdaki kaynak bu ölçümün ortak parçalarını kuruyor: sonuçları sınıflandırıp yazdıran bir yardımcı, isteğin yolunu ve bir üstbilgisini okuyup gövdenin uzunluğunu yanıt olarak yazan bir çekirdek işleyici, ve bu çekirdeği saran iki ara katman.
// istekcekirdek.go — istek isleyicisinin ve ara katmanin ortak parcalari
package main
import (
"fmt"
"io"
"net/http"
)
var parcaSayisi, kayboluyorSayisi int
var sonOkunanGovde []byte
// olcumSatiri, bu dersin sinif adlarindan birini (korunuyor / yeniden / kayboluyor)
// bir kanitla birlikte yazdirir ve toplami tutar.
func olcumSatiri(parca, sinif, kanit string) {
parcaSayisi++
if sinif == "kayboluyor" {
kayboluyorSayisi++
}
fmt.Printf("%-30s %-10s %s\n", parca, sinif, kanit)
}
// cekirdekIsleyici, gelen istegin yolunu, bir ustbilgisini ve govdesini okuyup
// duz metin bir yanit yazar. Yanita sabit bir icerik tipi ipucu ekler; bu ipucunun
// dogru olup olmadigini denetlemek bu islevin isi degildir.
func cekirdekIsleyici(w http.ResponseWriter, r *http.Request) {
govde, _ := io.ReadAll(r.Body)
sonOkunanGovde = govde
kimlik := r.Header.Get("X-Istek-Kimligi")
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, "yol=%s kimlik=%s govde-uzunlugu=%d", r.URL.Path, kimlik, len(govde))
}
// araGunlukleme, sardigi isleyiciyi cagirmadan once bir sayaci artirir; govdeye
// ya da baglantiya dokunmuyor.
func araGunlukleme(sonraki http.Handler, sayac *int) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
*sayac++
sonraki.ServeHTTP(w, r)
})
}
// araBasliklandirma, yanita sabit bir ustbilgi ekleyip zinciri surdurur.
func araBasliklandirma(sonraki http.Handler, sayac *int) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
*sayac++
w.Header().Set("X-Zincir-Adimi", "gecti")
sonraki.ServeHTTP(w, r)
})
}
Ölçüm bu çekirdeği bir istekle koşturuyor. İstek net/http/httptest ile kuruluyor —
httptest.NewRequest gerçek bir bağlantı noktası açmadan bellekte bir *http.Request
üretiyor, httptest.NewRecorder ise gerçek bir bağlantı yerine yanıtı bir arabelleğe
yazan sahte bir http.ResponseWriter veriyor. İstemci ile sunucu arasında hiçbir gerçek
soket, adres ya da süre yok.
// istekolcumu.go — bir istegin tasidigi parcalarin tip bilgisi ve ara katman zinciri
package main
import (
"bytes"
"fmt"
"net/http"
"net/http/httptest"
)
func main() {
fmt.Println("-- isleyicinin aldigi sey: bayt ve metin --")
govde := bytes.NewBufferString("adet=3")
istek := httptest.NewRequest(http.MethodPost, "/urunler/42", govde)
istek.Header.Set("X-Istek-Kimligi", "1001")
var gunlukSayaci, basliklandirmaSayaci int
zincir := araGunlukleme(
araBasliklandirma(http.HandlerFunc(cekirdekIsleyici), &basliklandirmaSayaci),
&gunlukSayaci,
)
kaydedici := httptest.NewRecorder()
zincir.ServeHTTP(kaydedici, istek)
olcumSatiri("istek yolu (r.URL.Path)", "kayboluyor",
fmt.Sprintf("deger=%q tip=%T", istek.URL.Path, istek.URL.Path))
olcumSatiri("istek ustbilgisi (Header.Get)", "kayboluyor",
fmt.Sprintf("deger=%q tip=%T", istek.Header.Get("X-Istek-Kimligi"), istek.Header.Get("X-Istek-Kimligi")))
olcumSatiri("istek govdesi (okunan bayt)", "kayboluyor",
fmt.Sprintf("tip=%T uzunluk=%d", sonOkunanGovde, len(sonOkunanGovde)))
fmt.Println("-- ara katman zinciri --")
fmt.Println("zincir: araGunlukleme( araBasliklandirma( cekirdekIsleyici ) ) - 3 halka")
fmt.Printf("gunlukleme cagrisi=%d basliklandirma cagrisi=%d (ikisi de bu dersin yazdigi kod)\n",
gunlukSayaci, basliklandirmaSayaci)
fmt.Printf("durum kodu=%d zincir ustbilgisi=%s\n", kaydedici.Code, kaydedici.Header().Get("X-Zincir-Adimi"))
fmt.Println("yanit govdesi:", kaydedici.Body.String())
fmt.Printf("bu istegin %d parcasindan tip bilgisi tasiyani=%d (kayboluyor=%d)\n",
parcaSayisi, parcaSayisi-kayboluyorSayisi, kayboluyorSayisi)
}
-- isleyicinin aldigi sey: bayt ve metin -- istek yolu (r.URL.Path) kayboluyor deger="/urunler/42" tip=string istek ustbilgisi (Header.Get) kayboluyor deger="1001" tip=string istek govdesi (okunan bayt) kayboluyor tip=[]uint8 uzunluk=6 -- ara katman zinciri -- zincir: araGunlukleme( araBasliklandirma( cekirdekIsleyici ) ) - 3 halka gunlukleme cagrisi=1 basliklandirma cagrisi=1 (ikisi de bu dersin yazdigi kod) durum kodu=200 zincir ustbilgisi=gecti yanit govdesi: yol=/urunler/42 kimlik=1001 govde-uzunlugu=6 bu istegin 3 parcasindan tip bilgisi tasiyani=0 (kayboluyor=3)
AG1. Yolun "/urunler/42" olması, 42 rakamının bir tamsayı anlamına gelmesi
gerektiğini düşündürüyor — kaynak kodda bu değer bir ürün kimliğiydi. Ama ölçüm %T ile
sorduğunda yanıt tektir: string. Go’nun yönlendiricisi yolu ayrıştırırken hiçbir rakam
dizisini otomatik olarak sayıya çevirmiyor; /urunler/42 ile /urunler/kirk-iki aynı tipte
iki dizgidir, aralarındaki fark yalnızca içerik. Aynı gözlem üstbilgi için de geçerli: istek
kimliği üstbilgisindeki "1001" değeri de bir dizgi, isteyen tarafın kafasında bir sayısal
kimlik olsa bile. Kablonun üzerinden geçen hiçbir şey kendi tipini taşımıyor; taşıyan tek şey
karakterler.
AG2. Gövde bu üçünün en açık örneği: io.ReadAll(r.Body) her zaman []byte döndürüyor,
gövdenin içeriği ister bir sayı, ister bir yapı, ister rastgele metin olsun. Ölçümde
uzunluk=6 çıkıyor çünkü "adet=3" dizgisi altı bayt — ama bu uzunluk gövdenin neyi
temsil ettiğini söylemiyor, yalnızca kaç bayt olduğunu söylüyor. Bu üç satırın üçü de
kayboluyor sınıfında; bu dersin ölçtüğü isteğin üç parçasından tip bilgisi taşıyanı sıfır.
Kursun on üç sınır geçişinin tam dağılımı (1 korunuyor, 5 yeniden kuruluyor, 7 kayboluyor)
JSON Dizileştirme dersinde kuruluyor; bu ders yalnız kendi diliminin üç satırını basıyor.
Ara Katman: Bir İşleyiciyi Saran Başka Bir İşleyici
http.Handler, Go’nun standart kitaplığında tek bir yöntemle tanımlanan bir arayüz:
ServeHTTP(http.ResponseWriter, *http.Request). Bu küçüklük bilinçli bir tasarım kararı —
bir işleyici, bu tek yöntemi gerçekleştiren her şey olabiliyor, dolayısıyla bir işleyiciyi
başka bir işleyicinin içine gömmek özel bir çerçeve gerektirmiyor, sıradan bir fonksiyon
çağrısı yeterli. araGunlukleme ve araBasliklandirma işlevlerinin ikisi de aynı kalıbı
izliyor: bir http.Handler alıyor, kendi mantığını çalıştıran yeni bir http.HandlerFunc
döndürüyor, ve döndürdüğü işleyicinin gövdesinde aldığı işleyicinin ServeHTTP yöntemini
çağırıyor. Sonuç bir zincir: dıştaki halka önce çalışıyor, işini yapıp içteki halkayı
çağırıyor, o da kendi işini yapıp bir sonrakini çağırıyor — en içte çekirdek işleyici durana
kadar.
Bu zincir kaynak kodda main işlevindeki tek bir ifadeyle, araGunlukleme çağrısının
içine araBasliklandirma çağrısının, onun içine de çekirdek işleyicinin konulmasıyla elle
kuruluyor: üç halka, üçü de bu dersin yazdığı kod, hiçbiri bir kayıt listesinden ya da bir
etiketten otomatik olarak
keşfedilmiyor. Çıktıdaki gunlukleme cagrisi=1 ve basliklandirma cagrisi=1 satırları bunu
doğrudan sayıyor: zincirdeki iki ara katmanın ikisi de tam olarak bir kez çağrıldı, çünkü
istek de tam olarak bir kez geldi. Sunucunun görevi, istek işleme zincirinin genel yapısı ve
ara katmanın neden var olduğu önceki kurslarda kavram olarak kuruldu; burada ölçülen şey o
kavramın Go’nun standart kitaplığındaki somut karşılığı — bir işleyicinin başka bir işleyiciyi
sarması dışında hiçbir gizli düzenek yok.
AG3. Zincirin sırası sonucu etkiliyor. araBasliklandirma, X-Zincir-Adimi üstbilgisini
kendi ServeHTTP gövdesinde, iç işleyiciyi çağırmadan önce ayarlıyor; ama bu üstbilgi
yanıta yalnızca w.WriteHeader çağrıldığı anda kilitleniyor, ve WriteHeaderi çağıran
cekirdekIsleyici. Yani araBasliklandirmanın eklediği üstbilgi, çekirdek henüz yanıtı
yazmadan önce ayarlandığı için başarıyla yanıta giriyor — çıktıdaki
zincir ustbilgisi=gecti satırı bunu doğruluyor. Sıra tersine çevrilseydi, yani
araBasliklandirma
çekirdekten sonra çalışsaydı, WriteHeader zaten çağrılmış olacağı için üstbilgi
sessizce yok sayılırdı — Go bu durumda hata vermiyor, yalnızca üstbilgiyi görmezden geliyor.
AG4. İki ara katmanın da imzası aynı: bir http.Handler ve bir sayaç işaretçisi alıp
yeni bir http.Handler döndürüyorlar. Bu tekdüzelik, üçüncü bir ara katmanın aynı zincire
eklenmesini tek satırlık bir sarmalamaya indiriyor — zincire yeni bir halka eklemek, listeye
yeni bir eleman eklemekten farklı bir işlem değil, sadece bir fonksiyon çağrısını bir
öncekinin içine koymak. Ama bu kolaylığın bir bedeli var: zincir ne kadar uzarsa, bir isteğin
cekirdekIsleyiciye ulaşana kadar kaç halkadan geçtiğini görmek o kadar çok, iç içe geçmiş
fonksiyon çağrısını okumayı gerektiriyor. Zincirin kendisi hiçbir yerde bir liste olarak
durmuyor — yalnızca main işlevindeki tek bir ifade.
Bu, zincirin tip bilgisiyle ilişkisini de belirliyor: araGunlukleme ve
araBasliklandirma işlevlerinin ikisi de kendi işlerini görmek için isteğin ya da yanıtın
içeriğine hiç bakmıyor, yalnızca *http.Request ve http.ResponseWriter değerlerini
olduğu gibi bir sonraki halkaya aktarıyor. Bir ara katman isteğin gövdesindeki bir alanı
okumak isteseydi, bu ders boyunca görülen aynı sınırla karşılaşırdı — gövde yine yalnızca
bayt, ara katman da yalnızca kendi yazdığı ayrıştırma koduyla o baytlara bir anlam
yükleyebilirdi.
Sınırlayıcı Ölçüm: İçerik Tipi Bir İpucudur, Tip Değil
Çekirdek işleyicinin yanıtına eklediği Content-Type: application/json üstbilgisi, ilk
bakışta yanıt gövdesinin tipini garanti ediyormuş gibi görünüyor. Değil. Bu üstbilgi bir
dizgiden başka bir şey değil ve onu kim okursa okusun, gövdenin gerçekten o biçimde olup
olmadığını denetleyen hiçbir mekanizma araya girmiyor. Aşağıdaki ölçüm bunu bilerek kırıyor:
işleyici application/json ipucu veriyor ama gövdeye düz metin yazıyor. Bu, önceki ölçümdeki
üç kayboluyor satırından farklı bir gözlem: oradaki satırlar isteğin taşıdığı bilginin
hiç var olmadığını ölçtü, buradaki ölçüm ise var olan bir bilginin (üstbilginin kendisi)
yanlış olabileceğini ve bunun hiçbir yerde yakalanmadığını gösteriyor.
// yanlisipucu.go — icerik tipi bir ipucudur, hatasi kosarken degil cozerken cikar
package main
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
)
// yalanciIsleyici, govdeye duz metin yazip ustbilgide "application/json" ipucu verir.
// Ipucu ile govde arasindaki uyumu denetleyen hicbir kod yoktur.
func yalanciIsleyici(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, "bu govde json degil, duz metin")
}
func main() {
fmt.Println("-- icerik tipi bir ipucudur, tip degil --")
istek := httptest.NewRequest(http.MethodGet, "/rapor", nil)
kaydedici := httptest.NewRecorder()
http.HandlerFunc(yalanciIsleyici).ServeHTTP(kaydedici, istek)
fmt.Printf("durum kodu=%d (ipucu yanlisken bile basarili)\n", kaydedici.Code)
fmt.Printf("ustbilgideki ipucu=%s\n", kaydedici.Header().Get("Content-Type"))
var hedef map[string]any
cozHatasi := json.Unmarshal(kaydedici.Body.Bytes(), &hedef)
fmt.Printf("govdeyi json olarak cozme hatasi var mi=%t\n", cozHatasi != nil)
fmt.Println("hata sunucuda degil, govdeyi ipucuna gore cozmeye calisan tarafta cikiyor")
}
-- icerik tipi bir ipucudur, tip degil -- durum kodu=200 (ipucu yanlisken bile basarili) ustbilgideki ipucu=application/json govdeyi json olarak cozme hatasi var mi=true hata sunucuda degil, govdeyi ipucuna gore cozmeye calisan tarafta cikiyor
AG5. Durum kodu 200 — yani işleyici, gövdenin ipucuyla uyuşmadığını hiç fark etmeden
başarıyla tamamlandı. http.ResponseWriter arayüzünde Content-Type üstbilgisini gövdeyle
karşılaştıran, ya da gövdenin gerçekten geçerli JSON olup olmadığını denetleyen hiçbir
adım yok — üstbilgi, alıcıya “bu gövdeyi böyle yorumlamayı dene” diyen bir öneri, alıcının
uyması gereken bir kural değil. Hata ancak bir taraf bu öneriye güvenip json.Unmarshal
çağırdığında ortaya çıkıyor; ölçümde bu çağrı main işlevinin kendisinde yapılıyor ve
cozHatasi != nil sonucu true çıkıyor. Sunucu tarafında hiçbir uyarı, hiçbir günlük satırı,
hiçbir hata dönüşü yok — sınır geçişinin kendisi tamamen sessiz.
AG6. Bu gözlem, dersin baştaki iddiasını netleştiriyor: sınırın ötesine geçen bir yanıtın
yanında taşıdığı tek ipucu böyle bir üstbilgidir, ve bu ipucu bir tip değil, bir dizgidir
— aynı r.URL.Path ya da r.Header.Get gibi, %T ile sorulduğunda yine string çıkıyordu.
İçerik tipi üstbilgisinin doğru olması, sunucuyu yazan tarafın disiplinine kalmış bir şey;
Go’nun derleyicisi ya da çalışma zamanı bu disiplini zorlamıyor. Bu sınırlayıcı ölçüm,
gövdenin kendisinin taşıdığı bilgiyle üstbilgide yazan bilginin birbirinden bağımsız iki
şey olduğunu, ikisinin birbirini doğrulamadığını gösteriyor.
Standart kitaplığın kendisi bu ikisini birbirine bağlayan hiçbir denetim eklemiyor; ekleyecek
olan, işleyiciyi yazan taraf. cekirdekIsleyici isteseydi, w.Write çağrısından önce
gövdenin gerçekten geçerli JSON olduğunu kendi kodunda doğrulayabilirdi — ama bu doğrulama
dilin bir parçası değil, yazarın bir kararı. Sınırın bu tarafında kalan tek garanti, üstbilgi
ile gövde arasındaki tutarlılığı kimsenin otomatik olarak sağlamadığıdır; bu tutarlılığı
kuran her katman, kendi kodunda bunu açıkça yapmak zorunda.
Özet
- Bir HTTP isteğinin yolu, üstbilgileri ve gövdesi Go’nun standart kitaplığında sırasıyla
dizgi, dizgi ve bayt dilimi olarak temsil ediliyor; üçü de
%Tile sorgulandığında hiçbir özel tip taşımadığı görülüyor. - Ölçüm
net/http/httptestile gerçek bir bağlantı noktası açmadan kuruluyor:httptest.NewRequestbellekte bir istek üretiyor,httptest.NewRecorderyanıtı bir arabelleğe yazan sahte bir yanıt yazıcı sağlıyor. - Ara katman kalıbı,
http.Handlerarayüzünün tek yöntemli olması sayesinde bir işleyiciyi başka bir işleyicinin içine sarmaktan ibaret; zincirdeki her halka çağıranın yazdığı sıradan bir fonksiyon çağrısı, otomatik keşif ya da kayıt yok. - Ara katmanların çalışma sırası sonucu etkiliyor: bir üstbilgi ancak
WriteHeaderçağrılmadan önce eklenirse yanıta giriyor, sıra tersine dönerse üstbilgi hatasızca yok sayılıyor. - İçerik tipi üstbilgisi bir ipucu, bir garanti değil: yanlış bir ipucu sunucu tarafında hiçbir hata üretmiyor, hata yalnızca gövdeyi o ipucuna güvenerek çözmeye çalışan tarafta ortaya çıkıyor.
- Kursun on üç sınır geçişinin tam dağılımı bu derste kurulmuyor; bu ders yalnız isteğin sunucu tarafındaki üç parçasını ölçüyor, tam tabloyu JSON Dizileştirme dersi kuruyor.
Sonraki Adım
Bu ders, sınırı alıcı tarafından ölçtü: bir isteğin sunucuya ne getirdiğini, ve sunucunun gövdeye ne kadar körü körüne güvendiğini. Sıradaki ders aynı sınırı gönderen taraftan soruyor: bir istemci bir isteği gönderirken neyi garanti etmek zorunda, ve bir yanıtı aldıktan sonra hangi sorumlulukları devralıyor. Bir yanıt gövdesi de bu dersteki istek gövdesi gibi bir bayt akışı, ama bu kez akışı kimin ne zaman kapatacağı, ve zaman aşımının bir süre yerine neyle kurulduğu ölçülüyor.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.