Skip to content

🇬🇧 English | 🇺🇦 Українська

← До змісту

1. Вступ до NATS

Цей розділ — не про плагін. Він про сам NATS: що це за система, якими словами вона розмовляє і навіщо в ній два різні режими — Core і JetStream. Якщо ви вже знайомі з NATS, сміливо переходьте одразу до 2. Швидкого старту. Якщо ні — прочитайте цей розділ повністю: усі наступні розділи спираються на терміни, введені тут.


Що таке NATS

NATS — це брокер повідомлень: окрема програма (сервер), яка приймає повідомлення від одних клієнтів і роздає їх іншим. Ваша гра — теж клієнт, який підключається до цього сервера звичайним TCP-з'єднанням.

Навіщо взагалі окремий сервер, якщо актори в Unreal і так можуть викликати функції одне в одного? Бо NATS вирішує три задачі, які на прямих викликах не працюють:

  • Учасники не повинні знати одне про одного. Сервер гри публікує подію game.match.finished — і йому байдуже, чи є зараз хоч один підписник. Аналітика, логер і система досягнень підпишуться на цю подію незалежно одне від одного, кожен своїм екземпляром гри чи окремим сервісом.
  • Учасники можуть бути в різних процесах і на різних машинах. Гравець на одному комп'ютері, dedicated server — на іншому, сервіс аналітики — третій процес. NATS-сервер — єдина точка, до якої підключається кожен.

Сам плагін нічого не реплікує по мережі Unreal — див. попередження в FAQ → «Чи плагін реплікує щось мережею Unreal?». - Публікатор не чекає на підписників. Publish повертає керування одразу — сервер сам розносить копію повідомлення всім, хто підписаний, паралельно, без черги запит-відповідь на кожного.

Subject: адреса повідомлення

Усе в NATS адресується subject — рядком з токенів, розділених крапкою: game.events.player.join, chat.lobby.42, orders.created. Це не файлова система й не URL — просто ієрархічна адреса без наперед визначеної структури: скільки токенів і що вони означають, вирішуєте ви самі в межах свого проєкту.

Публікатор завжди вказує точний subject. Підписник може вказати точний subject або шаблон із двома спеціальними символами:

Символ Значення Приклад Підходить Не підходить
* Рівно один токен game.events.* game.events.join game.events.player.join
> Один або більше токенів, до кінця game.events.> game.events.join, game.events.player.join game.other

> має сенс лише останнім токеном шаблону. Приклад для цілої гри:

game.events.player.join       ← конкретна подія
game.events.player.*          ← усі одиночні події гравця (join, leave, death — не damage.taken)
game.events.>                 ← геть усе під game.events, будь-якої глибини

Один і той самий підписник може одночасно отримувати повідомлення від кількох публікаторів на різних subject — досить підписатися на спільний шаблон.

Publish/Subscribe: базовий патерн

Це основа всього NATS. Публікатор надсилає повідомлення на subject; кожен, хто зараз підписаний на цей (або відповідний шаблону) subject, отримує свою копію.

Publisher                    NATS-сервер                  Subscriber A (subject: chat.>)
   │                              │                              │
   ├─ Publish "chat.lobby.1" ────►│                              │
   │                              ├──────── копія ───────────────►│  отримав
   │                              │
   │                              │                        Subscriber B (subject: chat.lobby.2)
   │                              │                              │
   │                              │            (не отримав — інший subject)

Найважливіший наслідок цієї моделі: якщо на subject немає жодного підписника в момент публікації, повідомлення просто зникає. NATS Core нічого не зберігає й нікому нічого не винен — це «fire-and-forget» у буквальному сенсі. Підписник, що приєднався на секунду пізніше, вже нічого не отримає. Якщо вам потрібно, щоб повідомлення дочекалося підписника, який ще не підключився, — це причина, з якої існує JetStream (нижче).

Request/Reply: коли потрібна відповідь

Publish/Subscribe — однобічний потік. Коли потрібна саме відповідь на конкретний запит («дай дані гравця за ID»), NATS не додає новий примітив — той самий Publish/Subscribe використовується хитріше:

  1. Запитувач створює тимчасовий subject-«скриньку» (inbox), унікальний для цього запиту.
  2. Запитувач підписується на цю скриньку.
  3. Запитувач публікує запит на цільовий subject, вказуючи скриньку як reply-to.
  4. Відповідач, отримавши повідомлення, бачить reply-to і публікує відповідь саме туди.
  5. Запитувач отримує відповідь на своїй скриньці — або спливає тайм-аут, якщо ніхто не відповів.
Requester                         NATS                          Responder
   │                                │                                │
   ├─ Subscribe "_INBOX.abc123" ───►│                                │
   ├─ Publish "svc.users.get"       │                                │
   │  reply-to="_INBOX.abc123" ────►│──────── доставлено ────────────►│
   │                                │                                │  обробив запит
   │                                │◄─── Publish "_INBOX.abc123" ────┤
   │◄──────── доставлено ───────────┤                                │
   │  відповідь отримана           │                                │

Плагін ховає всю цю механіку за одним викликом — Request Async — детальніше в 5. Основний обмін повідомленнями.

Заголовки: метадані поруч з даними

Окрім самих даних, повідомлення може нести заголовки — пари ключ-значення, схожі на заголовки HTTP: тип вмісту, ідентифікатор трасування, версія схеми. Дані й заголовки передаються окремо, тому парсити заголовки з тіла повідомлення не треба.

Core проти JetStream: два режими одного сервера

Усе вище — це NATS Core: найпростіший, найшвидший режим, без збереження на диск чи в пам'ять понад миттєву доставку. Той самий сервер, за бажання, вмикає й другий режим — JetStream: шар персистентності поверх Core.

Core JetStream
Що зберігається Нічого. Немає підписника — повідомлення втрачено Повідомлення потрапляють у стрім (Stream) і лишаються там
Хто отримує Лише ті, хто підписаний у момент публікації Будь-хто, хто створить споживача (Consumer) — навіть через годину після публікації
Підтвердження доставки Немає Споживач підтверджує кожне повідомлення (ACK); без підтвердження — повторна доставка
Швидкість Найвища Трохи повільніша — сервер записує повідомлення, перш ніж підтвердити публікацію
Типове застосування Живі події: рух гравця, чат у реальному часі, стан лобі Усе, що не можна втратити: замовлення, досягнення, журнал подій, чергу завдань

Важливо розуміти: JetStream не замінює Core, а додається до нього. Subject orders.created можна публікувати і без жодного стріму — просто ніхто його не збереже. Щойно ви створюєте стрім, налаштований на цей subject, сервер починає його зберігати — клієнтський код публікації при цьому не змінюється.

Стрім (Stream): де зберігаються повідомлення

Стрім — це іменоване сховище повідомлень, схоже на таблицю в базі даних. Ви створюєте стрім один раз, вказавши:

  • ім'я — наприклад ORDERS;
  • subject(и), які він захоплює — наприклад orders.>. Усе, що публікується на такий subject, автоматично потрапляє в стрім, доки він існує;
  • скільки зберігати — за кількістю повідомлень, за розміром чи за часом;
  • де зберігати — у пам'яті (швидко, зникає при перезапуску сервера) чи на диску (переживає перезапуск).

Публікатор нічого не знає про стріми — він просто публікує на subject. Сервер сам вирішує, чи є стрім, що захоплює цей subject, і якщо є — зберігає копію.

Споживач (Consumer): курсор читання зі стріму

Стрім зберігає повідомлення, але сам їх нікому не надсилає — для читання потрібен споживач, курсор із власною позицією в стрімі. Кілька споживачів можуть незалежно читати той самий стрім, кожен зі своєї позиції.

Є два режими доставки:

  • Push-споживач — сервер сам надсилає повідомлення вам, щойно вони з'являються. Схоже на звичайну підписку Core, тільки з підтвердженням і гарантією, що нічого не загубиться.
  • Pull-споживач — ви самі просите: «дай мені до 10 повідомлень». Зручно, коли швидкість обробки має контролювати саме споживач — наприклад, черга завдань, яку сервер обробляє пакетами.

Після обробки кожного повідомлення споживач має його підтвердити (ACK). Без підтвердження протягом заданого часу сервер вважає, що повідомлення не оброблено, і надішле його ще раз.

Key-Value: сховище поверх стріму

Key-Value (KV) — надбудова над JetStream, яка виглядає як звичайний словник «ключ → значення»: Put("player_42", "..."), потім Get("player_42"). Під капотом це той самий стрім, де кожен запис — окреме повідомлення на службовому subject виду $KV.<бакет>.<ключ>, а «остання версія ключа» — просто останнє повідомлення з таким subject. Звідси й корисні побічні ефекти:

  • Ревізії. Кожен запис ключа отримує номер версії, що зростає — можна перевіряти, чи ключ не змінили паралельно (оптимістична конкурентність).
  • Watch. Можна підписатися на зміни ключа чи групи ключів у реальному часі — це те саме Core-підписування на службовий subject, просто плагін ховає деталі.
  • Історія. Якщо бакет налаштовано зберігати кілька версій, можна прочитати попередні значення ключа.

Що далі

Тепер, коли термінологія на місці, розділ 2. Швидкий старт проведе вас через перше підключення, першу публікацію й перший стрім — уже конкретними нодами й кодом цього плагіна.


Далі: 2. Швидкий старт