前几天写过一篇博文InfluxDB 写入&查询 Qt工程(C++ 客户端 API),主要介绍了整理的Qt工程的使用方法,今天介绍一下其具体实现(C++),这里主要借助于curl工具进行的数据库远程操作。
写入数据的标准URL为[protocol]://[username:password@]host:port[/?db=database][/write/measure,tags field]
,所以第一步需要创建数据库,并且确定访问协议,其次当然是将用户名和密码设置好,并且赋予读写权限,如果权限不足则会导致数据库访问失败,程序异常退出。然后是host:port
要申请好,当然本文中采用本地的influxdb,所以两者默认为localhost:8086
,然后确定写入操作的关键字/write
,之后加入操作的命令字符串/measure,tags field
即可,这样的一个URL就可以直接操作数据库了,使用浏览器也可以实现操作。
当使用本工程中的API时,则可以直接使用http://admin:admin@localhost:8086/?db=mydb
这样一个URL设置参数,并使用write
,addTag
,addField
三个函数配合实现写入操作。
具体的调用格式为:
auto influxdb = influxdb::InfluxDBFactory::Get("http://admin:admin@localhost:8086/?db=mydb");
influxdb->write(influxdb::Point{ "cpu" }
.addTag("host", "localhost")
.addTag("region", "china")
.addField("north_latitude", 36)
.addField("east_longitude", 114)
);
下面讲解一下写入操作中的每一步的详细实现过程的代码实现:
第一步是输入URL的解析,当然这是为了方便用户使用设计的,读者也可以通过参数直接设置上述的所有参数。 相信读者可以通过函数名了解每一个解析函数的功能,其中ParseHttpUrl
是将URL
static inline int ExtractPort(std::string &hostport) {
int port;
std::string portstring = TailSlice(hostport, ":");
try { port = atoi(portstring.c_str()); }
catch (std::exception &e) { port = -1; }
return port;
}
static inline std::string ExtractPath(std::string &in) { return TailSlice(in, "/", true); }
static inline std::string ExtractProtocol(std::string &in) { return HeadSlice(in, "://"); }
static inline std::string ExtractSearch(std::string &in) { return TailSlice(in, "?"); }
static inline std::string ExtractPassword(std::string &userpass) { return TailSlice(userpass, ":"); }
static inline std::string ExtractUserpass(std::string &in) { return HeadSlice(in, "@"); }
//--- Public Interface -------------------------------------------------------------~
static inline url ParseHttpUrl(std::string &in) {
url ret;
ret.urlString = in;
ret.port = -1;
ret.protocol = ExtractProtocol(in);
ret.search = ExtractSearch(in);
ret.path = ExtractPath(in);
std::string userpass = ExtractUserpass(in);
ret.password = ExtractPassword(userpass);
ret.user = userpass;
ret.port = ExtractPort(in);
ret.host = in;
return ret;
}
第二步是写入操作的命令字符串/measure,tags fields
的获取。 在该工程中每一条操作(或信息)都是通过一个Point类实现的,在该类初始化时,measure
作为参数输入,因为在Influxdb中写入时的字符串就是查询时获取的全部信息,所以这样一个类变可以用于写入和读取了,由于Tag和Field可以是多个,所以这里展示了Tag和Field的添加操作以及两者合并为操作字符串的操作,实际上就是字符串管理,主要是分隔符的合理使用。这里使用两个字符串对象:mTags 和 mFields
来存储两个属性的字符串存储格式。具体操作如下
Point&& Point::addField(std::string_view name, std::variant<int, long long int, std::string, double> value)
{
std::stringstream convert;
if (!mFields.empty()) convert << ","; //Field间用逗号分隔
convert << name.data() << "=";
std::visit(overloaded {
[&convert](int value) { convert << value << 'i'; },
[&convert](long long int value) { convert << value << 'i'; },
[&convert](double value) { convert << value; },
[&convert](const std::string& value) { convert << '"' << value << '"'; },
}, value);
mFields += convert.str();
return std::move(*this);
}
Point&& Point::addTag(std::string_view key, std::string_view value)
{
mTags += ","; //Tag间用逗号分隔
mTags += key;
mTags += "=";
mTags += value;
return std::move(*this);
}
//合并为操作字符串 measure,tags fields
std::string Point::toLineProtocol() const
{
return mMeasurement + mTags + " " + mFields + " " + std::to_string(
std::chrono::duration_cast <std::chrono::nanoseconds>(mTimestamp.time_since_epoch()).count()
); //Fields和Tags间用空格分隔
}
第三部就是post
操作了 ,通过toLineProtocol
函数获取的字符串将传入下面的函数进行Post操作,这里借助的是libcurl库。
void HTTP::send(std::string&& post)
{
CURLcode response;
long responseCode;
#ifdef DEBUG_OUTPUT
std::cout << "post string : " << post << std::endl;
#endif
curl_easy_setopt(writeHandle, CURLOPT_POSTFIELDS, post.c_str());
curl_easy_setopt(writeHandle, CURLOPT_POSTFIELDSIZE, (long) post.length());
response = curl_easy_perform(writeHandle);
curl_easy_getinfo(writeHandle, CURLINFO_RESPONSE_CODE, &responseCode);
if (response != CURLE_OK) {
throw std::runtime_error(curl_easy_strerror(response));
}
if (responseCode < 200 || responseCode > 206) {
throw std::runtime_error("Response code : " + std::to_string(responseCode));
}
}
第一步是设置读取的操作命令,该程序未对读取进一步的封装,所以读取字符串例如SELECT * FROM cpu
将直接传送至InfluxDB::query
函数中便可以获取操作命令的相应报文。所以查询操作调用格式如下:
auto points = influxdb->query("SELECT * FROM cpu");
std::cout << "query over!" << std::endl;
std::cout << "name : " << points.back().getName() <<std::endl
<< "field : "<<points.back().getFields() << std::endl
<< "timestamp : " << points.back().getTimestamp().time_since_epoch().count() << std::endl;
这里的points
是以std::vector
的类对象,可以直接使用迭代器对每一条数据进行读取和操作。
而在InfluxDB::query
函数中第一步也就是读取操作的第二步是通过curl获取反馈的报文字符串。
这里详细展示一下curl获取反馈的报文字符串的函数实现,仍然是调用curl API,设置参数后访问URL。
std::string HTTP::query(const std::string& query)
{
CURLcode response;
std::string buffer;
#ifdef DEBUG_OUTPUT
std::cout << "start http query!" <<std::endl;
#endif
char* encodedQuery = curl_easy_escape(readHandle, query.c_str(), query.size());
auto fullUrl = mReadUrl + std::string(encodedQuery);
#ifdef DEBUG_OUTPUT
std::cout << "full url : " << fullUrl<< std::endl;
#endif
curl_easy_setopt(readHandle, CURLOPT_URL, fullUrl.c_str());
curl_easy_setopt(readHandle, CURLOPT_WRITEDATA, &buffer);
#ifdef DEBUG_OUTPUT
std::cout << "get buffer : " << buffer <<std::endl;
#endif
response = curl_easy_perform(readHandle);
if (response != CURLE_OK) {
throw std::runtime_error(curl_easy_strerror(response));
}
return buffer;
}
第三步是进行报文的解析,所获取的信息流主要在result
中的以series
呈现,series
包含了columns
(主要有measurement Tags标签 Fields标签 time)以及values
(时间序列数据)。注意这里的TIME_TYPE是为了获取时间设置的,当然时间的形式各有不同,自己的可以根据浏览器获取的报文格式来设置,本地默认的时间报文格式为"%Y-%m-%dT%H:%M:%S.%fZ"
。
// TIME_TYPE is used to get time stamp
#define TIME_TYPE "%Y-%m-%dT%H:%M:%S.%fZ"
std::vector<Point> InfluxDB::query(const std::string& query)
{
auto response = mTransport->query(query);//获取反馈的字符串。
std::stringstream ss;
ss << response;
std::vector<Point> points;
boost::property_tree::ptree pt;
boost::property_tree::read_json(ss, pt);
#ifdef DEBUG_OUTPUT
std::cout << "start query process !" <<std::endl;
#endif
for (auto& result : pt.get_child("results")) {
auto isResultEmpty = result.second.find("series");
if (isResultEmpty == result.second.not_found()) return {};
for (auto& series : result.second.get_child("series")) {
auto columns = series.second.get_child("columns");
for (auto& values : series.second.get_child("values")) {
Point point{series.second.get<std::string>("name")};
auto iColumns = columns.begin();
auto iValues = values.second.begin();
for (; iColumns != columns.end() && iValues != values.second.end(); iColumns++, iValues++) {
auto value = iValues->second.get_value<std::string>();
auto column = iColumns->second.get_value<std::string>();
if (!column.compare("time")) {
std::tm tm = {};
std::stringstream ss;
ss << value;
ss >> std::get_time(&tm, TIME_TYPE);
point.setTimestamp(std::chrono::system_clock::from_time_t(std::mktime(&tm)));
continue;
}
// By observing whether it is numerical data, we can judge whether it is field or tag
try { point.addField(column, boost::lexical_cast<double>(value)); }
catch(...) { point.addTag(column, value); }
}
points.push_back(std::move(point));
}
}
}
return points;
}
本文主要讲解的是influxdb查询写入的c++实现,主要还是使用数据库本身的命令进行数据访问所以这里进行influxdb数据库操作的命令供大家参考。
-- 查看所有的数据库
show databases;
-- 使用特定的数据库
use database_name;
-- 查看所有的measurement
show measurements;
-- 查询10条数据
select * from measurement_name limit 10;
-- 查看一个measurement中所有的tag key
show tag keys
-- 查看一个measurement中所有的field key
show field keys
当然我感觉也可以用到其他数据库读取中,不是本职工作就没有尝试,如果读者有需求可以自己实现尝试一下,如果有疑问的话可以留言提问,虽然我也是萌新。