İçeriğe geç
academia.sh

Ders 03 / 34

REST İlkeleri

Kaynak temelli biçemin arkasındaki kısıt kümesi: durumsuzluğun aynı isteği iki ayrı süreçte sınayarak gösterilmesi, tek biçim arayüzün kaynak adını bilmeyen bir istemciyle ölçülmesi, önbelleklenebilirliğin varlık etiketiyle taşınan bayta etkisi ve katmanlı sistemin bu kısıtlardan doğması.

İçindekiler

Önceki ders kaynak temelli biçemi ölçtü ve en pahalı seçenek olarak buldu: beş istek, kırk yedi taşınan alan, %15 kullanım. Buna karşın ödünç servislerinin çoğu bu biçemle yazılır. Çelişki, ölçüme girmeyen özelliklerden gelir.

O özellikler bir kısıt kümesinin sonuçlarıdır. REST (representational state transfer), bir protokol ya da bir kitaplık değil, bir arayüzün uyabileceği kısıtların adıdır. Bu ders kısıtlardan üçünü — durumsuzluk, tek biçim arayüz ve önbelleklenebilirlik — çalıştırarak sınar.

Kısıtlar ve Kaynak Yönelimi

REST’in temel birimi kaynaktır (resource): bir adla anılabilen her kavram. Kütüphane servisinde kitap, üye, ödünç ve raf birer kaynaktır. İstemcinin gördüğü şey kaynağın kendisi değil, bir gösterimidir (representation): o kaynağın belirli bir biçimdeki kopyası. Aynı kaynağın JSON gösterimi de HTML gösterimi de olabilir; ikisi de aynı şeyi anlatır.

Kısıtlar bu birimin üstüne kurulur:

  • İstemci–sunucu ayrımı: iki taraf ayrı gelişir, aralarındaki tek bağ arayüzdür.
  • Durumsuzluk: her istek kendi kendine yeter; sunucu istekler arasında istemciye ait bir bağlam tutmaz.
  • Önbelleklenebilirlik: yanıt, saklanabilir olup olmadığını kendisi bildirir.
  • Tek biçim arayüz: kaynaklar aynı yolla adlandırılır ve aynı yöntemlerle işlenir.
  • Katmanlı sistem: istemci, karşısındaki tarafın son sunucu mu ara bir katman mı olduğunu bilmek zorunda değildir.

Aşağıdaki sunucu bu kısıtları hem uygulayan hem de ihlal eden iki tasarımı yan yana taşır; bağlantı noktasını dışarıdan aldığı için iki kopyası birlikte çalıştırılabilir.

// sunucu.mjs — ayni kodun iki kopyasi calistirilabilsin diye baglanti noktasi disaridan verilir:
// node sunucu.mjs 8461
import { createServer } from "node:http";
import { createHash, randomUUID } from "node:crypto";

const KAPI = Number(process.argv[2] ?? 8461);
const KAYNAKLAR = {
  kitaplar: [{ id: "978-0262033848", ad: "Algoritmalara Giris" }, { id: "978-0201896831", ad: "Programlama Sanati" },
             { id: "978-0131103627", ad: "C Programlama Dili" }, { id: "978-0596007126", ad: "Head First" }],
  uyeler:   [{ id: "u1", ad: "Ayse Kaya" }, { id: "u2", ad: "Baris Demir" }],
  oduncler: [{ id: "o11", uye: "u1", kitap: "978-0262033848" }],
  raflar:   [{ id: "R-12", kat: 2 }, { id: "R-03", kat: 1 }],   // sonradan eklenen kaynak
};
const imlecler = new Map();          // sunucu tarafinda tutulan durum: yalniz bu kopyada var

const json = (yanit, kod, govde, etiket) => {
  const metin = JSON.stringify(govde);
  yanit.setHeader("Content-Type", "application/json; charset=utf-8");
  yanit.setHeader("X-Surec", process.pid);
  if (etiket) yanit.setHeader("ETag", etiket);
  yanit.setHeader("Content-Length", Buffer.byteLength(metin));
  yanit.writeHead(kod).end(metin);
};
const etiketle = (govde) =>
  `"${createHash("sha256").update(JSON.stringify(govde)).digest("hex").slice(0, 16)}"`;

createServer((istek, yanit) => {
  yanit.sendDate = false;
  const adres = new URL(istek.url, "http://yerel");
  const [, ilk, ikinci] = adres.pathname.split("/");

  if (ilk === "") return json(yanit, 200, { kaynaklar: Object.keys(KAYNAKLAR) });

  // Durumlu sayfalama: imlec sunucuda tutulur, istek yalniz imlec kimligini tasir.
  if (ilk === "imlec" && istek.method === "POST") {
    const kimlik = randomUUID().slice(0, 8);
    imlecler.set(kimlik, 0);
    return json(yanit, 201, { imlec: kimlik });
  }
  if (ilk === "imlec" && istek.method === "GET") {
    if (!imlecler.has(ikinci)) return json(yanit, 404, { hata: "imlec_bilinmiyor" });
    const ofset = imlecler.get(ikinci);
    imlecler.set(ikinci, ofset + 2);
    return json(yanit, 200, { ofset, sayfa: KAYNAKLAR.kitaplar.slice(ofset, ofset + 2) });
  }

  // Tek bicim arayuz: her kaynak ayni yolla adlandirilir, ayni yontemlerle okunur.
  const kume = KAYNAKLAR[ilk];
  if (!kume) return json(yanit, 404, { hata: "kaynak_yok" });
  if (ikinci) {
    const oge = kume.find((k) => k.id === ikinci);
    if (!oge) return json(yanit, 404, { hata: "bulunamadi" });
    const etiket = etiketle(oge);
    if (istek.headers["if-none-match"] === etiket) {         // kosullu istek: govde gonderilmez
      yanit.setHeader("ETag", etiket);
      yanit.setHeader("X-Surec", process.pid);
      return yanit.writeHead(304).end();
    }
    return json(yanit, 200, oge, etiket);
  }
  // Durumsuz sayfalama: sayfa bilgisi istegin icindedir.
  const ofset = Number(adres.searchParams.get("ofset") ?? 0);
  const adet = Number(adres.searchParams.get("adet") ?? kume.length);
  return json(yanit, 200, { ofset, adet, oge: kume.slice(ofset, ofset + adet) });
}).listen(KAPI, "127.0.0.1", () => console.log(`kopya :${KAPI} pid=${process.pid}`));

Ölçüm istemcisi üç bölümden oluşur ve istekleri iki kopya arasında dönüşümlü dağıtır.

// olc.mjs — uc ilkeyi sinar: durumsuzluk, tek bicim arayuz, onbelleklenebilirlik
import { Agent, request } from "node:http";
import { connect } from "node:net";

let alinan = 0;
const ajan = new Agent({ keepAlive: true });
ajan.createConnection = (s) => { const b = connect(s); b.on("data", (p) => (alinan += p.length)); return b; };
const iste = (kapi, yol, secenek = {}) => new Promise((coz, hata) => {
  const r = request({ agent: ajan, host: "127.0.0.1", port: kapi, path: yol,
    method: secenek.yontem ?? "GET", headers: secenek.basliklar ?? {} }, (y) => {
    let m = ""; y.on("data", (p) => (m += p));
    y.on("end", () => coz({ kod: y.statusCode, etiket: y.headers.etag, surec: y.headers["x-surec"],
                            govde: m ? JSON.parse(m) : null }));
  });
  r.on("error", hata); r.end();
});

// 1) DURUMSUZLUK — ayni istek iki ayri surecte ayni yaniti veriyor mu?
console.log("-- 1) durumsuzluk: istekler iki kopya arasinda donusumlu dagitiliyor --");
const { govde: kur } = await iste(8461, "/imlec", { yontem: "POST" });
for (const kapi of [8461, 8462, 8461]) {
  const y = await iste(kapi, `/imlec/${kur.imlec}`);
  console.log(`  durumlu   :${kapi} pid=${y.surec} -> ${y.kod} ${JSON.stringify(y.govde).slice(0, 62)}`);
}
for (const kapi of [8461, 8462, 8461]) {
  const y = await iste(kapi, "/kitaplar?ofset=2&adet=2");
  console.log(`  durumsuz  :${kapi} pid=${y.surec} -> ${y.kod} ${JSON.stringify(y.govde).slice(0, 62)}`);
}

// 2) TEK BICIM ARAYUZ — ayni istemci kodu, kaynak adini bilmeden her kaynagi okuyor mu?
console.log("\n-- 2) tek bicim arayuz: kaynak adlari sunucudan ogreniliyor --");
const { govde: kok } = await iste(8461, "/");
for (const ad of kok.kaynaklar) {
  const liste = await iste(8461, `/${ad}`);
  const ilk = liste.govde.oge[0];
  const tek = await iste(8461, `/${ad}/${ilk.id}`);
  console.log(`  ${ad.padEnd(9)} liste=${liste.kod} adet=${liste.govde.oge.length}` +
              `  tek=${tek.kod} etiket=${tek.etiket}`);
}
const yok = await iste(8461, "/subeler");
console.log(`  subeler   liste=${yok.kod} ${JSON.stringify(yok.govde)}`);

// 3) ONBELLEKLENEBILIRLIK — kosullu istek tasinan bayti ne kadar dusuruyor?
console.log("\n-- 3) onbelleklenebilirlik: ayni kayit on kez isteniyor --");
const YOL = "/kitaplar/978-0262033848";
alinan = 0;
for (let i = 0; i < 10; i++) await iste(8461, YOL);
const kosulsuz = alinan;
alinan = 0;
const ilk = await iste(8461, YOL);
for (let i = 0; i < 9; i++) await iste(8461, YOL, { basliklar: { "If-None-Match": ilk.etiket } });
const kosullu = alinan;
const son = await iste(8461, YOL, { basliklar: { "If-None-Match": ilk.etiket } });
console.log(`  kosulsuz  10 istek -> ${kosulsuz} B`);
console.log(`  kosullu   10 istek -> ${kosullu} B  (son yanit ${son.kod}, kazanc %${
  Math.round((100 * (kosulsuz - kosullu)) / kosulsuz)})`);
ajan.destroy();
node sunucu.mjs 8461 & a=$!
node sunucu.mjs 8462 & b=$!
for k in 8461 8462; do
  curl -s --retry 20 --retry-connrefused --retry-delay 0 -o /dev/null "http://127.0.0.1:$k/"
done
node olc.mjs
kill $a $b
kopya :8461 pid=8582
kopya :8462 pid=8583
-- 1) durumsuzluk: istekler iki kopya arasinda donusumlu dagitiliyor --
  durumlu   :8461 pid=8582 -> 200 {"ofset":0,"sayfa":[{"id":"978-0262033848","ad":"Algoritmalara
  durumlu   :8462 pid=8583 -> 404 {"hata":"imlec_bilinmiyor"}
  durumlu   :8461 pid=8582 -> 200 {"ofset":2,"sayfa":[{"id":"978-0131103627","ad":"C Programlama
  durumsuz  :8461 pid=8582 -> 200 {"ofset":2,"adet":2,"oge":[{"id":"978-0131103627","ad":"C Prog
  durumsuz  :8462 pid=8583 -> 200 {"ofset":2,"adet":2,"oge":[{"id":"978-0131103627","ad":"C Prog
  durumsuz  :8461 pid=8582 -> 200 {"ofset":2,"adet":2,"oge":[{"id":"978-0131103627","ad":"C Prog

-- 2) tek bicim arayuz: kaynak adlari sunucudan ogreniliyor --
  kitaplar  liste=200 adet=4  tek=200 etiket="9ad1f08038f3c1fa"
  uyeler    liste=200 adet=2  tek=200 etiket="5d5092c2e89cc21d"
  oduncler  liste=200 adet=1  tek=200 etiket="de830fee13237e05"
  raflar    liste=200 adet=2  tek=200 etiket="8703c0e7db6c6311"
  subeler   liste=404 {"hata":"kaynak_yok"}

-- 3) onbelleklenebilirlik: ayni kayit on kez isteniyor --
  kosulsuz  10 istek -> 2240 B
  kosullu   10 istek -> 1277 B  (son yanit 304, kazanc %43)

Süreç kimlikleri her çalıştırmada değişir. Kimlik X-Surec başlığında taşındığı için basamak sayısı toplam baytı da birkaç bayt kaydırır; oran ise değişmez. Bayt sayıları ayrıca çalışma zamanının yazdığı başlık kümesine bağlıdır.

Durumsuzluk: İsteğin Kendi Kendine Yetmesi

İlk bölüm iki sayfalama tasarımını aynı koşulda sınar: istekler iki sunucu kopyası arasında dönüşümlü dağıtılır. Bu, önceki kursta kurulan vekil arkasındaki çoklu kopya kurulumunun küçük bir örneğidir.

Durumlu tasarımda imleç sunucunun belleğinde durur. İlk istek 8461’e düşer ve çalışır. İkinci istek 8462’ye düştüğünde aynı imleç orada bulunmadığı için 404 alınır. Üçüncü istek yine 8461’e düştüğünde ikinci sayfa gelir — yani ikinci kopyanın verdiği hata, imlecin durumunu da bozmuştur: istemci bir sayfayı hiç görmeden atlamıştır.

Durumsuz tasarımda sayfa bilgisi isteğin içindedir. Aynı istek üç kez, iki ayrı sürece gitmiş ve üçünde de aynı gövdeyi vermiştir. Sunucu kimliğinin yanıt üzerinde hiçbir etkisi yoktur.

Durumsuzluğun bedeli her istekte bağlamın yeniden gönderilmesidir: kimlik bilgisi, sayfalama konumu, süzme ölçütleri her seferinde taşınır. Karşılığında elde edilen şey, sunucu kopyalarının birbirinin yerine geçebilmesidir. Bir kopya düştüğünde istek başka bir kopyaya yönlendirilebilir, kopya sayısı yük altında artırılabilir ve hiçbiri istemcinin oturumunu taşımak zorunda kalmaz.

Tek Biçim Arayüz: Aynı İstemci Kodu, Dört Kaynak

İkinci bölümdeki istemci hiçbir kaynak adını bilmez. Kök adresten kaynak listesini okur, sonra her kaynak için aynı iki isteği yapar: koleksiyonu al, ilk ögenin kimliğiyle tek kaydı al. Dördü de aynı kodla okunmuştur; sonradan eklenen raflar kaynağı için istemcide tek satır değişmemiştir.

Tek biçim arayüzün somut karşılığı budur: kaynaklar adreslerle adlandırılır, aynı yöntemler her kaynakta aynı anlama gelir ve yanıtlar kendini betimler — gövdenin nasıl okunacağı içerik türü başlığından, kaydın sürümü varlık etiketinden anlaşılır. Var olmayan bir kaynak da aynı biçimde, 404 ile karşılanır.

Bunun bedeli esneklik kaybıdır. Bir işlem tek bir kaynağa oturmuyorsa — “şu üç kitabı aynı anda uzat ve gecikme cezasını yeniden hesapla” gibi — tek biçim arayüz o işlemi doğal karşılamaz. Bu tür işlemler ya kendileri birer kaynağa dönüştürülür ya da arayüz bu noktada uzak yordam çağrısına yaklaşır.

Önbelleklenebilirlik: Aynı Kaydı On Kez İstemek

Üçüncü bölüm aynı kitabı on kez ister. Bu çalıştırmada koşulsuz istekler 2240 bayt taşır. İkinci turda istemci ilk yanıttaki varlık etiketini saklar ve sonraki dokuz isteğe If-None-Match başlığıyla ekler; sunucu kaydın değişmediğini görüp gövdesiz 304 döndürür. Taşınan bayt 1277’ye iner — %43 azalma.

Varlık etiketi Statik Dosya Sunumu dersinde tanıtılmıştı; buradaki fark, aynı düzeneğin hesaplanmış bir kayda uygulanmasıdır. Kazancın kaynağı gövdenin gönderilmemesidir; başlıklar yine taşınır, bu yüzden azalma %100 olamaz. Kayıt büyüdükçe oran yükselir: bu ölçümdeki kayıt yalnız iki alan taşıyor.

Önbelleklenebilirliğin durumsuzlukla bağı doğrudandır. Bir yanıtın saklanabilmesi, o yanıtın yalnız isteğin içeriğine bağlı olmasını gerektirir. Sunucuda tutulan bir imlece bağlı yanıt saklanamaz, çünkü aynı adres iki farklı sonuç verebilir.

Katmanlı Sistem

Üç ölçümün hepsi bir dördüncü kısıtı da mümkün kılar. Yanıt yalnız isteğe bağlıysa ve saklanabilir olduğunu kendisi bildiriyorsa, istemci ile sunucu arasına bir katman konulabilir: bir ters vekil, bir önbellek ya da bir yönlendirici. İstemci karşısındakinin son sunucu mu ara katman mı olduğunu bilmez ve bilmesi gerekmez.

Önceki kursun Web Sunucusu Kavramı dersinde kurulan ters vekil tam olarak budur. O ders vekilin bilgi kaybı da getirdiğini göstermişti; buradaki ek gözlem şudur: vekilin araya girebilmesi, arayüzün durumsuz ve önbelleklenebilir olmasına dayanır. Sunucuda oturum tutan bir tasarımda vekil, isteği herhangi bir kopyaya değil, oturumun bulunduğu kopyaya göndermek zorunda kalır.

Özet

  • REST bir protokol değil, kaynak yönelimli bir arayüzün uyduğu kısıtlar kümesidir; temel birim kaynak, taşınan şey ise kaynağın gösterimidir.
  • Durumsuzluk ölçümde doğrudan görüldü: sunucuda tutulan imleç ikinci kopyada bulunamadı ve bir sayfa atlandı; istek içinde taşınan sayfa bilgisi iki kopyada da aynı yanıtı verdi.
  • Tek biçim arayüz, kaynak adını bilmeyen tek bir istemci kodunun dört kaynağı da okuyabilmesi demektir; sonradan eklenen kaynak istemcide değişiklik gerektirmedi.
  • Koşullu istek aynı kaydın on kez alınmasında taşınan baytı yaklaşık %43 düşürdü; kazanç gövdenin gönderilmemesinden gelir, başlıklar taşınmaya devam ettiği için tam olmaz.
  • Katmanlı sistem bu iki kısıta dayanır: yanıt yalnız isteğe bağlı ve saklanabilir olduğunda araya vekil ya da önbellek konulabilir.

Sonraki Adım

REST’in kısıtları arayüzü ağın kurallarına yaslar: adresler, yöntemler, durum kodları, başlıklar. Bunun karşılığında sözleşme metin biçiminde ve gevşek kalır — bir alanın tipi ya da bir yöntemin imzası, ancak belgeye bakılarak bilinir. Bunun tersini yapan bir yaklaşım vardır: sözleşmeyi önce ayrı bir şema dosyasında yazmak, istemci ve sunucu tarafını o şemadan üretmek ve teli metin yerine ikili biçimde kullanmak. Sonraki ders bu yaklaşımı kurar: bir şema dosyası yazar, ondan bir doğrulayıcı üretir, kendi ikili çerçevelemesini yazar ve aynı kaydın ikili ile metin biçimindeki bayt sayısını karşılaştırır.

İ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