Глава 33 / 51
Том 5 · Расширения, эксплуатация и практика

Расширение XTP2: говорим бинарное поверх TCP

TCP даёт поток байтов, XTP2 даёт границы сообщений. Глава ставит кадровую библиотеку (задел гл. 23) в EventData и получает протокольный сервис, устойчивый к склейкам и враждебным кадрам.

Обзор

Позиционирование: xtp2 — «кодек-примитив» (см. справочник API), глава — «технология сборки»: где живёт сеансовое состояние, как переносятся полупакеты, как рвутся битые кадры. Готовое изделие — практика 2.

33.1 Введение

Боль бинарных протоколов никогда не в «разработке формата кадра» — она в дисциплине реализации: обработка границ (склейки/полупакеты), проверка длины (враждебные сверхкадры), типобезопасность (время жизни представлений). xtp2 сделал все три дела библиотекой: 32-байтовый фиксированный заголовок + четыре типа сообщений (REQUEST/RESPONSE/PUSH/EVENT) + потоковый разборщик; вам остаётся кормить его байтами и обрабатывать сообщения. Предусловие: вариант build.bat xtp.

33.2 Кадры и сеансовый буфер

Сборка начинается с сеансовой структуры: обработчик на соединение + буфер остатков полупакетов (паттерн реестра гл. 21 в TCP-редакции):

Сеансовая структура (реестр + обработчик)
typedef struct XtpSession {
    xtp2_processor* pRx;      /* разборщик на соединение */
    xbuffer*        pPending; /* остаток полупакета (хвост, не переваренный feed) */
} XtpSession;
/* создаётся в Open (в реестр) · удаляется в Close (обработчик и буфер уничтожаются) */

Зачем pPending: xtp2_feed даёт максимум одно сообщение за вызов — одна доставка TCP может нести «полторы» и больше; остаток при XTP2_MORE ждёт следующего EventData.

33.3 Сборка протокола в EventData

Ядро сборки (официальный пример в сеансовой редакции)сигнатуры из lib/xtp2.h
static void EventData(XS_HostInfo* pHost, XS_StreamConn* pConn, xnetbuf* pBuffer)
{
    XtpSession* pS = SessionFind(pConn);      /* сеанс из реестра */
    xbuffer* pIn = pS->pPending;
    BufferAppendNetBuf(pIn, pBuffer);          /* цепочка буферов → сплошной буфер */

    for ( ;; ) {
        size_t iUsed = 0;
        xtp2_message tMsg;
        xtp2_result r = xtp2_feed(pS->pRx,
            BufferData(pIn), BufferSize(pIn), &iUsed, &tMsg);
        BufferConsume(pIn, iUsed);             /* съесть потреблённое */
        if ( r < 0 ) {                     /* битый кадр: залипающая ошибка */
            SessionDrop(pConn);                 /* разрыв: решение 33.4 */
            return;
        }
        if ( r == XTP2_MESSAGE ) {
            DispatchMessage(pConn, &tMsg);     /* представление «использовал — выбросил» */
            continue;                      /* в буфере могут быть ещё целые кадры */
        }
        break;  /* XTP2_MORE: хвост ждёт следующего раунда */
    }
}

Что только что произошло — все четыре пункта технологии сборки здесь: буфер остатков сшивает раунды, потребление точно по consumed, представления сообщений используются в раунде, ошибка сразу рвёт (у залипающей ошибки нет «попробовать ещё»). Направление ответа пакуется через xtp2_measure + encode и отправляется xrtNetStreamSend — id из REQUEST, возвращённый в RESPONSE, даёт связку.

33.4 limits и безопасность

  • Линия длины: max_packet (умолчание 1 МиБ) вступает в силу ещё на разборе заголовка — враждебный сверхкадр отвергается до чтения тела; память не обманешь заголовком «объявляю 4 ГБ»
  • Линия структуры: перекрёстная сверка длин таблицы параметров, проверки границ полей заголовка, беззнаковые преобразования status в обход поведения, зависящего от реализации
  • Политика разрыва: залипающая ошибка протокола значит, что потоку больше нельзя верить — проверенный fuzz-ом инвариант сквозного соответствия гарантирует «что декодируется, то легально»; наоборот, продолжать кормить после битого кадра — только множить ошибки. Разрыв + лог — самое честное обращение с бинарным протоколом
  • Линия времени: TCP idle_timeout ловит медленную атаку «подключился и шлёт ползаголовка»

33.5 Контракт

  • Сеанс: обработчик на соединение; буфер остатков сшивает раунды; терминал Close всё уничтожает единообразно.
  • Сообщения: представления из feed используются в раунде; хранить — сначала копируйте.
  • Ошибки: залипающая ошибка = разрыв; limits ужаты до реальных потребностей бизнеса.

Частые ошибки

consumed проигнорирован, весь раунд скармливается заново
Симптом
Сообщения обрабатываются дважды или разбор смещается.
Причина
consumed у feed сообщает, сколько байтов съедено в этом вызове — не потребив их, следующий раунд разберёт с начала. Операции с буфером обязаны двигаться строго по consumed.
reset-повтор после битого кадра
Симптом
После reset, очистившего ошибку, продолжение того же потока даёт перепутанные сообщения.
Причина
reset лишь сбрасывает состояние разборщика — он не чинит уже смещённый поток байтов; вторая половина битого сообщения читается как новый заголовок. Верное применение reset — «переиспользовать объект обработчика» (новое соединение/новый поток).

Упражнения

БазовыйЭхо-кадр

Реализуйте минимальный сервис PING REQUEST → PONG RESPONSE (клиент собирает кадры через xtp2_encode).

Приёмка: id возвращается верно; три кадра одной посылкой все отвечены (склейка).
ПродвинутыйБомбардировка фрагментами

Клиент режет кадр на одно-байтовые куски и шлёт, проверяя буфер полупакетов.

Приёмка: после побайтового финиша одного сообщения сервер выдаёт его ровно один раз.
ВызовТест враждебных кадров

Постройте три атаки — сверхлимитная длина в заголовке, обрезанный кадр, нелегальный magic — и убедитесь: память сервера ровная, разрывы чистые.

Приёмка: все три рвут соединение сразу; RSS ровная на всём пути; нормальные клиенты не задеты.

Итоги

ТезисВывод
Позиционированиекодек-примитив: формат кадра + дисциплина разбора в виде библиотеки
Сборкасеанс = обработчик + буфер остатков; потребление точно по consumed
Сообщенияпредставления feed — в раунде; id REQUEST возвращается для связки
Безопасностьlimits действуют с заголовка; битый кадр = разрыв; idle как подстраховка
Что дальше по xrt