---
title: 'Statik Dosya Sunumu'
source: 'https://academia.sh/tr/kurslar/sunucu-temelleri/statik-dosya-sunumu'
course: 'Sunucu Tarafı Temelleri'
language: tr
updated: '2026-08-17T18:06:53+00:00'
license: 'CC BY-SA 4.0'
---

# 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ı.

Ö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.

```js
// 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.

```bash
#!/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.

```bash
#!/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.

```js
// 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.
