İçeriğe geç
academia.sh

Ders 03 / 16

Web Sunucusu Kavramı

Sunucu tarafının üç ayrı rolü; dosyaları doğru başlıklarla sunmak, istekleri yola göre ayırmak ve isteği arka bir sunucuya devretmek ayrı süreçler olarak kurulup ölçülür.

İçindekiler

Önceki iki derste “sunucu” tek bir program gibi ele alındı: bağlantı noktasını dinleyen, isteği okuyan, kuralı uygulayan ve yanıtı yazan tek süreç. Kütüphane uygulamasının gerçek trafiği ise türdeş değildir. Aynı bağlantı noktasına hem /stil.css isteği hem /api/kitaplar isteği gelir; birincisi diskteki bir dosyanın olduğu gibi verilmesidir, ikincisi bir hesabın sonucudur.

Bu ders o ayrımı üç role böler ve her rolü ayrı bir süreç olarak çalıştırır: statik sunum, yönlendirme ve vekillik. Roller ayrı ayrı kurulduğunda hangi işin hangi bileşene ait olduğu görünür hâle gelir; tek bir programda toplandıklarında da aynı roller vardır, yalnız sınırları görünmez.

Statik Sunum: Dosyayı Bulmak ve Doğru Başlıkla Vermek

Statik sunum, bir isteğin yolunu dosya sistemindeki bir dosyaya eşleyip içeriği olduğu gibi göndermektir. Görünüşte tek satırlık bir iştir; sorumluluklar sayıldığında dört tanedir.

// statik-sunucu.mjs — varliklar/ dizinindeki dosyalari sunar, disina cikilmasini engeller
import { createServer } from "node:http";
import { readFile } from "node:fs/promises";
import { join, normalize, extname, sep } from "node:path";

const KOK = join(process.cwd(), "varliklar");
const TIPLER = { ".html": "text/html; charset=utf-8", ".css": "text/css; charset=utf-8",
  ".js": "text/javascript; charset=utf-8", ".json": "application/json; charset=utf-8" };

createServer(async (istek, yanit) => {
  yanit.sendDate = false;
  yanit.setHeader("x-sunan", "statik");
  const yol = decodeURIComponent(new URL(istek.url, "http://yerel").pathname);
  const tam = normalize(join(KOK, yol === "/" ? "/index.html" : yol));

  if (!tam.startsWith(KOK + sep)) {                       // kok disina cikan yol reddedilir
    return yanit.writeHead(403, { "content-type": "text/plain" }).end("kok disi\n");
  }
  try {
    const icerik = await readFile(tam);
    yanit.writeHead(200, {
      "content-type": TIPLER[extname(tam)] ?? "application/octet-stream",
      "content-length": icerik.length,
      "cache-control": "public, max-age=31536000, immutable",
    }).end(icerik);
  } catch {
    yanit.writeHead(404, { "content-type": "text/plain" }).end("bulunamadi\n");
  }
}).listen(8427, "127.0.0.1", () => console.log("statik 127.0.0.1:8427"));

Örneği çalıştırmak için sunucu dosyasının yanında bir varliklar dizini ve içinde iki dosya bulunmalıdır:

mkdir -p varliklar
printf 'body { font-family: system-ui; }\n.kitap { padding: 8px; }\n' > varliklar/stil.css
printf '<!doctype html>\n<html lang="tr"><head><meta charset="utf-8"><title>Kutuphane</title>\n<link rel="stylesheet" href="/stil.css"></head>\n<body><h1>Kutuphane Odunc Servisi</h1></body></html>\n' > varliklar/index.html
#!/usr/bin/env bash
# statik-sunucu.mjs'yi baslatir, dort istegi dener, durdurur. varliklar/ dizini yaninda olmalidir.
node statik-sunucu.mjs & sunucu=$!
sleep 0.5
for yol in / /stil.css /yok.css '/%2e%2e%2f%2e%2e%2fetc%2fhosts'; do
  printf '%-32s -> ' "$yol"
  curl -sS -o /dev/null --path-as-is -w '%{http_code}  tip=%{content_type}  boy=%{size_download} B\n' \
    "http://127.0.0.1:8427$yol"
done
echo "--- yanit basliklari ---"
curl -sS -o /dev/null -D - --path-as-is http://127.0.0.1:8427/stil.css
kill "$sunucu"
statik 127.0.0.1:8427
/                                -> 200  tip=text/html; charset=utf-8  boy=186 B
/stil.css                        -> 200  tip=text/css; charset=utf-8  boy=58 B
/yok.css                         -> 404  tip=text/plain  boy=11 B
/%2e%2e%2f%2e%2e%2fetc%2fhosts   -> 403  tip=text/plain  boy=9 B
--- yanit basliklari ---
HTTP/1.1 200 OK
x-sunan: statik
content-type: text/css; charset=utf-8
content-length: 58
cache-control: public, max-age=31536000, immutable
Connection: keep-alive
Keep-Alive: timeout=5

Dört sorumluluk çıktıda ayrı ayrı görünür. Yol eşleme: / isteği dizin girişine karşılık gelir. İçerik tipi: aynı sunucu iki farklı tip üretmiştir; tip yanlış olduğunda tarayıcı dosyayı yorumlayamaz ve stil uygulanmaz. Bulunamayan kaynak: eksik dosya bir çökme değil, 404 durum kodudur. Kök sınırı: son istek 403 almıştır.

Son satır ayrıca bir uyarı taşır. new URL() yapısı yol üzerindeki .. bileşenlerini kendi kendine sadeleştirir; bu yüzden düz bir /../../etc/hosts isteği sunucuya ulaşmadan temizlenir. Kodlanmış biçim (%2e%2e%2f) ise sadeleşmeden geçer ve decodeURIComponent sonrası dizin dışına çıkan bir yola dönüşür. Denetimin çözümlenmiş mutlak yol üzerinde, kök dizinle karşılaştırılarak yapılmasının nedeni budur: yolun metin hâline bakan bir denetim, kodlamanın her biçimini önceden saymak zorunda kalır.

Yanıttaki cache-control başlığı, statik sunumun ayırt edici özelliğini taşır: içerik isteğe göre değişmediği için uzun süreli saklanabilir. Bu başlığın anlamı ve ödünleşimleri Tarayıcı ve Web Platformu kursunda kurulmuştu; burada önemli olan, aynı başlığın dinamik bir yanıta konulamayacağıdır.

Yönlendirme ve Vekillik: İsteği Doğru Bileşene Götürmek

Statik sunucu /api/kitaplar isteğine ne yapacağını bilmez; ödünç uygulaması da CSS dosyasını okumakla uğraşmamalıdır. Aradaki iş bölümünü kuran bileşen ters vekildir (reverse proxy): istemciden gelen isteği kabul eder, bir kurala göre arka sunuculardan birine iletir ve gelen yanıtı istemciye geri yazar.

// uygulama-sunucu.mjs — odunc uygulamasi; istegin kendisine hangi baglantidan geldigini bildirir
import { createServer } from "node:http";

const KITAPLAR = [{ isbn: "978-0262033848", ad: "Algoritmalara Giris", raf: "R-12" }];

createServer((istek, yanit) => {
  yanit.sendDate = false;
  yanit.setHeader("x-sunan", "uygulama");
  yanit.setHeader("content-type", "application/json; charset=utf-8");
  yanit.writeHead(200).end(JSON.stringify({
    yol: istek.url,
    kitaplar: KITAPLAR,
    baglantiSahibi: `${istek.socket.remoteAddress}:${istek.socket.remotePort}`,
    forwarded: istek.headers.forwarded ?? null,
  }));
}).listen(8428, "127.0.0.1", () => console.log("uygulama 127.0.0.1:8428"));
// vekil.mjs — one duran ters vekil: yola gore iki arka sunucudan birine iletir
import { createServer, request } from "node:http";

const HEDEF = (yol) => (yol.startsWith("/api/") ? 8428 : 8427);

createServer((istekDisi, yanitDisi) => {
  yanitDisi.sendDate = false;
  const kaynak = istekDisi.socket.remoteAddress;
  const basliklar = { ...istekDisi.headers, forwarded: `for="${kaynak}";proto=http` };

  const istekIc = request(
    { host: "127.0.0.1", port: HEDEF(istekDisi.url), path: istekDisi.url,
      method: istekDisi.method, headers: basliklar },
    (yanitIc) => {
      yanitDisi.writeHead(yanitIc.statusCode, { ...yanitIc.headers, "x-vekil": "gecti" });
      yanitIc.pipe(yanitDisi);
    },
  );
  istekIc.on("error", () => yanitDisi.writeHead(502).end("arka sunucuya ulasilamadi\n"));
  istekDisi.pipe(istekIc);
}).listen(8429, "127.0.0.1", () => console.log("vekil 127.0.0.1:8429"));
#!/usr/bin/env bash
# Uc sunucuyu birlikte baslatir: statik (8427), uygulama (8428), vekil (8429).
node statik-sunucu.mjs & s1=$!
node uygulama-sunucu.mjs & s2=$!
node vekil.mjs & s3=$!
sleep 0.6

echo "--- vekil uzerinden: hangi arka sunucu yanitladi ---"
for yol in /stil.css /api/kitaplar; do
  printf '%-16s -> ' "$yol"
  curl -sS -o /dev/null -D - "http://127.0.0.1:8429$yol" | grep -iE '^(HTTP|x-sunan|x-vekil)' | tr -d '\r' | paste -sd' ' -
done

echo "--- uygulamanin gordugu baglanti: dogrudan istek ve vekil uzerinden istek ---"
printf 'dogrudan : '; curl -sS http://127.0.0.1:8428/api/kitaplar | tr ',' '\n' | grep -E 'baglantiSahibi|forwarded' | paste -sd' ' -
printf 'vekille  : '; curl -sS http://127.0.0.1:8429/api/kitaplar | tr ',' '\n' | grep -E 'baglantiSahibi|forwarded' | paste -sd' ' -

echo "--- ek atlamanin suresi (ayni yanit, iki yol) ---"
curl -sS -o /dev/null -w 'dogrudan : %{time_total} s\n' http://127.0.0.1:8428/api/kitaplar
curl -sS -o /dev/null -w 'vekille  : %{time_total} s\n' http://127.0.0.1:8429/api/kitaplar

kill "$s1" "$s2" "$s3"
uygulama 127.0.0.1:8428
vekil 127.0.0.1:8429
statik 127.0.0.1:8427
--- vekil uzerinden: hangi arka sunucu yanitladi ---
/stil.css        -> HTTP/1.1 200 OK x-sunan: statik x-vekil: gecti
/api/kitaplar    -> HTTP/1.1 200 OK x-sunan: uygulama x-vekil: gecti
--- uygulamanin gordugu baglanti: dogrudan istek ve vekil uzerinden istek ---
dogrudan : "baglantiSahibi":"127.0.0.1:57388" "forwarded":null}
vekille  : "baglantiSahibi":"127.0.0.1:57387" "forwarded":"for=\"127.0.0.1\";proto=http"}
--- ek atlamanin suresi (ayni yanit, iki yol) ---
dogrudan : 0.000551 s
vekille  : 0.000962 s

Üç sunucunun açılış satırlarının çıktıdaki sırası çalıştırmadan çalıştırmaya değişir; geçici bağlantı noktası numaraları ve süre alanları da her çalıştırmada farklıdır.

İstemci tek bir adrese, 8429’a istek yapmıştır. x-sunan başlığı iki isteğin iki ayrı programdan geldiğini gösterir. İstemci açısından bu ayrım görünmez: tek köken, tek adres, tek yapılandırma. Yönlendirme kuralı burada tek satırdır — yol /api/ ile başlıyorsa uygulamaya, başlamıyorsa statik sunucuya.

Vekilin Getirdiği ve Götürdüğü

Vekil ücretsiz değildir. Ölçümdeki iki süre, aynı yanıt için doğrudan bağlantı ile vekil üzerinden geçen bağlantıyı karşılaştırır: vekilli yol daha uzundur, çünkü ikinci bir TCP bağlantısı kurulur, istek yeniden yazılır ve yanıt yeniden aktarılır. Geri döngü arayüzünde bu fark küçüktür; gerçek bir dağıtımda vekil ile arka sunucu arasındaki ağ da hesaba katılır.

Karşılığında birkaç iş tek noktada toplanır. Şifreli bağlantı önde sonlandırılabilir; arka sunucular düz HTTP konuşur ve sertifika yönetimi tek bileşene iner. Aynı yol kuralı birden çok arka örneğe dağıtım yapabilir: hedef seçimi tek bir bağlantı noktası yerine bir liste üzerinden yapıldığında vekil bir yük dağıtıcıya dönüşür. Statik varlıklar uygulama sürecine hiç uğramaz, bu yüzden uygulamanın eşzamanlılık bütçesi yalnız hesaplı isteklere harcanır.

Bedelin bir kısmı ise bilgi kaybıdır ve çıktıda görünür. Uygulama, isteği kimin açtığını istek.socket üzerinden okur; vekil devreye girdiğinde bu bağlantıyı açan taraf vekildir, istemci değil. Ölçüm aynı makinede yapıldığı için iki satırdaki adres de 127.0.0.1’dir; ayırt edici olan, bağlantının sahibinin değişmiş olmasıdır. İstemcinin kim olduğu bilgisi ancak vekilin eklediği Forwarded başlığıyla taşınır ve çıktıda yalnız vekilli satırda doludur.

Bu başlık bir iddiadır, ölçüm değil. İstemci de aynı başlığı gönderebilir. Bu yüzden uygulama, Forwarded başlığını yalnız bilinen bir vekilden geldiğini doğruladığı bağlantılarda dikkate alır; doğrudan erişime açık bir uygulamada başlık yok sayılır. Aynı akıl yürütme ikinci derste kurulmuştu: istemciden gelen hiçbir değer, sunucunun kendi denetiminde olmadığı sürece karar sayılmaz.

Roller mi, Programlar mı?

Üç dosya üç ayrı süreç başlattı, ama bu bir zorunluluk değil bir gösterim tercihidir. Aynı üç rol tek bir programda da bulunabilir: uygulama, /api/ ile başlamayan yolları kendi dosya okuma dalına yönlendirebilir. Ayrımın kalıcı olan yanı programların sayısı değil, sorumlulukların içeriğidir.

  • Web sunucusu rolü: bağlantıyı kabul eder, HTTP iletisini ayrıştırır, statik içeriği doğrudan sunar, yolları hedeflere eşler. Bu rolün bilmediği şey iş kurallarıdır.
  • Uygulama sunucusu rolü: isteği bir işleyiciye taşır, iş kuralını çalıştırır, yanıt gövdesini üretir. Bu rolün bilmediği şey dosya sisteminin düzenidir.
  • Vekillik rolü: iki tarafı birbirine bağlar, kaynak bilgisini taşır, hedefler arasında dağıtım yapar, arka sunucu erişilemediğinde 502 üretir.

Rolleri ayırmanın pratik sonucu, her birinin ayrı ölçeklenebilmesidir: statik trafiği artan bir sistem statik tarafı çoğaltır, hesaplı istekleri artan bir sistem uygulama tarafını. Rollerin tek bir programda toplandığı bir kurulumda bu iki ihtiyaç birbirine bağlanır ve ikisi birden çoğaltılır.

Özet

  • Statik sunum dört sorumluluk taşır: yolun dosyaya eşlenmesi, doğru içerik tipi, eksik dosyanın 404 ile karşılanması ve kök dizin sınırının çözümlenmiş mutlak yol üzerinden denetlenmesi.
  • Adres çözümlemesi .. bileşenlerini sadeleştirdiği için yol denetimi metin üzerinde değil, birleştirilmiş ve normalleştirilmiş yolun kök dizinle karşılaştırılmasıyla yapılır.
  • Ters vekil, tek bir adrese gelen istekleri bir kurala göre arka sunuculara dağıtır; ölçümde x-sunan başlığı aynı adrese yapılan iki isteğin iki ayrı programdan geldiğini gösterir.
  • Vekil bir atlama ekler ve bağlantının sahibini değiştirir; istemcinin kimliği ancak Forwarded başlığıyla taşınır ve bu başlık yalnız bilinen bir vekilden geldiğinde dikkate alınır.
  • Web sunucusu, uygulama sunucusu ve vekillik birer roldür; ayrı süreçlere bölünmeleri zorunlu değildir, ama ayrı ölçeklenebilmelerini bu bölünme sağlar.

Sonraki Adım

Vekilin arkasındaki uygulama sunucusu bu derste tek bir süreç olarak çalıştı ve istekleri sırayla karşıladı. Bir istek uzun sürdüğünde diğerlerine ne olur? Aynı uygulamanın iki kopyası aynı bağlantı noktasını nasıl paylaşır ve istekler aralarında nasıl dağılır? Ölçekleme kararının birimi süreç müdür, iş parçacığı mıdır, yoksa başka bir şey midir? Sonraki ders uygulamanın altındaki çalışma zamanına iner: olay döngüsünün tek bir isteği nasıl beklettiğini ölçer, sonra aynı bağlantı noktasını paylaşan işçi süreçler başlatıp isteklerin dağılımını sayar.

İ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