环境建立
Visual Studio 2015,Vcpkg
vcpkg install boost
vcpkg install spdlog
目标
spdlog是一个C++日志库,本身提供了向流、标准输出、文件、系统日志、调试器等目标输出日志的能力,这里将实现其向UDP服务器目标输出日志,使用的是Boost.Asio作为网络通信库。
测试UDP服务器实现
处于测试目的,实现一个简单的UDP服务器,采用同步阻塞的方式来获取外部发送来的信息并输出到std::cout
。
实现思路如下:
- 构造IO服务
- 构造监听socket
- while循环读取并输出发送来的信息
using namespace boost::asio;
using namespace boost::asio::ip;
try
{
boost::asio::io_service io; //构造IO服务,由于非异步,无需run
udp::socket socket(io, udp::endpoint(udp::v4(), 1024));//构造socket并绑定到1024端口
for (;;)
{
std::array<char, 1024> recv_buf;//接收缓冲
udp::endpoint remote_endpoint; //发送端信息
boost::system::error_code error;
//阻塞读取
auto size = socket.receive_from(boost::asio::buffer(recv_buf), remote_endpoint, 0, error);
if (error && error != boost::asio::error::message_size)
{
throw boost::system::system_error(error);
}
std::cout.write(recv_buf.data(),size);//输出结果
}
}
catch (std::exception& e)
{
std::cerr << e.what() << std::endl;
}
扩展spdlog
的目标
spdlog
的输出目标叫做sink
,其基类为spdlog::sinks::sink
,只需要实现两个虚接口即可:
virtual void log(const details::log_msg& msg) = 0;
virtual void flush() = 0;
其中log
接口即为日志输出,flush
接口用来强制输出内部缓存的所有内容到目标。
同时spdlog
提供了base_sink
模板类,模板参数为互斥锁,用来提供多线程和单线程版本的目标,在log
处用互斥锁进行保护,其日志输出的接口为_sink_it
。
获取要发送的日志内容
spdlog
使用的是fmt进行日志的格式化处理,并提供丰富的日志调用方式,在进行日志输出时,其信息被封装到了log_msg
中:
struct log_msg
{
......
fmt::MemoryWriter raw; //原始的内容
fmt::MemoryWriter formatted; //经过格式化的内容
};
获取formatted
并得到其中的char*
内容地址和内容大小即可得到日志内容。
同步UDP日志输出实现
同步输出实现比较简单,根据host和port构造socket,然后将数据发送出去即可:
struct UDPSink::UDPSinkImpl
{
public:
explicit UDPSinkImpl(const char* host, unsigned short port):host_(host),port_(port){};
public:
void send(const char* data, std::size_t size)
{
using namespace boost::asio;
try
{
boost::asio::io_service io;
ip::udp::resolver resolver(io);
auto endpoint_iter = resolver.resolve({ host_,std::to_string(port_) });
ip::udp::socket socket(io);
socket.open(ip::udp::v4());
boost::system::error_code ec;
socket.send_to(boost::asio::buffer(data, size), *endpoint_iter);
}
catch (std::exception& e)
{
throw spdlog::spdlog_ex("Fail Send message to UDPServer "+std::string(e.what()));
}
}
private:
std::string host_;
unsigned short port_;
};
实现UDPSink
:
class UDPSink :public spdlog::sinks::base_sink<std::mutex>
{
public:
explicit UDPSink(const char* host,unsigned short port)
:impl_(new UDPSinkImpl(host, port)){};
virtual ~UDPSink(){};
protected:
virtual void flush() override{;};
virtual void _sink_it(const spdlog::details::log_msg& msg) override
{
auto size = msg.formatted.size();
auto data = msg.formatted.data();
impl_->send(data, size);
}
private:
struct UDPSinkImpl;
std::shared_ptr<UDPSinkImpl> impl_;
};
异步UDP日志输出实现
相对来讲,异步UDP实现要比较复杂,原因在于:
由于是异步发送,必须保证发送的内容在未完成发送之前必须有效,在发送完成后则需要正确析构。
异步发送
首先将要发送的内容复制到缓存中,然后发送,在发送完成时释放缓存:
void send(const char* data, int size)
{
try
{
char* pbuf = new char[size];
std::memcpy(pbuf, data, size); //复制日志内容到缓存
socket_.async_send_to(boost::asio::buffer(pbuf,size),ep_,
[pbuf](boost::system::error_code ec, std::size_t byte_transfer){
delete[] pbuf; //发送完成,释放申请的缓存
});
}
catch (std::exception& e)
{
throw spdlog::spdlog_ex("Fail Send message to UDPServer " + std::string(e.what()));
}
}
保证IO服务一直运行
一旦需要使用异步,则必须使用boost::asio::io_service
的run
方法,该方法会执行直到所有的异步回调完成。
不过调用了run
方法也不能保证能够一直接收到发送完成回调,有两种方法可以保证一直运行:
- 在发送完成回调中再次发起异步发送,保证一直有异步回调
- 使用
boost::asio::io_service::work
来保证io_service
一直运行直到调用io_service
的stop
等方法停止其运行。
鉴于run
方法会阻塞,需要另起线程运行,需要注意的是在另外的线程执行run
那么异步回调就会在另外的线程执行,也就是说,run
方法接收到异步操作完成后调用了异步回调。
异步UDP输出
struct AsyncUDPSink::AsyncUDPSinkImpl
{
public:
AsyncUDPSinkImpl(const char* host, unsigned short port)
:work_(io_),socket_(io_),ep_(ip::address::from_string(host),port)
{
//启动后台线程保证回调正常执行
std::thread t([&](){ io_.run(); });
t.detach(); //避免阻塞
socket_.open(ip::udp::v4());
}
void send(const char* data, int size)
{
try
{
char* pbuf = new char[size];
std::memcpy(pbuf, data, size);
socket_.async_send_to(boost::asio::buffer(pbuf,size),ep_,
[pbuf](boost::system::error_code ec, std::size_t byte_transfer){
delete[] pbuf;
});
}
catch (std::exception& e)
{
throw spdlog::spdlog_ex("Fail Send message to UDPServer " + std::string(e.what()));
}
}
~AsyncUDPSinkImpl()
{
io_.stop();//停止IO保证后台线程正常退出
}
private:
boost::asio::io_service io_;
boost::asio::io_service::work work_;
ip::udp::socket socket_;
ip::udp::endpoint ep_;
};
实现AsyncUDPSink
class AsyncUDPSink :public spdlog::sinks::sink
{
public:
explicit AsyncUDPSink(const char* host, unsigned short port)
:impl_(new AsyncUDPSinkImpl(host,port)){};
virtual ~AsyncUDPSink(){}
protected:
virtual void flush() override{};
virtual void log(const spdlog::details::log_msg& msg) override
{
auto size = msg.formatted.size();
auto data = msg.formatted.data();
impl_->send(data, size);
}
private:
struct AsyncUDPSinkImpl;
std::shared_ptr<AsyncUDPSinkImpl> impl_;
};
如何使用
spdlog
在创建日志时可以指定sink
,创建完成后会被保存起来,可以再次获取。
示例如下:
//同步UDP日志输出
auto udpsink = std::make_shared<UDPSink>("127.0.0.1",1024);
auto udplog = spdlog::create("udplog",udpsink);
udplog->info("Welcome to spdlog!");
udplog->info("Message will send to UDPServer");
//异步UDP日志输出
auto audpsink = std::make_shared<AsyncUDPSink>("127.0.0.1", 1024);//创建sink
auto audplog = spdlog::create("asyncudplog", audpsink);//创建logger
//获取logger
auto plog = spdlog::get("asyncudplog");
plog->info("Welcome to spdlog!");
plog->info("Message will send to UDPServer");
效果
总结
以上只是尽可能简单地实现了UDP日志服务,实际上在使用日志服务的客户端还需要考虑日志格式、效率等等诸多问题,在服务器端还需要正确保存日志供后续分析等使用。
网友评论