我目前正在编写一个非常简单的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服务器在现实中处理此问题的方式。
那么,我的问题。
Web服务器如何知道何时完全接收到HTTP请求?
HTTP 1.1中的GET
请求似乎不包含Content-Length标头。参见例如this link。
答案 0 :(得分:4)
HTTP / 1.1是基于文本的协议,二进制POST数据以某种有点怪异的方式添加。为HTTP编写“接收循环”时,您无法将数据接收部分与HTTP解析部分完全分开。这是因为在HTTP中,某些字符具有特殊含义。特别是,CRLF
(0x0D 0x0A
)令牌用于分隔标头,但也使用两个CRLF
令牌一个接一个地结束请求。
因此,要停止接收,您需要继续接收数据,直到发生以下情况之一:
CRLF
–依次解析请求,然后根据需要进行响应(正确解析?请求有意义吗?发送数据?)也许还有其他极端情况。另请注意,这仅适用于没有正文的请求。对于POST请求,您首先要等待两个CRLF
令牌,然后另外读取Content-Length
个字节。当客户端使用多部分编码时,这甚至更加复杂。
答案 1 :(得分:2)
请求标头以空行终止(两个CRLF之间没有任何内容)。
因此,当服务器接收到请求标头,然后接收到空行,并且如果请求是GET
(没有有效负载)时,它就知道请求已完成并且可以继续执行处理回应。在其他情况下,它可以继续读取价值 Content-Length 的有效负载并采取相应的措施。
这是the syntax的可靠且定义明确的属性。
GET
不需要 Content-Length 也不有用:内容始终为零长度。假设的 Header-Length 更像您要询问的内容,但是您必须先解析标头才能找到它,所以它不存在,因此我们使用了语法代替。但是,因此,您可能会考虑在正常解析的基础上添加一个人工超时和最大缓冲区大小,以保护自己免受偶尔的恶意缓慢或长时间请求的侵害。
答案 2 :(得分:2)
解决方案在您的链接之内
HTTP 1.1中的GET请求似乎未包含Content-Length标头。参见例如这个link。
上面写着:
它必须使用CRLF行结尾,并且必须以\ r \ n \ r \ n结尾
答案 3 :(得分:1)
答案是在HTTP协议规范 1 中正式定义的:
在W3C's spec中用于HTTP 0.9。
在RFC 1945中用于HTTP 1.0,特别是在Section 4: HTTP Message,Section 5: Request和Section 7: Entity中。
在RFC 2616中用于HTTP 1.1,特别是在Section 4: HTTP Message中,尤其是在4.3: Message Body和4.4: Message Length中。
在RFC 7230(和7231 ... 7235)中用于HTTP 1.1,特别是在Section 3: Message Format,尤其是3.3: Message Body中。
因此,总而言之,服务器首先读取消息的首字母start-line
以确定请求类型。如果HTTP版本为0.9,则请求完成,因为唯一受支持的请求是GET
,没有任何标头。否则,服务器然后读取消息的message-header
,直到到达终止的CRLF
。然后,仅当请求类型具有已定义的消息正文时,服务器才根据请求标头概述的传输格式读取该正文(请求和响应不限于在HTTP 1.1中使用Content-Length
标头)。 / p>
对于GET
请求,由于没有定义消息正文,因此消息在HTTP 0.9中的start-line
之后和{{ 1}}在HTTP 1.0和1.1中。
1:我不会进入HTTP 2.0,这是一个完全不同的局面。