環(huán)境建立
Visual Studio 2015,Vcpkg
vcpkg install boost
vcpkg install spdlog
目標(biāo)
spdlog是一個(gè)C++日志庫哲思,本身提供了向流吩案、標(biāo)準(zhǔn)輸出徘郭、文件、系統(tǒng)日志胧后、調(diào)試器等目標(biāo)輸出日志的能力抱环,這里將實(shí)現(xiàn)其向UDP服務(wù)器目標(biāo)輸出日志镇草,使用的是Boost.Asio作為網(wǎng)絡(luò)通信庫。
測(cè)試UDP服務(wù)器實(shí)現(xiàn)
處于測(cè)試目的竖伯,實(shí)現(xiàn)一個(gè)簡(jiǎn)單的UDP服務(wù)器条辟,采用同步阻塞的方式來獲取外部發(fā)送來的信息并輸出到std::cout
羽嫡。
實(shí)現(xiàn)思路如下:
- 構(gòu)造IO服務(wù)
- 構(gòu)造監(jiān)聽socket
- while循環(huán)讀取并輸出發(fā)送來的信息
using namespace boost::asio;
using namespace boost::asio::ip;
try
{
boost::asio::io_service io; //構(gòu)造IO服務(wù),由于非異步,無需run
udp::socket socket(io, udp::endpoint(udp::v4(), 1024));//構(gòu)造socket并綁定到1024端口
for (;;)
{
std::array<char, 1024> recv_buf;//接收緩沖
udp::endpoint remote_endpoint; //發(fā)送端信息
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);//輸出結(jié)果
}
}
catch (std::exception& e)
{
std::cerr << e.what() << std::endl;
}
擴(kuò)展spdlog
的目標(biāo)
spdlog
的輸出目標(biāo)叫做sink
杭棵,其基類為spdlog::sinks::sink
,只需要實(shí)現(xiàn)兩個(gè)虛接口即可:
virtual void log(const details::log_msg& msg) = 0;
virtual void flush() = 0;
其中log
接口即為日志輸出先舷,flush
接口用來強(qiáng)制輸出內(nèi)部緩存的所有內(nèi)容到目標(biāo)滓侍。
同時(shí)spdlog
提供了base_sink
模板類撩笆,模板參數(shù)為互斥鎖,用來提供多線程和單線程版本的目標(biāo)氮兵,在log
處用互斥鎖進(jìn)行保護(hù)歹鱼,其日志輸出的接口為_sink_it
弥姻。
獲取要發(fā)送的日志內(nèi)容
spdlog
使用的是fmt進(jìn)行日志的格式化處理,并提供豐富的日志調(diào)用方式铃绒,在進(jìn)行日志輸出時(shí)螺捐,其信息被封裝到了log_msg
中:
struct log_msg
{
......
fmt::MemoryWriter raw; //原始的內(nèi)容
fmt::MemoryWriter formatted; //經(jīng)過格式化的內(nèi)容
};
獲取formatted
并得到其中的char*
內(nèi)容地址和內(nèi)容大小即可得到日志內(nèi)容定血。
同步UDP日志輸出實(shí)現(xiàn)
同步輸出實(shí)現(xiàn)比較簡(jiǎn)單,根據(jù)host和port構(gòu)造socket灾票,然后將數(shù)據(jù)發(fā)送出去即可:
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_;
};
實(shí)現(xiàn)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日志輸出實(shí)現(xiàn)
相對(duì)來講,異步UDP實(shí)現(xiàn)要比較復(fù)雜濒析,原因在于:
由于是異步發(fā)送号杏,必須保證發(fā)送的內(nèi)容在未完成發(fā)送之前必須有效,在發(fā)送完成后則需要正確析構(gòu)主经。
異步發(fā)送
首先將要發(fā)送的內(nèi)容復(fù)制到緩存中,然后發(fā)送穗酥,在發(fā)送完成時(shí)釋放緩存:
void send(const char* data, int size)
{
try
{
char* pbuf = new char[size];
std::memcpy(pbuf, data, size); //復(fù)制日志內(nèi)容到緩存
socket_.async_send_to(boost::asio::buffer(pbuf,size),ep_,
[pbuf](boost::system::error_code ec, std::size_t byte_transfer){
delete[] pbuf; //發(fā)送完成,釋放申請(qǐng)的緩存
});
}
catch (std::exception& e)
{
throw spdlog::spdlog_ex("Fail Send message to UDPServer " + std::string(e.what()));
}
}
保證IO服務(wù)一直運(yùn)行
一旦需要使用異步迷扇,則必須使用boost::asio::io_service
的run
方法爽哎,該方法會(huì)執(zhí)行直到所有的異步回調(diào)完成课锌。
不過調(diào)用了run
方法也不能保證能夠一直接收到發(fā)送完成回調(diào),有兩種方法可以保證一直運(yùn)行:
- 在發(fā)送完成回調(diào)中再次發(fā)起異步發(fā)送雏胃,保證一直有異步回調(diào)
- 使用
boost::asio::io_service::work
來保證io_service
一直運(yùn)行直到調(diào)用io_service
的stop
等方法停止其運(yùn)行瞭亮。
鑒于run
方法會(huì)阻塞固棚,需要另起線程運(yùn)行,需要注意的是在另外的線程執(zhí)行run
那么異步回調(diào)就會(huì)在另外的線程執(zhí)行厂汗,也就是說娶桦,run
方法接收到異步操作完成后調(diào)用了異步回調(diào)汁汗。
異步UDP輸出
struct AsyncUDPSink::AsyncUDPSinkImpl
{
public:
AsyncUDPSinkImpl(const char* host, unsigned short port)
:work_(io_),socket_(io_),ep_(ip::address::from_string(host),port)
{
//啟動(dòng)后臺(tái)線程保證回調(diào)正常執(zhí)行
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保證后臺(tái)線程正常退出
}
private:
boost::asio::io_service io_;
boost::asio::io_service::work work_;
ip::udp::socket socket_;
ip::udp::endpoint ep_;
};
實(shí)現(xiàn)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
在創(chuàng)建日志時(shí)可以指定sink
,創(chuàng)建完成后會(huì)被保存起來祈争,可以再次獲取送爸。
示例如下:
//同步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);//創(chuàng)建sink
auto audplog = spdlog::create("asyncudplog", audpsink);//創(chuàng)建logger
//獲取logger
auto plog = spdlog::get("asyncudplog");
plog->info("Welcome to spdlog!");
plog->info("Message will send to UDPServer");
總結(jié)
以上只是盡可能簡(jiǎn)單地實(shí)現(xiàn)了UDP日志服務(wù)袭厂,實(shí)際上在使用日志服務(wù)的客戶端還需要考慮日志格式纹磺、效率等等諸多問題,在服務(wù)器端還需要正確保存日志供后續(xù)分析等使用秘症。