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çevedeprotocolVersion: 1bulunur; başka değerUnsupportedProtocolhatası 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.
typealanı çerçeve türünü belirler vePascalCase’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,i64aralığında. Ayrıntı: sayılar ve kodlama.protocolVersionveschemaVersionküçü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, sonraAuthenticatedışındaki çerçeveler geçerlidir; yanlış fazdaki çerçeveUnexpectedFramealı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ık | Zorunlu | Not |
|---|---|---|
x-crx-env | evet | Oyuncunun ortamı. Oturumun ortamıyla uyuşmalıdır. |
Origin | evet | Ortamı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 çerezi | yalnı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:
403ve düz metinpermission denied. Modül kapalıysa (plugins)404ve{"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.v1istenmelidir; sunucu bunu seçer. İstenmezse yükseltme reddedilir.Originzorunludur 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 metinunauthorized); modül kapalıysa404. - 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"}
| Alan | Anlam |
|---|---|
connectionId | Bu soketin sunucu kimliği. |
playerId | Doğrulanmış oyuncu. |
sessionEpoch | Bu 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. |
authorityGeneration | Ortamı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"}}
| Alan | Tür | Anlam |
|---|---|---|
requestId | UUID | Deneme kimliği. |
pluginId | metin | Modülün sahibi eklenti (eklentiler). |
moduleId | metin | Eklentinin tanımladığı realtime modülü; eklentinin şema belgesindeki modules bölümünde listelenir. |
target.kind | metin | PlayerSelf, Room, Scene ya da InterestSet. |
target.roomId / sceneId / interestSetId | UUID | Hedefin kimliği (kind’a göre; PlayerSelf kimlik taşımaz). |
cursor | UUID, 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}}
| Alan | Anlam |
|---|---|
messageType, schemaVersion | Modülün şema belgesindeki girdi tanımıyla birebir eşleşmelidir. |
subscriptionId | Joined’dan gelen abonelik. |
sessionEpoch | Ready’deki değer. |
inputSeq | Artan 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. |
payload | Eklentinin 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"}}
| Alan | Anlam |
|---|---|
idempotencyKey | Zorunlu 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. |
moduleId | Oda kapsamlı komutlarda modül bağlamı; yoksa null. |
payload | Eklentinin 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
type | Ne zaman | Alanlar |
|---|---|---|
Ready | Authenticate başarılı | protocolVersion, connectionId, playerId, sessionEpoch, authorityGeneration |
Joined | Join yetkilendirildi | requestId, subscriptionId, target |
Left | Leave işlendi | requestId, subscriptionId |
CommandResult | Command kesinleşti | requestId, messageType, schemaVersion, payload |
Ack | Input işlendi | requestId, inputSeq, status, revision (isteğe bağlı) |
Snapshot | Geçici durum yayını | subscriptionId, resourceId, messageType, schemaVersion, streamEpoch, revision, payload |
DurableRecovery | Kalıcı akış için kurtarma bariyeri | subscriptionId, cursor, barrierSequence, coveredEventIds, snapshot (boş olabilir) |
DurableEvent | Kalıcı, sıralı eklenti olayı | subscriptionId, streamId, sequence, eventId, messageType, schemaVersion, cursor, payload |
Pong | Ping yanıtı | nonce |
Error | Hata | requestId (varsa), code, retryable |
Ack durumları
status | Anlam | Ne yapmalı |
|---|---|---|
Applied | Girdi durumu ilerletti. revision yeni revizyondur. | — |
Replayed | Aynı sıra numarası ve içerik zaten işlenmişti. | Yoksayın; güvenli tekrar. |
Stale | Sı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. |
Mismatch | Aynı sıra numarası farklı içerikle geldi. | İstemci hatasıdır; sıra numarasını yeniden üretmeyin, tekrar etmeyin. |
ResyncRequired | Sı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
Snapshotgeçici durumdur:streamEpoch(sunucu önbelleği yeniden başladığında artar) verevision(monoton) birlikte sıralanır. AynıstreamEpochiçinde daha eski birrevision, yeni durumu ezmemelidir.streamEpochdeğişmişse yeni bir akış başlamıştır; eskirevisionile karşılaştırmayın.DurableRecoverykalıcı akışta taban çizgisidir:cursorbu noktaya bağlı yeni imleçtir,barrierSequenceanlık görüntünün ve tekrar oynatmanın kapsadığı akış başıdır,coveredEventIdsanlık görüntünün zaten içerdiği olay kimlikleridir (bunları ayrıca uygulamayın).snapshotyalnız tazeJoin’de dolu gelir;cursorile devam ederken yoktur.DurableEvent’lersequencesırasıyla gelir;eventIduçtan uca tekilleştirme anahtarıdır,cursoro olaya kadar ilerlemiş yeni imleçtir. İmleci saklayın: yeniden bağlanırkenJoin.cursorolarak 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.
code | Ne zaman | retryable | Soket |
|---|---|---|---|
UnexpectedFrame | Fazına uymayan çerçeve (ör. kimlik doğrulamadan önce Join, ya da ikinci Authenticate) | hayır | kapanır |
UnsupportedProtocol | protocolVersion 1 değil | hayır | kapanır |
InvalidPayload | Bozuk 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 |
AuthenticationFailed | Bilet geçersiz/kullanılmış/süresi dolmuş, 5 sn zaman aşımı, oturum iptal edildi | hayır | kapanır |
PermissionDenied | Hedef, modül ya da işlem için yetki yok; yasaklı hesap | hayır | açık kalır |
PayloadTooLarge | Çerçeve bütçesini aştı (4 KiB kimlik doğrulama, 16 KiB sonrası) | hayır | kapanır |
CapacityExceeded | Hı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 |
Unavailable | Bağımlılık geçici kullanılamıyor ya da yerleşim hazır değil | genellikle 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ı
| Kod | Neden | Anlam |
|---|---|---|
4100 | idle_timeout | Oyuncu etkinliği gelmedi; boşta kapanma (ayrıntı). Tipli Error çerçevesi olmaz. |
1008 | module_disabled | plugins modülü kapatıldı. |
1013 | unavailable | Modü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.