在上一篇文章中,我们已经实现了一个最基础的 Web 服务器:
- 使用 Boost.Asio 接收 TCP 连接
- 为每个连接创建一个
Session - 通过
async_read读取客户端数据
但此时服务器其实还没有真正理解 HTTP 请求。
服务器收到的其实只是 一段原始的字符串数据。
例如浏览器发来的请求可能是:
POST /api/login HTTP/1.1
Host: localhost:8080
Content-Type: application/json
Content-Length: 34{"username":"admin","password":"123"}
如果我们想要处理这个请求,就必须完成几件事情:
- 解析 请求行
- 解析 HTTP Header
- 读取 HTTP Body
- 构造 HTTP Response
本篇是手写 C++ Web 服务器系列第二篇,这一篇我们就来实现一个最基础的 HTTP 请求解析器。源码地址:https://github.com/Fall-Rain/asio_web_service
一、Session 的基本结构
在服务器中,每一个客户端连接都会对应一个 Session 对象。
构造函数如下:
Session::Session(boost::asio::ip::tcp::socket &socket): client_socket_(std::move(socket)) {}
这里我们将 socket 移动到 Session 中保存。
这样每个 Session 都拥有一个独立的 TCP 连接。
当连接建立后,服务器会调用:
void Session::start() {
do_read();
}
开始读取客户端请求。
二、读取 HTTP 请求头
HTTP 请求头的结束标志是:
\r\n\r\n
因此可以使用 Asio 的:
boost::asio::async_read_until
代码如下:
boost::asio::async_read_until(
client_socket_,
client_buffer_,
"\r\n\r\n",
callback
);
这里的意思是:
一直读取 socket 数据,直到遇到
\r\n\r\n。
当读取完成后,就会进入回调函数。
三、解析请求头字符串
读取完成后,我们先把 buffer 转换成字符串:
std::string request_header(
boost::asio::buffers_begin(self->client_buffer_.data()),
boost::asio::buffers_begin(self->client_buffer_.data()) +
bytes_transferred
);
此时 request_header 内容类似:
POST /login HTTP/1.1
Host: localhost
Content-Type: application/json
Content-Length: 34
接下来需要按行拆分:
std::vector<string> headers;boost::split(
headers,
request_header,
boost::is_any_of("\r\n"),
boost::token_compress_on
);
拆分后:
headers[0] = 请求行
headers[1] = Host
headers[2] = Content-Type
headers[3] = Content-Length
四、解析请求行
HTTP 第一行叫做 Request Line:
POST /login HTTP/1.1
代码:
std::vector<string> line;
boost::split(line, headers[0], boost::is_any_of(" "));
拆分后:
line[0] = method
line[1] = uri
line[2] = http_version
于是我们得到:
method = POST
uri = /login
http_version = HTTP/1.1
五、解析 HTTP Header
HTTP 头格式是:
Key: Value
例如:
Content-Type: application/json
代码:
std::for_each(
headers.begin() + 1,
headers.end() - 1,
[&](std::string v)
{
std::vector<string> header; boost::split(header, v, boost::is_any_of(":")); self->header_map.insert(
std::pair<string,string>(
header[0],
header[1]
)
);
});
最终保存到:
header_map
例如:
Host -> localhost
Content-Type -> application/json
Content-Length -> 34
为了方便调试,我还打印了请求信息:
std::cout << "请求头" << std::endl;
std::cout << "method:" << method << std::endl;
std::cout << "uri:" << uri << std::endl;
输出示例:
请求头
method:POST
uri:/login
http_version:HTTP/1.1
Host => localhost
Content-Length => 34
六、读取 HTTP Body
如果请求头中存在:
Content-Length
说明请求包含 Body。
代码:
auto content_length = self->header_map.find("Content-Length");
如果存在,就需要继续读取 body。
首先计算当前 buffer 中多出来的数据:
std::size_t excess_data_length = self->client_buffer_.size() - bytes_transferred;
因为有时候 async_read_until 读取的数据 可能已经包含了部分 body。
于是我们先把这部分提取出来:
std::vector<char> excess_data(excess_data_length);boost::asio::buffer_copy(
boost::asio::buffer(excess_data),
self->client_buffer_.data() + bytes_transferred
);
然后转换为字符串:
self->request_body = string(excess_data.data(), excess_data_length);
七、继续读取剩余 Body
接下来根据 Content-Length 判断还需要读取多少数据:
size_t content_length = std::stoi(self->header_map.find("Content-Length")->second);
如果 body 没读完:
if (content_length - self->request_body.size() > 0)
就继续读取:
boost::asio::read(
self->client_socket_,
boost::asio::buffer(buffer),
boost::asio::transfer_exactly(
content_length - self->request_body.size()
)
);
最终得到完整 body:
{"username":"admin","password":"123"}
并打印:
std::cout << "请求体:" << std::endl;
std::cout << self->request_body << std::endl;
八、返回 HTTP 响应
最后服务器返回一个简单的 HTTP 响应:
response_stream << "HTTP/1.1 200 OK\r\n";
response_stream << "Content-Type: text/html\r\n";
response_stream << "Content-Length:"
<< self->request_body.size()
<< "\r\n";
response_stream << "\r\n";
response_stream << self->request_body;
这个服务器会:
直接把客户端发送的 body 原样返回。
例如客户端发送:
hello
服务器响应:
HTTP/1.1 200 OK
Content-Length:5hello
最后通过 async_write 发送:
boost::asio::async_write(
client_socket_,
boost::asio::buffer(response_stream.str()),
callback
);
九、小结
这一篇我们实现了一个最基础的 HTTP 请求解析流程:
TCP Socket
│
▼
读取 HTTP Header
│
▼
解析 Request Line
│
▼
解析 Header
│
▼
读取 Body
│
▼
返回 HTTP Response
虽然这个服务器还非常简单,但已经具备了 Web 服务器的核心能力:
- 解析 HTTP 协议
- 读取请求体
- 构造响应
在下一篇文章中,我们将进一步完善服务器的能力,例如:
- URL 参数解析
- Content-Type 处理
- JSON / 表单数据解析
让服务器逐渐演变成一个真正的 C++ Web 框架。
文章评论