架构分析

项目架构分析

​ 在本文中,我将基于一个项目的结构进行一些分析,目的是为了清晰自己对于一个项目的熟悉。避免在之后不断扩展时丢失对于整个项目的掌控性。

入口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include "CServer.h"

int main() {
try {
unsigned short port = static_cast<unsigned short>(8080);
net::io_context ioc{ 1 };
boost::asio::signal_set signals(ioc, SIGINT, SIGTERM);
signals.async_wait([&ioc](const boost::system::error_code& error, int signal_number) {
if (error) {
return;
}
ioc.stop();
});
std::make_shared<CServer>(ioc, port)->Start();
std::cout << "GateServer listen the port " << port << std::endl;
ioc.run();
}
catch (std::exception const& e)
{
std::cerr << "Error: " << e.what() << std::endl;
return EXIT_FAILURE;
}
}

​ 这里是一个十分经典且简单的服务器启动流程,使用ASIO内部的信号已经事件驱动机制来实现优雅退出。并没有什么好说的。在这个入口逻辑中,我们需要知道的是其在错误处理逻辑的防御性编程之后,其进行了一个对应的服务器的启动,我们接下来来看这里的启动逻辑。

高层网关CServer

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
void CServer::Start()
{
auto self = shared_from_this();
_acceptor.async_accept(_socket, [self](beast::error_code ec) {
try {
//出错放弃该连接并监听其他连接
if (ec) {
//self->Start();
std::cout << "Accept error: " << ec.message() << std::endl;
return;
}

//创建新连接,并且创建HttpConnection管理该连接
std::make_shared<HttpConnection>(std::move(self->_socket))->Start();

//继续监听
self->Start();

}catch (std::exception& exp) {
std::cerr << "Exception: " << exp.what() << std::endl;
}
}
);
}

​ 在这个启动函数中,可以看到,其本身并不复杂,本质就是一个监听套接字的创建以及对应的监听行为注册。这里需要注意的是对于异步监听的回调函数的注册。这里使用的是匿名函数来实现回调逻辑的。并且这里通过传递一个自身对象来延长对象的生命周期避免被杀死。这里的回调处理逻辑中使用了又一次的匿名构造去实现对应的请求处理。由于我们本身设计的其实是一个Http服务器,所谓我们在设计中是存在一个HttpConnection类来处理所有的来自外部的请求的,一但出现一个请求,就单独创建一个对应的处理类进行处理。

​ 但是这里其实存在一个问题,不知道你有没有看到,下面是给出的.h文件以及修改后的start方法,自行查找现有的错误逻辑处。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include "const.h"
#include "HttpConnection.h"

class CServer :public std::enable_shared_from_this<CServer>
{
public:
CServer() = delete;
CServer(boost::asio::io_context& ioc, unsigned short& port);
void Start();

private:
tcp::acceptor _acceptor;
net::io_context& _ioc;
tcp::socket _socket;
};

修改后的start方法

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
void CServer::Start()
{
auto self = shared_from_this();

// 使用智能指针管理每个新的 socket
auto new_socket = std::make_shared<tcp::socket>(_ioc);

_acceptor.async_accept(*new_socket, [self,new_socket](beast::error_code ec) {
try {
//出错放弃该连接并监听其他连接
if (ec) {
//self->Start();
std::cout << "Accept error: " << ec.message() << std::endl;
return;
}

//创建新连接,并且创建HttpConnection管理该连接
std::make_shared<HttpConnection>(std::move(*new_socket))->Start();

//继续监听
self->Start();

}catch (std::exception& exp) {
std::cerr << "Exception: " << exp.what() << std::endl;
}
}
);
}

请注意,这里是基于当前有限的逻辑流中去实现的逻辑,如果看源码,你会发现其中并没有创建新的套接字进行对应的传递,而这种操作的原因涉及到了对应的套接字的传递方式。简单说一下,就是通过俩次std::move方法来实现一个套接字的控制逻辑的转交

​ 好了,到此为止,我们理清了目前服务器的简单逻辑。在主函数中去启动对应的高层网关服务器。接着,其会将逻辑流转到对应的CServer的执行流,在start函数中,其会进行对应的异步监听事件的调用,注册对应的处理函数,由于我们职责的解耦,所以我们这里的逻辑其实就是将对应的连接套接字交给对应的HttpConnection类进行对应的处理。

​ 此时,由于我们在设计上的职责的解耦,此时提到的多层调用结构将不必关心我们这里的连接套接字,其会交由对应的处理类进行处理,我们还需要做的就是重新注册我们的对应监听逻辑来实现一个逻辑的闭环,实现服务器的不断监听。对于现有的俩层的分析结束。接下来需要看到我们的对于连接套接字进行处理的HttpConnection处理逻辑。

HttpConnection逻辑处理

​ 对于该类,我们在设计上希望其能实现的功能是对于来自外部的请求的处理。我们前面已经了解到,在高层逻辑结束后,该类所收到的其实是一个成功与客户端建立起连接的监听套接字。接下来该类将可以通过该套接字与客户端之间进行通信,同时将不必再去关注其他的高层逻辑。

​ 接下来,我们来看到对应的类设计模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#pragma once
#include "const.h"
class HttpConnection :public std::enable_shared_from_this<HttpConnection>
{
public:
friend class LoginSystem;
HttpConnection(tcp::socket socket);
void Start();

private:
void CheckDeadline();
void WriteResponse();
void HandleReq();
void PreParseGetParam();
tcp::socket _socket;
beast::flat_buffer _buffer{ 8192 };
http::request<http::dynamic_body> _request;
http::response<http::dynamic_body>_response;
//使用花括号使用时能够以更好的可读性的格式进行列出
net::steady_timer deadline_{ _socket.get_executor(),std::chrono::seconds(60) };

std::string _get_url; //储存要解析的url
std::unordered_map<std::string, std::string> _get_params; //储存本次在参数解析中解析出的参数列表
};

​ 首先需要再强调一点,在网络编程中,由于使用裸指针可能导致的对象过期而导致的严重内存问题,所以一般来说都是使用的智能指针来进行内存管理。但是智能指针在异步设计的一些函数本身也存在一些问题。因为异步函数是不会立刻被调用的,所以如果我们不注意对应的智能指针的引用计数变化,则可能会导致智能指针管理的对象会在一些不希望的地方就直接被析构掉。所以有一个很朴素的思想。对于所有需要用到一个智能指针所管理着的对象的回调函数,我们都往其中去传递一个智能指针的对象来增加我们的引用计数来保证我们的生命周期,这样能够保证在函数调用时智能指针的引用计数永远不可能为0,也就是说在回调函数使用对应的对象管理着的指针时,其永远有效。

​ 当然,这里也需要一个额外的帮助,就是ASIO对于它底层的事件队列中过期事件的处理,如果一个连接关闭了,有关其的所有事件都应该从队列中清除。这样我们对应的智能指针的引用计数才能够减少为0,对应的对象才能够被清除。否则,其会永远留存在内存中,导致我们的内存泄漏。

​ 这里也就是为什么我们经常能够看到继承public std::enable_shared_from_this<HttpConnection>的类出现的原因,有了这个,我们才能够对于类本身进行一个智能指针对象的拷贝,具体的可以自己去了解。


​ 接下来我们来看到这个类的设计逻辑。

​ 对于该类,我们需要的是对应的来自套接字的请求的逻辑处理,这其中最基本的应该是我们的读写处理,只有基于读写的操作才能够实现我们的双端通信。由于我们使用的是基于beast网络库的http实现,所以这里也就直接创建了对应的成员

启动逻辑

​ 首先来看到一个有意思的构造函数,其能够解决我们前面为什么不使用一个新的套接字也能够实现CServer的套接字紧缺的问题。

1
HttpConnection::HttpConnection(tcp::socket socket) :_socket(std::move(socket)){}

​ 接下来是我们的http处理类的逻辑执行入口start函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void HttpConnection::Start()
{
auto self = shared_from_this();
http::async_read(_socket, _buffer, _request, [self](beast::error_code ec,std::size_t bytes_transferred) {
try {
if (ec) {
std::cout << "Http read error is " << ec.what() << std::endl;
return;
}
boost::ignore_unused(bytes_transferred);
self->HandleReq();
self->CheckDeadline();
}
catch (std::exception& ec) {
std::cout << "exception is " << ec.what() << std::endl;
}
});
}

​ 我们来分析一下这个函数的调用逻辑。对于外部,其会在创建一个由智能指针管理着的类对象后直接调用这里的Start逻辑。

1
std::make_shared<HttpConnection>(std::move(*new_socket))->Start();

​ 此时我们的套接字在一系列的逻辑转移中已经成功绑定到了对应的建立起连接的套接字上了。也就是说我们接下来的所有读写操作,都是直接对于对应的连接套接字进行的。对于这里的异步读函数逻辑,其传递了一个缓冲区进行数据的接受,指定了最终的数据写入位置。这里我们还需要一点beast的基础,对于http::request<http::dynamic_body> _request;对象,其是一个容器,内部储存的是一个http报文请求,具体的格式自行了解。这里只需要了解的是,在http通信中,客户端发送过来的应该是一个标准的http请求,那么服务器端就能够基于该请求去直接解析对应的数据,而beast网络库为我们完成了对应的解析逻辑的封装。也就是说,在一个正确运行的http服务器之下,我们的_socket中开头的http请求内容将会被储存到我们的_request结构中去。

​ 在接受处理完对应的数据之后,其应该处理的是对应的回调函数。在该回调函数中,其进行了一定的防御式编程。我们主要注意到我们的self->HandleReq(); self->CheckDeadline();逻辑,其是对于我们的http请求的逻辑处理以及对于该次连接中我们的处理事件,如果一个请求的处理事件过长,我们直接判断其为恶意的,并且直接断开该连接。这里就显示这样简单的处理。


​ 了解完我们的Http处理类对于一些基础逻辑的设计之后,我们需要来进行对应的数据包处理逻辑

处理逻辑

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
void HttpConnection::HandleReq()
{
//设置回答的版本,关于HTTP回答头的部分通用属性
_response.version(_request.version());
_response.keep_alive(false);
if (_request.method() == http::verb::get) {
PreParseGetParam();
//我们的LoginSystem中预先注册了一些URL所匹配的事务,如果没有,则会返回false
bool success=LoginSystem::GetInstance()->HandleGet(_get_url, shared_from_this());
if (!success) {
_response.result(http::status::not_found);
_response.set(http::field::content_type, "type/plain");
beast::ostream(_response.body()) << "url not found\r\n";
WriteResponse();
return;
}
_response.result(http::status::ok);
_response.set(http::field::server, "GateServer");
WriteResponse();
return;
}
else if (_request.method() == http::verb::post) {
PreParseGetParam();
bool success = LoginSystem::GetInstance()->HandlePost(_request.target(), shared_from_this());
if (!success) {
_response.result(http::status::not_found);
_response.set(http::field::content_type, "type/plain");
beast::ostream(_response.body()) << "url not found\r\n";
WriteResponse();
return;
}
_response.result(http::status::ok);
_response.set(http::field::server, "GateServer");
WriteResponse();
return;
}

}

​ 明确一下本函数的功能,其是为了在该处理类在接收解析完对应的http请求之后,进行我们的更加具体的逻辑处理。其最终的目的是构建出一个能够发送给客户端的数据包。

​ 在该处理函数中,我们进行了一定的逻辑分流,其先基于我们当前的一些需求设置了一些回复报头的属性,这部分可以自行进行定制,这里的硬编码对于部分人来说可能会看起来很难受。接下来,我们前面已经提到,我们的_rquest中已经储存了对应的http请求头中的一些格式信息,其中就包括了我们当前的请求类型,到底是一个GET类型还是一个POST类型。这里进行了一次分流,后续可能考虑独立出对应的处理逻辑。

​ 无论是在对应的GET逻辑,POST逻辑,还是之后可能扩展的一系列逻辑中,其本质的处理方式都时大差不差的,所以这里只取出一种来进行分析

​ 我们来看到对应的GET处理逻辑

​ 其内部的设计其实很简单,因为我们其实把具体的逻辑处理又给分发出去了,这是我们接下来需要了解的地方。在当前的逻辑中,我们会现对于整个HTTP请求中的URI进行解析,其中会包括一系列的解析函数,有兴趣可以自己分析一下。在解析完对应的URI之后,我们使用其中的URL进行对应的逻辑处理,由于我们这里的请求是GET类型。逻辑上分发的任务应该只是对应的静态资源的获取,所以这里直接传递了对应的url给我们的LoginSystem类对象。

​ 该方法会返回一个bool值标识该操作是否成功实现了对应的功能,我们接下来会根据对应的状态来设计我们的回复报文,这块的逻辑设计自行分析。这一块先这样简单的理解,接下来我们需要进入LoginSystem来观察到底的实现逻辑是怎么样的。

LoginSystem逻辑处理

​ 前文提到,我们在外部需要调用LoginSystem类来进行一些额外的底层逻辑的处理。对于该类,其的职责很明确,就是实现对于外部发送来的信息的解析已经处理,这系列信息将会是有关登录逻辑的。就比如我们登录验证码的收发等等,但是这里先不必深入如此。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#pragma once
#include "const.h"

class HttpConnection;

typedef std::function<void(std::shared_ptr<HttpConnection>)>HttpHandler;

class LoginSystem :public Singleton<LoginSystem>
{
friend class Singleton<LoginSystem>;
public:
~LoginSystem();
bool HandleGet(std::string path,std::shared_ptr<HttpConnection> con);
bool HandlePost(std::string path, std::shared_ptr<HttpConnection> con);
void RegGet(std::string,HttpHandler handler);
void RegPost(std::string url, HttpHandler handler);

private:
LoginSystem();
std::map<std::string, HttpHandler>_post_handlers;
std::map<std::string, HttpHandler> _get_handlers;
};

​ 对于该类的处理,我们使用单例来实现对象的创建。毕竟在一个系统中并不需要多个登录类来对于我们处理逻辑的分流(至少我们这里不需要)。对于其内部的逻辑,其实是一种类似的事件驱动机制,就相当于外部会传入一些数据,该类将这些数据转换为一些有关的触发逻辑,然后根据这些触发逻辑在类内进行对应的逻辑跳转。

​ 我们来看到该类的核心

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
LoginSystem::LoginSystem()
{
RegGet("/get_test", [](std::shared_ptr<HttpConnection> connection) {
beast::ostream(connection->_response.body()) << "receive get_test req";
int i = 0;
for (auto& elem : connection->_get_params) {
i++;
beast::ostream(connection->_response.body()) << "\nparam " << i << ":key is " << elem.first;
beast::ostream(connection->_response.body()) << "\nparam " << i << ":value is " << elem.second << std::endl;
}
});

RegPost("/get_varifycode", [](std::shared_ptr<HttpConnection>connection) {
auto body_str = boost::beast::buffers_to_string(connection->_request.body().data());
std::cout << "received body id " << body_str << std::endl;
connection->_response.set(http::field::content_type, "text/json");
Json::Value root;
Json::Reader reader;
Json::Value src_root;
bool parse_success = reader.parse(body_str,src_root);
if (!parse_success) {
std::cout << "Failed to parse JSON data!" << std::endl;
root["error"] = ErrorCodes::Error_Json;
std::string jsonStr = root.toStyledString();
beast::ostream(connection->_response.body()) << jsonStr;
return true;
}

//将合法的JSON加载到对应的root中去
if (!src_root.isMember("email"))
{
std::cout << "Failed to parse JSON data!" << std::endl;
root["error"] = ErrorCodes::Error_Json;
std::string jsonStr = root.toStyledString();
beast::ostream(connection->_response.body()) << jsonStr;
return true;
}

auto email = src_root["email"].asString();
std::cout << "email is " << email << std::endl;
root["error"] = 0;
root["email"] = src_root["email"];
std::string jsonStr = root.toStyledString();
beast::ostream(connection->_response.body()) << jsonStr;
return true;
});
}

​ 这里的处理逻辑是根据我们的高层规定去限制的,就比如Http请求的格式是GET,那么域名所紧跟着的内容就应该是串对应的资源地址,总总这些,都是我们高层协议的一部分。为了更好的分析,我们来使用RegPost的逻辑进行分析,对于该方法,其实需要了解我们类内是怎么设计对应的注册函数的。如下

1
2
3
4
5
6
7
8
9
void LoginSystem::RegGet(std::string url, HttpHandler handler)
{
_get_handlers.insert(make_pair(url, handler));
}

void LoginSystem::RegPost(std::string url, HttpHandler handler)
{
_post_handlers.insert(make_pair(url, handler));
}

​ 其实就是简单的将我们的对应的URL与可调用对象进行一次键值对的关联,通过解析用户输入的URI来实现URL的传输,进而来触发我们这里的处理逻辑。

​ 回到我们的RegPost处理逻辑。其现在的逻辑处理相对简单,就是对于当前连接的请求体数据进行一次解析,由于我们程序内部的使用都是面向一个字符串的,所以需要使用方法将我们的字节流数据转换为我们可以使用的字符串数据。接着我们根据对应的类型再去设置一些回复报文的信息。再然后,我们使用转换后的字符串流信息来进行解析,其内包含的即是客户端所发送来的信息,一但该解析过程中出现错误或者最终的解析结果中存在一些我们规定之外的数据。我们往其中写入一些错误信息并返回。如果各种都没有发生错误,这里简单的使用一个回写进行测试并返回。

​ 最后再来处理一下Handle系列函数的逻辑,这里只是简单的进行我们注册在映射逻辑中的方法的调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
bool LoginSystem::HandleGet(std::string path, std::shared_ptr<HttpConnection> con)
{
if (_get_handlers.find(path) == _get_handlers.end()) {
return false;
}
_get_handlers[path](con);
return true;
}

bool LoginSystem::HandlePost(std::string path, std::shared_ptr<HttpConnection> con)
{
if (_post_handlers.find(path) == _post_handlers.end()) {
return false;
}
_post_handlers[path](con);
return true;
}

​ 通过这些,实现了一种简单的事件驱动。


总结

​ 接下来进行一下这里的整个程序逻辑的简短分析。

​ 首先,其简单的启动一个服务器,然后将逻辑转向我们的CServer类,在该类中,我们启动了一个监听套接字并绑定了对应的回调函数。在回调函数中,我们实现了更一步的逻辑转移,将来自客户端的连接转移到对应的HttpConnection中去,由其来进行处理,其内部会进行我们http报文的解析以及http回复报文的构建。在这其中,会存在着来自LoginSystem模块的协助,对应的HttpConnection需要改模块来实现对应的逻辑实现,也就是说,该类进行的操作是一些简单的解析以及逻辑处理的分发。使得各个模块各司其职,逐渐构建其一个健壮的HTTP服务器。

-------------本文结束 感谢阅读-------------