QT聊天项目(4)

简介

今天我们使用beast搭建http server

一、创建项目

我们利用visual studio创建一个空项目,项目名字为GateServer,然后按照上一期的方法配置boost库和jsoncpp配置好后,我们添加一个新的类,名字叫CServer。添加成功后生成的CServer.h和CServer.cpp也会自动加入到项目中。

CServer类构造函数接受一个端口号,创建acceptor接受新到来的链接。

我们创建const.h将文件件和一些作用于声明放在const.h里,这样以后创建的文件包含这个const.h即可,不用写那么多头文件了。

cpp 复制代码
#include <boost/beast/http.hpp>
#include <boost/beast.hpp>
#include <boost/asio.hpp>
namespace beast = boost::beast;         // from <boost/beast.hpp>
namespace http = beast::http;           // from <boost/beast/http.hpp>
namespace net = boost::asio;            // from <boost/asio.hpp>
using tcp = boost::asio::ip::tcp;       // from <boost/asio/ip/tcp.hpp>

CServer.h中声明acceptor, 以及用于事件循环的上下文iocontext,和构造函数

cpp 复制代码
class CServer:public std::enable_shared_from_this<CServer>
{
public:
    CServer(boost::asio::io_context& ioc, unsigned short& port);
    void Start();
private:
    tcp::acceptor  _acceptor;
    net::io_context& _ioc;
    boost::asio::ip::tcp::socket   _socket;
};

cpp中实现构造函数如下

cpp 复制代码
#include "CServer.h"
#include"HttpConnection.h"

CServer::CServer(boost::asio::io_context& ioc, unsigned short& port):_ioc(ioc),
_acceptor(ioc,tcp::endpoint(tcp::v4(),port)),_socket(ioc)
{

}

二、创建HttpConnection类

Start函数内创建HttpConnection类型智能指针,将_socket内部数据转移给HttpConnection管理,_socket继续用来接受写的链接。

新建HttpConnection类文件,在头文件添加声明

cpp 复制代码
#pragma once
#include"const.h"
class HttpConnection:public std::enable_shared_from_this<HttpConnection>
{
public:
	friend class LogicSystem;

	HttpConnection(tcp::socket socket);
	void Start();

private:
	//检测超时
	void CheckDeadLine();
	//应答
	void WriteResponse();
	//处理请求
	void HandleReq();
	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)
	};
};

实现HttpConnection构造函数

cpp 复制代码
HttpConnection::HttpConnection(tcp::socket socket)
    : _socket(std::move(socket)) {
}

我们考虑在HttpConnection::Start内部调用http::async_read函数,其源码为

cpp 复制代码
async_read(
    AsyncReadStream& stream,
    DynamicBuffer& buffer,
    basic_parser<isRequest>& parser,
    ReadHandler&& handler)

第一个参数为异步可读的数据流,大家可以理解为socket.

第二个参数为一个buffer,用来存储接受的数据,因为http可接受文本,图像,音频等多种资源文件,所以是Dynamic动态类型的buffer。

第三个参数是请求参数,我们一般也要传递能接受多种资源类型的请求参数。

第四个参数为回调函数,接受成功或者失败,都会触发回调函数,我们用lambda表达式就可以了。

实现HttpConnection的Start函数

cpp 复制代码
void CServer::Start()
{
	auto self = shared_from_this();
	_acceptor.async_accept(_socket, [self](beast::error_code ec)
		{
			try
			{
				//出错放弃连接
				if (ec)
				{
					//继续监听其他连接
					self->Start();
					return;
				}
				//创建新连接,并且创建HttpConnection类管理这个连接
				std::make_shared<HttpConnection>(std::move(self->_socket))->Start();
				//继续监听其他连接
				self->Start();
			}
			catch (const std::exception& e)
			{
				std::cout << "exception is" << ec.what() << std::endl;
			}
		});
}

实现HandleReq

cpp 复制代码
void HttpConnection::HandleReq()
{
	//设置版本
	_response.version(_request.version());
	_response.keep_alive();
	if (_request.method() == http::verb::get)
	{
		bool success =LogicSystem::GetInstance()->HandleGet(_request.target(), shared_from_this());
		if (!success)
		{
			_response.result(http::status::not_found);
			_response.set(http::field::content_type, "text/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;
	}
}

三、LogicSystem单例类

为了方便我们先实现Get请求的处理,根据请求类型为get调用LogicSystem的HandleGet接口处理get请求,根据处理成功还是失败回应数据包给对方。

我们先实现LogicSystem,采用单例模式,单例基类之前讲解过了

cpp 复制代码
#include <memory>
#include <mutex>
#include <iostream>
template <typename T>
class Singleton {
protected:
    Singleton() = default;
    Singleton(const Singleton<T>&) = delete;
    Singleton& operator=(const Singleton<T>& st) = delete;
    static std::shared_ptr<T> _instance;
public:
    static std::shared_ptr<T> GetInstance() {
        static std::once_flag s_flag;
        std::call_once(s_flag, [&]() {
            _instance = shared_ptr<T>(new T);
            });
        return _instance;
    }
    void PrintAddress() {
        std::cout << _instance.get() << endl;
    }
    ~Singleton() {
        std::cout << "this is singleton destruct" << std::endl;
    }
};
template <typename T>
std::shared_ptr<T> Singleton<T>::_instance = nullptr;

实现LogicSystem单例类

cpp 复制代码
#pragma once
#include"const.h"
#include"HttpConnection.h"
class HttpConnection;
typedef std::function<void(std::shared_ptr<HttpConnection>)> HttpHandler;
class LogicSystem:public Singleton<LogicSystem> 
{
	friend class Singleton<LogicSystem>;

public:
	~LogicSystem();
	bool HandleGet(std::string, std::shared_ptr<HttpConnection>);
	//注册
	void RegGet(std::string,HttpHandler handler);
private:
	LogicSystem();
	std::map<std::string, HttpHandler> _post_handlers;
	std::map<std::string, HttpHandler> _get_handlers;

};

_post_handlers和_get_handlers分别是post请求和get请求的回调函数map,key为路由,value为回调函数。

我们实现RegGet函数,接受路由和回调函数作为参数

cpp 复制代码
void LogicSystem::RegGet(std::string url, HttpHandler handler) {
    _get_handlers.insert(make_pair(url, handler));
}

在构造函数中实现具体的消息注册

cpp 复制代码
LogicSystem::LogicSystem() {
    RegGet("/get_test", [](std::shared_ptr<HttpConnection> connection) {
        beast::ostream(connection->_response.body()) << "receive get_test req";
    });
}

为防止互相引用,以及LogicSystem能够成功访问HttpConnection,在LogicSystem.cpp中包含HttpConnection头文件

并且在HttpConnection中添加友元类LogicSystem, 且在HttpConnection.cpp中包含LogicSystem.h文件

cpp 复制代码
 bool LogicSystem::HandleGet(std::string path, std::shared_ptr<HttpConnection> connect)
{
	if(_get_handlers.find(path) == _get_handlers.end())
	{
		return false;
	}

	_get_handlers[path](connect);
	return true;
}

这样我们在HttpConnection里实现WriteResponse函数

cpp 复制代码
void HttpConnection::WriteResponse()
{
	auto self = shared_from_this();
	_response.content_length(_response.body().size());
	http::async_write(_socket, _response, [self](beast::error_code ec, ::std::size_t bytes_transferred)
		{
			self->_socket.shutdown(tcp::socket::shutdown_send, ec);
			self->deadline.cancel();
		});
}

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

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

我们实现检测超时的函数

cpp 复制代码
void HttpConnection::CheckDeadLine()
{
	auto self = shared_from_this();
	deadline.async_wait([self](beast::error_code ec)
		{
			if (!ec)
			{
				self->_socket.close(ec);
			}
		});
}

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

cpp 复制代码
#include <iostream>
#include <json/json.h>
#include <json/value.h>
#include <json/reader.h>
#include "CServer.h"
int main()
{

	try
	{
		unsigned short port = static_cast<unsigned short>(8085);
		net::io_context ioc{ 1 };
		boost::asio::signal_set signals(ioc, SIGINT, SIGTERM);
		signals.async_wait([&ioc](const beast::error_code& error, int signal_num)
			{
				if(error)
				{
					std::cout << "signal err is" << error.what() << std::endl;
					return;
				}
				ioc.stop();

			});
		std::make_shared<CServer>(ioc, port)->Start();
		ioc.run();
	}
	catch (const std::exception& e)
	{
		std::cerr << "exception is" << e.what() << std::endl;
		return EXIT_FAILURE;
	}
}
  1. ``启动服务器,在浏览器输入`http://localhost:8085/get_test```
  2. ``会看到服务器回包`receive get_test req```
  3. ``如果我们输入带参数的url请求`http://localhost:8085/get_test?key1=value1&key2=value2```
  4. ``会收到服务器反馈`url not found```
  5. 所以对于get请求带参数的情况我们要实现参数解析,我们可以自己实现简单的url解析函数

将十进制的char转为16进制,如果是数字不超过9则加48转为对应的ASCII码的值

如果字符是大于9的,比如A~Z, a~z等则加55,获取到对应字符的ASCII码值

详细的ASCII码表大家可以看这个https://c.biancheng.net/c/ascii/

接下来实现从16进制转为十进制的char的方法

cpp 复制代码
unsigned char FromHex(unsigned char x)
{
    unsigned char y;
    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;
}

接下来我们实现url编码工作

cpp 复制代码
std::string UrlEncode(const std::string& str)
{
    std::string strTemp = "";
    size_t length = str.length();
    for (size_t i = 0; i < length; i++)
    {
        //判断是否仅有数字和字母构成
        if (isalnum((unsigned char)str[i]) ||
            (str[i] == '-') ||
            (str[i] == '_') ||
            (str[i] == '.') ||
            (str[i] == '~'))
            strTemp += str[i];
        else if (str[i] == ' ') //为空字符
            strTemp += "+";
        else
        {
            //其他字符需要提前加%并且高四位和低四位分别转为16进制
            strTemp += '%';
            strTemp += ToHex((unsigned char)str[i] >> 4);
            strTemp += ToHex((unsigned char)str[i] & 0x0F);
        }
    }
    return strTemp;
}

我们先判断str[i]是否为字母或者数字,或者一些简单的下划线,如果是泽直接拼接,否则判断是否为空字符,如果为空则换成'+'拼接。否则就是特殊字符,我们需要将特殊字符转化为'%'和两个十六进制字符拼接。现拼接'%',再将字符的高四位拼接到strTemp上,最后将低四位拼接到strTemp上。

url解码的工作正好相反

cpp 复制代码
std::string UrlDecode(const std::string& str)
{
    std::string strTemp = "";
    size_t length = str.length();
    for (size_t i = 0; i < length; i++)
    {
        //还原+为空
        if (str[i] == '+') strTemp += ' ';
        //遇到%将后面的两个字符从16进制转为char再拼接
        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;
}

接下来实现get请求的参数解析, 在HttpConnection里添加两个成员

cpp 复制代码
std::string _get_url;
std::unordered_map<std::string, std::string> _get_params;

参数解析如下

cpp 复制代码
void HttpConnection::PreParseGetParam() {
    // 提取 URI  
    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;
    size_t pos = 0;
    while ((pos = query_string.find('&')) != std::string::npos) {
        auto pair = query_string.substr(0, pos);
        size_t eq_pos = pair.find('=');
        if (eq_pos != std::string::npos) {
            key = UrlDecode(pair.substr(0, eq_pos)); // 假设有 url_decode 函数来处理URL解码  
            value = UrlDecode(pair.substr(eq_pos + 1));
            _get_params[key] = value;
        }
        query_string.erase(0, pos + 1);
    }
    // 处理最后一个参数对(如果没有 & 分隔符)  
    if (!query_string.empty()) {
        size_t eq_pos = query_string.find('=');
        if (eq_pos != std::string::npos) {
            key = UrlDecode(query_string.substr(0, eq_pos));
            value = UrlDecode(query_string.substr(eq_pos + 1));
            _get_params[key] = value;
        }
    }
}

HttpConnection::HandleReq函数略作修改

cpp 复制代码
void HttpConnection::HandleReq() {
    //...省略
    if (_request.method() == http::verb::get) {
        PreParseGetParam();
        bool success = LogicSystem::GetInstance()->HandleGet(_get_url, shared_from_this());
    }
    //...省略
}

我们修改LogicSytem构造函数,在get_test的回调里返回参数给对端

cpp 复制代码
LogicSystem::LogicSystem() {
    RegGet("/get_test", [](std::shared_ptr<HttpConnection> connection) {
        beast::ostream(connection->_response.body()) << "receive get_test req " << std::endl;
        int i = 0;
        for (auto& elem : connection->_get_params) {
            i++;
            beast::ostream(connection->_response.body()) << "param" << i << " key is " << elem.first;
            beast::ostream(connection->_response.body()) << ", " <<  " value is " << elem.second << std::endl;
        }
    });
}

在浏览器输入http://localhost:8080/get_test?key1=value1&key2=value2 测试一下

相关推荐
lly2024061 小时前
PostgreSQL DELETE 操作详解
开发语言
今天你TLE了吗1 小时前
JVM学习笔记:第二章——类加载子系统
java·开发语言·jvm·笔记
AutumnorLiuu2 小时前
C++并发编程学习(四)——死锁及其预防
开发语言·c++·学习
_OP_CHEN2 小时前
【前端开发之JavaScript】(二)JS基础语法上篇:吃透变量 / 类型 / 输入输出
开发语言·javascript·html·ecmascript·前端开发·网页开发
Maguyusi2 小时前
go 批量生成 c++与lua的proto文件
开发语言·后端·golang·protobuf
万能的小裴同学2 小时前
饥荒Mod
java·开发语言·junit
foxsen_xia2 小时前
Kamailio通过Lua写路由
开发语言·lua·信息与通信
燃于AC之乐2 小时前
深入解剖STL set/multiset:接口使用与核心特性详解
开发语言·c++·stl·面试题·set·multiset
REDcker2 小时前
Paho MQTT C 开发者快速入门
c语言·开发语言·mqtt