Ders 13 / 16
Statik Dosya Sunumu
Diskteki bir dosyanın HTTP yanıtına dönüşmesi: kök dizin ve yol çözümü, içerik türü ve uzunluğu, varlık etiketiyle koşullu istek, saklama yönergesinin iki sınıfı ve yükün içerik dağıtımına devredilmesi kararı.
İçindekiler
Önceki bölüm uygulamanın iskeletini kurdu: yapılandırma ortamdan okunuyor, kütük yazılıyor, hatalar ortak bir biçimde yanıta dönüşüyor. İskelet ayakta ama dışarıya henüz hiçbir şey vermiyor. Kütüphane ödünç servisinin ilk vereceği şey de en yalın olan: diskte duran bir dosya. Katalog sayfasının belgesi, biçem dosyası, betiği.
Bu ders o dosyayı ağ üzerinden vermenin neyi gerektirdiğini kurar. Dosyayı okuyup yanıta yazmak işin küçük parçasıdır; asıl kararlar hangi dosyanın okunacağına izin verildiği, alıcının içeriği nasıl yorumlayacağı, aynı dosyanın ikinci kez istendiğinde ne olacağı ve bu yükün sunucuda kalıp kalmayacağıdır.
Kök Dizin ve Yol Çözümü
Sunulacak dosyalar tek bir dizinin altında toplanır. Bu dizine kök dizin (document root) denir ve sunucunun dosya sisteminde erişmeye yetkili olduğu tek bölgedir. İstek yolunun kök dizinle birleştirilmesine yol çözümü (path resolution) denir.
Yol çözümünün tek başına yeterli olmadığı yer şurasıdır: istek yolu kök dizinin dışını gösteren bileşenler taşıyabilir. Birleştirme sonrası ortaya çıkan mutlak yol, hâlâ kök dizinin altında mı diye sınanmalıdır. Sınamayı birleştirmeden sonra yapmak zorunludur, çünkü birleştirme işlemi yolu sadeleştirir ve sadeleştirmeden önce yapılan bir denetim yanıltıcı olur.
// sunucu.mjs — kutuphane katalogunun statik varliklarini sunan dosya sunucusu import { createServer } from "node:http"; import { createHash } from "node:crypto"; import { readFile } from "node:fs/promises"; import { extname, join, sep } from "node:path"; const KOK = join(process.cwd(), "genel"); const TIPLER = { ".html": "text/html; charset=utf-8", ".css": "text/css; charset=utf-8", ".js": "text/javascript; charset=utf-8", ".svg": "image/svg+xml", ".json": "application/json; charset=utf-8", }; // Icerik karmasi tasiyan ad (katalog.7f3a91c2.css) sonsuza kadar saklanabilir; // belge her istekte dogrulanmalidir. const KARMALI = /\.[0-9a-f]{8}\.(css|js)$/; const saklama = (yol) => KARMALI.test(yol) ? "public, max-age=31536000, immutable" : "no-cache"; // join() yolu birlestirip sadelestirir; sonuc kokun altinda degilse istek reddedilir. const guvenliYol = (istekYolu) => { const tam = join(KOK, decodeURIComponent(istekYolu.split("?")[0])); return tam === KOK || tam.startsWith(KOK + sep) ? tam : null; }; createServer(async (istek, yanit) => { yanit.sendDate = false; const yol = guvenliYol(istek.url === "/" ? "/index.html" : istek.url); if (!yol) return yanit.writeHead(403).end(); let govde; try { govde = await readFile(yol); } catch { return yanit.writeHead(404).end(); } const etiket = `"${createHash("sha256").update(govde).digest("hex").slice(0, 16)}"`; yanit.setHeader("ETag", etiket); yanit.setHeader("Cache-Control", saklama(yol)); yanit.setHeader("Content-Type", TIPLER[extname(yol)] ?? "application/octet-stream"); if (istek.headers["if-none-match"] === etiket) return yanit.writeHead(304).end(); yanit.setHeader("Content-Length", govde.length); yanit.writeHead(200).end(govde); }).listen(8310, "127.0.0.1", () => console.log("dinleniyor: 127.0.0.1:8310"));
Sunucunun beklediği dosyalar genel/ dizininde durur. Aşağıdaki blok bu dizini üretir;
sunucu dosyasıyla aynı yerde çalıştırılır.
#!/usr/bin/env bash # genel/ dizinini ve sunucunun bekledigi dort varligi olusturur. mkdir -p genel cat > genel/index.html <<'SON' <!doctype html> <html lang="tr"><head><meta charset="utf-8"><title>Kutuphane Katalogu</title> <link rel="stylesheet" href="/katalog.7f3a91c2.css"> <script src="/katalog.4b8e0d15.js" defer></script></head> <body><h1>Kutuphane Katalogu</h1><div id="sonuc"></div></body></html> SON cat > genel/katalog.7f3a91c2.css <<'SON' body { font-family: system-ui, sans-serif; margin: 2rem auto; max-width: 46rem; } h1 { font-size: 1.5rem; } .kitap { border-bottom: 1px solid #ddd; padding: .5rem 0; } .kitap .yazar { color: #555; } SON cat > genel/katalog.4b8e0d15.js <<'SON' const kok = document.getElementById("sonuc"); const kitaplar = await (await fetch("/kitaplar.json")).json(); kok.innerHTML = kitaplar.map((k) => `<div class="kitap">${k.baslik} <span class="yazar">${k.yazar}</span></div>`).join(""); SON cat > genel/kitaplar.json <<'SON' [{"isbn":"9789750718533","baslik":"Kayip Zamanin Izinde","yazar":"Marcel Proust"}, {"isbn":"9789944885300","baslik":"Tutunamayanlar","yazar":"Oguz Atay"}, {"isbn":"9789750726439","baslik":"Kar","yazar":"Orhan Pamuk"}] SON
İçerik Türü ve Uzunluk
Ağ üzerinden giden şey bayt dizisidir; baytın kendisi ne olduğunu söylemez. Alıcının içeriği ne olarak işleyeceğini içerik türü (content type) başlığı belirler. Dosya uzantısından türe eşleme, sunucunun bildiği tür kümesiyle sınırlı tutulur ve bilinmeyen uzantı için genel ikili tür verilir. Metin türlerinde karakter kodlaması da başlıkta bildirilir; bildirilmediğinde alıcı tahmine düşer ve tahmin dosyanın kendi kodlamasıyla çakışabilir.
İçerik uzunluğu (content length) başlığı gövdenin bayt sayısını verir. Alıcı böylece yanıtın nerede bittiğini bağlantıyı beklemeden bilir ve indirme ilerlemesini hesaplayabilir. Bu başlık ancak gövdenin tamamı elde hazırken yazılabilir: dosya belleğe okunduğu için burada mümkündür, gövde parça parça üretiliyorsa yerini parçalı aktarım alır.
Uzunluk başlığının 304 yanıtında yazılmadığına dikkat edin. O yanıtın gövdesi yoktur ve başlık, gövdesi olmayan bir yanıtta yanlış bilgi verir.
Doğrulayıcı ve Koşullu İstek
Aynı dosya ikinci kez istendiğinde, içerik değişmemişse baytları yeniden taşımak boş iştir. Bunu saptamanın yolu doğrulayıcıdır (validator): sunucunun içerikten türettiği ve içerik değişince değişen kısa bir değer. HTTP’de bu değer varlık etiketi (entity tag) başlığıyla taşınır.
Yukarıdaki sunucu etiketi gövdenin özetinden üretir. Değiştirilme zamanından üretmek de mümkündür ama zaman damgası saniye çözünürlüğündedir ve dosya kopyalandığında değişir; içerikten türetilen etiket bu iki sorunu da taşımaz.
İstemci elindeki etiketi bir sonraki istekte geri gönderir. Sunucu etiketleri karşılaştırır: aynıysa gövdesiz bir 304 yanıtı döner. Bu alışverişe koşullu istek (conditional request) denir ve İnternet Nasıl Çalışır kursunda tanıtılan mekanizmanın sunucu tarafındaki karşılığıdır.
Saklama Yönergesinin İki Sınıfı
Koşullu istek gövdeyi taşımaz ama isteğin kendisini yine de sunucuya kadar getirir. İsteği hiç yapmamak, saklama yönergesiyle (cache directive) sağlanır: sunucu, yanıtın ne kadar süre yeniden sorulmadan kullanılabileceğini bildirir.
Karar, dosyanın adının içeriğine bağlı olup olmadığıyla belirlenir. Sunucudaki
katalog.7f3a91c2.css adı bir içerik karması (content hash) taşır: içerik
değiştiğinde ad da değişir, dolayısıyla bu ad altındaki bayt dizisi asla değişmez. Böyle
bir varlık uzun ömürle ve değişmeyeceği bildirilerek saklanabilir. Belgenin adı ise
sabittir ve içeriği değişebilir; onun için saklama izni verilir ama her kullanımda
doğrulama istenir.
Bu ayrım, statik sunumun tek gerçek tasarım kararıdır. Adı sabit olan bir dosyaya uzun ömür verilirse eski içerik günlerce sunulur; adı karmalı olan bir dosyaya kısa ömür verilirse hiç gerekmeyen doğrulama istekleri üretilir.
Ölçüm
Aşağıdaki betik sunucuyu ayağa kaldırır, üç davranışı sınar ve durdurur. Bağlantı noktası 8310 keyfîdir ve boş olmalıdır.
#!/usr/bin/env bash # sunucu.mjs'yi baslatir, saklama ve kosullu istek davranisini olcer, sonra durdurur. node sunucu.mjs > /dev/null & sunucu=$! sleep 1 A=http://127.0.0.1:8310 echo "--- ilk istek: karmali varlik ---" curl -sS -D - -o /dev/null "$A/katalog.7f3a91c2.css" echo "--- ilk istek: belge ---" curl -sS -D - -o /dev/null "$A/index.html" ETIKET=$(curl -sS -D - -o /dev/null "$A/index.html" | awk '/^ETag:/ {print $2}' | tr -d '\r') echo "--- kosullu istek (If-None-Match: $ETIKET) ---" curl -sS -D - -o /dev/null -H "If-None-Match: $ETIKET" "$A/index.html" echo "--- indirilen bayt: kosulsuz / kosullu ---" printf 'kosulsuz %s B\n' "$(curl -sS -o /dev/null -w '%{size_download}' "$A/index.html")" printf 'kosullu %s B\n' "$(curl -sS -o /dev/null -w '%{size_download}' -H "If-None-Match: $ETIKET" "$A/index.html")" echo "--- kok disina cikma denemesi ---" curl -sS -o /dev/null -w 'GET /../sunucu.mjs -> %{http_code}\n' --path-as-is "$A/../sunucu.mjs" kill "$sunucu"
--- ilk istek: karmali varlik --- HTTP/1.1 200 OK ETag: "6e73c0c61b184ac3" Cache-Control: public, max-age=31536000, immutable Content-Type: text/css; charset=utf-8 Content-Length: 199 Connection: keep-alive Keep-Alive: timeout=5 --- ilk istek: belge --- HTTP/1.1 200 OK ETag: "69db6e7560de7215" Cache-Control: no-cache Content-Type: text/html; charset=utf-8 Content-Length: 275 Connection: keep-alive Keep-Alive: timeout=5 --- kosullu istek (If-None-Match: "69db6e7560de7215") --- HTTP/1.1 304 Not Modified ETag: "69db6e7560de7215" Cache-Control: no-cache Content-Type: text/html; charset=utf-8 Connection: keep-alive Keep-Alive: timeout=5 --- indirilen bayt: kosulsuz / kosullu --- kosulsuz 275 B kosullu 0 B --- kok disina cikma denemesi --- GET /../sunucu.mjs -> 403
Etiket değerleri dosya içeriğinden türetilir; dosyaların birebir yukarıdaki gibi üretilmediği bir dizinde farklı çıkar. Bağlantı başlıkları çalışma zamanının varsayılan ayarına bağlıdır.
Üç sonuç okunur. Karmalı varlık ile belge farklı saklama yönergeleri almıştır. Koşullu istek 275 bayt yerine 0 bayt taşımıştır; kazanç, dosya büyüdükçe oransal olarak aynı kalır çünkü 304 yanıtının gövdesi her boyutta boştur. Kök dizinin dışını gösteren istek 403 ile karşılanmıştır: yol denetimi birleştirme sonrasında yapıldığı için sadeleştirme sonucu dışarı çıkan yol yakalanmıştır.
İçerik Dağıtımına Devretme Kararı
Statik varlıkların sunumu bir noktadan sonra uygulama sunucusunun işi olmaktan çıkar. Aynı baytları binlerce kez göndermek uygulama mantığı gerektirmez; bunu kullanıcıya daha yakın duran bir katman yapabilir. İnternet Nasıl Çalışır kursunda tanıtılan içerik dağıtım ağı (content delivery network) tam olarak bu katmandır ve uygulama sunucusu bu düzende kaynak sunucu (origin server) adını alır.
Devretme kararı iki oranla verilir. Aşağıdaki betik önce her varlığın gerçek boyutunu ve saklama yönergesini kaynak sunucudan öğrenir, sonra aynı iş yükünü önünde saklama katmanı varmış gibi hesaplar.
// devretme.mjs — ayni is yukunu once kaynak sunucuda olcer, sonra onunde // paylasimli bir saklama katmani varsayarak kaynaga kalan yuku hesaplar. const A = "http://127.0.0.1:8310"; const SAYFA = ["/index.html", "/katalog.7f3a91c2.css", "/katalog.4b8e0d15.js", "/kitaplar.json"]; const ZIYARET = 500; // ayni sayfayi acan ziyaretci sayisi // 1) Gercek boyut ve saklama yonergesini kaynaktan ogren. const varlik = new Map(); for (const yol of SAYFA) { const y = await fetch(A + yol); const govde = await y.arrayBuffer(); varlik.set(yol, { bayt: govde.byteLength, saklanabilir: (y.headers.get("cache-control") ?? "").includes("max-age"), }); } // 2) Kaynak sunucu yalniz basina: her ziyaret her varligi ondan ceker. const kaynakIstek = ZIYARET * SAYFA.length; const kaynakBayt = ZIYARET * SAYFA.reduce((t, y) => t + varlik.get(y).bayt, 0); // 3) Onunde saklama katmani: saklanabilir varlik bir kez cekilir, gerisi kenardan // karsilanir. Saklanamayan belge her ziyarette dogrulanir; yanit 304 oldugu icin // istek kaynaga ulasir ama govde tasinmaz. let kenarIstek = 0, kenarBayt = 0; for (const yol of SAYFA) { const v = varlik.get(yol); if (v.saklanabilir) { kenarIstek += 1; kenarBayt += v.bayt; } else { kenarIstek += ZIYARET; kenarBayt += v.bayt; } } const oran = (a, b) => ((1 - a / b) * 100).toFixed(1); console.log(`is yuku: ${ZIYARET} ziyaret x ${SAYFA.length} varlik`); console.log(`kaynak sunucu yalniz : ${kaynakIstek} istek, ${kaynakBayt} B`); console.log(`saklama katmani onde : ${kenarIstek} istek, ${kenarBayt} B`); console.log(`istek isabet orani : %${oran(kenarIstek, kaynakIstek)}`); console.log(`bayt isabet orani : %${oran(kenarBayt, kaynakBayt)}`);
is yuku: 500 ziyaret x 4 varlik kaynak sunucu yalniz : 2000 istek, 464500 B saklama katmani onde : 1002 istek, 929 B istek isabet orani : %49.9 bayt isabet orani : %99.8
İki oran birbirinden çok uzaktır ve bu ayrım kararın kendisidir. İstek isabet oranı (request hit ratio) kaynağa hiç ulaşmayan isteklerin payıdır; burada yaklaşık yarıdır, çünkü saklanamayan iki varlık her ziyarette doğrulanır. Bayt isabet oranı (byte hit ratio) kaynaktan hiç çıkmayan gövdelerin payıdır; burada neredeyse tamdır, çünkü doğrulama yanıtları gövdesizdir.
Karar bu yüzden sunucuda hangi kaynağın kıt olduğuna bakar. Darboğaz bant genişliğiyse devretme yükü neredeyse sıfıra indirir. Darboğaz istek başına ödenen bağlantı ve işlem maliyetiyse, kazanç ancak saklanamayan varlıkların payı kadar sınırlıdır ve iyileştirme saklama yönergelerini gözden geçirmekten geçer. Katalog verisinin de kısa ömürlü bir saklama yönergesiyle sunulması, ikinci oranı doğrudan yukarı çeker.
Özet
- Statik sunum, istek yolunun kök dizinle birleştirilmesi ve sonucun hâlâ kök dizinin altında olduğunun birleştirmeden sonra sınanmasıyla başlar.
- İçerik türü alıcının baytları nasıl yorumlayacağını, içerik uzunluğu gövdenin nerede bittiğini bildirir; uzunluk başlığı gövdesiz yanıtlarda yazılmaz.
- Varlık etiketi içerikten türetilen bir doğrulayıcıdır; koşullu istek eşleşince yanıt gövdesiz döner ve ölçümde 275 bayt yerine 0 bayt taşınır.
- Saklama yönergesi iki sınıfa ayrılır: adı içerik karması taşıyan varlık uzun ömürle saklanır, adı sabit olan belge her kullanımda doğrulanır.
- İçerik dağıtımına devretme kararı istek isabet oranı ile bayt isabet oranına bakılarak verilir; ölçümde bu iki oran %49,9 ve %99,8 çıkmış, hangi kaynağın kıt olduğuna göre farklı sonuçlara işaret etmiştir.
Sonraki Adım
Buraya kadar sunulan her şey diskte hazır duruyordu: dosya okundu, başlıklar yazıldı, baytlar gönderildi. Katalog sayfası ise içeriğini betiğe yaptırıyor — belge boş geliyor, kitap listesi tarayıcıda kuruluyor. Aynı listeyi sunucunun kendisi belgeye yazabilir. Bunun için veri ile işaretlemeyi birleştiren bir mekanizma gerekir ve bu mekanizmanın merkezinde tek bir soru vardır: veriden gelen bir metin işaretlemenin içine konurken ne olur? Sonraki ders bu birleştirmeyi kendi şablon değerlendiricisini yazarak kurar ve kaçışlamanın çıktı üzerindeki etkisini ölçer.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.