web服务器如何知道何时完全接收到HTTP请求

How can a web server know when an HTTP request is fully received?

本文关键字:HTTP 请求 服务器 何知道 何时完 web      更新时间:2023-10-16

我目前正在编写一个非常简单的web服务器,以了解有关低级套接字编程的更多信息。更具体地说,我使用C++作为我的主要语言,并试图用更高级的API将低级C系统调用封装在C++类中。

我已经编写了一个Socket类,它管理套接字文件描述符并使用RAII处理打开和关闭。这个类还公开了面向连接的套接字(TCP)的标准套接字操作,如绑定、侦听、接受、连接等。

在阅读了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系统调用上被阻塞。我通过使用setsockopt在recv调用上设置超时来解决这个问题。基本上,一旦超时,我会返回到目前为止接收到的所有字节。这感觉是一个非常不雅的解决方案,我不认为这是网络服务器在现实中处理这个问题的方式。

那么,关于我的问题。

web服务器如何知道何时已完全接收到HTTP请求

HTTP1.1中的GET请求似乎不包括Content-Length标头。请参见例如此链接。

HTTP/1.1是一个基于文本的协议,以一种有点古怪的方式添加了二进制POST数据。在为HTTP编写"接收循环"时,不能将数据接收部分与HTTP解析部分完全分离。这是因为在HTTP中,某些字符具有特殊的含义。特别地,CRLF(0x0D 0x0A)令牌用于分离报头,但也用于使用两个相继的CRLF令牌来结束请求。

因此,要停止接收,您需要继续接收数据,直到发生以下情况之一:

  • 超时–然后发送超时响应
  • 请求中有两个CRLF——然后解析请求,然后根据需要进行响应(解析正确?请求有意义?发送数据?)
  • 数据过多–某些HTTP漏洞旨在耗尽服务器资源,如内存或进程(例如,请参阅慢速loris)

也许还有其他边缘情况。还要注意,这只适用于没有正文的请求。对于POST请求,首先等待两个CRLF令牌,然后再读取Content-Length字节。当客户端使用多部分编码时,这就更加复杂了。

请求标头由一行空行终止(两个CRLF之间没有任何内容)。

因此,当服务器收到一个请求头,然后收到一个空行,并且如果请求是GET(没有有效负载),它就知道请求已经完成,可以继续处理形成响应的问题。在其他情况下,它可以继续读取内容长度的有效载荷,并采取相应的行动。

这是一个可靠的、定义良好的语法属性。

不需要内容长度或对GET有用:内容始终为零长度。假设的Header Length更像您所问的,但您必须首先解析标头才能找到它,因此它不存在,我们使用语法的此属性。因此,您可以考虑在正常解析的基础上添加一个人工超时最大缓冲区大小,以保护自己免受偶尔恶意的慢速或长时间请求的影响。

解决方案在您的链接中

HTTP1.1中的GET请求似乎不包括Content-Length标头。请参见例如此链接。

上面写着:

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

答案在HTTP协议规范中正式定义1:

  • 在W3C的HTTP 0.9规范中。

  • 在RFC 1945的HTTP 1.0中,特别是在第4节:HTTP消息、第5节:请求和第7节:实体中。

  • 在用于HTTP 1.1的RFC 2616中,特别是在第4节:HTTP消息中,尤其是在4.3节:消息体和4.4节:消息长度中。

  • 在HTTP 1.1的RFC 7230(和7231…7235)中,特别是在第3节:消息格式,特别是3.3节:消息正文中。

总之,服务器首先读取消息的初始start-line,以确定请求类型。如果HTTP版本是0.9,那么请求就完成了,因为唯一支持的请求是没有任何头的GET。否则,服务器读取消息的message-headers,直到到达终止CRLF。然后,只有当请求类型具有定义的消息正文时,服务器才会根据请求标头概述的传输格式读取正文(请求和响应不限于使用HTTP1.1中的Content-Length标头)

GET请求的情况下,没有定义消息主体,因此在HTTP 0.9中,消息在start-line之后结束,在HTTP 1.0和1.1中,在message-header的终止CRLF之后结束。

1:我不打算讨论HTTP2.0,这是一个完全不同的游戏