---
title: 'İstemci ve Sunucu Sorumluluk Sınırı'
source: 'https://academia.sh/tr/kurslar/sunucu-temelleri/istemci-ve-sunucu-sorumluluk-siniri'
course: 'Sunucu Tarafı Temelleri'
language: tr
updated: '2026-08-17T18:06:52+00:00'
license: 'CC BY-SA 4.0'
---

# İstemci ve Sunucu Sorumluluk Sınırı

Aynı hesabın iki tarafta da yapılabildiği durumlarda seçimi belirleyen ölçütler; taşınan bayt, ağ turu sayısı ve kararın bağlayıcılığı ölçülerek karşılaştırılır.

Önceki ders doğruluk kaynağının sunucu olduğunu üç sorumluluk üzerinden kurdu: kalıcılık,
iş kuralları ve tümleştirme. Bu, her hesabın sunucuda yapılacağı anlamına gelmez. Kütüphane
uygulamasında pek çok işlem — bir listenin süzülmesi, bir tutarın hesaplanması, bir arama
teriminin düzeltilmesi — iki tarafta da yazılabilir ve ikisi de doğru çalışır.

Bu ders sınırın nereden geçtiğini üç ölçüyle inceler: taşınan bayt, ağ turu sayısı ve
kararın bağlayıcılığı. İlk ikisi ölçülür, üçüncüsü ölçülemez ama en belirleyicisidir.

## Aynı Sonuç, İki Yol: Taşınan Veri

Kütüphanenin kataloğu beş bin kitap içeriyor. Kullanıcı "ağ" konusundaki kitapların ilk
sayfasını görmek istiyor. İki yol vardır: sunucu kataloğun tamamını gönderir ve istemci
süzer; ya da sunucu süzer, sayfalar ve yalnız görüntülenecek dilimi gönderir. Aşağıdaki
sunucu ikisini de sunar.

```js
// katalog-sunucusu.mjs — ayni katalogu iki bicimde sunar: tamami ve suzulmus sayfa
import { createServer } from "node:http";

const KONULAR = ["algoritma", "veritabani", "ag", "isletim-sistemi", "derleyici"];
const KATALOG = Array.from({ length: 5000 }, (_, i) => ({
  isbn: `978-0${(1000000 + i).toString()}`,
  ad: `Kitap ${i + 1}: ${KONULAR[i % 5]} uzerine inceleme`,
  konu: KONULAR[i % 5],
  yil: 1970 + (i % 55),
  raf: `R-${Math.floor(i / 50) + 1}`,
}));

createServer((istek, yanit) => {
  yanit.sendDate = false;
  const adres = new URL(istek.url, "http://yerel");
  yanit.setHeader("content-type", "application/json; charset=utf-8");

  if (adres.pathname === "/kitaplar/tumu") {
    return yanit.writeHead(200).end(JSON.stringify(KATALOG));
  }
  if (adres.pathname === "/kitaplar") {
    const konu = adres.searchParams.get("konu");
    const sayfa = Number(adres.searchParams.get("sayfa") ?? 1);
    const boy = 20;
    const suzulmus = konu ? KATALOG.filter((k) => k.konu === konu) : KATALOG;
    const dilim = suzulmus.slice((sayfa - 1) * boy, sayfa * boy);
    return yanit.writeHead(200).end(JSON.stringify({ toplam: suzulmus.length, sayfa, kitaplar: dilim }));
  }
  yanit.writeHead(404).end(JSON.stringify({ hata: "yol_yok" }));
}).listen(8424, "127.0.0.1", () => console.log("katalog 127.0.0.1:8424"));
```

```bash
#!/usr/bin/env bash
# Ayni sonucu ureten iki yaklasimin tasidigi baytlari ve sureyi olcer.
node katalog-sunucusu.mjs & sunucu=$!
sleep 0.5
curl -sS -o /dev/null 'http://127.0.0.1:8424/kitaplar?konu=ag'   # isinma

BICIM='  govde %{size_download} B   ilk bayt %{time_starttransfer} s\n'
echo "--- tum katalog istemciye tasiniyor (suzme istemcide) ---"
curl -sS -o /dev/null -w "$BICIM" 'http://127.0.0.1:8424/kitaplar/tumu'
echo "--- suzme ve sayfalama sunucuda ---"
curl -sS -o /dev/null -w "$BICIM" 'http://127.0.0.1:8424/kitaplar?konu=ag&sayfa=1'

echo "--- iki yanitin ilk satirlari ---"
curl -sS 'http://127.0.0.1:8424/kitaplar?konu=ag&sayfa=1' | head -c 220; echo
kill "$sunucu"
```

```
katalog 127.0.0.1:8424
--- tum katalog istemciye tasiniyor (suzme istemcide) ---
  govde 563494 B   ilk bayt 0.001898 s
--- suzme ve sayfalama sunucuda ---
  govde 1956 B   ilk bayt 0.000569 s
--- iki yanitin ilk satirlari ---
{"toplam":1000,"sayfa":1,"kitaplar":[{"isbn":"978-01000002","ad":"Kitap 3: ag uzerine inceleme","konu":"ag","yil":1972,"raf":"R-1"},{"isbn":"978-01000007","ad":"Kitap 8: ag uzerine inceleme","konu":"ag","yil":1977,"raf":
```

Süre alanları makineye ve o andaki yüke bağlıdır; her çalıştırmada değişir. Sabit olan
bayt oranıdır: 563 494 bayta karşılık 1 956 bayt, yaklaşık 288 kat. Kullanıcı ekranda
yirmi satır görecektir; ilk yolda bu yirmi satır için beş bin kaydın tamamı taşınmıştır.

Fark yalnız bant genişliği değildir. Kataloğun tamamı istemciye indiğinde bellekte de
tutulur, ayrıştırılır ve her süzme işleminde yeniden gezilir; katalog büyüdükçe bu maliyet
istemcinin en zayıf aygıtında ödenir. Buna karşılık ikinci yolda her süzme değişikliği yeni
bir istek demektir: kullanıcı konu değiştirdiğinde ağa çıkılır, sonuç beklenir.

Ölçüt bu iki maliyet arasındaki orandır. Veri kümesi ekrana sığmayacak kadar büyükse
süzme sunucuya aittir. Veri kümesi küçük ve zaten indirilmişse — örneğin bir üyenin altı
açık ödüncü — aynı listeyi tekrar tekrar sunucudan istemek gereksiz turlar üretir.

## Ölçümün Karar Veremediği Kalem: Bağlayıcılık

Yukarıdaki karar başarım üzerinden verilebilir; her iki seçenek de doğru sonucu üretir.
Bazı hesaplarda ise seçenek yoktur ve gerekçe ölçüme dayanmaz.

Gecikme cezası böyle bir hesaptır. Ödünç süresi on dört gündür, gecikilen her gün için
birim ücret alınır. Hesap iki çarpma ve bir çıkarmadır; istemci de yapabilir. Aşağıdaki
sunucu istemcinin gönderdiği tutarı ve istemcinin bildirdiği saati okumaz.

```js
// iade-sunucusu.mjs — gecikme cezasini istemcinin bildirdigine bakmadan kendisi hesaplar
import { createServer } from "node:http";

const GUN = 86_400_000;
const SURE_GUN = 14;        // odunc suresi
const GUNLUK_CEZA = 2.5;    // gecikme basina birim ucret

// Tohum veri: sunucu ayaga kalkarken 20 gun once verilmis bir odunc olusturulur.
const ODUNCLER = new Map([["U-1002:978-0201896831", { verilis: Date.now() - 20 * GUN }]]);

const govdeOku = (istek) =>
  new Promise((coz, red) => {
    let veri = "";
    istek.on("data", (parca) => (veri += parca));
    istek.on("end", () => { try { coz(veri ? JSON.parse(veri) : {}); } catch (h) { red(h); } });
  });

createServer(async (istek, yanit) => {
  yanit.sendDate = false;
  yanit.setHeader("content-type", "application/json; charset=utf-8");
  const govde = await govdeOku(istek);                 // govde.tutar ve govde.simdi okunmaz
  const kayit = ODUNCLER.get(`${govde.uye}:${govde.isbn}`);
  if (!kayit) return yanit.writeHead(404).end(JSON.stringify({ hata: "odunc_yok" }));

  const sonGun = kayit.verilis + SURE_GUN * GUN;
  const gecikme = Math.max(0, Math.floor((Date.now() - sonGun) / GUN));
  const tutar = Number((gecikme * GUNLUK_CEZA).toFixed(2));
  yanit.writeHead(200).end(JSON.stringify({
    uye: govde.uye, gecikmeGun: gecikme, tutar,
    istemcininBildirdigi: govde.tutar ?? null,
  }));
}).listen(8425, "127.0.0.1", () => console.log("iade 127.0.0.1:8425"));
```

```bash
#!/usr/bin/env bash
# Istemcinin bildirdigi tutar ve saat degistiginde sunucunun yaniti degisiyor mu?
node iade-sunucusu.mjs & sunucu=$!
sleep 0.5

gonder() {
  printf '%-62s\n  -> ' "$1"
  curl -sS -w '\n' -X POST -H 'content-type: application/json' -d "$1" http://127.0.0.1:8425/iade
}

gonder '{"uye":"U-1002","isbn":"978-0201896831"}'
gonder '{"uye":"U-1002","isbn":"978-0201896831","tutar":0}'
gonder '{"uye":"U-1002","isbn":"978-0201896831","tutar":0,"simdi":"2020-01-01T00:00:00Z"}'
kill "$sunucu"
```

```
iade 127.0.0.1:8425
{"uye":"U-1002","isbn":"978-0201896831"}                      
  -> {"uye":"U-1002","gecikmeGun":6,"tutar":15,"istemcininBildirdigi":null}
{"uye":"U-1002","isbn":"978-0201896831","tutar":0}            
  -> {"uye":"U-1002","gecikmeGun":6,"tutar":15,"istemcininBildirdigi":0}
{"uye":"U-1002","isbn":"978-0201896831","tutar":0,"simdi":"2020-01-01T00:00:00Z"}
  -> {"uye":"U-1002","gecikmeGun":6,"tutar":15,"istemcininBildirdigi":0}
```

Üç istek de aynı sonucu almıştır: altı gün gecikme, on beş birim ücret. İkinci istek sıfır
tutar bildirmiştir, üçüncüsü ayrıca kendi saatini altı yıl geriye almıştır. Sunucunun
yanıtı değişmemiştir, çünkü sunucu iki girdiyi de yalnız kayda geçirmiş, hesabı kendi
verisinden ve kendi saatinden yapmıştır.

Buradaki ilke bir başarım ilkesi değil, bir **güven sınırı** (trust boundary) ilkesidir:
sunucu, kendi denetimindeki verilerden türetemediği hiçbir değeri bağlayıcı saymaz. Bu
listeye istemciden gelen tutarlar, saat bilgisi, kullanıcı kimliği iddiaları, fiyatlar ve
yetki bilgileri girer. İstemci bunların hepsini gönderebilir; sunucu hepsini **girdi**
olarak alır, hiçbirini **karar** olarak almaz.

İstemcinin aynı hesabı yapması yine de yararlıdır. Üye iade ekranını açtığında tutarı
anında görür, sürpriz yaşamaz. Bu hesap bir tahmindir ve sunucunun hesabıyla çeliştiğinde
sunucununki geçerlidir. Aynı kural birinci derste ödünç sınırında görülmüştü: istemci
gösterir, sunucu bağlar.

## Ters Yön: Sunucuya Taşınmaması Gereken Hesap

Sınır tek yönlü değildir. Bir hesabı gereksiz yere sunucuya taşımak da bir tasarım
hatasıdır ve bedeli tur sayısıyla ölçülür.

Katalog arama kutusunda kullanıcı yazdıkça arama terimi düzeltilir: baştaki ve sondaki
boşluklar atılır, küçük harfe çevrilir, iç boşluklar teke indirilir. Türkçe'de küçük harfe
çevirme yerel ayara duyarlıdır ve bu düzeltmenin sunucuda yapılması için bir gerekçe gibi
görünebilir. Aşağıdaki ölçüm bedeli gösterir.

```js
// tur-maliyeti.mjs — arama kutusundaki her tus icin sunucuya gitmenin bedeli
import { createServer } from "node:http";

const TUSLAR = ["a", "al", "alg", "algo", "algor", "algori", "algorit", "algoritma"];
const duzelt = (metin) => metin.trim().toLocaleLowerCase("tr").replace(/\s+/g, " ");

const sunucu = createServer((istek, yanit) => {
  yanit.sendDate = false;
  const q = new URL(istek.url, "http://yerel").searchParams.get("q") ?? "";
  yanit.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify({ q: duzelt(q) }));
});

await new Promise((coz) => sunucu.listen(8426, "127.0.0.1", coz));
await fetch("http://127.0.0.1:8426/duzelt?q=isinma");            // isinma turu

const t0 = performance.now();
for (const tus of TUSLAR) await (await fetch(`http://127.0.0.1:8426/duzelt?q=${tus}`)).json();
const sunucuda = performance.now() - t0;

const t1 = performance.now();
for (const tus of TUSLAR) duzelt(tus);
const istemcide = performance.now() - t1;

sunucu.close();

console.log(`tus sayisi: ${TUSLAR.length}`);
console.log(`istemcide duzeltme  : ${istemcide.toFixed(3)} ms`);
console.log(`sunucuda duzeltme   : ${sunucuda.toFixed(3)} ms  (geri dongu arayuzunde)`);
console.log("");
console.log("ag turu eklendiginde toplam bekleme (tus basina bir tur):");
console.log("  " + "tur".padEnd(10) + "sunucuda".padStart(12) + "istemcide".padStart(12));
for (const tur of [0, 20, 60, 150]) {
  const s = sunucuda + TUSLAR.length * tur;
  console.log("  " + `${tur} ms`.padEnd(10) + `${s.toFixed(1)} ms`.padStart(12) +
    `${istemcide.toFixed(1)} ms`.padStart(12));
}
```

```
tus sayisi: 8
istemcide duzeltme  : 0.007 ms
sunucuda duzeltme   : 12.076 ms  (geri dongu arayuzunde)

ag turu eklendiginde toplam bekleme (tus basina bir tur):
  tur           sunucuda   istemcide
  0 ms           12.1 ms      0.0 ms
  20 ms         172.1 ms      0.0 ms
  60 ms         492.1 ms      0.0 ms
  150 ms       1212.1 ms      0.0 ms
```

Süre alanları makineye bağlıdır; oran korunur. Geri döngü arayüzünde bile fark üç
mertebedir, çünkü yerel bir fonksiyon çağrısı ile bir HTTP isteği aynı büyüklükte işler
değildir. Tablo, ağ turu eklendiğinde farkın nasıl büyüdüğünü gösterir: seksen milisaniyelik
bir turda sekiz tuş vuruşu neredeyse bir saniye bekleme üretir.

Bu hesabın sunucuya taşınmasında bir kazanç yoktur, çünkü sonuç bağlayıcı değildir: arama
terimini düzeltmek yanlış yapıldığında veriyi bozmaz, yalnız arama sonucunu değiştirir. Ve
sunucu zaten kendi tarafında aynı düzeltmeyi yapmak zorundadır — gelen terimi olduğu gibi
sorguya vermeyecektir. Yani düzeltme iki tarafta da vardır; istemcideki tepkiyi hızlandırır,
sunucudaki doğruluğu güvence altına alır. **Aynı hesabın iki tarafta da bulunması bir
yineleme değil, iki farklı görevin gereğidir.**

## Sınırı Çizen Üç Soru

Yukarıdaki üç ölçüm tek bir karar akışına indirgenebilir. Bir hesabın hangi tarafa ait
olduğu şu sırayla sorulur:

**Hesap bağlayıcı mı?** Yanlış sonuç veriye ya da paraya yazılıyorsa hesap sunucudadır ve
tartışma biter. İstemcide aynı hesabın bir kopyası bulunabilir, ama yalnız gösterim için.

**Girdi nerede duruyor?** Hesabın ihtiyaç duyduğu veri sunucudaysa ve büyükse, hesabı veriye
götürmek veriyi hesaba götürmekten ucuzdur — kataloğun süzülmesi budur. Veri zaten
istemciye indirilmişse tersi geçerlidir.

**Kaç kez yapılıyor?** Kullanıcı etkileşimi başına birden çok kez tekrarlanan bir hesap
sunucuya taşındığında maliyet tur sayısıyla çarpılır. Tuş başına bir istek, saniyede birkaç
tur demektir.

Üç soru çelişebilir. Bir arama kutusu hem sık tekrarlanır hem de girdisi sunucudadır: bu
durumda hesabın kendisi sunucuda kalır, ama istek sıklığı gecikmeli tetikleme ile düşürülür.
Çelişki, sorulardan birinin iptal edilmesiyle değil, iki maliyetin birlikte azaltılmasıyla
çözülür.

## Özet

- Aynı sonucu üreten iki yerleşim arasındaki seçim ölçülebilir: beş bin kayıtlık katalogda
  süzmenin sunucuya alınması taşınan gövdeyi 563 494 bayttan 1 956 bayta indirmiştir.
- Süzmenin yeri veri kümesinin büyüklüğüne bağlıdır; küçük ve zaten indirilmiş bir liste
  için her etkileşimde sunucuya gitmek gereksiz tur üretir.
- Bağlayıcı hesaplar ölçümle seçilmez: sunucu, kendi verisinden ve kendi saatinden
  türetemediği hiçbir değeri karar olarak almaz; istemcinin bildirdiği tutar ve saat
  yanıtı değiştirmemiştir.
- Sunum hesabını sunucuya taşımanın bedeli tur sayısıyla çarpılır; sekiz tuş vuruşu için
  ölçülen fark geri döngüde üç mertebedir, gerçek bir turda saniyelere çıkar.
- Karar üç soruyla verilir: hesap bağlayıcı mı, girdi nerede duruyor, kaç kez tekrarlanıyor.

## Sonraki Adım

Buraya kadar "sunucu" tek bir program gibi anıldı: bağlantı noktasını dinleyen, isteği
okuyan, kuralı uygulayan ve yanıtı yazan tek bir süreç. Gerçek dağıtımlarda bu iş birden
çok bileşene bölünür. Kütüphane uygulamasının kapak görselleri ile arayüz dosyalarını sunan
bileşenin, ödünç kuralını çalıştıran bileşenle aynı olması gerekmez; hatta bir bileşenin
öndeki tek görevi isteği doğru yere iletmek olabilir. Sonraki ders bu iş bölümünü kurar:
statik dosyaları sunmak, istekleri yola göre ayırmak ve bir isteği başka bir sunucuya
devretmek ayrı ayrı çalıştırılarak gösterilir.
