美文网首页C和C++常用
1.使用Boost.Asio与spdlog实现UDP日志输出

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

作者: 长不胖的Garfield | 来源:发表于2017-01-11 16:50 被阅读0次

    环境建立

    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<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_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<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日志服务,实际上在使用日志服务的客户端还需要考虑日志格式、效率等等诸多问题,在服务器端还需要正确保存日志供后续分析等使用。

    相关文章

      网友评论

        本文标题:1.使用Boost.Asio与spdlog实现UDP日志输出

        本文链接:https://www.haomeiwen.com/subject/wndrbttx.html