1.使用Boost.Asio与spdlog实现UDP日志输出

环境建立

Visual Studio 2015,Vcpkg

vcpkg install boost
vcpkg install spdlog

目标

spdlog是一个C++日志库,本身提供了向流、标准输出、文件、系统日志、调试器等目标输出日志的能力,这里将实现其向UDP服务器目标输出日志,使用的是Boost.Asio作为网络通信库。

测试UDP服务器实现

处于测试目的,实现一个简单的UDP服务器,采用同步阻塞的方式来获取外部发送来的信息并输出到std::cout

实现思路如下:

  1. 构造IO服务
  2. 构造监听socket
  3. 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 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
{
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 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_servicerun方法,该方法会执行直到所有的异步回调完成。

不过调用了run方法也不能保证能够一直接收到发送完成回调,有两种方法可以保证一直运行:

  1. 在发送完成回调中再次发起异步发送,保证一直有异步回调
  2. 使用boost::asio::io_service::work来保证io_service一直运行直到调用io_servicestop等方法停止其运行。

鉴于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 impl_;
};

如何使用

spdlog在创建日志时可以指定sink,创建完成后会被保存起来,可以再次获取。

示例如下:

//同步UDP日志输出
auto udpsink = std::make_shared("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("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");
1.使用Boost.Asio与spdlog实现UDP日志输出_第1张图片
效果

总结

以上只是尽可能简单地实现了UDP日志服务,实际上在使用日志服务的客户端还需要考虑日志格式、效率等等诸多问题,在服务器端还需要正确保存日志供后续分析等使用。

你可能感兴趣的:(1.使用Boost.Asio与spdlog实现UDP日志输出)