İçeriğe geç
academia.sh

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.url yalnızca yol ve sorguyu taşır, alan adı host baş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.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat