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
404ile 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-sunanbaş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
Forwardedbaş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.