Построение ответов: заголовки, chunked и keep-alive
Ответ — поток байтов, который вы пишете сами. Глава разбирает и собирает заново ответные помощники демо — тот самый набор, на котором routebench крутил десять тысяч маршрутов; это прототип вашего modules/http.h.
Обзор
15.1 Введение
«Нет объекта Response» означает: три дела ваши — генерация байтов заголовка (xrtHttp1ResponseWrite), запись в транспорт (помощник ConnSend) и поддержание семантики протокола (Content-Length точен, HEAD не шлёт тело). Выглядит рутиной, но покупает ноль скрытого поведения — а написанный однажды код становится вашим modules/http.h, переиспользуемым всем приложением.
15.2 Полный ответ: паттерн ReplyRaw
/* 1. транспортный помощник: отправка через транспорт, который использует запрос */
static bool ConnSend(XS_HttpReq* pReq, const void* pData, size_t iSize)
{
size_t iWritten = 0;
if ( pReq->tls != NULL ) {
/* TLS допускает успешную короткую запись: готово только при полном приёме */
return xrtTlsStreamSend(pReq->tls, pData, iSize,
&iWritten) == XTLS_OK && iWritten == iSize;
}
return pReq->tcp != NULL &&
xrtNetStreamSend(pReq->tcp, pData, iSize) == XNET_RESULT_OK;
}
/* 2. полный ответ: заголовок + тело пишутся разом */
static bool ReplyRaw(XS_HttpReq* pReq, uint16 iStatus,
const char* sContentType, const void* pBody, size_t iBodySize)
{
char arrHead[1024], arrLength[32];
xhttpfield arrField[12];
size_t iFieldCount = 0, iHeadSize = 0;
snprintf(arrLength, sizeof(arrLength), "%llu",
(unsigned long long)iBodySize);
arrField[iFieldCount].Name = XRT_STR_LITERAL("Content-Length");
arrField[iFieldCount].Value = xrtStrView(arrLength);
iFieldCount++;
/* ... необязательные Content-Type и доп. поля ... */
if ( !xrtHttp1ResponseWrite(XHTTP_VERSION_1_1, iStatus,
xrtHttpStatusText(iStatus), arrField, iFieldCount,
arrHead, sizeof(arrHead), &iHeadSize) ) return false;
if ( !ConnSend(pReq, arrHead, iHeadSize) ) return false;
return pReq->head->MethodCode == XHTTP_METHOD_HEAD ||
ConnSend(pReq, pBody, iBodySize);
}
/* 3. семантические обёртки */
static bool ReplyText(XS_HttpReq* pReq, uint16 iStatus, const char* sText)
{
return ReplyRaw(pReq, iStatus, "text/plain; charset=utf-8",
sText, strlen(sText));
}
Что только что произошло — четыре ключевых момента:
xhttpfield— пара представлений {Name, Value}; представления обязаны жить до возврата ResponseWrite, поэтому строка длины лежит в стековом массиве и только потом xrtStrViewxrtHttp1ResponseWriteтолько генерирует байты заголовка (в ваш буфер), не отправляет — генерация и транспорт разделены- Content-Length вычисляете и строките вы — это якорь корректности keep-alive
- Ветка HEAD: заголовок сохраняется (с Content-Length), отправка тела пропускается
15.3 HEAD и keep-alive
- keep-alive: соединения переиспользуются по умолчанию (семантика HTTP/1.1); при
Connection: closeот клиента закрытие после этого ответа. После XS_OK драйвер вычитывает остаток тела и читает следующий запрос — при условии, что ваш Content-Length точен; лишнее или недостающее смещает разбор следующего запроса - HEAD: семантика — «метаданные GET»: заголовки должны совпадать с GET (включая Content-Length), тело не пишется. В ReplyRaw это одна строка
- Ограниченный пайплайнинг: следующий запрос не разбирается, пока не допишется статический файл — ответы приложения следуют той же дисциплине «дописал — верни соединение»
15.4 Потоковый chunked
Контент неизвестной длины или генерируемый кусками идёт chunked-кодированием — каждый кусок пакует xrtHttp1ChunkWrite, завершает терминальный кусок:
/* заголовок: Transfer-Encoding: chunked (без Content-Length) */
xhttpfield arrField[1] = {
{ XRT_STR_LITERAL("Transfer-Encoding"),
XRT_STR_LITERAL("chunked") } };
xrtHttp1ResponseWrite(XHTTP_VERSION_1_1, 200,
xrtHttpStatusText(200), arrField, 1, arrHead, sizeof(arrHead), &iHeadSize);
ConnSend(pReq, arrHead, iHeadSize);
/* кусок за куском */
while ( HasMore(&tGen) ) {
char arrChunk[4096];
size_t iChunkSize = NextChunk(&tGen, arrChunk, sizeof(arrChunk));
char arrFrame[4096 + 32];
size_t iFrameSize = 0;
if ( xrtHttp1ChunkWrite(arrChunk, iChunkSize,
arrFrame, sizeof(arrFrame), &iFrameSize) )
ConnSend(pReq, arrFrame, iFrameSize);
}
/* терминальный кусок (нулевой длины) — тоже через ChunkWrite */
В chunked-ответе HEAD так же не пишет тело (заголовку даже не обязательно нести Transfer-Encoding — можно выродить в пустой заголовок в стиле GET; выберите политику и держите её consistently).
15.5 Контракт
- Длина: Content-Length обязан точно равняться числу байтов тела — линия жизни keep-alive.
- HEAD: заголовки как у GET, тело пропущено; обрабатывается в транспортном помощнике, а не в каждом обработчике.
- Переиспользование: ответные помощники живут в modules/http.h с полными комментариями владения (ReplyJSON лишь заимствует pObject).
Частые ошибки
- Симптом
- На keep-alive последующие запросы смещаются; клиент зависает или сообщает о битом ответе.
- Причина
- Лишний/недостающий подсчёт тела (strlen по бинарным данным с \0, изменение содержимого после подсчёта длины).
- Симптом
- Под HTTPS случайные обрезанные ответы.
- Причина
- Успех определяется только по возврату xrtTlsStreamSend — TLS допускает успешную короткую запись; обязательно сверяйте written == size (как в ConnSend).
xrtTlsStreamSend(pReq->tls, p, n,
&iWritten); /* iWritten не проверяется */== XTLS_OK && iWritten == nУпражнения
По паттерну главы реализуйте ReplyText / ReplyHTML / ReplyJSON / ReplyBinary (заголовок вложения Content-Disposition).
curl -I проверяет поведение маршрутов на HEAD; при 405 чинится масками методов из главы 17.
Реализуйте /counter на chunked: счёт от 1 до 100, кусок в секунду — фундамент для push в стиле SSE из главы 22.
Итоги
| Тезис | Вывод |
|---|---|
| Триада | ConnSend (транспорт) / ReplyRaw (заголовок+тело) / семантические обёртки |
| Генерация ≠ транспорт | ResponseWrite лишь производит байты заголовка в ваш буфер |
| Content-Length | считать точно — линия жизни keep-alive; сверять короткие записи TLS |
| HEAD | заголовки как GET, тело пропущено, обработка в транспортном слое |
| chunked | ChunkWrite пакует куски, терминальный кусок закрывает |
- справочник xrt · http1 — полные сигнатуры ResponseWrite / ChunkWrite / HttpStatusText
- справочник xrt · http_trailer — чтение и запись chunked-трейлеров (≤8 полей)