我目前正在编写一个非常简单的Web服务器,以了解有关低级套接字编程的更多信息。更具体地说,我使用C++作为主要语言,并且尝试使用更高级的API将低级C系统调用封装在C++类中。
我编写了一个Socket
类,该类管理套接字文件描述符并使用RAII处理打开和关闭操作。此类还公开了面向连接的套接字(TCP)的标准套接字操作,例如绑定(bind),监听,接受,连接等。
在阅读了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 header 。参见例如this link。 最佳答案
HTTP/1.1是基于文本的协议(protocol),二进制POST数据以某种有点怪异的方式添加。为HTTP编写“接收循环”时,不能将数据接收部分与HTTP解析部分完全分开。这是因为在HTTP中,某些字符具有特殊含义。特别是,CRLF
(0x0D 0x0A
) token 用于分隔 header ,但也可以使用两个CRLF
token 一个接一个地结束请求。
因此,要停止接收,您需要继续接收数据,直到发生以下情况之一:
CRLF
–依次解析请求,然后根据需要进行响应(正确解析?请求有意义吗?发送数据?)也许还有其他情况。另请注意,这仅适用于没有正文的请求。对于POST请求,您首先要等待两个
CRLF
token ,然后另外读取Content-Length
字节。当客户端使用多部分编码时,这甚至更加复杂。