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 ; } 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 ); 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 ; } } }
收到数据之后,我们将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 (){ 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; } }); }
我们在构造函数里面实现/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 () { if (_request.method () == boost::beast::http::verb::get) { PreParseGetParam (); bool sucess = LogicSystem::GetInstance ()->HandleGet (_get_url, shared_from_this ()); } }
实现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 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 () { 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){ 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包。
这么做的好处就是客户端增加了配置,而且以后修改参数也方便。