Перейти к основному содержанию
Теперь оно влито (merged) 🎉

Основы

Чтобы подключиться к серверу, нам нужно иметь все файлы контента, которые указывает сервер. Это все текстуры, prototype, сборки кода и всё остальное, что должно быть загружено engine. Сервер предоставляет следующую информацию о необходимой ему сборке. Эта информация передаётся через /info в HTTP status API. Информация, предоставляемая серверами, выглядит следующим образом:
  • Fork ID и Version: Используются исключительно как эвристика для определения того, какие старые версии следует удалять в первую очередь из ContentDB launcher. Не используются для критически важных операций безопасности (всегда проверяются криптографическими хешами фактического игрового контента). Fork ID — это, по сути, «имя кодовой базы», а fork version — идентификатор версии.
  • Engine version: версия engine для использования при запуске и разрешении модулей engine.
  • Zip information: Используется для zip-загрузок контента:
    • Zip download URL: URL для загрузки zip-файла, содержащего весь контент сервера.
    • Zip SHA-2561 hash: ожидаемый SHA-256 хеш указанного zip-файла.
  • Manifest information: Используется для загрузок контента на основе манифеста:
    • Manifest URL: URL для загрузки манифеста контента.
    • Manifest BLAKE2b hash: хеш манифеста.
    • Manifest download URL: URL для запроса загрузки контента.
Существует два основных режима работы launcher при управлении и загрузке контента: на основе zip и на основе манифеста. Zip-based — это устаревшая модель. Она не поддерживает дельта-обновления и существует для обратной совместимости и простоты. Manifest-based может поддерживать дельта-обновления (т.е. загрузку только изменённых файлов).

ContentDB Launcher

Отслеживаемые launcher файлы игрового контента хранятся в виде blob-объектов в SQLite БД с режимом WAL. Идентичность файлов сравнивается исключительно по BLAKE2b-хешу содержимого, и может опционально применяться сжатие ZSTD, если это экономит место. Эти blob-объекты отслеживаются в таблице Content. Каждая «версия» контента, который launcher загрузил и в данный момент хранит, сохраняется в таблице ContentVersion. Весь ContentManifest (= список файлов) хранится для каждой версии, содержа путь к файлу ресурса и указывая, какой blob Content использовать. Эти ContentVersion также всегда хранят хеш манифеста (см. ниже) для идентификации своего контента. Они могут опционально хранить SHA-256 хеш zip для обратной совместимости с zip-загрузками. Индексируя фактические blob-объекты ресурсов по BLAKE2b-хешу и сохраняя только пути для каждого манифеста, мы можем дедуплицировать blob-объекты ресурсов как в рамках одной версии, так и между разными версиями и даже форками. Экономия места! Мы также отслеживаем, какая версия Robust и какие модули (включая их точные версии) необходимы для каждой версии, в таблице ContentEngineDependency. Это немного неудобно для launcher, потому что фактическая версия контента (идентифицируемая в первую очередь каким-либо хешем) на самом деле не связана с используемой версией engine: в конце концов, версия engine указывается сервером через status API. Она не содержится в загрузке контента (но список модулей для использования содержится, так как он хранится в манифесте контента). Решение заключается в том, что мы заставляем launcher дублировать версию и манифест, если одна и та же версия контента пытается использоваться с другой версией engine. При этом также приходится переразрешать версии модулей.

Загрузки через манифест

При использовании подхода на основе манифеста сервер сообщает «манифест контента», который содержит хеши всех файлов контента и их пути. Этот манифест, в свою очередь, снова хешируется, и этот хеш является окончательным источником истины для идентификации определённого набора контента. Этот манифест загружается и сравнивается при попытке подключения. Если у нас нет этого манифеста, мы загружаем его с сервера. Затем мы находим все blob-объекты, которых у нас ещё нет (по хешу), и запрашиваем их с сервера через указанный им manifest download URL. Эта загрузка через манифест использует простой бинарный формат запроса/ответа через HTTP POST2 по соображениям производительности. На момент написания этого текста SS14 имеет около 13 000 файлов в своём resource pack (я хочу сократить количество RSI, что помогло бы, но всё равно). Выполнение отдельных HTTP-запросов для каждого файла было бы абсурдом, даже с конвейеризацией запросов или прочим безумием. Бинарный протокол описан ниже.

Формат манифеста

Формат файлов манифеста контента выглядит следующим образом:
Robust Content Manifest 1
<BLAKE2b 256-битный хеш в верхнем регистре hex> <путь к файлу>
<BLAKE2b 256-битный хеш в верхнем регистре hex> <путь к файлу>
<BLAKE2b 256-битный хеш в верхнем регистре hex> <путь к файлу>
<BLAKE2b 256-битный хеш в верхнем регистре hex> <путь к файлу>
...
Да, всё так просто. Только одинарные новые строки, без дурацких CRLF. Заголовок сверху — просто версионный заголовок. Пожалуйста, с завершающей новой строкой. Сортируйте записи по полному пути к файлу в обычном порядке. Python-код для генерации этого манифеста из zip-файла:
import codecs
import hashlib
import io
import zipfile

def generate_manifest_hash(file: str) -> str:
    zip = zipfile.ZipFile(file)
    infos = zip.infolist()
    infos.sort(key=lambda i: i.filename)

    bytesIO = io.BytesIO()
    writer = codecs.getwriter("UTF-8")(bytesIO)
    writer.write("Robust Content Manifest 1\n")

    for info in infos:
        if info.filename[-1] == "/":
            continue

        bytes = zip.read(info)
        hash = hashlib.blake2b(bytes, digest_size=32).hexdigest().upper()
        writer.write(f"{hash} {info.filename}\n")

    manifestHash = hashlib.blake2b(bytesIO.getbuffer(), digest_size=32)

    return manifestHash.hexdigest().upper()
BLAKE2b-хеш этого файла используется как единый идентификатор для определения идентичности набора ресурсов.

Детали протокола запроса загрузки

Ладно, да, это немного сложнее, чем просто HTTP POST. Клиент выполняет HTTP GET манифеста контента по URL, указанному сервером. Сервер должен3 поддерживать сжатие контента через HTTP Accept-Encoding/Content-Encoding: launcher принимает zstd, brotli, gzip и deflate. ZSTD рекомендуется, так как это современная технология. Манифест — это просто текстовая конструкция, описанная выше. Для фактической загрузки контента сначала выполняется HTTP OPTIONS на URL, предоставленный сервером. Этот OPTIONS должен возвращать заголовки ответа X-Robust-Download-Min-Protocol и X-Robust-Download-Max-Protocol, которые launcher сможет использовать в будущем для обратной и прямой совместимости. Возможно, я излишне усложняю это. Текущая «версия протокола» — 1. После этого HTTP OPTIONS launcher отправляет POST-запрос на тот же URL. Запрос содержит заголовок X-Robust-Download-Protocol с текущей версией протокола, чтобы сервер мог его понять. В теле запроса находится полный список запрашиваемых файлов по протоколу, описанному ниже. Content-Type должен быть application/octet-stream. Клиент также снова отправляет Accept-Encoding, чтобы разрешить сжатие всего тела HTTP-ответа: сервер может решить следовать этому, если сочтёт компромисс оправданным.4 Тело запроса — это просто последовательность 32-битных LE индексов (с нуля) в манифесте контента, каждый из которых указывает на blob в манифесте для загрузки.5 Индексы не должны запрашиваться дважды в одном запросе. Тело ответа более сложное и в настоящее время имеет следующий формат:
<заголовок потока>:
    int32 LE поле флагов заголовка потока:
      бит 0 (pre-compress): если установлен, blob-ы потока индивидуально предварительно сжаты ZSTD.
ДЛЯ каждого файла в теле запроса:
    <заголовок файла>:
        int32 LE размер blob: Несжатый размер blob файла
        ЕСЛИ pre-compress установлен:
            int32 LE сжатый размер: Сжатый размер blob.
                                      Если ноль, blob не сжат, и следует использовать несжатый размер.
    <содержимое файла>:
        N байт содержимого файла, см. заголовок файла выше для размера

Бинарный формат

Zip-based загрузки

Zip-based подход частично предназначен для меньшей сложности и является старой моделью обновления. Сервер указывает zip-файл и его SHA256 хеш. Этот хеш проверяется и сравнивается, и zip-файл загружается при необходимости. Локальные файлы загружаются из zip-файла в ContentDB launcher. Хеш манифеста автоматически генерируется и сохраняется. Это сделано для обеспечения прямой совместимости с методами обновления на основе манифеста.

Методы дельта-обновлений

Внутрифайловые дельты (diff): мы могли бы использовать zstd с его поддержкой дельта-сжатия.

Будущие идеи

Сейчас манифесты довольно большие, даже при сжатии. Во многом это связано с тем, что 32-байтовые хеши по своей природе несжимаемы, а файлов огромное количество. Более продвинутая система могла бы лучше использовать Merkle Trees для уменьшения объёма манифеста, который необходимо отправлять. Это позволило бы загружать небольшие изменения в YAML-файлах размером в единицы килобайт вместо текущих 450+ KiB, без необходимости в явной системе дельт между версиями. Veloren реализует их инкрементальные обновления, используя HTTP range requests на стандартных CDN. Вероятно, это менее эффективно по сырой пропускной способности, но возможность использования обычных CDN трудно переоценить, поскольку наша система требует активного сервера. Это стоит изучить в будущем, если нам понадобится масштабировать производительность.

Footnotes

  1. Да, zip-хеши — SHA-256, все остальные хеши — 256-битные BLAKE2b. BLAKE2b считается гораздо быстрее, и это действительно сказывается на реальных примерах использования, поэтому используется для нового функционала. Zip-файлы используют SHA-256 для обратной совместимости.
  2. В идеале вместо POST стоило бы использовать что-то вроде HTTP QUERY, так как по сути это GET-with-request-body. Однако на момент написания QUERY ещё не стандартизирован и, вероятно, совершенно нигде не поддерживается.
  3. Видите, я использую язык в стиле RFC, как важный.
  4. Сжатие потока снижает использование пропускной способности ценой увеличения нагрузки на CPU сервера и launcher во время передачи. Обычно, если сервер использует сжатие потока, он не будет использовать индивидуально сжатые blob-ы. Из-за этого blob-ы также будут храниться менее компактно в ContentDB launcher после завершения загрузки.
  5. Это индексы в манифесте вместо прямых сырых хешей blob, потому что хеши заняли бы слишком много пропускной способности.
Последнее изменение 21 июня 2026 г.