Ders 10 / 20
HTTP Sunucusu
İstek ve yanıt nesnelerinin akış karşılığı, yol ve yöntem üzerinden dağıtım, gövde okumada boyut sınırı, uzunluk bildirimi ile parçalı aktarım arasındaki seçim.
İçindekiler
İnternet Nasıl Çalışır kursu bir HTTP isteğini satır satır çözümledi: istek satırı, başlıklar, boş satır, gövde. Orada bu metni okuyan taraf bir örnek programdı ve protokolün ayrıştırılması gösterilmemişti.
Bu ders o işi yerleşik modüle devreder ve ölçüm toplayıcıyı bir hizmete dönüştürür. Anahtar gözlem şudur: istek nesnesi okunabilir, yanıt nesnesi yazılabilir bir akıştır. Akışlar dersinde kurulan her şey burada doğrudan geçerlidir.
Özet Birikimini Ayrı Modüle Almak
Hizmet kodunu yazmadan önce, kayıt doğrulama ve özet birikimi ayrı bir modüle alınmalıdır. Ağdan gelen veriyle dosyadan gelen veri aynı yoldan geçmeli ve bu yol sunucudan bağımsız sınanabilmelidir.
// ozetleyici.mjs export function satirdanKayit(satir) { const kayit = JSON.parse(satir); if (typeof kayit.dugum !== 'string' || typeof kayit.metrik !== 'string') { throw new TypeError('dugum ve metrik dizgi olmali'); } if (!Number.isFinite(kayit.deger)) { throw new TypeError('deger sonlu bir sayi olmali'); } return kayit; } export class Ozetleyici { #kovalar = new Map(); ekle({ dugum, metrik, deger }) { const anahtar = `${dugum}/${metrik}`; const kova = this.#kovalar.get(anahtar) ?? { sayi: 0, toplam: 0, enBuyuk: -Infinity }; kova.sayi += 1; kova.toplam += deger; kova.enBuyuk = Math.max(kova.enBuyuk, deger); this.#kovalar.set(anahtar, kova); } ozet() { return [...this.#kovalar].map(([anahtar, { sayi, toplam, enBuyuk }]) => ({ anahtar, sayi, ortalama: Number((toplam / sayi).toFixed(2)), enBuyuk, })); } }
Doğrulama fonksiyonu yalnızca ayrıştırmakla kalmaz, alan türlerini de sınar. Ağdan gelen
veriye güvenilmez: deger alanı dizgi geldiğinde toplama işlemi sessizce birleştirmeye
döner ve özet anlamsızlaşır.
Sunucu ve İşleyici
node:http modülündeki createServer, her istek için çağrılan bir işleyici alır.
İşleyicinin iki argümanı vardır ve her ikisi de akıştır.
// hizmet.mjs import { createServer } from 'node:http'; import { createReadStream } from 'node:fs'; import { createInterface } from 'node:readline'; import { pipeline } from 'node:stream/promises'; import { Ozetleyici, satirdanKayit } from './ozetleyici.mjs'; const ozetleyici = new Ozetleyici(); // Acilista dosyayi akisla okuyup kovalari doldur async function baslangicVerisiniYukle(dosya) { const satirlar = createInterface({ input: createReadStream(dosya), crlfDelay: Infinity, }); let sayac = 0; for await (const satir of satirlar) { if (satir.trim() === '') continue; ozetleyici.ekle(satirdanKayit(satir)); sayac += 1; } return sayac; } async function govdeyiOku(istek, enCokBayt = 64 * 1024) { const parcalar = []; let boyut = 0; for await (const parca of istek) { // istek okunabilir bir akistir boyut += parca.length; if (boyut > enCokBayt) { istek.destroy(); throw Object.assign(new Error('govde cok buyuk'), { durum: 413 }); } parcalar.push(parca); } return Buffer.concat(parcalar).toString('utf8'); } function jsonYaz(yanit, durum, deger) { const govde = JSON.stringify(deger) + '\n'; yanit.writeHead(durum, { 'content-type': 'application/json; charset=utf-8', 'content-length': Buffer.byteLength(govde), }); yanit.end(govde); } const sunucu = createServer(async (istek, yanit) => { const adres = new URL(istek.url, `http://${istek.headers.host}`); try { if (istek.method === 'GET' && adres.pathname === '/ozet') { jsonYaz(yanit, 200, ozetleyici.ozet()); return; } if (istek.method === 'GET' && adres.pathname === '/ham') { yanit.writeHead(200, { 'content-type': 'application/x-ndjson; charset=utf-8' }); await pipeline(createReadStream('olcumler.ndjson'), yanit); // govde akisla yazilir return; } if (istek.method === 'POST' && adres.pathname === '/olcum') { const govde = await govdeyiOku(istek); let sayac = 0; for (const satir of govde.split('\n')) { if (satir.trim() === '') continue; ozetleyici.ekle(satirdanKayit(satir)); sayac += 1; } jsonYaz(yanit, 201, { alinan: sayac }); return; } jsonYaz(yanit, 404, { hata: 'bulunamadi' }); } catch (hata) { jsonYaz(yanit, hata.durum ?? 400, { hata: hata.message }); } }); const yuklenen = await baslangicVerisiniYukle('olcumler.ndjson'); sunucu.listen(8791, '127.0.0.1', () => { const { address, port } = sunucu.address(); console.log(`${yuklenen} kayit yuklendi; dinleniyor: http://${address}:${port}`); });
node hizmet.mjs
12 kayit yuklendi; dinleniyor: http://127.0.0.1:8791
Örnekteki 8791 sayısı bir seçimdir; makinenizde kullanılan bir bağlantı noktası varsa başka bir sayı yazın. Sonraki derste bu değer yapılandırmadan okunacak.
listen çağrısının ikinci argümanı olan 127.0.0.1, sunucunun yalnızca geri döngü
arayüzünü dinlemesini sağlar; bu adres İnternet Nasıl Çalışır kursunda tanıtılmıştı.
Argüman verilmediğinde sunucu bütün arayüzlerden erişilebilir olur — geliştirme
sırasında istenmeyen bir açıklıktır.
İsteği Ayrıştırmak
İstek nesnesi, protokolün ayrıştırılmış hâlini alan olarak sunar. istek.method
istek satırının ilk alanı, istek.url ikinci alanı, istek.headers ise küçük harfe
çevrilmiş başlık adlarından oluşan bir nesnedir.
istek.url yalnızca yol ve sorgu bölümünü taşır; alan adı host başlığındadır.
Bu ayrım, İnternet Nasıl Çalışır kursunda ada dayalı sanal barındırmanın dayanağı
olarak açıklanmıştı. İki parçayı birleştirip URL nesnesi kurmak, yol ile sorguyu
elle ayırmaktan daha güvenlidir: kaçış çözme ve normalleştirme kuralları nesnenin
içindedir.
Gövde, alanlarda bulunmaz — akıştır. Bunun nedeni, gövdenin keyfi büyüklükte
olabilmesidir. Yukarıdaki govdeyiOku, for await ile parçaları toplarken bir
sınır da uygular. Sınır zorunludur: sınırsız gövde okuyan bir uç, tek bir istekle
sürecin belleğini tüketebilir.
Yanıtı Yazmak
Yanıt nesnesi yazılabilir bir akıştır. writeHead durum kodunu ve başlıkları
belirler; end gövdeyi yazıp yanıtı kapatır.
curl -s -i http://127.0.0.1:8791/ozet
HTTP/1.1 200 OK
content-type: application/json; charset=utf-8
content-length: 420
Date: Mon, 27 Jul 2026 04:54:03 GMT
Connection: keep-alive
Keep-Alive: timeout=5
[{"anahtar":"kenar-01/sicaklik","sayi":3,"ortalama":21.87,"enBuyuk":22.3},{"anahtar":"kenar-01/nem","sayi":2,"ortalama":47.6,"enBuyuk":48},{"anahtar":"kenar-02/sicaklik","sayi":3,"ortalama":20.23,"enBuyuk":20.7},{"anahtar":"kenar-02/nem","sayi":1,"ortalama":52.5,"enBuyuk":52.5},{"anahtar":"kenar-03/sicaklik","sayi":2,"ortalama":24.35,"enBuyuk":24.6},{"anahtar":"kenar-03/nem","sayi":1,"ortalama":41.3,"enBuyuk":41.3}]
Date başlığındaki değer isteğin anına göre değişir; Connection ve Keep-Alive
başlıkları da istemcinin pazarlığına bağlıdır. Sabit olan, işleyicinin yazdığı iki
başlık ile gövdedir.
content-length başlığı burada elle hesaplandı. Karakter sayısı değil bayt sayısı
yazılmalıdır; arabellekler dersindeki ayrım tam olarak bu yüzden önemliydi.
Uzunluğu önceden bilmediğiniz yanıtlarda bu başlık yazılmaz. Gövde akışla yazıldığında çalışma zamanı parçalı aktarıma geçer:
curl -s -i http://127.0.0.1:8791/ham | head -8
HTTP/1.1 200 OK
content-type: application/x-ndjson; charset=utf-8
Date: Mon, 27 Jul 2026 04:54:03 GMT
Connection: keep-alive
Keep-Alive: timeout=5
Transfer-Encoding: chunked
{"zaman":"2024-02-07T09:12:44Z","dugum":"kenar-01","metrik":"sicaklik","deger":21.4}
Transfer-Encoding: chunked başlığı, gövdenin her parçasının önüne kendi uzunluğunun
yazıldığını bildirir; alıcı sonu bu yolla anlar. İnternet Nasıl Çalışır kursunda
“uzunluk önceden bilinmiyorsa gövde parçalar hâlinde gönderilir” diye anlatılan
düzenek budur.
Seçim şöyle yapılır: yanıt küçük ve bellekte hazırsa uzunluk bildirmek istemciye
ilerleme bilgisi verir; yanıt büyük veya üretilirken akıyorsa parçalı aktarım, belleği
yanıt boyunca meşgul etmemeyi sağlar. pipeline kullanılması, geri basıncın
uygulanmasını da güvence altına alır: istemci yavaşsa dosya okuması da yavaşlar.
Veri Göndermek ve Hatalar
Ölçüm göndermek gövdeli bir istekle yapılır:
curl -s -X POST --data-binary '{"dugum":"kenar-01","metrik":"sicaklik","deger":25.9}' -w 'durum: %{http_code}\n' http://127.0.0.1:8791/olcum
{"alinan":1}
durum: 201
201 kodu, İnternet Nasıl Çalışır kursundaki sınıflandırmaya göre “yeni kaynak oluşturuldu” anlamına gelir; 200’den ayrı seçilmesi isteğin etkisini bildirir.
Bozuk gövde farklı bir sınıfa düşer:
curl -s -X POST --data-binary 'bozuk' -w 'durum: %{http_code}\n' http://127.0.0.1:8791/olcum
{"hata":"Unexpected token 'b', \"bozuk\" is not valid JSON"}
durum: 400
curl -s -w 'durum: %{http_code}\n' http://127.0.0.1:8791/yok
{"hata":"bulunamadi"}
durum: 404
400 sınıfı isteği gönderenin hatasını, 500 sınıfı sunucunun hatasını bildirir. Bu ayrımı doğru yapmak yalnızca nezaket değil, işletim gereğidir: 500 oranı bir arıza göstergesidir, 400 oranı ise istemci davranışının göstergesi. İkisi karışırsa gösterge anlamını yitirir.
Yukarıdaki işleyicideki hata yakalayıcının bir eksiği vardır: her yakalanan hatayı 400 olarak bildiriyor, oysa özetleme sırasında oluşan bir programcı hatası 500 olmalıdır. Bu ayrım Hata Yönetimi Stratejisi dersinde düzeltilecek.
Bir uyarı: işleyici eşzamansız olduğu için, içinde oluşan ve yakalanmayan bir hata
createServer tarafından görülmez. try/catch bloğunun bütün gövdeyi sarması bu
yüzden gereklidir — sarmazsa yanıt hiç yazılmaz ve istemci zaman aşımına uğrar.
Özet
- İstek nesnesi okunabilir, yanıt nesnesi yazılabilir akıştır; akışlar dersindeki kuralların tümü burada geçerlidir.
- İstek satırı ve başlıklar ayrıştırılmış alanlar olarak sunulur;
istek.urlyalnızca yol ve sorguyu taşır, alan adıhostbaşlığındadır. - Gövde akış olduğundan boyutu sınırlanmalıdır; sınırsız okuma tek istekle belleği tüketebilir.
- Uzunluk bildiren yanıtta bayt sayısı yazılır; uzunluk bilinmiyorsa çalışma zamanı parçalı aktarıma geçer ve akışla yazma geri basıncı korur.
- Durum kodu sınıfı, hatanın kaynağını bildirir; istemci ve sunucu hatalarının karışması işletim göstergelerini anlamsızlaştırır.
Sonraki Adım
Hizmet tek bir süreçte çalışıyor ve bütün işi tek iş parçacığında yapıyor. Ölçüm verisi üzerinde pahalı bir hesap gerektiğinde — örneğin uzun bir dönemin yeniden özetlenmesi — bu tek iş parçacığı bütün istekleri bekletir. Sonraki ders sürecin dışına iş çıkarmanın ilk yolunu ele alır: alt süreç başlatmak, onunla akışlar üzerinden konuşmak ve dış komutları güvenli çağırmak.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.