Глава 23 / 51
Том 3 · Разработка протоколов

Потоковые сервисы TCP: три колбэка в мире голых байтов

Игровые шлюзы, приём IoT, приватные протоколы — они не говорят по HTTP. Класс tcp/tcps отдаёт вам голый поток; всё остальное определяете вы.

Обзор

Позиционирование: поверхность колбэков проще, чем у HTTP (всего три), но данные сырее (цепочки буферов). Конец главы стыкуется с гл. 33 (XTP2): собственный протокол поверх TCP — тема второй практики.

23.1 Введение

Ценность HTTP — «договорились, как выглядит запрос»; ценность TCP — «ни о чём не договорились». Поэтому обязанности хоста по TCP сжимаются до трёх: построить поток, отдать данные, доложить терминальное состояние — право проектировать протокол остаётся целиком за вами. TLS (tcps) лишь подключает слой шифрования к потоку; поверхность колбэков та же.

23.2 Представление и колбэки

Три колбэка + представление (в стиле контрактных исходников)
/* XS_StreamConn: ровно один из tcp/tls не пуст (под tcps запрещено писать в tcp в обход tls) */
static void EventOpen(XS_HostInfo* pHost, XS_StreamConn* pConn);
static void EventData(XS_HostInfo* pHost, XS_StreamConn* pConn, xnetbuf* pBuffer);
static void EventClose(XS_HostInfo* pHost, XS_StreamConn* pConn,
    xnetresult iResult, const xerror* pError);
  • EventOpen: соединение установлено — момент для приветственного баннера и контекста сеанса
  • EventData: пришли данные. pBuffer — цепочка буферов без копирования (заимствована в колбэке): размер через xrtNetBufSize(pBuffer), данные потребляются итерационными API буферов xrt — это не сплошной блок памяти, не трактуйте его как char*
  • EventClose: терминал — iResult даёт вердикт сетевого слоя, pError — детали (может быть NULL)
  • Отправка: xrtNetStreamSend(pConn->tcp, ...) / xrtTlsStreamSend(pConn->tls, ...) по непустой стороне

23.3 Баннер и эхо

Минимальный TCP-сервис
static void EventOpen(XS_HostInfo* pHost, XS_StreamConn* pConn)
{
    if ( pConn->tcp != NULL )
        (void)xrtNetStreamSend(pConn->tcp, "hello\n", 6);
    /* tcps: проверять pConn->tls + xrtTlsStreamSend (сверять written) */
}

static void EventData(XS_HostInfo* pHost, XS_StreamConn* pConn, xnetbuf* pBuffer)
{
    /* эхо: отправить полученное тем же путём обратно.
     * реальная реализация: обходить каждый блок цепочки и Send по сегментам;
     * размер: xrtNetBufSize(pBuffer) */
}
Терминал · реальный вид
$ telnet 127.0.0.1 9000
hello
(каждая введённая строка эхом возвращается как есть)

Что только что произошло: баннер при подключении — конвенция многих текстовых протоколов (SMTP/FTP/IRC); эхо — «hello world» потокового сервиса. Реальный протокол (заголовок кадра + длина + тело) добавляет поверх автомат состояний — XTP2 из гл. 33 и есть готовая кадровая библиотека.

23.4 Жизненный цикл и лимиты

  • Жизненным циклом соединения владеет хост: перехват Accept → реестр → деструкция на Close — приложение не вызывает Destroy у соединения (в отличие от класса custom, где всё вручную)
  • SNI: под tcps сертификаты хостов строят таблицу SNI; каждому соединению сертификат по SNI
  • idle_timeout: простаивающие соединения закрываются сервером по истечении (в func_test есть замеренный кейс закрытия на 4,5 с)
  • recv_limit: меньше блока чтения по умолчанию — размер чтения синхронно сжимается (конфиг не вызывает генерализованных отказов)
  • Контекст сеанса: изоморфен WS — собственный реестр (соединение → контекст), вставка в Open, удаление в Close

23.5 Контракт

  • Заимствование: pConn/pBuffer/pError действительны в колбэке; данные цепочки потребляются как кадры, не хранятся навсегда.
  • Направление: под tcps запрещено писать в tcp в обход tls — открытый текст на той стороне читается как шум TLS-записей.
  • Жизненный цикл: соединения у хоста; контекст сеанса у вас, чистка в терминале Close.

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

xnetbuf принят за сплошную память
Симптом
Перепутанные данные / выход за границы.
Причина
Это цепочка буферов (структура с полями Head/Tail/Size) — потребление только через итерационные API буферов xrt поблочно; единого «указателя на всё» нет.
pConn сохранён между колбэками и используется самовольно
Симптом
Отправка/приём по указателю соединения из чужого потока — падение или перепутанный порядок.
Причина
Активный ввод-вывод вне колбэков — режим «перехвата» (XS_TAKEOVER у HTTP / собственные потоки требуют осторожности); в обычном режиме логика событийная и укладывается в три такта Open/Data/Close.

Упражнения

БазовыйБаннер-эхо сервис

Реализуйте сервис 23.3 и проверьте telnet.

Приёмка: hello при подключении; ввод эхом; после выхода Ctrl+] в логе виден EventClose.
ПродвинутыйРазборщик строкового протокола

Кадрируйте по \n между раундами EventData (буфер полустроки) и соберите мини-KV с командами GET/SET/QUIT.

Приёмка: команда, разорванная на две посылки ("GE" + "T key\n"), разбирается верно.
ВызовДвухстековый tcps

Включите TLS-порт с сертификатом; одни и те же колбэки обслуживают оба эндпоинта — открытый и шифрованный.

Приёмка: telnet по открытому и openssl s_client по TLS ведут себя одинаково; в логе две строки bound.

Итоги

ТезисВывод
Поверхность колбэковтри такта Open/Data/Close; проектирование протокола целиком ваше
Форма данныхxnetbuf — цепочка без копий, заимствуется в колбэке, потребляется поблочно
tcpsодно из двух в представлении; обход tls запрещён; SNI выдаёт сертификат на соединение
Жизненный циклсоединения у хоста; контекст у вас (реестр + чистка в Close)
Лимитыidle_timeout закрывает простои; recv_limit сжимает блоки чтения
Что дальше по xrt