İçeriğe geç
academia.sh

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 %T ile sorgulandığında hiçbir özel tip taşımadığı görülüyor.
  • Ölçüm net/http/httptest ile gerçek bir bağlantı noktası açmadan kuruluyor: httptest.NewRequest bellekte bir istek üretiyor, httptest.NewRecorder yanıtı bir arabelleğe yazan sahte bir yanıt yazıcı sağlıyor.
  • Ara katman kalıbı, http.Handler arayü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.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat