İçeriğe geç
academia.sh

Ders 06 / 12

Gerçek Zamanlı İletişim

Uzun süreli bir bağlantı, gerçek bağlantı noktası açılmadan, io.Reader ve io.Writer'ı gerçekleştiren bir bayt arabelleğiyle modelleniyor. Sunucu üç mesajı art arda, aralarında bir istek beklemeden itiyor. Çerçevesiz bir akışta üç itme tek bir bayt dizisine karışıyor ve sabit boyutlu okumalar hiçbir mesajı doğru ayıramıyor (0/3); uzunluk ön ekiyle çerçevelenen akışta üçü de doğru geri ayrışıyor (3/3). Sınırlayıcı ölçüm: çerçeveleme mesajın nerede bittiğini veriyor, içindeki değerin tipini vermiyor.

İçindekiler

Önceki ders iki tarafın tek bir alışveriş yaptığı bir sınırı ölçtü: gönderen bir kez yazdı, alıcı bir kez okudu, iki taraf hiç aynı anda çalışmadı. Bu ders bu varsayımı kaldırıyor. Bir bağlantı uzun sürdüğünde — açılıp kapanmadan art arda birçok geçişi taşıdığında — sınırı geçen tek şey artık bir bayt dizisinin içeriği değil, o bayt dizisinin nerede bittiği. Kursun sorusu aynı kalıyor, ama bu kez sorulduğu yer bir tek değerin sınırı değil, zaman içinde uzayan bir akış.

Gerçek bir ağ bağlantısı bu derste hiç açılmıyor. Bunun yerine bir bytes.Buffer kullanılıyor — standart kitaplığın bayt arabelleği, io.Reader ve io.Writer arayüzlerinin ikisini de gerçekleştiriyor, tıpkı gerçek bir ağ bağlantısının gerçekleştireceği gibi. Bu ders bağlantıyı bu arabellekle modelliyor; ölçülen kod yalnızca bu iki arayüzü kullanıyor, ve aynı kod gerçek bir bağlantı üzerinde de değişmeden çalışırdı, çünkü ikisi de aynı sözleşmeyi karşılıyor. Adres, bağlantı noktası ve süre hiçbir yerde basılmıyor, çünkü hiçbiri bu modelde hiç var olmuyor.

Bu, kursun HTTP Sunucusu ve HTTP İstemcisi derslerinin izlediği stratejinin bir devamı. O iki ders de gerçek bir bağlantı noktası açmadan, bir test yardımcısıyla ölçüm yapmıştı; burada aynı fikir farklı bir katmanda uygulanıyor. HTTP’nin kendi test yardımcısı tek bir istek–yanıt çiftini modellemek için yeterliydi, çünkü HTTP’nin sınırı zaten netti — bir istek başlar, bir yanıt biter, bağlantı kapanır ya da bir sonraki isteğe hazır bekler. Uzun süreli bir bağlantıda böyle bir sınır yok; sunucu istediği zaman yazabiliyor, istemci istediği zaman okuyabiliyor, ve tam da bu yüzden bu dersin ölçtüğü şey “bir isteğin ne taşıdığı” değil, “akışın neresinde bir mesajın bittiği”.

Bu fark ölçüm aracının seçimini de belirliyor. HTTP dersleri gerçek bir istemci ve sunucu nesnesi kurup aralarında tek bir alışverişi ölçtü; bu ders tek bir paylaşılan bayt arabelleğini hem yazan hem okuyan tarafın ortak zemini olarak kullanıyor, çünkü ölçülmek istenen şey iki ayrı tarafın davranışı değil, aradaki akışın kendisinin yapısı — kaç mesajın art arda geldiği ve bu mesajların akış içinde nasıl ayırt edildiği.

Bağlantı Modelleniyor: Aynı Arayüz, Farklı Somut Uygulama

AG33. bytes.Buffer, io.Reader ile io.Writerı gerçekleştiriyor — tam olarak Go’nun standart kitaplığının bir ağ bağlantısı için tanımladığı arayüzlerin ikisi. Sistem ve Veri Erişimi konusunun ilk dersi bu iki arayüzü dosya, arabellek ve dizgi arasında paylaşan bir olguyu ölçecek; bu ders aynı olgunun bağlantı tarafını, ağ hiç açılmadan, önceden kuruyor. Aşağıdaki yordamlar bir mesajı önce dört baytlık büyük uçlu bir uzunluk öneki, sonra mesajın kendi baytlarıyla yazıyor; okuma tarafı önce bu dört baytı okuyup uzunluğu öğreniyor, sonra tam o kadar baytı istiyor.

Bu modelin bir sınırı var ve açıkça yazılması gerekiyor: gerçek bir ağ bağlantısında bir Write çağrısının karşı tarafa ne zaman ulaştığı, tek bir çağrının kaç parçaya bölünerek ilerlediği, gecikme ve sıralama gibi etkenlere bağlı olabilir. Bu ders bunların hiçbirini modellemiyor — bytes.Buffer üzerindeki bir yazma anında tamamlanıyor ve bir sonraki okuma her zaman en güncel hâli görüyor. Ölçülen şey gecikme ya da sıralama değil, yalnızca çerçevelemenin kendisi: bir akışın içine gömülü mesajların, akışı okuyan tarafından nasıl ayrıştırılacağı. Bu dar kapsam bilinçli; genişletilmiş bir model, aynı çerçeveleme kuralının üzerine kurulur, onu değiştirmez.

// cerceveleme.go — uzun sureli baglantida cercevenin mesaj sinirini nasil tasidiginin olcumu
package main

import (
	"bytes"
	"encoding/binary"
	"fmt"
	"io"
)

func cerceveleYaz(baglanti *bytes.Buffer, mesaj string) {
	var uzunluk [4]byte
	binary.BigEndian.PutUint32(uzunluk[:], uint32(len(mesaj)))
	baglanti.Write(uzunluk[:])
	baglanti.WriteString(mesaj)
}

func cerceveOku(baglanti *bytes.Buffer) (string, bool) {
	var uzunluk [4]byte
	if _, err := io.ReadFull(baglanti, uzunluk[:]); err != nil {
		return "", false
	}
	boyut := binary.BigEndian.Uint32(uzunluk[:])
	govde := make([]byte, boyut)
	if _, err := io.ReadFull(baglanti, govde); err != nil {
		return "", false
	}
	return string(govde), true
}

func main() {
	itilenler := []string{"merhaba", "veri", "kapanis"}

	var cercevesizBaglanti bytes.Buffer
	for _, m := range itilenler {
		cercevesizBaglanti.WriteString(m)
	}
	fmt.Println("-- cercevesiz baglantida uc itme tek bir bayt dizisine karisiyor --")
	dogruAyrisan := 0
	for {
		parca := make([]byte, 6)
		n, err := cercevesizBaglanti.Read(parca)
		if n == 0 {
			break
		}
		parcaMetni := string(parca[:n])
		fmt.Printf("okunan parca: %q\n", parcaMetni)
		for _, m := range itilenler {
			if parcaMetni == m {
				dogruAyrisan++
			}
		}
		if err != nil {
			break
		}
	}
	fmt.Printf("dogru ayrisan mesaj: %d/%d\n", dogruAyrisan, len(itilenler))

	var cerceveliBaglanti bytes.Buffer
	for _, m := range itilenler {
		cerceveleYaz(&cerceveliBaglanti, m)
	}
	fmt.Println()
	fmt.Println("-- cerceveli baglantida uc itme de sunucunun yazdigi sinirla geri ayrisiyor --")
	dogruAyrisanCerceveli := 0
	for i := 0; i < len(itilenler); i++ {
		m, tamam := cerceveOku(&cerceveliBaglanti)
		if !tamam {
			break
		}
		fmt.Printf("okunan mesaj: %q\n", m)
		if m == itilenler[i] {
			dogruAyrisanCerceveli++
		}
	}
	fmt.Printf("dogru ayrisan mesaj: %d/%d\n", dogruAyrisanCerceveli, len(itilenler))

	var sayisalItme bytes.Buffer
	cerceveleYaz(&sayisalItme, "42")
	govde, _ := cerceveOku(&sayisalItme)
	fmt.Println()
	fmt.Println("-- sinirlayici olcum: cerceve dogru govdeyi veriyor ama govdenin tipini vermiyor --")
	fmt.Printf("govde: %q, tipi: %T\n", govde, govde)
}
-- cercevesiz baglantida uc itme tek bir bayt dizisine karisiyor --
okunan parca: "merhab"
okunan parca: "averik"
okunan parca: "apanis"
dogru ayrisan mesaj: 0/3

-- cerceveli baglantida uc itme de sunucunun yazdigi sinirla geri ayrisiyor --
okunan mesaj: "merhaba"
okunan mesaj: "veri"
okunan mesaj: "kapanis"
dogru ayrisan mesaj: 3/3

-- sinirlayici olcum: cerceve dogru govdeyi veriyor ama govdenin tipini vermiyor --
govde: "42", tipi: string

AG34. itilenler dizisindeki üç mesaj cercevesizBaglanti ile cerceveliBaglantiya art arda yazılıyor — aralarında hiçbir okuma, hiçbir “istek” yok. Bu, HTTP sunucusu ve istemcisi derslerinde kurulan istek–yanıt kalıbından farklı: orada her yanıt bir isteği takip ediyordu, burada sunucu tarafı üç kez de kendi inisiyatifiyle itiyor. Uzun süreli bir bağlantının veri itme özelliği tam olarak bu — karşı taraftan bir tetikleyici gelmeden yazabilme.

Sunucunun bir isteği nasıl karşıladığı Backend Geliştirme müfredatının sekiz kursunda kavram olarak kuruldu ve burada tekrarlanmıyor; bu dersin payı yalnızca Go’nun standart kitaplığındaki iki arayüzün (io.Reader, io.Writer) böyle bir itmeyi taşıyıp taşımadığı. Yanıt burada evet: bytes.Buffer.Write çağrısı hiçbir isteğe bağlı değil, yalnızca çağrıldığı anda arabelleğin sonuna ekleme yapıyor — bu, bir sunucunun bir istemciye önceden haber vermeden veri göndermesinin, standart kitaplık düzeyinde ne kadar sade bir işlem olduğunu gösteriyor. Karmaşıklık başka bir yerde: karşı tarafın bu itmelerin nerede bittiğini bilmesi gerekiyor, ve bu bilgiyi taşıyan şey Writein kendisi değil, birazdan ölçülecek olan çerçeveleme.

Çerçevesiz Akış: Üç İtme Tek Bir Bayt Dizisine Karışıyor

AG35. cercevesizBaglantiya yazılan üç mesaj ("merhaba", "veri", "kapanis") aralarında hiçbir ayraç olmadan doğrudan art arda ekleniyor — sonuç, on sekiz baytlık tek bir dizi. Okuyan taraf bu dizide bir mesajın nerede bitip ötekinin nerede başladığını hiç bilmiyor; bildiği tek şey sabit boyutlu (altı baytlık) parçalar okumak. Bu parçalardan hiçbiri özgün üç mesajdan biriyle eşleşmiyor — "merhab", "averik", "apanis" — çünkü altı bayt sınırı mesaj sınırlarıyla hiçbir ilişkisi olmayan, tamamen keyfi bir bölme. Ölçüm bunu 0/3 olarak veriyor: sabit boyutlu okuma, mesaj sınırlarını korumanın hiçbir garantisini taşımıyor.

Altı bayt seçimi ölçümün bir ayrıntısı değil, tam olarak bu keyfiliği göstermek için seçildi: üç mesajın uzunlukları (7, 4, 7) hiçbiri altıya bölünmüyor, bu yüzden hiçbir okuma bir mesajın tam sınırında durmuyor. Okuma boyutu dört olsaydı ya da sekiz olsaydı sonuç yine 0/3 olurdu, çünkü sorun okuma boyutunun büyüklüğü değil, okuma boyutunun mesaj uzunluklarından bağımsız, dışarıdan seçilen bir sayı olması. Bu, uzun süreli bir bağlantının temel bir özelliğine işaret ediyor: io.Readerın bir Read çağrısı, kaç bayt istendiyse o kadarını (ya da o an elde ne kadar varsa) döndürüyor — hangi baytların bir mesajın başı, hangilerinin ortası olduğuna dair hiçbir bilgi taşımıyor. Bu bilgi akışın kendisinde yoksa, okuyan tarafın onu icat etmesi gerekir; sabit boyutlu okuma böyle bir icat, ve bu ölçüm onun neden işe yaramadığını gösteriyor.

Çerçeveli Akış: Sınır Bilgisiyle Geri Ayrışıyor

AG36. cerceveliBaglantiya yazılan her mesajdan önce, o mesajın uzunluğunu taşıyan dört baytlık bir önek duruyor. Okuyan taraf artık tahmin etmiyor: önce dört baytı okuyup uzunluğu öğreniyor, sonra tam o uzunlukta bir okuma yapıyor — ne eksik, ne fazla. Üç mesajın üçü de bayt bayt doğru geri geliyor, 3/3. Aradaki fark okuma boyutunun büyüklüğü değil (çerçevesiz denemede altı bayt, mesajlardan bazılarından uzun bazılarından kısaydı); fark, okuma boyutunun her seferinde mesajın kendisinden geldiği, dışarıdan sabit bir sayı olarak dayatılmadığı.

cerceveOku iki okumasında da io.ReadFullı kullanıyor, düz bir Read çağrısını değil. Bu seçim rastgele değil: io.Reader arayüzünün sözleşmesi, istenen bayt sayısından daha az döndürmeye izin veriyor, elde daha fazla veri olsa bile — bytes.Buffer bu ölçümde her zaman istenen kadarını verdi, ama bu, arayüzün genel sözleşmesinin bir parçası değil, bu belirli somut tipin bir davranışı. io.ReadFull, istenen bayt sayısı tam olarak karşılanana kadar (ya akış bitene ya da bir hata çıkana kadar) okumayı tekrarlıyor; dört baytlık uzunluk önekinin ya da değişken uzunluklu gövdenin tek bir Read çağrısında parça parça, eksik gelmesi ihtimaline karşı bu bir güvence. Çerçeveleme kuralının kendisi kadar, onu okuyan kodun bu ayrıntıya dikkat etmesi de doğru ayrışmanın bir parçası.

Uzunluk öneki, çerçevelemenin tek olası biçimi değil — bir mesajın sonunu işaretleyen özel bir bayt (bir ayraç) da aynı işi görebilirdi. Ama ayraç tabanlı bir çerçeveleme, mesajın içeriğinde ayracın kendisiyle aynı baytın geçme ihtimalini taşır: "veri" mesajı "i" harfiyle bitiyor, ayraç olarak "i" seçilmiş olsaydı bu mesaj kendi içinde erken kesilmiş gibi görünürdü. Uzunluk öneki bu riski taşımıyor, çünkü mesajın içeriğine hiç bakmıyor — önce “kaç bayt geleceğini” söylüyor, sonra o kadar baytı, içinde ne olursa olsun, tek bir birim olarak okuyor. Bu ders bu iki stratejiyi karşılaştırmıyor, yalnızca ikincisini ölçüyor, ama tercihin gerekçesi bu.

Kursun on üç sınır geçişinin tam dağılımı (1 korunuyor, 5 yeniden kuruluyor, 7 kayboluyor; 03. derste kurulan sayı) bu on üç geçişin dışında kalan, ayrı bir alt problemi çözüyor: o dağılım tek bir değerin sınırı geçerken tipine ne olduğunu sayıyordu, bu ders ise bir akışın neresinin bir değer olduğunu belirliyor. Çerçeveleme bu sorunun cevabı olmadan, önceki derslerin JSON çözücüsü de tarif üretici de çalışamazdı — ikisi de baştan “burada bir mesaj bitti” bilgisine ihtiyaç duyuyor, ve bu bilgiyi taşıyan tek şey çerçeveleme.

Bilgisayar Ağları müfredatının Uygulama Katmanı Protokolleri kursu ile Frontend Geliştirme müfredatının Uygulama Mimarisi: Yönlendirme, Durum ve Veri kursu, uzun süreli bağlantılar ve gerçek zamanlı veri akışını kavram olarak zaten kurdu; burada tekrarlanmıyor, ölçülen yalnızca Go’nun standart kitaplığının bir bayt akışını arayüz düzeyinde nasıl taşıdığı.

AG37. Son ölçüm bir uyarı: cerceveleYaz ile yazılıp cerceveOku ile geri gelen gövde, "42" dizgisi — çerçeveleme burada da kusursuz çalışıyor, gövde bayt bayt doğru. Ama govdenin Go tipi string, int değil. Çerçeveleme mesajın nerede bittiğini söylüyor, içindeki baytların bir tamsayıyı mı, bir dizgiyi mi, yoksa bir yapıyı mı temsil ettiğini söylemiyor. Bu soruyu yanıtlamak, önceki dersin ölçtüğü türden bir tarif gerektiriyor — çerçeveleme ile tarif, uzun süreli bir bağlantıda birbirini tamamlayan iki ayrı katman: biri sınırı çiziyor, öbürü sınırın içindekini adlandırıyor.

Bu ayrımın önemi, "42" gibi bir gövdenin iki türlü de okunabilir olmasında ortaya çıkıyor. Karşı taraf bu gövdeyi bir tamsayıya çevirmeye çalışırsa (strconv.Atoi ile) başarılı olur ve 42 sayısını elde eder; hiç çevirmeden dizgi olarak tutarsa yine geçerli bir değeri olur. Çerçeveleme her iki yorumu da eşit derecede mümkün bırakıyor, çünkü gövdenin baytları kendi başına hangi yorumun doğru olduğunu söylemiyor. Önceki derste görülen sözleşme temelli tarif tam olarak bu belirsizliği kapatan şeydi: alan adı ve tipini önceden sabitleyerek, gövdenin nasıl okunacağı konusunda iki tarafı da aynı karara bağlıyordu. Uzun süreli bir bağlantıda bu iki katman genellikle birlikte kuruluyor — çerçeveleme her mesajın sınırını, tarif her mesajın içindeki alanların anlamını taşıyor — ama bu ders onları bilinçli olarak ayırdı, çünkü ikisi birbirinden bağımsız başarabilir ya da başarısız olabilir: bir akış doğru çerçevelenip yine de yanlış yorumlanabilir.

Özet

  • Uzun süreli bir bağlantı, gerçek bir ağ bağlantısı açılmadan, io.Reader ve io.Writerı gerçekleştiren bir bytes.Buffer ile modellendi; ölçülen kod yalnızca bu iki arayüzü kullandığı için aynı mantık gerçek bir bağlantı üzerinde de değişmeden çalışırdı.
  • Sunucu tarafı üç mesajı aralarında bir istek beklemeden itti — uzun süreli bir bağlantının veri itme özelliği, önceki derslerin istek–yanıt kalıbından bu yönüyle ayrılıyor.
  • Çerçevesiz bir akışta üç mesaj tek bir bayt dizisine karıştı ve sabit boyutlu okumalar hiçbirini doğru ayıramadı (0/3); uzunluk ön ekiyle çerçevelenen akışta üçü de bayt bayt doğru geri geldi (3/3), çünkü okuma boyutu her seferinde mesajın kendisinden geldi.
  • io.ReadFull kullanımı, io.Reader sözleşmesinin istenen bayt sayısından azını döndürmeye izin verdiği durumlar için bir güvenceydi — çerçeveleme kuralının doğru çalışması, onu okuyan kodun bu ayrıntıyı gözetmesine de bağlıydı.
  • Sınırlayıcı ölçüm: çerçeveleme doğru gövdeyi verdi ama gövdenin tipini vermedi — "42" dizgisi doğru ayrıştı, ama bunun bir tamsayı mı bir dizgi mi olduğu çerçevelemenin dışında kalan, ayrı bir tarif sorusu.
  • Çerçeveleme ve tarif, uzun süreli bir bağlantıda birbirini tamamlayan iki ayrı katman: biri mesajın sınırını, öbürü sınırın içindekinin anlamını taşıyor.

Sonraki Adım

Bu ders bir akışın neresinin bir mesaj olduğunu belirleyen tek bilgiyi — çerçevelemeyi — ölçtü, ve bu bilginin bir mesajın tipini hiç taşımadığını gösterdi: aynı çerçeve içinde ne olduğu yine bir tarif gerektiriyor. Kursun bu konusu (Ağ ve Veri Biçimleri) altı dersi boyunca sınırı hep ağ üzerinden — bir istek, bir yanıt, bir şablon çıktısı, iki sürecin paylaştığı bir tarif, uzun süreli bir bağlantı — geçen değerleri ölçtü. Sıradaki konu aynı soruyu programın kendi sınırlarında soruyor: dosya, arabellek ve gömülü bir kaynak da, tıpkı bu dersteki bağlantı gibi, bayt düzeyinde konuşuyor — ve orada da tip bilgisinin sınırın hangi tarafında durduğu, hangi tarafta durmadığı yeniden ölçülecek.

İ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