Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Realtime protokolü

Bu sayfa, /v1/realtime/ws soketinin tel (wire) protokolünü anlatır: bilet alma, bağlanma, kimlik doğrulama ve her istemci ve sunucu çerçevesinin alanları. TypeScript SDK’sını kullanıyorsanız bu protokolü elle yazmazsınız; ama davranışı anlamak, başka bir istemci yazmak ya da sorun gidermek için buradan okuyun.

Not

Protokol sürümü 1’dir. Her çerçevede protocolVersion: 1 bulunur; başka değer UnsupportedProtocol hatası alır (sürümleme).

Genel kurallar

  • Her çerçeve tek bir WebSocket metin mesajı olarak giden bir JSON nesnesidir. İkili (binary) mesaj kabul edilmez.
  • type alanı çerçeve türünü belirler ve PascalCase’dir (Authenticate, Join, …). Diğer alan adları camelCase’dir. Bilinmeyen alan içeren çerçeve reddedilir.
  • Büyük tam sayılar (sıra numaraları, epoch, revizyon, sürüm) JSON sayısı değil ondalık metindir: "sessionEpoch": "7". Kurallı biçim: boş değil, baştaki sıfır yok, yalnız rakam, negatif değil, i64 aralığında. Ayrıntı: sayılar ve kodlama. protocolVersion ve schemaVersion küçük tam sayıdır ve JSON sayısı olarak gider.
  • requestId (UUID) istemcinin her deneme için ürettiği ilişki kimliğidir; sunucu yanıtı ve hatayı aynı kimlikle döndürür.
  • Kimlik doğrulamadan önce yalnız Authenticate, sonra Authenticate dışındaki çerçeveler geçerlidir; yanlış fazdaki çerçeve UnexpectedFrame alır.

Bağlanma akışı

sequenceDiagram
    participant C as "İstemci"
    participant H as "HTTP (REST)"
    participant S as "WebSocket /v1/realtime/ws"
    C->>H: "POST /v1/realtime/tickets"
    H-->>C: "200 ticket, expiresAt"
    C->>S: "Upgrade (Sec-WebSocket-Protocol openplay.realtime.v1)"
    C->>S: "Authenticate(protocolVersion 1, ticket)"
    S-->>C: "Ready(connectionId, playerId, sessionEpoch, authorityGeneration)"
    C->>S: "Join(pluginId, moduleId, target)"
    S-->>C: "Joined(subscriptionId)"
    S-->>C: "Snapshot veya DurableRecovery"
    C->>S: "Input veya Command"
    S-->>C: "Ack veya CommandResult"

1. Bilet al

POST /v1/realtime/tickets HTTP/1.1
Host: oyun.example.com
Origin: https://oyun.example.com
x-crx-env: 0192a3b4-0000-7000-8000-000000000001
Cookie: __Secure-crx.session=<oturum>; __Secure-crx.csrf=<csrf-çerezi>
x-csrf-token: <csrf-jetonu>
HTTP/1.1 200 OK
Cache-Control: no-store
Content-Type: application/json

{"ticket":"tkt_AbC123-xyz","expiresAt":"2026-11-03T10:15:30+00:00","protocol":"openplay.realtime.v1"}
BaşlıkZorunluNot
x-crx-envevetOyuncunun ortamı. Oturumun ortamıyla uyuşmalıdır.
OriginevetOrtamın izinli kökenlerinden biri olmalı; bilet bu kökene bağlanır.
Cookie ya da Authorization: Bearer <oturum jetonu>evetÇerez varsa her zaman çerez kipi seçilir; bozuk çerezde Bearer’a düşülmez.
x-csrf-token + __Secure-crx.csrf çereziyalnız çerez kipindeÇift gönderim (double submit). Başarılı her istekte yeni CSRF değeri döner; yanıttaki x-csrf-token başlığını ve Set-Cookie’yi saklayın (oturumlar ve CSRF).
  • Bilet tek kullanımlıktır, 30 saniye geçerlidir ve Origin’e bağlıdır. Soket açmadan önce alın; bekletmeyin.
  • Bu uçta kimlik, CSRF ve Origin hataları aynı yanıtı alır: 403 ve düz metin permission denied. Modül kapalıysa (plugins) 404 ve {"code":"module_disabled",…}.

2. Soketi aç

  • Adres: wss://<oyun-alan-adınız>/v1/realtime/ws. Sorgu dizesi olmamalıdır; olursa yükseltme reddedilir.
  • Sec-WebSocket-Protocol: openplay.realtime.v1 istenmelidir; sunucu bunu seçer. İstenmezse yükseltme reddedilir.
  • Origin zorunludur ve ortamın izinli kökenlerinden biri ya da genel izinli listede olmalıdır. Tarayıcı bunu kendisi gönderir.
  • Yükseltme reddi 401 (düz metin unauthorized); modül kapalıysa 404.
  • Adres başına ve oyuncu başına bağlantı sınırları vardır (limitler).

3. Authenticate gönder

Soket açılınca 5 saniye içinde ilk çerçeve gelmelidir; gelmezse sunucu AuthenticationFailed hatası yazıp kapatır. Kimlik doğrulama fazında çerçeve en çok 4 KiB olabilir.

{"type":"Authenticate","protocolVersion":1,"ticket":"tkt_AbC123-xyz"}

Sunucu bileti tüketir (kökeni ve alt protokolü doğrular), oturumu yeniden doğrular ve cevap verir:

{"type":"Ready","protocolVersion":1,"connectionId":"0192a3b4-0000-7000-8000-0000000000a1","playerId":"0192a3b4-0000-7000-8000-0000000000b2","sessionEpoch":"7","authorityGeneration":"42"}
AlanAnlam
connectionIdBu soketin sunucu kimliği.
playerIdDoğrulanmış oyuncu.
sessionEpochBu soketin yazma dönemi (epoch). Input çerçevelerinde aynen geri verilir. Aynı oyuncunun daha yeni bir soketi aynı kaynağa yazmaya başlarsa eski soketin girdileri Stale alır.
authorityGenerationOrtamın yetki nesli; sunucu tarafı sınırlama içindir, istemci yalnız saklar.

Geçersiz, kullanılmış, süresi dolmuş ya da başka kökene ait bilet AuthenticationFailed ile reddedilir; sunucu soketi kapatır. Yeni bilet alıp yeniden bağlanın.

İstemci çerçeveleri

Ready’den sonra gönderilebilir. Çerçeve boyutu en çok 16 KiB’dir; aşan çerçeve PayloadTooLarge alır.

Join: aboneliğe katıl

{"type":"Join","protocolVersion":1,"requestId":"0192a3b4-0000-7000-8000-000000000010","pluginId":"package-ride","moduleId":"package-ride.street","target":{"kind":"Scene","sceneId":"0192a3b4-0000-7000-8000-000000000031"}}
AlanTürAnlam
requestIdUUIDDeneme kimliği.
pluginIdmetinModülün sahibi eklenti (eklentiler).
moduleIdmetinEklentinin tanımladığı realtime modülü; eklentinin şema belgesindeki modules bölümünde listelenir.
target.kindmetinPlayerSelf, Room, Scene ya da InterestSet.
target.roomId / sceneId / interestSetIdUUIDHedefin kimliği (kind’a göre; PlayerSelf kimlik taşımaz).
cursorUUID, isteğe bağlıÖnceki bir DurableRecovery’den gelen opak devam imleci.

Sunucu hedefi yetkilendirir; ham bir Redis anahtarı ya da konu adı verilmez. Başarı: Joined. Ardından modülün türüne göre bir Snapshot ya da DurableRecovery gelir.

Leave: abonelikten çık

{"type":"Leave","protocolVersion":1,"requestId":"…","subscriptionId":"…"}

Yanıt Left’tir; tekrarlanan Leave zararsızdır.

Input: geçici girdi

Sunucu-otoriter modüllerde oyuncu girdisi (hareket, direksiyon). Kayıp kabul edilmez, sıra numarası zorunludur.

{"type":"Input","protocolVersion":1,"requestId":"…","pluginId":"arena","moduleId":"arena.movement","messageType":"arena.movement.input","schemaVersion":1,"subscriptionId":"…","sessionEpoch":"7","inputSeq":"42","payload":{"delta":1}}
AlanAnlam
messageType, schemaVersionModülün şema belgesindeki girdi tanımıyla birebir eşleşmelidir.
subscriptionIdJoined’dan gelen abonelik.
sessionEpochReady’deki değer.
inputSeqArtan sıra numarası (ondalık metin). Aynı numara ve aynı içerik tekrar gönderilirse Replayed alırsınız; aynı numara farklı içerikle Mismatch.
payloadEklentinin girdi şemasına uyan nesne. Kodlanmış boyutu en çok 4 KiB ve modülün maxBytes değeri kadardır; aşım InvalidPayload alır.

Girdi hızı modülün şema belgesindeki ratePerSecond ve burst değerleriyle tanımlıdır; bağlantı başına genel çerçeve bütçesi ayrıca uygulanır (limitler).

Command: kalıcı komut

Veritabanı işlemiyle kalıcı yazan komut (ödül, teslimat); sonuç yalnız işlem kesinleştikten sonra döner. HTTP komut ucuyla aynı kurallara tabidir (eklentileri çağırma).

{"type":"Command","protocolVersion":1,"requestId":"…","pluginId":"package-ride","moduleId":null,"messageType":"package-ride.progression-missions.claim.input","schemaVersion":1,"idempotencyKey":"claim-7f3a","expectedVersion":"12","payload":{"missionId":"daily-1"}}
AlanAnlam
idempotencyKeyZorunlu mantıksal tekrar anahtarı. Aynı anahtar ve aynı içerik aynı sonucu döndürür (idempotency).
expectedVersionİsteğe bağlı iyimser sürüm (ondalık metin). Uyuşmazlık hata olur.
moduleIdOda kapsamlı komutlarda modül bağlamı; yoksa null.
payloadEklentinin komut şemasına uyar; kodlanmış boyutu en çok 16 KiB’dir, aşım InvalidPayload alır.

Ping

{"type":"Ping","protocolVersion":1,"nonce":"a1"}

Yanıt {"type":"Pong","nonce":"a1"}. nonce isteğe bağlıdır. Uygulama düzeyi Ping bağlantı canlılığını ölçer; oyuncu etkinliği sayılmaz, boşta kapanmayı ertelemez (boşta kapanma).

Sunucu çerçeveleri

typeNe zamanAlanlar
ReadyAuthenticate başarılıprotocolVersion, connectionId, playerId, sessionEpoch, authorityGeneration
JoinedJoin yetkilendirildirequestId, subscriptionId, target
LeftLeave işlendirequestId, subscriptionId
CommandResultCommand kesinleştirequestId, messageType, schemaVersion, payload
AckInput işlendirequestId, inputSeq, status, revision (isteğe bağlı)
SnapshotGeçici durum yayınısubscriptionId, resourceId, messageType, schemaVersion, streamEpoch, revision, payload
DurableRecoveryKalıcı akış için kurtarma bariyerisubscriptionId, cursor, barrierSequence, coveredEventIds, snapshot (boş olabilir)
DurableEventKalıcı, sıralı eklenti olayısubscriptionId, streamId, sequence, eventId, messageType, schemaVersion, cursor, payload
PongPing yanıtınonce
ErrorHatarequestId (varsa), code, retryable

Ack durumları

statusAnlamNe yapmalı
AppliedGirdi durumu ilerletti. revision yeni revizyondur.—
ReplayedAynı sıra numarası ve içerik zaten işlenmişti.Yoksayın; güvenli tekrar.
StaleSıra numarası güncel yazarın gerisinde ya da daha yeni bir soket aldı.Bu girdiyi bırakın; yeni soket varsa bu soket artık yazar değildir.
MismatchAynı sıra numarası farklı içerikle geldi.İstemci hatasıdır; sıra numarasını yeniden üretmeyin, tekrar etmeyin.
ResyncRequiredSıra boşluğu ya da kurtarılamayan durum.Taze anlık görüntü için yeniden Join edin (cursor olmadan).

Applied ve Replayed dışında revision bulunmayabilir. Alan yokken null gönderilmez; alan hiç yoktur.

Snapshot ve DurableRecovery

  • Snapshot geçici durumdur: streamEpoch (sunucu önbelleği yeniden başladığında artar) ve revision (monoton) birlikte sıralanır. Aynı streamEpoch içinde daha eski bir revision, yeni durumu ezmemelidir. streamEpoch değişmişse yeni bir akış başlamıştır; eski revision ile karşılaştırmayın.
  • DurableRecovery kalıcı akışta taban çizgisidir: cursor bu noktaya bağlı yeni imleçtir, barrierSequence anlık görüntünün ve tekrar oynatmanın kapsadığı akış başıdır, coveredEventIds anlık görüntünün zaten içerdiği olay kimlikleridir (bunları ayrıca uygulamayın). snapshot yalnız taze Join’de dolu gelir; cursor ile devam ederken yoktur.
  • DurableEvent’ler sequence sırasıyla gelir; eventId uçtan uca tekilleştirme anahtarıdır, cursor o olaya kadar ilerlemiş yeni imleçtir. İmleci saklayın: yeniden bağlanırken Join.cursor olarak verirsiniz (yeniden bağlanma).

Hata çerçevesi

{"type":"Error","requestId":"0192a3b4-0000-7000-8000-000000000010","code":"InvalidPayload","retryable":false}

Hata çerçevesi gerekçe metni taşımaz: yalnız kod ve retryable. Bir eklenti reddi (ör. iş kuralı) bile InvalidPayload olarak görünür; neden istemciye telden verilmez.

codeNe zamanretryableSoket
UnexpectedFrameFazına uymayan çerçeve (ör. kimlik doğrulamadan önce Join, ya da ikinci Authenticate)hayırkapanır
UnsupportedProtocolprotocolVersion 1 değilhayırkapanır
InvalidPayloadBozuk JSON, bilinmeyen alan, şemaya uymayan yük, eklenti/iş kuralı reddi, kaynak yok, sürüm çakışması, modül kapalıhayırçerçeve bazlı hatada açık kalır; ayrıştırma hatasında kapanır
AuthenticationFailedBilet geçersiz/kullanılmış/süresi dolmuş, 5 sn zaman aşımı, oturum iptal edildihayırkapanır
PermissionDeniedHedef, modül ya da işlem için yetki yok; yasaklı hesaphayıraçık kalır
PayloadTooLargeÇerçeve bütçesini aştı (4 KiB kimlik doğrulama, 16 KiB sonrası)hayırkapanır
CapacityExceededHız ya da kapasite sınırı dolduçerçeve bütçesi ve oran sınırında evet; oyuncu/adres başına soket sayısı sınırında hayırçerçeve bütçesi ve soket sayısı sınırında kapanır
UnavailableBağımlılık geçici kullanılamıyor ya da yerleşim hazır değilgenellikle evet (IdempotencyExpired hâli hayır)duruma göre
ResyncRequiredİmleç yok, süresi dolmuş, uyuşmuyor, boşluklu ya da tekrar bütçesini aştı—açık kalır

Uyarı

Soketin kapanıp kapanmayacağını yalnız retryable’dan çıkarmayın. İstemci, Error çerçevesinden sonra soketin hâlâ açık olup olmadığını bağlantı durumundan (kapanış olayı) öğrenmelidir. Uzun ömürlü soketlerde çerçeve bazlı hatalar (InvalidPayload, PermissionDenied) bağlantıyı düşürmez.

Kapanış kodları

KodNedenAnlam
4100idle_timeoutOyuncu etkinliği gelmedi; boşta kapanma (ayrıntı). Tipli Error çerçevesi olmaz.
1008module_disabledplugins modülü kapatıldı.
1013unavailableModül kapısı doğrulanamadı; sonra yeniden deneyin.

Örnek: elle bağlanma (özet)

Tarayıcıda SDK kullanın; aşağıdaki iskelet protokol sırasını gösterir.

const res = await fetch("/v1/realtime/tickets", {
  method: "POST",
  credentials: "include",
  headers: { "x-crx-env": environmentId, "x-csrf-token": csrfToken },
});
const { ticket } = await res.json();
const ws = new WebSocket(`wss://${location.host}/v1/realtime/ws`, ["openplay.realtime.v1"]);
ws.onopen = () => ws.send(JSON.stringify({ type: "Authenticate", protocolVersion: 1, ticket }));
ws.onmessage = (event) => {
  const frame = JSON.parse(event.data as string);
  if (frame.type === "Ready") {
    // Join gönderin, frame.sessionEpoch'u Input çerçeveleri için saklayın.
  }
};

Sıradaki adım

Bağlantı koptuğunda imleç ve epoch ile nasıl devam edeceğiniz, limitler ve sıkıştırma için yeniden bağlanma ve limitler.