利用c++实现基于rpc的远端服务器参数实时查询

前言

rpc作为一种优秀的分布式通信框架,应用十分广泛。出于学习的目的,利用c++实现基于rpc的远端服务器性能参数实时获取功能。文章主要包括服务端(brpc引入,docker搭建)、客户端(libevent适配baidu_std协议,qt界面)两方面,主要框架如下:

一、服务端

1、brpc

常用的rpc框架有thrift、grpc及brpc等,网上查阅了一番资料后,选择了brpc来学习(深入brpc - 知乎 (zhihu.com)),主要原因是:1、brpc对c/c++更友好;2、帮助文档详细;3、brpc的性能很高,另一方面,brpc的很多结构,如bthread、work steal等,十分值得学习。

a)brpc编译

brpc(github.com/apache/brpc) 的编译可以参考github.com/apache/brpc... 。需要说明的是:1、brpc的依赖库gflsgs、protobuf、leveldb等最好编译成动态库(指定-fPIC,或-DCMAKE_POSITION_INDEPENDENT_CODE=ON),不然编译brpc会提示找不到库文件;make install的路径最好用默认的/usr/lib或/usr/local/lib,这样brpc编译不用改路径就可以找到依赖;2、brpc是基于c++11的,protobuf版本最好是3.6.1的,protobuf版本太高会导致brpc编译失败。

b).proto文件定义

机器运行参数包括CPU核数、磁盘大小、内存大小、空闲CPU、空闲内存占用、占CPU最多的前几个进程等参数。这里将其分为固有参数和动态参数,提供两个服务来实现:

ini 复制代码
syntax = "proto2";
option cc_generic_services = true;
message SystemDynamicParameterTopOf {
      required string procedure_name = 1;
      required double procedure_percent = 2;
};
message SystemIntrinsicParameterRequest {
};
message SystemIntrinsicParameterResponse {
      required uint32 cpuCores = 1;
      required string diskInventory = 2;
      required string totalPhysicalMemory = 3;
};
service SystemIntrinsicParameterService {
      rpc SystemIntrinsicParameter(SystemIntrinsicParameterRequest) returns (SystemIntrinsicParameterResponse);
};
message SystemDynamicParameterRequest {
      required uint32 topOfCount = 1;
};
message SystemDynamicParameterResponse {
      required double freeCpuPercent = 1;
      required double freeDiskPercent = 2;
      required double freeMemoryPercent = 3;
      repeated SystemDynamicParameterTopOf topOfCpu = 4;
      repeated SystemDynamicParameterTopOf topOfMemory = 5;
};
service SystemDynamicParameterService {
      rpc SystemDynamicParameter(SystemDynamicParameterRequest) returns (SystemDynamicParameterResponse);
};

具体rpc server的实现可直接参考brpc/example/echo_c++/server.cpp at master · apache/brpc (github.com) 。有少个服务就调用AddService多少次。

c)利用c++获取机器运行参数

可以通过extern FILE *popen (const char *__command, const char *__modes) __wur; 函数来执行sh脚本,从而获取机器运行状态,示例如下:

c 复制代码
#define GET_CPU_CORES "grep -c \"model name\" /proc/cpuinfo"
#define GET_DISK_INVENTORY "df -h | grep \"/dev/vda1\" | awk '{print $2}'"
#define GET_TOTAL_PHYSICAL_MEMORY "free -h | grep Mem | awk '{print $2}'"
#define GET_FREE_CPU_PERCENT "top -b -n 1 | grep Cpu | awk '{print $8}' | cut -f 1 -d \"%\""
#define GET_FREE_DISK_INVENTORY "df -h | grep \"/dev/vda1\" | awk '{print $4}'"
#define GET_USED_PHYSICAL_MEMORY "free -h | grep Mem | awk '{print $3}'"
#define GET_TOP_OF_CPU(index) \
    (std::string("top -b -n 1 | sed -n '8,50p' | sort -r -k 9 | head -n ") + std::to_string(index) + std::string(" | awk '{print $9, $12}'")).c_str()
#define GET_TOP_OF_MEMORY(index) \
    (std::string("top -b -n 1 | sed -n '8,50p' | sort -r -k 10 | head -n ") + std::to_string(index) + std::string(" | awk '{print $10, $12}'")).c_str()

//...
FILE *fstream = NULL;
char buff[1024] = {0};
if (NULL == (fstream = popen(GET_FREE_CPU_PERCENT, "r")))
{
    LOG_ERROR("get cpu cores failed: %s", strerror(errno));
    return false;
}
fgets(buff, sizeof(buff), fstream);
for (int i = 0; i < 1024; ++i)
{
    if ('\n' == buff[i])
        buff[i] = '\0';
    if ('\0' == buff[i])
        break;
}
response->set_freecpupercent(strtod(buff, NULL));

2、docker搭建

一般情况下,服务端的进程会放在docker环境中运行,以达到自动化部署、隔离外界的效果。1、首先需要通过make install指令将所有依赖的动态库打包到一个目录下;2、编写如下Dockerfile,我的环境测试发现必须只能拷贝到系统目录(/lib64 /lib /usr/lib64等)下,本来想拷贝到新建的docker_brpc_server目录下,但运行一直报错缺少依赖库,具体原因还不清楚;3、执行docker run命令(docker run -d -p 4000:4000 brpc_server:latest),这个时候不会报缺少依赖库的错。

bash 复制代码
FROM centos:latest
LABEL org.opencontainers.image.authors="_zq_yy_lf_"
# RUN mkdir -p /docker_brpc_server
# COPY . /docker_brpc_server
COPY . /lib64
WORKDIR /lib64  # /docker_brpc_server
EXPOSE 4000
CMD ["./rpc_server", "--port=4000", "--idle_timeout_s=60"] 

二、客户端

1、rpc client实现

a)tcp client实现

虽然brpc有很多优点,但目前brpc不支持window(算是brpc的一个缺点),导致需要自己实现一套基于baidu_std协议的rpc client,这里采用libevent实现。protobuf其实也有一个不好的地方,高版本的protobuf无法兼容低版本的protobuf:前面讲到protobuf需要采用3.6.1版本以与brpc兼容,现在客户端Window上没有brpc,想着采用高版本的protobuf应该也没问题,实际是不行的。另外,Windows下采用vs2022编译protobuf3.6.1时必须要添加如下预定义_SILENCE_STDEXT_HASH_DEPRECATION_WARNINGS才能编译通过。 libevent是一种基于事件的异步网络库,就算是客户端也需要调用event_base_dispatch来循环进行事件派发,一般会将其放在一个线程中运行。在bufferevent创建好之后,通过evutil_socketpair创建一个本地sockpair,通过回调的方式在event_base_dispatch线程中调用上层的rpcChannel对象的函数,具体TcpClient实现如下:

ini 复制代码
bufferevent_setcb(m_bev, TcpClient::readCallBack, TcpClient::writeCallBack, TcpClient::event_cb, nullptr);
evutil_socketpair(AF_INET, SOCK_STREAM, 0, m_fdPair);
event *m_ev = event_new(m_base, m_fdPair[1], EV_READ | EV_PERSIST, TcpClient::onMessage, (void *)m_bev;  //2
bool TcpClient::sendTcpMessage(char *totalBuf, unsigned long len)
{
    int sendLen = ::send(m_fdPair[0], totalBuf, len, 0);
    return sendLen == len;
}
void TcpClient::onMessage(evutil_socket_t fd, short what, void *arg)
{
    char buf[1024] = {0};
    int len = ::recv(fd, buf, sizeof(buf), 0);
    struct bufferevent *bev = (struct bufferevent *)arg;
    bufferevent_write(bev, buf, len);
}
void TcpClient::readCallBack(struct bufferevent *bev, void *arg)
{
    char buf[4094] = {0};
    size_t readLen = bufferevent_read(bev, buf, sizeof(buf));
    if (readLen && gReadCallBack)
        gReadCallBack(buf, readLen);
}

b)rpc client实现

一次rpc client调用过程的关系如下图,CallMethod方法的逻辑如下,里面的RpcClientController是继承自google::protobuf::RpcController,用于设置错误码。resquest与response对象也都是由pb工具生成,均继承自google::protobuf::Message。

rust 复制代码
void RpcClientChannel::CallMethod(const google::protobuf::MethodDescriptor *method,
								  google::protobuf::RpcController *controller,
								  const google::protobuf::Message *request,
								  google::protobuf::Message *response,
								  google::protobuf::Closure *done)
{
	std::shared_ptr<std::vector<char>> totalBufPtr = nullptr;
	TcpClientCallBack::constructMessage(method, request, totalBufPtr);
	if (!m_tcpClientPtr->sendTcpMessage(totalBufPtr->data(), totalBufPtr->size())){
		controller->SetFailed("sendTcpMessage error");
		return;
	}
	std::unique_lock<std::mutex> ulk(gMx);
	gCv.wait_for(ulk, std::chrono::seconds(m_timeOut), [](){ return gBuf[0] != '\0' && gLen != 0; });
	if ('\0' == gBuf[0] || 0 == gLen){
		controller->SetFailed("sendTcpMessage timeout");
		return;
	}
	TcpClientCallBack::parseMessage(controller, response);
	resetBuf();
}

c)baidu_std协议转换

baidu_std协议可参考brpc/docs/cn/baidu_std.md at master · apache/brpc (github.com) 。具体转换如下,需要注意的是,不要尝试将发送的数据帧后面再补一个'\0',这样会导致后面的数据全部会解析失败。转换要用到brpc::policy::RpcMeta及brpc::policy::RpcRequestMeta等结构,这些是编译brpc生成的。

ini 复制代码
void TcpClientCallBack::constructMessage(const google::protobuf::MethodDescriptor *method,
										 const google::protobuf::Message *request,
										 std::shared_ptr<std::vector<char>> &totalBufPtr)
{
	std::unique_ptr<brpc::policy::RpcMeta> rpcMetaPtr(new brpc::policy::RpcMeta());
	brpc::policy::RpcRequestMeta *pRpcRequestMeta = new brpc::policy::RpcRequestMeta();
	pRpcRequestMeta->set_service_name(method->service()->name());
	pRpcRequestMeta->set_method_name(method->name());
	rpcMetaPtr->set_allocated_request(pRpcRequestMeta); //RpcMeta析构时会自动析构RpcRequestMeta
	std::string rpcMetaStr = rpcMetaPtr->SerializeAsString();

	std::string sendbuf = request->SerializeAsString();

	unsigned long packBodyLen = rpcMetaStr.size() + sendbuf.size();
	char packBodyLenStr[4] = {0};
	packBodyLenStr[3] = packBodyLen & 0x000000FF;
	packBodyLenStr[2] = packBodyLen & 0x0000FF00;
	packBodyLenStr[1] = packBodyLen & 0x00FF0000;
	packBodyLenStr[0] = packBodyLen & 0xFF000000;

	unsigned long packBodyMetaLen = rpcMetaStr.size();
	char packBodyLenMetaStr[4] = {0};
	packBodyLenMetaStr[3] = packBodyMetaLen & 0x000000FF;
	packBodyLenMetaStr[2] = packBodyMetaLen & 0x0000FF00;
	packBodyLenMetaStr[1] = packBodyMetaLen & 0x00FF0000;
	packBodyLenMetaStr[0] = packBodyMetaLen & 0xFF000000;

	totalBufPtr.reset(new std::vector<char>(12 + packBodyLen, '\0'));
	memcpy(totalBufPtr->data(), "PRPC", 4);
	memcpy(totalBufPtr->data() + 4, packBodyLenStr, 4);
	memcpy(totalBufPtr->data() + 8, packBodyLenMetaStr, 4);
	memcpy(totalBufPtr->data() + 12, rpcMetaStr.c_str(), rpcMetaStr.size());
	memcpy(totalBufPtr->data() + 12 + rpcMetaStr.size(), sendbuf.c_str(), sendbuf.size());
}

void TcpClientCallBack::parseMessage(google::protobuf::RpcController *controller,
									 google::protobuf::Message *response)
{
	if (gLen <= 4)
	{
		controller->SetFailed("total size < 4");
		return;
	}

	char pProtocol[5] = {0};
	memcpy(pProtocol, gBuf, 4);
	if (std::string(pProtocol).compare("PRPC"))
	{
		controller->SetFailed("protocol is not PRPC");
		return;
	}

	int packBodyLen = 0;
	packBodyLen |= gBuf[4] & 0xFF << 8 * 3;
	packBodyLen |= gBuf[5] & 0xFF << 8 * 2;
	packBodyLen |= gBuf[6] & 0xFF << 8 * 1;
	packBodyLen |= gBuf[7] & 0xFF << 8 * 0;
	int packBodyMetaLen = 0;
	packBodyMetaLen |= gBuf[8] & 0xFF << 8 * 3;
	packBodyMetaLen |= gBuf[9] & 0xFF << 8 * 2;
	packBodyMetaLen |= gBuf[10] & 0xFF << 8 * 1;
	packBodyMetaLen |= gBuf[11] & 0xFF << 8 * 0;
	if (packBodyLen <= 0 || packBodyMetaLen <= 0)
	{
		controller->SetFailed("packBodyLen <= 0 || packBodyMetaLen <= 0");
		return;
	}

	brpc::policy::RpcMeta rpcMeta;
	rpcMeta.ParseFromArray(gBuf + 12, packBodyMetaLen);
	if (rpcMeta.response().error_code())
	{
		controller->SetFailed("rpcResponseMeta error: " + rpcMeta.response().error_text());
		return;
	}

	response->ParseFromArray(gBuf + 12 + packBodyMetaLen, packBodyLen - packBodyMetaLen);
}

qt界面

利用cmake搭建qt环境,上一篇文章已经介绍。前面介绍的rpc的两个服务,固有参数服务只需要在启动的时候调用一次,动态参数服务每隔4s调用一次。程序一共有三个线程,tcpClient的event_base_dispatch占一个线程,由于libevent与qt都是基于事件的,因此采用异步方式进行定时调用rpcClientChannel对象,占一个线程,qt界面占用一个线程,qt主要交互逻辑如下。最后得到的简陋界面如下:

rust 复制代码
static QEvent::Type gOnMessageEvent = (QEvent::Type)QEvent::registerEventType();
class RpcClientDispatchThread : public base_common::IThreadHandle {
public:
    RpcClientDispatchThread(const std::shared_ptr<RpcClientChannel> &rpcClientChannelPtr,
                            const std::chrono::milliseconds &waitTime)
        : m_rpcClientChannelPtr(rpcClientChannelPtr), m_waitTime(waitTime)
    {
    }
    void threadhandle() override
    {
        SystemDynamicParameterRequest request;
        request.set_topofcount(TOP_OF_COUNT);
        while (m_rpcClientChannelPtr && !m_rpcClientChannelPtr->isStop())
        {
            if (!m_rpcClientChannelPtr->asyncCallMethod(SystemDynamicParameterService::descriptor()->method(0), &request))
            {
                LOG_ERROR("m_rpcClientChannelPtr asyncCallMethod failed!");
                return;
            }
            m_rpcClientChannelPtr->waitForStop(m_waitTime);
        }
    }

private:
    std::chrono::milliseconds m_waitTime;
    std::shared_ptr<RpcClientChannel> m_rpcClientChannelPtr;
};

bool QtStyleWidget::event(QEvent *e)
{
    if (gOnMessageEvent == e->type())
    {
        getDynamicResponse();
    }
    return QWidget::event(e);
}
相关推荐
计算机学姐9 分钟前
基于Python的高校成绩分析管理系统
开发语言·vue.js·后端·python·mysql·pycharm·django
wclass-zhengge41 分钟前
SpringCloud篇(服务拆分 / 远程调用 - 入门案例)
后端·spring·spring cloud
A_cot1 小时前
一篇Spring Boot 笔记
java·spring boot·笔记·后端·mysql·spring·maven
tryCbest2 小时前
java8之Stream流
java·后端
白总Server2 小时前
JVM 处理多线程并发执行
jvm·后端·spring cloud·微服务·ribbon·架构·数据库架构
@sinner2 小时前
【Spring Boot 入门五】Spring Boot中的测试 - 确保应用质量
spring boot·后端·log4j
江梦寻3 小时前
解决SLF4J: Class path contains multiple SLF4J bindings问题
java·开发语言·spring boot·后端·spring·intellij-idea·idea
LightOfNight3 小时前
Redis设计与实现第9章 -- 数据库 总结(键空间 过期策略 过期键的影响)
数据库·redis·后端·缓存·中间件·架构
每天写点bug3 小时前
golang 常用的占位符 %w, %v, %s
开发语言·后端·golang
鸡鸭扣3 小时前
springboot苍穹外卖实战:五、公共字段自动填充(aop切面实现)+新增菜品功能+oss
java·spring boot·后端