使用beast网路库实现Http通讯


Note: 此项目原博客地址

https://llfc.club/category?catid=225RaiVNI8pFDD5L4m807g7ZwmF#!aid/2eIaI00hKJDhilXBVmIqPERpqXq

代码整体思路与流程图

流程图:

整体思路:
首先由Server类来接收客户端发送过来的信息,但是Server类并不对接收的信息进行具体处理,而是下发给HttpConnectionMgr类,让HttpConnectionMgr来判断接收的到底是GET信息还是POST信息,然后解析出URL,将URL传递给逻辑层LogicSystem类,通过JSON解析出邮箱,然后将相应的信息注入_response(回复的信息)中,然后返回HttpConnection类发送_response给客户端

创建项目

打开Visual stdidual,创建GateServer项目,选择控制台程序,创建完成后开始配置boost环境和json环境,如何配置boost与json在上篇文章已经介绍过了,这里就不多赘述

创建Server

Server类用于接收客户端发送的数据,以下是Server类的声明

1
2
3
4
5
6
7
8
9
10
11
12
class Server: public std::enable_shared_from_this<Server>
{
public:
Server(boost::asio::io_context& ioc, unsigned short& port);
void Start();

private:
boost::asio::io_context& _io_context;
boost::asio::ip::tcp::acceptor _acceptor;
boost::asio::ip::tcp::socket _socket;
unsigned short& _port;
};

Server的类构造函数需要接受一个端口号,创建acceptor来接收新到来的链接,代码如下

1
2
3
4
5
Server::Server(boost::asio::io_context& ioc, unsigned short& port)
: _io_context(ioc), _socket(ioc), _port(port), _acceptor(ioc, boost::asio::ip::tcp::endpoint(boost::asio::ip::tcp::v4(), port))
{

}

Start函数是用来实现监听新链接,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void Server::Start() {
auto self = shared_from_this();
_acceptor.async_accept(_socket, [self](boost::beast::error_code ec) {
try {
//服务器不能停止,所以出现错误也要继续监听
if (ec) {
self->Start();
return;
}

//没有出现错误,所以创建HttpConnectinMgr类来管理HTTP连接
std::make_shared<HttpConnectionMgr>(std::move(self->_socket))->Start();
self->Start();
}
catch (std::exception& error) {
std::cout << "Server start error, error is: " << error.what() << std::endl;
self->Start();
}
});
}

在Start函数内创建HttpConnectionMgr的智能指针,将_socket数据转移到HttpConnectionMgr管理,Server类里面原来的_socket就可以继续用来接收写链接

创建const.h并将常用的头文件放进去,代码如下

1
2
3
4
5
#pragma once

#include <boost/beast/http.hpp>
#include <boost/beast.hpp>
#include <boost/asio.hpp>

创建HttpConnectionMgr

创建HttpConnectionMgr类,添加以下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class HttpConnectionMgr:public std::enable_shared_from_this<HttpConnectionMgr>
{
friend class LogicSystem;
public:
HttpConnectionMgr(boost::asio::ip::tcp::socket socket);
void Start();

private:
void CheckDeadLine();
void WriteResponse();
void HandleRequest();

boost::asio::ip::tcp::socket _socket;
boost::beast::flat_buffer _buffer{ 8192 };
boost::beast::http::request<boost::beast::http::dynamic_body> _request;
boost::beast::http::response<boost::beast::http::dynamic_body> _response;
boost::asio::steady_timer _deadline{ _socket.get_executor(), std::chrono::seconds(60) };
};

_buffer用来接收接收数据

_request用来解析请求

_response用来回复客户端

_deadline用来检验请求是否超时

以下是类构造函数的实现

1
2
3
 HttpConnectionMgr::HttpConnectionMgr(boost::asio::ip::tcp::socket socket): _socket(std::move(socket)){

}

Start()函数的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
 void HttpConnectionMgr::Start(){
auto self = shared_from_this();
boost::beast::http::async_read(_socket, _buffer, _request, [self](boost::beast::error_code ec, std::size_t byte_transfered) {
try {
if (ec) {
std::cout << "Read Http is failed, error2 is: " << ec.what() << std::endl;
return;
}

//处理收到的数据
boost::ignore_unused(byte_transfered);
self->HandleRequest();
self->CheckDeadLine();
}
catch (std::exception& error) {
std::cout << "Read Http is failed, error1 is: " << error.what() << std::endl;
}
});
}

Start()函数用来接收客户端的信息,通过异步读的方式将客户端发送过来的信息存入到 _buffer和 _request里面

接下来我们实现HandleRequest()函数

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
 void HttpConnectionMgr::HandleRequest() {
//设置版本
_response.version(_request.version());
//设置为短链接
_response.keep_alive(false);
//发送的是GET请求
if (_request.method() == boost::beast::http::verb::get) {
bool sucess = LogicSystem::GetInstance()->HandleGet(_request.target(), shared_from_this());
if (sucess) {
_response.result(boost::beast::http::status::ok);
_response.set(boost::beast::http::field::server, "GateServer");
WriteResponse();
return;
}
else {
_response.result(boost::beast::http::status::not_found);
_response.set(boost::beast::http::field::content_type, "text/plain");
boost::beast::ostream(_response.body()) << "url not found\r\n";
WriteResponse();
return;
}
}

//发送的是POST请求

}

收到数据之后,我们将GET请求的具体处理丢给LogicSystem类,在HttpConnectionMgr类里面只根据处理成功还是失败回应数据包给对方。

创建LogicSystem

为了保证程序的安全性,所以实现LogicSystem类我们采用单例模式

单例类的实现

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
#pragma once

#include <mutex>
#include <memory>
#include <iostream>

template <typename T>
class SingleTon {
protected:
SingleTon() = default;
SingleTon(const SingleTon<T>&) = delete;
SingleTon& operator=(const SingleTon<T>&) = delete;

static std::shared_ptr<T> _instance;

public:
static std::shared_ptr<T> GetInstance() {
static std::once_flag _flag;
std::call_once(_flag, [&]() {
_instance = std::shared_ptr<T>(new T);
});
return _instance;
}

void PrintAddress() {
std::cout << _instance.get() << std::endl;
}

~SingleTon() {
std::cout << "this singleton is destructed!" << std::endl;
}
};

template <typename T>
std::shared_ptr<T> SingleTon<T>::_instance = nullptr;

接下来实现LogicSystem单例类

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

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

class LogicSystem:public SingleTon<LogicSystem>
{
friend class SingleTon<LogicSystem>;
public:
~LogicSystem();
bool HandleGet(std::string url, std::shared_ptr<HttpConnectionMgr> connection);
void RegsiterGet(std::string url, HttpHandler handler);

private:
LogicSystem();

std::map<std::string, HttpHandler> _post_handler;
std::map<std::string, HttpHandler> _get_handler;
};

_post_handler是POST请求的回调函数的map,_get_handler是GET请求的回调函数的map,key为路由,value为回调函数,只不过这里将回调函数是使用std::function包装成HttpHandler

接下来实现LogicSystem类的构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
LogicSystem::LogicSystem(){
//处理GET请求
RegsiterGet("/get_test", [](std::shared_ptr<HttpConnectionMgr> connection) {
boost::beast::ostream(connection->_response.body()) << "recieve get_test req" << std::endl;
int i = 0;
for (auto& elem : connection->_get_params) {
i++;
boost::beast::ostream(connection->_response.body()) << "parm" << i << "key is " << elem.first;
boost::beast::ostream(connection->_response.body()) << "," << "value is " << elem.second << std::endl;
}
});

//处理POST请求

}

我们在构造函数里面实现/get_test路由的回调函数

实现RegsiterGet()函数用来接收客户端的信息

1
2
3
void LogicSystem::RegsiterGet(std::string url, HttpHandler handler) {
_get_handler.insert(std::make_pair(url, handler));
}

在RegsiterGet()函数里面添加新的key和value

接下来实现HandleGet()函数

1
2
3
4
5
6
7
bool LogicSystem::HandleGet(std::string url, std::shared_ptr<HttpConnectionMgr> connection) {
if (_get_handler.find(url) == _get_handler.end()) {
return false;
}
_get_handler[url](connection);
return true;
}

LogicSystem类的函数已经基本实现,接下来实现HttpConnectionMgr类里面的WriteResponse()函数与CheckDeadLine()函数

WriteResponse()函数的实现

1
2
3
4
5
6
7
8
9
10
void HttpConnectionMgr::WriteResponse() {
auto self = shared_from_this();
_response.content_length(_response.body().size());
boost::beast::http::async_write(_socket, _response, [self](boost::beast::error_code ec, std::size_t) {
if (ec) {
self->_socket.shutdown(boost::asio::ip::tcp::socket::shutdown_send);
self->_deadline.cancel();
}
});
}

因为http是短链接,所以发送完数据后不需要再监听对方链接,直接断开发送端即可。

http处理请求需要有一个时间约束,发送的数据包不能超时。所以在发送时我们启动一个定时器,收到发送的回调后取消定时器。

这是检测超时函数CheckDeadLine()的实现

1
2
3
4
5
6
7
8
void HttpConnectionMgr::CheckDeadLine() {
auto self = shared_from_this();
_deadline.async_wait([self](boost::beast::error_code ec) {
if (!ec) {
self->_socket.close(ec);
}
});
}

实现主函数main

我们在主函数中初始化上下文iocontext以及启动信号监听ctr-c退出事件, 并且启动iocontext服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int main()
{
try {
unsigned short port = static_cast<unsigned short>(8080);
boost::asio::io_context ioc{ 1 };
boost::asio::signal_set signals(ioc, SIGINT, SIGTERM);
signals.async_wait([&ioc](const boost::system::error_code& ec, int signal_number) {
if (ec) {
return;
}
ioc.stop();
});
std::make_shared<Server>(ioc, port)->Start();
ioc.run();
}
catch (std::exception const& ec) {
std::cout << "Exception is: " << ec.what() << std::endl;
return EXIT_FAILURE;
}
return 0;
}

实现GET请求

实现GET请求的时候我们需要先解码,分析数据,再编码发送给客户端,为了实现GET请求解码编码的函数,我们需要10进制char转换为16进制或者将16进制的char转化为10进制

10进制的char转化为16进制

1
2
3
unsigned char ToHex(unsigned short x) {
return x > 9 ? x + 55 : x + 48;
}

16进制的char转化为10进制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
unsigned char FromHex(unsigned short x) {
unsigned char y = 0;
if (x > 'A' && x < 'Z') {
y = x - 'A' + 10;
}
else if (x > 'a' && x < 'z') {
y = x - 'a' + 10;
}
else if (x > '0' && x < '9') {
y = x - '0';
}
else {
assert(0);
}

return y;
}

编码函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
std::string UrlEncode(const std::string& str) {
std::string strTemp = "";
std::size_t length = str.length();
for (int i = 0;i < length;i++) {
if (isalnum((unsigned short)str[i]) ||
(str[i] == '-') ||
(str[i] == '_') ||
(str[i] == '.') ||
(str[i] == '~'))
{
strTemp += str[i];
}
else if (str[i] == ' ') {
strTemp += "+";
}
else {
strTemp += "%";
strTemp += ToHex(str[i] >> 4);
strTemp += ToHex(str[i] & 0x0F);
}
}
return strTemp;
}

首先判断str[i]是否为数字或者一些简单的下划线等字符,如果是直接拼接就可以,之后再判断str[i]是否为空,如果为空则拼上一个‘+’;剩下的都是特殊字符。需要将特殊字符转化为’%’和两个十六进制字符拼接;先拼接“%“字符,,再将字符的高四位拼接到strTemp上,最后将低四位拼接到strTemp上。

解码函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
std::string UrlDecode(const std::string& str) {
std::string strTemp = "";
std::size_t length = str.length();
for (int i = 0;i < length;i++) {
if (str[i] == '+') {
strTemp += " ";
}
else if (str[i] == '%') {
assert(i + 2 < length);
unsigned char high = FromHex((unsigned char)str[++i]);
unsigned char low = FromHex((unsigned char)str[++i]);
strTemp += high * 16 + low;
}
else {
strTemp += str[i];
}
}
return strTemp;
}

解码函数的编写与编码函数原理一样,就不写注释了

先在HttpConnectionMgr类里添加两个私有成员,_get_url用来存储HTTP的url信息,_get_params用来存储url和相关信息,url就是_get_params的key值

1
2
std::string _get_url;
std::map<std::string, std::string> _get_params;

接下来添加解析URL的函数

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
void HttpConnectionMgr::PreParseGetParam() {
auto uri = _request.target();
auto query_pos = uri.find('?');
if (query_pos == std::string::npos) {
_get_url = uri;
return;
}

_get_url = uri.substr(0, query_pos);
std::string query_string = uri.substr(query_pos + 1);
std::string key;
std::string value;
std::size_t pos = 0;

while ((pos = query_string.find('&')) != std::string::npos) {

auto pair = query_string.substr(0, pos);
std::size_t ep_pos = pair.find('=');
if (ep_pos != std::string::npos) {
key = UrlDecode(pair.substr(0, ep_pos));
value = UrlDecode(pair.substr(ep_pos + 1));
_get_params[key] = value;
}
query_string.erase(0, pos + 1);
}

//还有一段信息未解析,解析最后一段数据
if (!query_string.empty()) {
std::size_t ep_pos = query_string.find('=');
if (ep_pos != std::string::npos) {
key = UrlDecode(query_string.substr(0, ep_pos));
value = UrlDecode(query_string.substr(ep_pos + 1));
_get_params[key] = value;
}
}
}

在HandleRequest()函数里面添加解析URL的函数

1
2
3
4
5
6
7
8
9
10
11
void HttpConnectionMgr::HandleRequest() {
//...
//发送的是GET请求
if (_request.method() == boost::beast::http::verb::get) {
PreParseGetParam();
bool sucess = LogicSystem::GetInstance()->HandleGet(_get_url, shared_from_this());
//...
}

//发送的是POST请求
}

实现POST请求

解析POST请求与解析GET请求的流程基本一致,首先在LogicSystem类里添加处理函数

1
2
bool HandlePost(std::string url, std::shared_ptr<HttpConnectionMgr> connection);
void RegsiterPost(std::string url, HttpHandler handler);

然后实现HandlePost()和RegsiterPost()这两个具体的函数

1
2
3
4
5
6
7
8
9
10
bool LogicSystem::HandlePost(std::string url, std::shared_ptr<HttpConnectionMgr> connection) {
if (_post_handler.find(url) == _post_handler.end()) {
return false;
}
_post_handler[url](connection);
}

void LogicSystem::RegsiterPost(std::string url, HttpHandler handler) {
_post_handler.insert(std::make_pair(url, handler));
}

在LogicSystem类的构造函数添加处理POST请求

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
//处理POST请求
RegsiterPost("/get_varifycode", [](std::shared_ptr<HttpConnectionMgr> connection) {
auto body_str = boost::beast::buffers_to_string(connection->_request.body().data());
std::cout << "recieve data body is: " << body_str << std::endl;
connection->_response.set(boost::beast::http::field::content_type, "test/json");
Json::Value root;
Json::Value src_root;
Json::Reader reader;
bool parse_sucess = reader.parse(body_str, src_root);
if (!parse_sucess) {
std::cout << "Failed to parse json data!" << std::endl;
root["error"] = ErrorCodes::Error_Json;
std::string jsonstr = root.toStyledString();
boost::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();
boost::beast::ostream(connection->_response.body()) << jsonstr;
return true;
});

在HttpConnectionMgr类里的HandleRequest() 函数里面添加处理POST请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void HttpConnectionMgr::HandleRequest() {
//...省略
//发送的是POST请求
if (_request.method() == boost::beast::http::verb::post) {
PreParseGetParam();
bool sucess = LogicSystem::GetInstance()->HandlePost(_get_url, shared_from_this());
if (sucess) {
_response.result(boost::beast::http::status::ok);
_response.set(boost::beast::http::field::server, "GateServer");
WriteResponse();
return;
}
else {
_response.result(boost::beast::http::status::not_found);
_response.set(boost::beast::http::field::content_type, "text/plain");
boost::beast::ostream(_response.body()) << "url not found\r\n";
WriteResponse();
return;
}
}
}

客户端增加POST逻辑

之前我们已经在Qt客户端实现了POST请求,现在我们实现发送HTTP的POST请求

找到RegisterDialog类的on_acquire_pushbutton_clicked()函数,添加如下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
void RegisterDialog::on_acquire_pushbutton_clicked()
{
//...省略
if(match){
//发送Http请求发送验证码
QJsonObject json_obj;
json_obj["email"] = email;
HttpMgr::GetInstance()->PostHttpRequest( ReqID::ID_GET_VARIFY_CODE, Modules::REGISTER_MOD, QUrl(gate_url_prefix + "/get_varifycode"), json_obj);
}
else{
ShowTips("邮箱地址不正确", false);
}
}

当服务器不启动,客户端输入邮箱,点击获取验证码,客户端会收到网络连接失败的提示

启动服务器后,再次获取验证码,就显示正确提示了,而且客户端输出了服务器回传的邮箱地址email is “1300164336@qq.com“,界面也刷新为正确显示

客户端添加ini文件

在代码所在的根目录下新建一个config.ini文件, 内部添加配置

1
2
3
[GateServer]
host=localhost
port=8080

点击项目右键选择添加现有文件,选中config.ini即可

打开项目的pro文件,在最下面添加以下代码

1
2
3
4
5
6
7
8
9
10
11
12
win32:CONFIG(release, debug | release)
{
#指定要拷贝的文件目录为工程目录下release目录下的所有dll、lib文件,例如工程目录在D:\QT\Test
TargetConfig = $${PWD}/config.ini
#将输入目录中的"/"替换为"\"
TargetConfig = $$replace(TargetConfig, /, \\)
#将输出目录中的"/"替换为"\"
OutputDir = $${OUT_PWD}/$${DESTDIR}
OutputDir = $$replace(OutputDir, /, \\)
//执行copy命令
QMAKE_POST_LINK += copy /Y \"$$TargetConfig\" \"$$OutputDir\"
}

因为我们的程序最终会输出的bin目录,所以在pro中添加拷贝脚本将配置也拷贝到bin目录

在globle.h中添加声明

1
extern QString gate_url_prefix;

在globle.cpp里面添加定义

1
QString gate_url_prefix = "";

在main函数里面添加解析配置的逻辑

1
2
3
4
5
6
7
8
9
10
QString app_path = QCoreApplication::applicationDirPath();
qDebug()<<"app_path is: "<<app_path;
QString file_name = "config.ini";
QString config_path = QDir::toNativeSeparators(app_path + QDir::separator() + file_name);
qDebug()<<"config_path is: "<<config_path;
QSettings settings(config_path, QSettings::IniFormat);
QString gate_host = settings.value("GateServer/host").toString();
QString gate_port = settings.value("GateServer/port").toString();
qDebug()<<"host is: "<<gate_host;
gate_url_prefix = "http://" + gate_host + ":" + gate_port;

将RegisterDialog类的on_acquire_pushbutton_clicked()函数里面的发送请求改为

1
HttpMgr::GetInstance()->PostHttpRequest( ReqID::ID_GET_VARIFY_CODE, Modules::REGISTER_MOD, QUrl(gate_url_prefix + "/get_varifycode"), json_obj);

再次测试仍旧可以收到服务器回馈的http包。

这么做的好处就是客户端增加了配置,而且以后修改参数也方便。