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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,406评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,732评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,711评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,380评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,432评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,301评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,145评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,008评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,443评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,649评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,795评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,501评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,119评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,731评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,865评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,899评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,724评论 2 354

推荐阅读更多精彩内容