Расширение XTP2: говорим бинарное поверх TCP
TCP даёт поток байтов, XTP2 даёт границы сообщений. Глава ставит кадровую библиотеку (задел гл. 23) в EventData и получает протокольный сервис, устойчивый к склейкам и враждебным кадрам.
Обзор
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
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 у feed сообщает, сколько байтов съедено в этом вызове — не потребив их, следующий раунд разберёт с начала. Операции с буфером обязаны двигаться строго по consumed.
- Симптом
- После reset, очистившего ошибку, продолжение того же потока даёт перепутанные сообщения.
- Причина
- reset лишь сбрасывает состояние разборщика — он не чинит уже смещённый поток байтов; вторая половина битого сообщения читается как новый заголовок. Верное применение reset — «переиспользовать объект обработчика» (новое соединение/новый поток).
Упражнения
Реализуйте минимальный сервис PING REQUEST → PONG RESPONSE (клиент собирает кадры через xtp2_encode).
Клиент режет кадр на одно-байтовые куски и шлёт, проверяя буфер полупакетов.
Постройте три атаки — сверхлимитная длина в заголовке, обрезанный кадр, нелегальный magic — и убедитесь: память сервера ровная, разрывы чистые.
Итоги
| Тезис | Вывод |
|---|---|
| Позиционирование | кодек-примитив: формат кадра + дисциплина разбора в виде библиотеки |
| Сборка | сеанс = обработчик + буфер остатков; потребление точно по consumed |
| Сообщения | представления feed — в раунде; id REQUEST возвращается для связки |
| Безопасность | limits действуют с заголовка; битый кадр = разрыв; idle как подстраховка |
- справочник xrt · buffer — модель consume/append для сеансовых буферов остатков