目录:

前言

我的 C++ 大作业基本完成了,这个假期也算是花了不少精力在这个作业上,于是准备用几篇文章来回顾一下,顺便在最后做一下 Code Review。

最开始的时候,我是准备写一个 HTTP 服务器作为大作业的。但只写 HTTP 服务器,到时候不好展示啊,于是就在 HTTP 服务器的基础上加入了一点点方便开发的功能。所以现在它可以勉强称为一个 Web 开发框架了。

这是项目的 GitHub 仓库地址:https://github.com/sticnarf/hane

网络 I/O

虽然是在造轮子,但什么轮子都从头造我还是不情愿的。网络 I/O 这块我暂时没什么兴趣自己搞,而且还有跨平台的考虑,自己写起来太麻烦了。最终我选了 Node.js 使用的异步 I/O 框架 libuv

libuv 虽然是一个在 GitHub 上星星无数的项目,但文档其实一点都不详尽。官方并没有什么教程,光看文档根本无从下手。只有第三方写的指南可以参考一下。不过幸好 libuv 并不是很复杂,稍微摸索一下还是学会如何使用了。

libuv 中几乎所有的异步操作函数都会接收一个 handle struct (类型名为 uv_xxx_t) 和一个回调函数,不同操作的 handle struct 之间有继承关系,指针的转换可以使用 reinterpret_cast。

在异步操作中传递数据全靠 handle struct 中的 data 属性。datavoid * 类型的,所以并不能将 C++ 中的智能指针扔进去,而只能手动管理。

为了内存安全,绝大部分的 handle struct 我都动态创建了,并在回调函数中 delete 掉。希望这不会有造成太大的性能问题及内存碎片化的问题。

服务器收到新连接时,会创建一个 Client 对象,并将 Client 对象与 uv_tcp_t 互相绑定到一起,一个 Client 就相当于一个连接,以后不管是从抽象层向下还是从底层向上都可以比较方便:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Client::Client(HttpServer *server)
: server(server), queued(0) {
tcp = new uv_tcp_t;
tcp->data = this;
}

void HttpServer::onNewConnection(uv_stream_t *serverTcp, int status) {
if (status < 0) {
Logger::getInstance().error("New connection error: {}", uv_strerror(status));
return;
}

auto *server = static_cast<HttpServer *>(serverTcp->data);
auto *client = new Client(server);

uv_tcp_init(uv_default_loop(), client->tcp);
auto uvTcp = reinterpret_cast<uv_stream_t *>(client->tcp);

if (uv_accept(serverTcp, uvTcp) == 0) {
// Success. Start reading from the connection:
uv_read_start(uvTcp, allocBuffer, readCallback);
} else {
// Fail. Close the connection directly:
uv_close(reinterpret_cast<uv_handle_t *>(client->tcp), closeCallback);
}
}

Client 对象是 libuv 层创建的,于是 Client 对象的销毁也放在 libuv 层(closeCallback)函数中。uv_tcp_t 对象是 Client 创建的,于是 uv_tcp_t 对象则在 Client 的析构函数中销毁。

读取到的数据会被传给 Client 对象的 Parser 对象,Parser 再将数据加入到 Buffer 中,然后开始解析。解析完成的请求会被放入 ClientcompleteRequests 队列中,再由 processRequest 函数来进行处理。

从解析请求到处理请求,其中产生了 HttpError (比如解析请求中产生了 400 Bad Request)会被最外层捕获到。HttpError 中包含了异常状态码和出错原因,服务器会将起发回给客户端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
void HttpServer::readCallback(uv_stream_t *clientTcp, ssize_t nread, const uv_buf_t *buf) {
auto *client = static_cast<Client *>(clientTcp->data);
if (nread > 0) { // Read successfully
try {
// Push data to client to parse
client->pushBuf(buf->base, nread);
} catch (const HttpError &e) {
Logger::getInstance().error("Error code {}: {}", static_cast<int>(e.getCode()), e.getReason());
auto errorResp = buildErrorResponse(e);
client->server->writeResponse(clientTcp, errorResp);
client->closeConnection();
}

}
if (nread < 0) {
if (nread != UV_EOF)
Logger::getInstance().error("Read error: {}", uv_strerror((int) nread));

uv_close(reinterpret_cast<uv_handle_t *>(clientTcp), closeCallback);
}
delete[] buf->base;
}

void Client::pushBuf(const char *buf, size_t len) {
parser.pushBuf(buf, len);
processRequest();
}

void Parser::pushBuf(const char *buf, size_t len) {
buffer->push(buf, len);
process();
}

void Parser::process() {
currentParser = currentParser->process();

if (currentParser->isFinished()) {
completeRequests.push(currentParser->getRequest());
currentParser = std::make_shared<StartLineParser>(std::make_shared<Request>(), buffer);
}
}

void Client::processRequest() {
if (!parser.hasCompleteRequest())
return;

// If there is no chunk in queue, the next request will be processed immediately.
// Otherwise, the process of the next request should be triggered by the chunk-writing callback.
if (queued == 0) {
RequestPtr req = parser.yieldRequest();
server->process(req, this->tcp);
}
}

解析请求

解析请求是 HTTP 服务器中的苦力活,需要大量查阅文档,十分枯燥无趣。

我在实现的过程中偷了很多的懒,很多地方没有严格按照 RFC 实现,也删去了很多的功能。

解析是分阶段进行的,首先我定义了所有的 parser 符合的接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class AbstractParser : public std::enable_shared_from_this<AbstractParser> {
protected:
RequestPtr partialRequest;
BufferPtr buffer;
bool finished;

AbstractParser(RequestPtr req, BufferPtr buffer);

public:
// Throws HttpError if it is a bad request
virtual ParserPtr process();
bool isFinished() const;
RequestPtr getRequest() const;
};

之后所有的 parser 都通过是通过 process 函数,从 buffer 中切出片段,解析出所需数据,放入 partialRequest 中。

原始数据从 StartLineParser 开始,到 FinalParser 结束。FinalParser 的工作仅仅是将 finished 设为 true。于是 partialRequest 也变成了完整的 Request。之后的事情就交给中间层去处理了。

此处用到的 Buffer 是自己写的一个集合类,主要作用是提供快速的切割和访问功能。底层类似 std::deque,是一个包含许多离散的小数组的 vector

不过我也栽在这个自己写的 Buffer 上好多次,各种小 bug 不断,最终性能也没有达到我的理想水平。