Как веб-сервер может узнать, когда HTTP-запрос полностью получен? - PullRequest
0 голосов
/ 08 января 2019

В настоящее время я пишу очень простой веб-сервер, чтобы узнать больше о программировании сокетов низкого уровня. В частности, я использую C ++ в качестве основного языка и пытаюсь инкапсулировать системные вызовы C низкого уровня в классах C ++ с помощью API более высокого уровня.

Я написал класс Socket, который управляет дескриптором файла сокета и управляет открытием и закрытием с помощью RAII. Этот класс также предоставляет стандартные операции с сокетами для сокетов, ориентированных на соединение (TCP), такие как bind, listen, accept, connect и т. Д.

После прочтения man-страниц для системных вызовов send и recv я понял, что мне нужно вызывать эти функции внутри некоторой формы цикла, чтобы гарантировать, что все байты успешно отправлено / получено.

Мой API для отправки и получения выглядит примерно так

void SendBytes(const std::vector<std::uint8_t>& bytes) const;
void SendStr(const std::string& str) const;
std::vector<std::uint8_t> ReceiveBytes() const;
std::string ReceiveStr() const;

Для функции отправки я решил использовать блокирующий send вызов внутри цикла, такого как этот (это внутренняя вспомогательная функция, которая работает как для std :: string, так и для std :: vector).

template<typename T>
void Send(const int fd, const T& bytes)
{
   using ValueType = typename T::value_type;
   using SizeType = typename T::size_type;

   const ValueType *const data{bytes.data()};
   SizeType bytesToSend{bytes.size()};
   SizeType bytesSent{0};
   while (bytesToSend > 0)
   {
      const ValueType *const buf{data + bytesSent};
      const ssize_t retVal{send(fd, buf, bytesToSend, 0)};
      if (retVal < 0)
      {
          throw ch::NetworkError{"Failed to send."};
      }
      const SizeType sent{static_cast<SizeType>(retVal)};
      bytesSent += sent;
      bytesToSend -= sent;
   }
}

Кажется, что это работает нормально и гарантирует, что все байты будут отправлены после того, как функция-член вернется без выдачи исключения.

Однако у меня начались проблемы, когда я начал реализовывать функцию приема. Для моей первой попытки я использовал блокирующий вызов recv внутри цикла и вышел из цикла, если recv вернул 0, указывая, что основное TCP-соединение было закрыто.

template<typename T>
T Receive(const int fd)
{
   using SizeType = typename T::size_type;
   using ValueType = typename T::value_type;

   T result;

   const SizeType bufSize{1024};
   ValueType buf[bufSize];
   while (true)
   {
      const ssize_t retVal{recv(fd, buf, bufSize, 0)};
      if (retVal < 0)
      {
          throw ch::NetworkError{"Failed to receive."};
      }

      if (retVal == 0)
      {
          break; /* Connection is closed. */
      }

      const SizeType offset{static_cast<SizeType>(retVal)};
      result.insert(std::end(result), buf, buf + offset);
   }

   return result;
}

Это работает нормально, если отправитель закрывает соединение после отправки всех байтов. Однако это не тот случай, когда, например, Chrome для запроса веб-страницы. Соединение остается открытым, и моя функция приема-члена блокируется при системном вызове recv после получения всех байтов в запросе. Мне удалось обойти эту проблему, установив таймаут для вызова recv с помощью setsockopt . По сути, я возвращаю все байты, полученные на данный момент, по истечении времени ожидания. Это выглядит как очень не элегантное решение, и я не думаю, что именно так веб-серверы решают эту проблему в реальности.

Итак, на мой вопрос.

Как веб-сервер узнает, когда HTTP-запрос был полностью получен?

Запрос GET в HTTP 1.1, похоже, не включает заголовок Content-Length. Смотрите, например эта ссылка .

Ответы [ 4 ]

0 голосов
/ 08 января 2019

Ответ формально определен в спецификациях протокола HTTP 1 :

Итак, чтобы подвести итог, сервер сначала читает начальное сообщение start-line, чтобы определить тип запроса. Если версия HTTP равна 0,9, запрос выполняется, поскольку единственный поддерживаемый запрос - GET без заголовков. В противном случае сервер читает сообщение message-header с, пока не будет достигнут завершающий CRLF. Тогда, только если тип запроса имеет определенное тело сообщения, сервер считывает тело в соответствии с форматом передачи, обозначенным заголовками запроса (запросы и ответы не ограничиваются использованием заголовка Content-Length в HTTP 1.1).

В случае запроса GET тело сообщения не определено, поэтому сообщение заканчивается после start-line в HTTP 0,9 и после завершения CRLF из message-header s в HTTP 1.0 и 1.1.

1: Я не собираюсь вдаваться в HTTP 2.0 , который представляет собой совершенно другую игру.

0 голосов
/ 08 января 2019

HTTP / 1.1 - это текстовый протокол с бинарными данными POST, добавленными несколько хакерским способом. При написании «цикла приема» для HTTP вы не можете полностью отделить часть получения данных от части синтаксического анализа HTTP. Это связано с тем, что в HTTP определенные символы имеют особое значение. В частности, токен CRLF (0x0D 0x0A) используется для разделения заголовков, а также для завершения запроса с использованием двух токенов CRLF один за другим.

Таким образом, чтобы прекратить прием, вам нужно продолжать получать данные, пока не произойдет одно из следующих событий:

  • Тайм-аут - отправьте ответ о тайм-ауте
  • Два CRLF в запросе - выполните синтаксический анализ запроса, затем ответьте по мере необходимости (правильно проанализировано? Запрос имеет смысл? Отправить данные?)
  • Слишком много данных - определенные эксплойты HTTP направлены на исчерпание ресурсов сервера, таких как память или процессы (см., Например, slow loris)

И, возможно, другие крайние случаи. Также обратите внимание, что это относится только к запросам без тела. Для POST-запросов вы сначала ждете два CRLF токена, а затем дополнительно читаете Content-Length байт. И это еще сложнее, когда клиент использует многочастное кодирование.

0 голосов
/ 08 января 2019

Решение по вашей ссылке

Запрос GET в HTTP 1.1, похоже, не включает заголовок Content-Length. Смотрите, например это ссылка .

Там написано:

Он должен использовать окончания строки CRLF и заканчиваться на \ r \ n \ r \ n

0 голосов
/ 08 января 2019

Заголовок запроса завершается пустой строкой (два CRLF, между которыми ничего нет).

Итак, когда сервер получил заголовок запроса, а затем получил пустую строку, и если запрос был GET (который не имеет полезной нагрузки), он знает, что запрос завершен, и может перейти к работе с формирование ответа. В других случаях он может перейти к чтению полезной нагрузки Content-Length и действовать соответственно.

Это надежное, четко определенное свойство с синтаксисом .

Нет Длина содержимого требуется или полезна для GET: содержимое всегда имеет нулевую длину. Гипотетическая длина заголовка больше похожа на то, о чем вы спрашиваете, но вам нужно сначала проанализировать заголовок, чтобы найти его, поэтому он не существует, и мы используем это свойство синтаксиса вместо. Однако в результате этого вы можете рассмотреть возможность добавления искусственного тайм-аута и максимального размера буфера поверх обычного синтаксического анализа, чтобы защитить себя от случайного злонамеренно медленного или длинного запроса.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...