---
title: 'WebSocket Sunucusu'
source: 'https://academia.sh/tr/kurslar/eszamansiz-isleme/websocket-sunucusu'
course: 'Önbellekleme, Kuyruklar ve Eşzamansız İşleme'
language: tr
updated: '2026-08-17T18:06:48+00:00'
license: 'CC BY-SA 4.0'
---

# WebSocket Sunucusu

Çift yönlü bağlantının sıfırdan kurulması: HTTP üzerinden yükseltme el sıkışması ve kabul anahtarının üretimi, çerçeve biçiminin yazılması ve çözülmesi, yoklama–karşılık ile kapanış el sıkışması ve bağlantı durumunun yatay ölçeklemeye etkisi.

Sunucu gönderimli olaylar tek yönlüydü: sunucu konuşur, istemci dinler. Kütüphane
servisinde iki tarafın da konuştuğu durumlar var — görevlinin şube stok sayacını
düzeltmesi, üyenin rezervasyon sırasından çekilmesi. Bunların her biri için ayrı bir HTTP
isteği açmak, akışın kazandırdığı gecikmeyi geri verir.

**WebSocket**, bu ihtiyaca karşılık gelen aktarımdır: HTTP üzerinden başlayan, bir el
sıkışmadan sonra kendi çerçeve biçimine geçen ve iki yönde de mesaj taşıyan tek bir
bağlantı. Bu ders onu hazır bir paket kullanmadan kurar; çünkü kurmadan öğrenilmeyen tek
şey, bağlantının **neyi tuttuğu** ve neden ölçeklemede sorun çıkardığıdır.

## Yükseltme El Sıkışması

Bağlantı olağan bir HTTP isteğiyle başlar. İstemci `Upgrade: websocket` ve
`Connection: Upgrade` başlıklarını, ayrıca rastgele üretilmiş on altı baytlık bir değeri
base64 ile kodlayıp `Sec-WebSocket-Key` başlığında gönderir.

Sunucu bu anahtarı belirtimde sabitlenmiş bir GUID ile birleştirir, SHA-1 özetini alır,
base64 ile kodlar ve sonucu `Sec-WebSocket-Accept` başlığında geri verir. Yanıtın durum
kodu **101 Switching Protocols**'tür ve bu noktadan sonra bağlantı üzerinde HTTP yoktur.

```js
// kabul-anahtari.mjs — el sikismadaki kabul degerinin uretimi ve dogrulanmasi
import { createHash } from "node:crypto";

const GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";  // protokolde sabitlenmis deger
const anahtar = "dGhlIHNhbXBsZSBub25jZQ==";           // belirtimdeki ornek istemci anahtari
const kabul = createHash("sha1").update(anahtar + GUID).digest("base64");

console.log("Sec-WebSocket-Key   :", anahtar);
console.log("Sec-WebSocket-Accept:", kabul);
console.log("belirtimdeki deger  :", kabul === "s3pPLMBiTxaQ9kYGzzhZRbK+xOo=");
```

```
Sec-WebSocket-Key   : dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
belirtimdeki deger  : true
```

Bu hesabın **hiçbir güvenlik işlevi yoktur**. SHA-1 burada bir imza değil, bir
doğrulamadır: sunucunun isteği gerçekten WebSocket olarak anladığını ve aradaki bir
önbelleğin eski bir yanıtı tekrar oynatmadığını gösterir. Kimlik doğrulama, yükseltme
isteğinin kendisinde — çerezde, başlıkta ya da ilk mesajda — ayrıca yapılmalıdır.

## Çerçeve Biçimi

El sıkışmadan sonra bağlantı üzerinde **çerçeveler** (frame) taşınır. Bir çerçevenin
başlığı en az iki bayttır. İlk baytın en yüksek biti `FIN`, mesajın bu çerçeveyle bitip
bitmediğini söyler; alt dört bit **işlem kodudur**: metin, ikili, kapat, yokla, karşılık.

İkinci baytın en yüksek biti **maske bitidir**, kalan yedi bit uzunluktur. Uzunluk 125'e
kadar doğrudan yazılır; 126 değeri sonraki iki baytın, 127 değeri sonraki sekiz baytın
gerçek uzunluğu taşıdığını bildirir.

Maske kuralı tek yönlüdür: istemciden sunucuya giden her çerçeve maskelenmek
**zorundadır**, sunucudan istemciye giden hiçbiri maskelenmemelidir. Maskeleme gizlilik
sağlamaz — anahtar çerçevenin içindedir. Amacı, aradaki eski bir aracının çerçeve
gövdesini geçerli bir HTTP isteği sanıp önbelleğini zehirlemesini engellemektir.

```js
// cerceve.mjs — WebSocket cerceve bicimini yazar ve cozer
import { randomBytes } from "node:crypto";

export const ISLEM = { metin: 0x1, ikili: 0x2, kapat: 0x8, yokla: 0x9, karsilik: 0xa };

// Cerceve: [FIN + islem kodu][maske biti + uzunluk][genisletilmis uzunluk?]
//          [maske anahtari?][govde].  Istemciden gelen her cerceve maskeli olmak zorunda.
export function cerceveYaz(islem, govde, maskeli = false) {
  const veri = Buffer.isBuffer(govde) ? govde : Buffer.from(String(govde), "utf8");
  const n = veri.length;
  const ek = n < 126 ? 0 : n < 65536 ? 2 : 8;
  const baslik = Buffer.alloc(2 + ek + (maskeli ? 4 : 0));
  baslik[0] = 0x80 | islem;                        // FIN = 1: cerceve tek parca
  baslik[1] = (maskeli ? 0x80 : 0) | (ek === 0 ? n : ek === 2 ? 126 : 127);
  if (ek === 2) baslik.writeUInt16BE(n, 2);
  if (ek === 8) baslik.writeBigUInt64BE(BigInt(n), 2);
  if (!maskeli) return Buffer.concat([baslik, veri]);
  const anahtar = randomBytes(4);
  anahtar.copy(baslik, 2 + ek);
  const maskelenmis = Buffer.from(veri);
  for (let i = 0; i < n; i++) maskelenmis[i] ^= anahtar[i % 4];
  return Buffer.concat([baslik, maskelenmis]);
}

// Tampondan tam bir cerceve cozer; cerceve tamamlanmamissa null doner.
export function cerceveCoz(tampon) {
  if (tampon.length < 2) return null;
  const islem = tampon[0] & 0x0f, son = (tampon[0] & 0x80) !== 0;
  const maskeli = (tampon[1] & 0x80) !== 0;
  let n = tampon[1] & 0x7f, konum = 2;
  if (n === 126) { if (tampon.length < 4) return null; n = tampon.readUInt16BE(2); konum = 4; }
  else if (n === 127) {
    if (tampon.length < 10) return null;
    n = Number(tampon.readBigUInt64BE(2)); konum = 10;
  }
  const anahtar = maskeli ? tampon.subarray(konum, konum + 4) : null;
  if (maskeli) konum += 4;
  if (tampon.length < konum + n) return null;
  const govde = Buffer.from(tampon.subarray(konum, konum + n));
  if (anahtar) for (let i = 0; i < n; i++) govde[i] ^= anahtar[i % 4];
  return { islem, son, maskeli, govde, tuketilen: konum + n };
}
```

`cerceveCoz` işlevinin tamamlanmamış çerçevede `null` döndürmesi bir ayrıntı değil,
zorunluluktur. Alt katman TCP'dir ve TCP bir **bayt akışıdır**: gönderilen bir çerçeve
iki parça hâlinde gelebilir, iki çerçeve tek parçada gelebilir. Bu yüzden okuyan taraf
gelen baytları biriktirir ve tampondan çözebildiği kadar çerçeve çeker.

## Sunucu

Sunucu, HTTP sunucusunun `upgrade` olayına bağlanır. Bu olay, yükseltme isteği geldiğinde
ham yuvayı (socket) verir; yanıt satırını ve başlıklarını sunucunun kendisi yazar.

```js
// ws-sunucu.mjs — el sikismayi ve cerceve cozumlemesini kendisi yapan WebSocket sunucusu
import { createServer } from "node:http";
import { createHash } from "node:crypto";
import { ISLEM, cerceveYaz, cerceveCoz } from "./cerceve.mjs";

const GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
const kabulUret = (anahtar) => createHash("sha1").update(anahtar + GUID).digest("base64");

const musteri = new Set();
const kutuk = (...s) => console.log(...s);

const sunucu = createServer((istek, yanit) => {
  yanit.sendDate = false;
  yanit.writeHead(426, { "content-type": "text/plain", upgrade: "websocket" });
  yanit.end("bu uc yalniz WebSocket baglantisi kabul eder\n");
});

sunucu.on("upgrade", (istek, yuva) => {
  const anahtar = istek.headers["sec-websocket-key"];
  if (istek.headers.upgrade?.toLowerCase() !== "websocket" || !anahtar)
    return yuva.destroy();

  yuva.write("HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\n" +
             `Connection: Upgrade\r\nSec-WebSocket-Accept: ${kabulUret(anahtar)}\r\n\r\n`);
  musteri.add(yuva);
  kutuk(`baglanti acildi   (acik=${musteri.size})`);

  const gonder = (islem, govde) => yuva.write(cerceveYaz(islem, govde));  // sunucu maskelemez
  let tampon = Buffer.alloc(0);

  yuva.on("data", (parca) => {
    tampon = Buffer.concat([tampon, parca]);
    let c;
    while ((c = cerceveCoz(tampon))) {
      tampon = tampon.subarray(c.tuketilen);
      kutuk(`cerceve alindi    islem=0x${c.islem.toString(16)}` +
            ` maskeli=${c.maskeli} uzunluk=${c.govde.length}`);

      if (c.islem === ISLEM.metin) {
        const istem = JSON.parse(c.govde.toString());
        gonder(ISLEM.metin, JSON.stringify({ tur: "onay", sube: istem.sube, yeniStok: 41 }));
      } else if (c.islem === ISLEM.yokla) {
        gonder(ISLEM.karsilik, c.govde);            // yoklamaya ayni govdeyle karsilik
      } else if (c.islem === ISLEM.kapat) {
        const kod = c.govde.length >= 2 ? c.govde.readUInt16BE(0) : 1005;
        kutuk(`kapanis istegi    kod=${kod} neden=${c.govde.subarray(2).toString()}`);
        gonder(ISLEM.kapat, c.govde);               // kapanis el sikismasi: ayni kodla don
        yuva.end();
      }
    }
  });

  yuva.on("close", () => {
    musteri.delete(yuva);
    kutuk(`baglanti kapandi  (acik=${musteri.size})`);
  });
});

sunucu.listen(8381, "127.0.0.1", () => kutuk("dinleniyor: 127.0.0.1:8381"));
```

## İstemci

İstemci de aynı biçimi kullanır, iki farkla: el sıkışma isteğini kendisi yazar ve
gönderdiği her çerçeveyi maskeler.

```js
// ws-istemci.mjs — ham TCP uzerinde el sikisip cerceve gonderen kucuk WebSocket istemcisi
import { connect } from "node:net";
import { createHash, randomBytes } from "node:crypto";
import { ISLEM, cerceveYaz, cerceveCoz } from "./cerceve.mjs";

const GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
const anahtar = randomBytes(16).toString("base64");
const beklenenKabul = createHash("sha1").update(anahtar + GUID).digest("base64");

const kuyruk = [], bekleyen = [];
const cerceveAl = () => kuyruk.length
  ? Promise.resolve(kuyruk.shift())
  : new Promise((c) => bekleyen.push(c));
const cerceveGeldi = (c) => (bekleyen.length ? bekleyen.shift()(c) : kuyruk.push(c));

let tampon = Buffer.alloc(0), elSikisildi = false;
const yuva = connect(8381, "127.0.0.1", () => {
  yuva.write("GET /kanal HTTP/1.1\r\nHost: 127.0.0.1:8381\r\n" +
             "Upgrade: websocket\r\nConnection: Upgrade\r\n" +
             `Sec-WebSocket-Key: ${anahtar}\r\nSec-WebSocket-Version: 13\r\n\r\n`);
});

yuva.on("error", (h) => {                        // sunucu ayakta degilse sessizce cik
  console.log("sunucuya baglanilamadi:", h.code);
  process.exit(0);
});

yuva.on("data", (parca) => {
  tampon = Buffer.concat([tampon, parca]);
  if (!elSikisildi) {
    const son = tampon.indexOf("\r\n\r\n");
    if (son < 0) return;
    const basliklar = tampon.subarray(0, son).toString();
    tampon = tampon.subarray(son + 4);
    elSikisildi = true;
    console.log("durum satiri      :", basliklar.split("\r\n")[0]);
    console.log("kabul anahtari    :",
      /sec-websocket-accept: (.+)/i.exec(basliklar)?.[1] === beklenenKabul ? "dogru" : "yanlis");
    konus();
  }
  let c;
  while ((c = cerceveCoz(tampon))) { tampon = tampon.subarray(c.tuketilen); cerceveGeldi(c); }
});

async function konus() {
  const gonder = (islem, govde) => yuva.write(cerceveYaz(islem, govde, true));

  gonder(ISLEM.metin, JSON.stringify({ tur: "stok", sube: 3, delta: -1 }));
  const y1 = await cerceveAl();
  console.log("metin karsiligi   :", y1.govde.toString(), `(maskeli=${y1.maskeli})`);

  gonder(ISLEM.yokla, "canli-mi");
  const y2 = await cerceveAl();
  console.log("yoklama karsiligi : islem=0x" + y2.islem.toString(16), y2.govde.toString());

  const neden = Buffer.concat([Buffer.from([0x03, 0xe8]), Buffer.from("is bitti")]);
  gonder(ISLEM.kapat, neden);                     // 0x03e8 = 1000, olagan kapanis
  const y3 = await cerceveAl();
  console.log("kapanis karsiligi : islem=0x" + y3.islem.toString(16),
              "kod=" + y3.govde.readUInt16BE(0));
  yuva.end();
}
```

## Yaşam Döngüsü

Aşağıdaki betik önce yükseltmesiz bir HTTP isteği gönderiyor, sonra istemciyi
koşturuyor. Bağlantı noktası 8381 keyfîdir ve boş olmalıdır.

```bash
#!/usr/bin/env bash
# ws-sunucu.mjs'yi baslatir, once duz HTTP istegini, sonra istemciyi kosturur, durdurur.
node ws-sunucu.mjs > sunucu.log & sunucu=$!
sleep 1

echo "--- yukseltmesiz duz HTTP istegi ---"
curl -sS -i http://127.0.0.1:8381/kanal | head -3
echo "--- istemcinin gordugu ---"
node ws-istemci.mjs
sleep 0.3
echo "--- sunucunun kutugu ---"
cat sunucu.log
kill "$sunucu"
```

```
--- yukseltmesiz duz HTTP istegi ---
HTTP/1.1 426 Upgrade Required
content-type: text/plain
upgrade: websocket
--- istemcinin gordugu ---
durum satiri      : HTTP/1.1 101 Switching Protocols
kabul anahtari    : dogru
metin karsiligi   : {"tur":"onay","sube":3,"yeniStok":41} (maskeli=false)
yoklama karsiligi : islem=0xa canli-mi
kapanis karsiligi : islem=0x8 kod=1000
--- sunucunun kutugu ---
dinleniyor: 127.0.0.1:8381
baglanti acildi   (acik=1)
cerceve alindi    islem=0x1 maskeli=true uzunluk=34
cerceve alindi    islem=0x9 maskeli=true uzunluk=8
cerceve alindi    islem=0x8 maskeli=true uzunluk=10
kapanis istegi    kod=1000 neden=is bitti
baglanti kapandi  (acik=0)
```

Kütük yaşam döngüsünün dört evresini gösteriyor. **Açılış**: yükseltme kabul edilir,
bağlantı sayacı artar. **Veri**: istemciden gelen metin çerçevesi maskelidir, dönen
karşılık değildir. **Denetim**: yoklama (0x9) çerçevesine aynı gövdeyle karşılık (0xa)
verilir. **Kapanış**: 0x8 çerçevesi bir kapanış **isteğidir**, emir değil; karşı taraf
kendi kapanış çerçevesini gönderdikten sonra yuva kapanır.

Yoklama–karşılık çifti gerçek işlerde vazgeçilmezdir. TCP bağlantısı, karşı taraf sessizce
kaybolduğunda uzun süre açık görünebilir; düzenli aralıklarla gönderilen yoklamaya
karşılık gelmemesi, bağlantının ölü olduğunun tek güvenilir işaretidir. Aynı trafik, ara
katmanların boşta duran bağlantıyı kapatmasını da engeller — sunucu gönderimli olaylardaki
kalp atışı satırının burada karşılık gelen biçimidir.

## Ölçekleme

WebSocket ile HTTP arasındaki asıl fark başlıklarda değil, **durumdadır**. Olağan bir
HTTP isteği durumsuzdur; hangi sunucu örneğine düşerse düşsün aynı sonucu verir.
WebSocket bağlantısı ise bir örneğe **bağlanır** ve bağlantı süresince orada kalır.

Bunun üç sonucu vardır. Ölçekleme birimi artık istek değil bağlantıdır; sunucunun
kapasitesi saniyedeki istek sayısıyla değil, aynı anda tutabildiği yuva ve bellek
miktarıyla ölçülür. Yeniden dağıtım her açık bağlantıyı koparır; istemci tarafında
yeniden bağlanma ve durum tazeleme zorunludur. Ve en önemlisi: aynı kanalı dinleyen iki
istemci **farklı örneklere** düşmüş olabilir. Bir örneğe gelen mesaj, diğerinin
istemcisine kendiliğinden ulaşmaz.

Bu üçüncü sonuç, çok örnekli her gerçek zamanlı kurulumun temel sorunudur.

## Özet

- WebSocket bağlantısı HTTP isteğiyle başlar; sunucu istemci anahtarını sabit bir GUID ile birleştirip SHA-1 özetini base64 olarak geri verir ve 101 ile protokolü değiştirir.
- Kabul anahtarı bir güvenlik önlemi değil, isteğin WebSocket olarak anlaşıldığının ve eski bir yanıtın tekrarlanmadığının doğrulamasıdır.
- Çerçeve başlığı FIN bitini, işlem kodunu, maske bitini ve uzunluğu taşır; istemciden gelen her çerçeve maskeli, sunucudan gidenler maskesizdir.
- Alt katman bayt akışı olduğu için çözücü tamamlanmamış çerçevede beklemeli, tampondan çözebildiği kadar çerçeve çekmelidir.
- Yaşam döngüsü açılış, veri, yoklama–karşılık ve iki taraflı kapanış el sıkışmasıdır; bağlantının bir örneğe bağlanması, ölçekleme biriminin istek değil bağlantı olmasına yol açar.

## Sonraki Adım

Bu derste tek bir sunucu örneği ve tek bir istemci vardı; mesajın gideceği yer belliydi.
Gerçek kurulumda ise aynı kanalı dinleyen istemciler farklı örneklere dağılır: şube stok
sayacını izleyen üç görevliden ikisi birinci örneğe, biri ikinciye bağlanmış olabilir.
Birinci örneğe gelen bir güncelleme, ikinci örnekteki görevliye ulaşmaz — çünkü o mesajı
kimse ona iletmez. Sonraki ders bu boşluğu kapatan yapıyı kurar ve mesajın bir örnekten
diğerinin abonesine gerçekten ulaştığını koşturarak gösterir.
