一个Web服务器如何知道HTTP请求何时完全接收?

7
我正在编写一个非常简单的Web服务器,以了解更多关于低级套接字编程的知识。更具体地说,我使用C ++作为我的主要语言,并尝试将低级C系统调用封装在具有更高级API的C ++类中。
我编写了一个Socket类,管理套接字文件描述符并处理使用RAII打开和关闭。该类还公开了面向连接的套接字(TCP)的标准套接字操作,如绑定、监听、接受、连接等。
阅读sendrecv系统调用的手册后,我意识到需要在某种形式的循环内调用这些函数,以保证所有字节都成功发送/接收。
我的发送和接收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系统调用上。我通过使用setsockopt设置recv调用的超时来解决了这个问题。基本上,一旦超时到期,我就返回到目前为止接收到的所有字节。这感觉像一个非常不优雅的解决方案,我不认为这是Web服务器在现实中处理此问题的方式。
那么,接下来是我的问题。
Web服务器如何知道HTTP请求已完全接收?
HTTP 1.1中的GET请求似乎不包括Content-Length头。请参见this link

C++和C是不同的编程语言。这段代码中几乎没有任何行可以被视为C语言。 - François Andrieux
1
一个HTTP的GET请求并没有任何数据,它只有(可选的)头部字段和一个明确定义的终止。 - Some programmer dude
2
连接保持打开。请注意,这是HTTP协议中的一种常规做法:现有的连接可以被多次重用。没有任何理由时,不应该关闭它。 - Matt
@Matt 没错,这就是为什么我想要找出正确的标准来确定何时停止重复调用 recv。 - JonatanE
顺便说一下,你的 C++ 习惯用法看起来很不错。干得好! - Lightness Races in Orbit
4个回答

6

HTTP/1.1是一种基于文本的协议,使用了二进制POST数据的某种“hacky”方式。当编写HTTP的“接收循环”时,您无法完全将数据接收部分与HTTP解析部分分开。这是因为在HTTP中,某些字符具有特殊含义。特别地,CRLF0x0D 0x0A)token用于分隔标头,但也用于使用两个CRLF token连续结束请求。

所以为了停止接收,您需要持续接收数据,直到发生以下情况之一:

  • 超时 - 发送超时响应
  • 请求中有两个CRLF - 解析请求,然后根据需要响应(正确解析?请求合理?发送数据?)
  • 太多数据 - 某些HTTP攻击旨在耗尽服务器资源,如内存或进程(例如slow loris)

还有其他边缘情况。还要注意,这仅适用于没有正文的请求。对于POST请求,您首先等待两个CRLF tokens,然后再额外读取Content-Length字节。而且,当客户端使用多部分编码时,这甚至更加复杂。


谢谢您详细的回复!我已经知道了两组CRLF用于表示请求结束,也许我在问题中应该明确说明。从您的答案中得出的关键是,我需要不断接收数据,直到在字节流中找到此分隔符或根据其他标准提前退出。结果证明,我的超时想法并没有那么遥远。 - JonatanE
两个 CRLF 并不表示 请求 的结束,它们仅表示 请求头 的结束。可能有消息体跟随在请求头之后,也可能没有。您需要解析请求头来确定不仅是否存在消息体,还要确定以何种格式发送消息体,以便正确读取它。如果存在消息体,则请求在消息体的末尾结束;否则在请求头的末尾结束。如何确定消息体的结束取决于其传输格式。 - Remy Lebeau
@RemyLebeau 是的,我同意。我写了“请注意,这仅适用于没有正文的请求。”通常情况下,您可以通过解析头部来确定您正在处理的请求类型(方法),在接收到两个 CRLF 后。 - Aurel Bílý

3

请求头以空行(两个CRLF之间没有任何内容)结束。

因此,当服务器接收到请求头并接收到一个空行,如果请求是一个没有有效载荷的GET请求,它就知道请求已经完成,可以继续处理响应。在其他情况下,它可以继续读取相应长度的有效载荷并采取相应措施。

这是语法的一个可靠、明确定义的属性。

GET不需要或有用的Content-Length:内容始终为零长度。一个假设的Header-Length更像是你所问的,但你必须先解析头部才能找到它,所以它不存在,我们使用语法的这个属性。然而,由于这个原因,您可能需要在正常解析之上添加人工超时和最大缓冲区大小,以保护自己免受偶尔的恶意缓慢或长时间请求的影响。


1
从技术上讲,内容长度可以添加到任何请求动词中,包括GET。从技术上讲,GET请求可以包含内容。 - Michael Chourdakis
@Michael 当然可以,但在那种情况下没有用处。 - Lightness Races in Orbit
@Remy 感谢您的编辑。据我所知,每个HTTP请求都是这样工作的。是否存在一些差异? - Lightness Races in Orbit
@LightnessRacesinOrbit,你最初的措辞暗示每个HTTP请求都在头部后面的空行结束,这显然是不正确的。大多数HTTP请求在头部之后有一个消息体(即使该消息体为0字节)。GETHEAD请求在头部之后结束,因为它们没有消息体。其他请求在消息体之后结束。您必须分析每个请求的头部以确定是否存在消息体以及如何读取它。 - Remy Lebeau
@RemyLebeau 好的,我想我应该说请求_header_以空行终止。在我的理念中,请求是一件事,负载可选跟随,但你的看法可能不同。 - Lightness Races in Orbit
显示剩余2条评论

2

解决方案在你的链接中

HTTP 1.1版本中的GET请求似乎不包括Content-Length头部。例如,参见这个链接

上面说到:

它必须使用CRLF行尾,并且必须以\r\n\r\n结尾。


1
答案在HTTP协议规范中被正式定义1 因此,为了总结一下,服务器首先读取消息的初始start-line以确定请求类型。如果HTTP版本是0.9,则请求完成,因为唯一支持的请求是没有任何头信息的GET请求。否则,服务器会读取消息的message-header,直到达到终止的CRLF。然后,仅当请求类型具有定义的消息主体时,服务器才会根据请求头中概述的传输格式读取主体(在HTTP 1.1中,请求和响应不限于使用Content-Length头)。
GET请求的情况下,没有定义消息主体,因此在HTTP 0.9中,在start-line之后消息结束,在HTTP 1.0和1.1中,在message-header的终止CRLF之后消息结束。 1: 我不打算涉及HTTP 2.0,那是一个完全不同的领域。

我认为RFC 7230第3.3节完全足以回答这个问题。不确定为什么你觉得需要引用(如你所知)已过时的RFC 2616。点赞,因为这应该是被接受的答案。 - DaSourcerer
@DaSourcerer 许多网络服务器尚未更新以实现RFC 7230...7235,它们仍然实现RFC 2616。虽然RFC 7230-7235主要只是对RFC 2616进行了重组以使其更易理解,但它们也对协议进行了一些更改(例如弃用标题折叠,并扩展了如何确定消息长度)。这就是为什么我提到HTTP 1.1的两个RFC集。 - Remy Lebeau
每个头部都由CRLF分隔,如何知道哪个CRLF是终止符? - EntityinArray
请阅读我提供的规范,@EntityinArray。是的,每个头部都以CRLF结尾,但在头部完成后,还有另一个单独的CRLF。换句话说,头部以CRLF CRLF对终止。 - Remy Lebeau

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接