---
title: 'Adat és hálózat'
url: 'https://wiki.teletypegames.org/hu/engines/rubbs/data-and-network'
markdown: 'https://wiki.teletypegames.org/hu/engines/rubbs/data-and-network.md'
lang: hu
date: '2026-11-02'
description: 'Amit a tartalmas boardjaink korábban mind maguknak írtak meg, az a 0.33.0 és az 1.0.0 óta a rubbs része: egységes naplóformátum, háttérmunka, SQLite-fájl migrációkkal, korlátok közé szorított HTTP, cache, rate limit, ütemezett Wikipédia-kliens, vezérlőkaraktermentes szöveg, és a szabály, hogy hol …'
taxonomy:
  tag:
    - reference
---

# Adat és hálózat

> Amit a tartalmas boardjaink korábban mind maguknak írtak meg, az a 0.33.0 és az 1.0.0 óta a [rubbs](https://wiki.teletypegames.org/hu/engines/rubbs) része: egységes naplóformátum, háttérmunka, SQLite-fájl migrációkkal, korlátok közé szorított HTTP, cache, rate limit, ütemezett Wikipédia-kliens, vezérlőkaraktermentes szöveg, és a szabály, hogy hol kezeljük egy hiba következményeit. Mindegyik elem néhány sorral bevethető; a teljes szignatúrák az [API-referenciában](https://wiki.teletypegames.org/rdoc/bbs/latest/) vannak.

# A config-mezők

A `BBS.configure` alábbi mezői tartoznak ide (a többi az [Első lépések](https://wiki.teletypegames.org/hu/engines/rubbs/getting-started) és a [Telnet és a store](https://wiki.teletypegames.org/hu/engines/rubbs/telnet-and-store) oldalon van):

| Mező | Alapértelmezés | Jelentés |
|---|---|---|
| `log_tag` | `ENV['BBS_LOG_TAG']` vagy `bbs` | Amivel minden naplósor kezdődik: a board technikai neve, pl. `feedia-bbs`. |
| `user_agent` | `rubbs/VERSION (+https://teletypegames.org)` | A `BBS::HTTP` kérések `User-Agent`-je. |
| `max_sessions_per_ip` | `ENV['BBS_MAX_SESSIONS_PER_IP']` vagy `6` | Egy nyilvános címről egyszerre futó hívások; a következőnek szól, és bont (a naplóban `BBS crowded`). |
| `ai_concurrency` | `2` | A folyamatban egyszerre futó `BBS::AI::Client` kérések; a következő legfeljebb 5 s-ot vár, aztán `BBS::AI::Busy`-t kap (lásd [AI-karakterek](https://wiki.teletypegames.org/hu/engines/rubbs/ai)). |

```ruby
BBS.configure do |c|
  c.log_tag    = 'feedia-bbs'
  c.user_agent = 'Feedia BBS (+https://teletypegames.org)'
end
```

# Naplózás és háttérmunka

## Naplózás

Minden sor, amit a könyvtár vagy a board a naplóba ír, a `BBS.log(tag, message)`-en át megy a stderr-re, így tizenegy board sorai egy naplóban is egyformán olvashatók. Ha az üzenet kivétel, az osztálya és az üzenete kerül a sorba.

```ruby
BBS.log('catalog refresh', 'done in 3.2 s')   # feedia-bbs: catalog refresh: done in 3.2 s
BBS.log('catalog refresh', error)             # feedia-bbs: catalog refresh: SocketError: …
```

A könyvtár saját sorai is ezt a formát követik: `session`, `door …`, `gateway …`, `store`, `BBS busy`, `BBS crowded`. Egy `BBS::DiscordWebhook` `label:`-je is a log tag lett, úgyhogy már nem kell bele a board neve.

## Háttérmunka

Ami a hívók útjából kifér (az induláskor betöltött index, az óránként frissülő katalógus), az egy `BBS::Background` szálban fut. A hibát naplózza, és `retry_after` (5) másodperc múlva újrapróbálja, a szünetet `max_retry`-ig (300) duplázva: egy leállt szolgáltatás így pár naplósorba kerül, nem egy összeomlott boardba vagy egy pörgő ciklusba.

```ruby
BBS.background('index') { wiki.load_index }                         # until it succeeds once
BBS.background('catalog refresh', every: 3600) { library.refresh }  # every hour
```

Mindkettő a `Thread`-et adja vissza; a `BBS::Background.new(name, …) { … }.run` ugyanezt a hívó szálban futtatja (egy tesztben, egy parancssori eszközben).

# Tárolás

## `BBS::SQLite`

Egy SQLite-fájl és az egyetlen kapcsolata, amelyen a hívók szálai egy zár mögött osztoznak: WAL mód (a board egy parancssori eszköze közben is használhatja a fájlt), busy timeout, bekapcsolt idegen kulcsok. A séma migrációk listája, a `PRAGMA user_version` számozza őket: egy fájl csak azokat futtatja, amelyeken még nem ment át, mindegyiket egy tranzakcióban. A migráció SQL-string vagy lambda; a már lefutottat sose írd át, inkább fűzz hozzá újat.

```ruby
DB = BBS::SQLite.new('data/board.sqlite3')        # creates data/ when needed
DB.migrate(
  'CREATE TABLE posts (id INTEGER PRIMARY KEY, title TEXT NOT NULL)',
  'ALTER TABLE posts ADD COLUMN body TEXT',
  ->(db) { db.insert('INSERT INTO posts (title) VALUES (?)', 'Welcome') }
)
DB.query('SELECT * FROM posts')                    # => [{ 'id' => 1, 'title' => 'Welcome', 'body' => nil }]
DB.get('SELECT * FROM posts WHERE id = ?', 1)      # the first row or nil
DB.insert('INSERT INTO posts (title) VALUES (?)', 'Hi')   # => the new rowid
DB.change('DELETE FROM posts WHERE id = ?', 2)     # => rows changed
DB.transaction { … }                               # a nested call joins the outer one
```

## `BBS::Store`

A Flow `persist` lépésének egytáblás, kulcs szerint upsertelő store-ja a 0.33.0 óta `BBS::SQLite`-ra épül. A `.csv` útvonal a mellette lévő `.sqlite3` fájlt jelenti, egy régebbi board CSV-jét pedig az első használatkor beimportálja (a hibás sorokat egy `.rejected` fájlba teszi félre); a `store.open!` ezt induláskor elvégzi. Lásd [Telnet és a store](https://wiki.teletypegames.org/hu/engines/rubbs/telnet-and-store).

# Hálózat

## `BBS::HTTP`

Korlátok közé szorított kérések a szolgáltatások felé, amelyeket a board megmutat. Minden hiba `BBS::HTTP::Error`-t dob, amelynek az üzenete a címet a query string nélkül nevezi meg (így token nem kerül a naplóba); a `max_bytes`-nál nagyobb törzs az alosztályát, a `BBS::HTTP::TooLarge`-ot dobja. A nem 2xx státuszokat visszaadja, nem dob rájuk.

```ruby
response = BBS::HTTP.get('https://example.org/feed.xml', headers: { 'Accept' => 'application/rss+xml' })
response.ok?       # => true for 2xx
response.status    # => 200
response.body      # => binary String

BBS::HTTP.post(url, body: URI.encode_www_form(query: q),
               headers: { 'Content-Type' => 'application/x-www-form-urlencoded' })

File.open(path, 'wb') { |file| BBS::HTTP.get(url, to: file, max_bytes: 64 * 1024 * 1024) }
```

| Opció | Alapértelmezés |  |
|---|---|---|
| `open_timeout` / `read_timeout` | 5 / 15 s | kapcsolatonként / olvasásonként |
| `deadline` | 30 s | az egész kérés, az átirányításokkal együtt |
| `max_bytes` | 4 MiB | a törzs folyamosan jön, és e fölött a kérés elbukik |
| `redirects` | 3 (`post`: 0) | csak `http`/`https` címre követi |
| `public_only` | `true` | elutasítja a loopback, privát, link-local, CGNAT, ULA és más nem nyilvános címre feloldódó hostokat; az ellenőrzött címre kapcsolódik |
| `to` | `nil` | egy IO, amelybe a 2xx törzs érkezés közben íródik (a `body` ilyenkor `nil`) |

## `BBS::Cache`

TTL, a legrégebben használt elem kidobása, bájtkorlát, kulcsonként egyetlen számítás (az ugyanazt a kulcsot egyszerre hiányoló hívók megvárják, és osztoznak az eredményén), és ha a számítás hibát dob, a lejárt értéket adja.

```ruby
ARTICLES = BBS::Cache.new(ttl: 600, max_entries: 500, max_bytes: 8 * 1024 * 1024)
ARTICLES.fetch([lang, title]) { repository.article(lang, title) }

BBS.background('catalog', every: 3600) { CATALOG.refresh(:all) { repository.load } }
```

A `refresh(key) { … }` csak akkor cseréli le a tárolt értéket, ha a blokk sikerrel fut, így egy háttérfrissítés sosem üríti ki a cache-t. Továbbá: `delete(key)`, `clear`, `size`.

## `BBS::RateLimit`

Kulcsonkénti csúszó ablak, thread-safe; a csendes kulcsokat elfelejti. A `caller_key(session)` interneten át a hívó IP-címe, egy helyi proxy (bbs-web) mögött pedig a vonal session-azonosítója, mert ott mindenki ugyanarról a címről jön.

```ruby
TIPS = BBS::RateLimit.new(limit: 3, per: 3600)
key  = BBS::RateLimit.caller_key(session)
TIPS.allow?(key)   # => true, and counted; false when over the limit
TIPS.wait(key)     # => seconds until the next event is allowed

PER_CALLER = BBS::RateLimit.new(limit: 5, per: 600)
PER_BOARD  = BBS::RateLimit.new(limit: 100, per: 600)
BBS::RateLimit.allow_all?([[PER_CALLER, key], [PER_BOARD, :all]])   # counted in all or none

LOGINS = BBS::RateLimit.new(limit: 10, per: 600, store: DB, name: 'logins')
return locked_out unless LOGINS.room?(key)
LOGINS.record(key) unless password_ok?(name, password)
```

- Az `allow_all?` egyszerre több korlátot néz (a hívóét és az egész boardét): az eseményt vagy mindegyikbe beszámolja, vagy ha valamelyikben nincs hely, egyikbe sem.
- A `room?` csak ellenőriz, a `record` csak számol: arra az esetre, amikor egy esemény csak akkor számít, ha rosszul sült el (rossz jelszó).
- `store:`-ral (egy `BBS::SQLite`) az események a `rate_limit_events` táblájába kerülnek `name:` alatt, így egy belépési tiltás az újraindítást is túléli.

## `BBS::Wikipedia`

A Wikipédia Action API-ja úgy, ahogy egy board kérdezi: `BBS::HTTP`-n át, a board User-Agentjével (anélkül a Wikipédia 403-at adhat), ütemezve, hogy a hívók együtt se küldjenek másodpercenként `rate`-nél több kérést; amelyik kérés 3 s-nál tovább várna a sorára, el sem megy. A válasz feldolgozva jön (a `format=json` és a `formatversion=2` magától hozzáadódik); a hiba `BBS::Wikipedia::Error`, ami egy `BBS::HTTP::Error`. Az Alexandria BBS és a Commodex BBS használja.

```ruby
WIKIPEDIA = BBS::Wikipedia.new(user_agent: 'Alexandria BBS (+https://teletypegames.org)', rate: 5)
WIKIPEDIA.get('en', action: 'query', list: 'search', srsearch: 'Commodore 64')
# => { "query" => { "search" => [...] } }
```

## A gateway védelme és fordítása

A `BBS::Gateway` (lásd [Doorok](https://wiki.teletypegames.org/hu/engines/rubbs/doors)) egyszer oldja fel a hostot, és az 1.0.0 óta csak nyilvános címet hív: egy listázás, amely `127.0.0.1`-et vagy egy privát címet ad meg, nem teheti a boardot a saját hálózatába vezető átjáróvá. Az a board, amely a saját szomszédait tárcsázza, ezt ki is mondja; egy C64 pedig `translate: true`-val ASCII- és ANSI-terminálokra készült boardot is felhívhat (0.34.0 óta).

```ruby
BBS::Gateway.new(host: 'host.docker.internal', port: 2327, name: 'Feedia', public_only: false)
BBS::Gateway.new(host: board.host, port: board.port, translate: !board.petscii?).call(session)
```

# Szöveg és címek

- `BBS::Text.clean(text)`: érvényes UTF-8, C0 vezérlőkarakterek (a tab és a soremelés marad), DEL, C1 és kétirányú felülíró karakterek nélkül. `keep_newlines: false`-szal minden sorvég szóköz lesz, így két sor szavai nem ragadnak össze.
- `BBS::Text.line(text)`: egyetlen tiszta sor, a whitespace-sorozatok (a sorvégek és a tabok is) egy szóközzé válnak, a két végén nincs szóköz. Címhez, névhez, összefoglalóhoz.
- `BBS::Text.printable?(char)`: minden beviteli mező szabálya.
- `BBS::Address.public?(ip)` (a nyilvános interneten van) és `BBS::Address.local?(ip)` (privát, loopback, link-local).

A frame buffer sem tárol vezérlőkaraktert, így egy szolgáltatástól jött szöveg nem mozgathatja és nem színezheti át a hívó képernyőjét.

# Hol kezeljük a hibát

Egy board rétegei egy szolgáltatás hibáját egyetlen helyen kezelik:

- a **repository** (`lib/domain/repository`) beszél a külvilággal, és a hibáit továbbengedi: a `BBS::HTTP::Error`-t (és a `BBS::Wikipedia::Error`-t) úgy, ahogy jön, vagy a board egy domain-hibáját, ha magával a válasszal van baj. Nem alakítja őket `nil`-lé vagy üres listává, és nem ír szöveget a hívónak;
- a **service** dönti el, mit lát a hívó: elkapja ezeket a hibákat, ír egy `BBS.log` sort, és azt adja vissza, amit a UI megmutat (egy cache-elt választ, egy üres eredményt, egy saját üzenetet);
- a **UI** sosem kap el `BBS::HTTP::Error`-t; azt mutatja, amit a service adott.

Így URL-t vagy tokent tartalmazó hibaüzenet sosem jut el a hívóig, és minden hiba egyszer kerül a naplóba.

```ruby
class Library
  def search(query)
    repository.search(query)
  rescue BBS::HTTP::Error => e
    BBS.log('search', e)
    []
  end
end
```

# Lásd még

- [rubbs](https://wiki.teletypegames.org/hu/engines/rubbs) — az áttekintés
- [Telnet és a store](https://wiki.teletypegames.org/hu/engines/rubbs/telnet-and-store) — a session, a korlátok és a `BBS::Store`
- [Doorok](https://wiki.teletypegames.org/hu/engines/rubbs/doors) — gatewayek, ZMODEM és XMODEM
- [AI-karakterek](https://wiki.teletypegames.org/hu/engines/rubbs/ai) — a modellkliens és a párhuzamossági korlátja
- [Változásnapló](https://wiki.teletypegames.org/hu/engines/rubbs/changelog) — melyik elem mikor érkezett

'; var prevLevel = 2; for (var i = 0; i < headings.length; i++) { var h = headings\[i\]; var text = h.textContent.trim(); var slug = text.toLowerCase() .replace(/\[áàâä\]/g,'a').replace(/\[éèêë\]/g,'e').replace(/\[íìîï\]/g,'i') .replace(/\[óòôöő\]/g,'o').replace(/\[úùûüű\]/g,'u') .replace(/\[^a-z0-9\]+/g, '-').replace(/^-|-$/g, ''); if (!slug) slug = 'heading-' + i; if (used\[slug\]) { slug += '-' + i; } used\[slug\] = true; h.id = slug; var level = parseInt(h.tagName.charAt(1)); var cls = level > 2 ? ' class="toc-child"' : ''; html += '- [' + text + '](#'%20+%20slug%20+%20')
'; } html += '
'; toc.innerHTML = html; // Scroll-spy: highlight current heading var links = toc.querySelectorAll('a'); var ids = \[\]; for (var j = 0; j < links.length; j++) ids.push(links\[j\].getAttribute('href').slice(1)); function updateActive() { var current = ''; for (var k = 0; k < ids.length; k++) { var el = document.getElementById(ids\[k\]); if (el && el.getBoundingClientRect().top <= 80) current = ids\[k\]; } for (var m = 0; m < links.length; m++) { links\[m\].parentNode.classList.toggle('active', links\[m\].getAttribute('href') === '#' + current); } } window.addEventListener('scroll', updateActive, { passive: true }); updateActive(); })();

---

## Navigation

- Parent: [RuBBS](https://wiki.teletypegames.org/hu/engines/rubbs.md)
- Previous: [Commodore 64](https://wiki.teletypegames.org/hu/engines/rubbs/commodore.md)
- Next: [Doorok](https://wiki.teletypegames.org/hu/engines/rubbs/doors.md)
