在calluserservice.cc中,使用UserServiceRpc_Stub
类的时候,我们最终调用形式为:stub.Login(&controller,&request,&response,nullptr);
注意到其中有一个controller对象,这个是由MprpcController
类定义出来的对象,那么这个类的作用是什么呢?
- 首先我们来看 Login() 的底层实现,传入的controller到底是一个什么。
- 可以看到,controller实际上是RpcController* 类;
- RpcController* 类实际上是一个抽象类,底层封装了各类纯虚函数,我们通过继承这个类,并且重写对应的函数,来判断rpc的调用是否成功。
- 如果不判断是否调用成功就直接读取response ,是假设request成功的,在其中不会发生任何的错误,但是这种情况是理想化的,在其中会出现很多问题。如:网络建立连接错误 各种地方的return exit等 都会造成没有response响应。
MprpcController类
class MprpcController:public google::protobuf::RpcController
{
省略...........省略
};
- 很明确 它是继承了
google::protobuf::RpcController
类。
重要成员变量
bool m_failed;
- 记录rpc方法执行过程中的状态
std::string m_errText;
- 记录rpc方法执行过程中的错误信息
重要成员函数
构造函数
MprpcController::MprpcController()
{
m_failed = false;
m_errText = "";
}
- 初始化成员变量
void Reset();
void MprpcController::Reset()
{
m_failed = false;
m_errText = "";
}
- 重置成员变量的值
bool Failed() const;
bool MprpcController::Failed() const
{
return m_failed;
}
- 返回rpc方法执行过程中的状态,如果是false,我们将不会读取response值。
std::string ErrorText() const;
std::string MprpcController::ErrorText() const
{
return m_errText;
}
- 返回rpc方法执行过程中的错误信息。
void SetFailed(const std::string& reason);
void MprpcController::SetFailed(const std::string &reason)
{
m_failed = true;
m_errText = reason;
}
- 在我们调用的过程中,通过该函数,写错误原因。
例如
if(rpcHeader.SerializeToString(&rpc_header_str))
{
header_size=rpc_header_str.size();
}
else
{
controller->SetFailed("Serialize rpc header error!");
return;
}
整个项目的主体部分,就到此结束了,剩余一个logger类,这也是我们在做大型项目的必备类,通过日志,可以简单明了的帮我们分析到程序的问题所在,这里采用了异步,同时有多个worker线程都会向日志queue队列中写日志,而只有一个线程读日志queue,向指定文件中写日志文件。
Logger类
为什么需要异步记录日志
因为基于muduo网络库进行网络通讯的,muduo通过多线程来处理并发连接,要添加日志模块那么就会有多个线程写日志信息的情况。这样的话就必须要实现一个保证线程安全的日志队列。所以需要启动一个日志线程,专门对日志队列写日志。
保证线程安全的日志队列类
为了保证线程安全,项目中提供了模板类 lockqueue template<typename T>
,它用于实现异步写日志的日志队列,主要包含 push 和 pop 两个方法。
重要成员变量
std::queue<T> m_queue;
std::mutex m_mutex;
std::condition_variable m_condvariable;
- 队列
- 锁
- 条件变量
重要成员函数
void Push(const T &data)
void Push(const T &data)
{
std::lock_guard<std::mutex> lock(m_mutex);
m_queue.push(data);
m_condvariable.notify_one();
}
- push 方法可以被多个 worker 线程调用以将数据添加到日志队列中
T Pop()
T Pop()
{
std::unique_lock<std::mutex> lock(m_mutex);
while(m_queue.empty())
{
//日志队列为空,线程进入wait状态,并且释放锁
m_condvariable.wait(lock);
}
T data=m_queue.front();
m_queue.pop();
return data;
}
- pop 方法则只能由一个线程读取队列并将其内容写入日志文件。
实际上,各个线程通过push 方法使用了 std::lock_guardstd::mutex进行加锁,然后将数据添加到队列中,最后通过条件变量std::condition_variable唤醒 pop 方法所在的线程。pop 方法获得锁后,然后进入一个 while 循环,在循环中检查队列是否为空,如果为空,则调用条件变量的 wait 方法使当前线程阻塞等待日志的产生。当队列不为空时,将队头元素取出,并从队列中删除。最后释放锁并返回取出的队头元素。
优点:通过这种方式实现日志队列的异步操作,可以让写日志的线程和写文件的线程分别跑在不同的线程中,避免了日志写操作对主程序的性能影响。
Logger类
日志类属于是单例模式,确保了整个应用程序中只有一个logger实例。
重要成员变量
enum LogLevel //日志级别
{
INFO,//普通信息
ERROR,//错误信息
};
int m_loglevel;//记录日志级别
LockQueue<std::string> m_lckQue;//日志缓冲队列
重要成员函数
Logger()
Logger::Logger()
{
//启动专门的写日志线程
std::thread writeLogTask([&](){
for(;;)
{
//获取当天的日期,然后取日志信息,写入相应的日志文件当中 a+
time_t now=time(nullptr);
tm *nowtm = localtime(&now);
char file_name[128];
sprintf(file_name,"%d-%d-%d-log.txt",nowtm->tm_year+1900
,nowtm->tm_mon+1,nowtm->tm_mday);
FILE *pf = fopen(file_name,"a+");
if(pf==nullptr)
{
std::cout<<"logger file: "<<file_name<<" open error!"
<<std::endl;
exit(EXIT_FAILURE);
}
std::string msg=m_lckQue.Pop();
char time_buf[128]={0};
sprintf(time_buf,"%d:%d:%d=> [%s] "
,nowtm->tm_hour
,nowtm->tm_min
,nowtm->tm_sec
,(m_loglevel==INFO?"INFO":"ERROR"));
msg.insert(0,time_buf);
msg.append("\n");
fputs(msg.c_str(),pf);
fclose(pf);
}
});
//设置分离线程,守护线程
writeLogTask.detach();
}
- 在logger的构造函数中,发起了一个线程writelogtask,该线程循环执行以下操作, 该线程会一直运行,为整个应用程序提供日志服务;
- 调用系统
localtime
函数获取当前时间,尝试打开当日的日志文件 - 调用lockqueue类的
pop()
函数,从lockqueue中获取缓存的日志信息; - 获取时分秒时间,以及根据日志级别,添加日志级别前缀,并将该条日志写入日志文件中
- 设置分离线程,守护线程
static Logger& GetInstance();
Logger &Logger::GetInstance()
{
static Logger logger;
return logger;
}
- 获取唯一单例对象
void SetLogLevel(LogLevel level);
void Logger::SetLogLevel(LogLevel level)
{
m_loglevel=level;
}
- 设置日志级别
void Log(std::string msg);
void Logger::Log(std::string msg)
{
m_lckQue.Push(msg);
}
- 把日志信息写入Lockqueue缓冲区当中
宏
和muduo网络库中的实现类似,本项目也提供了日志的宏,它接受一个格式化的日志消息和可变数量的参数。并为了避免展开时出错,我们采用了do-while(0)语法在实际使用过程中,log_info(“xxx %d %s”, 20, “xxxx”) 可以被展开。
#define LOG_INFO(logmsgformat, ...)\
do\
{\
Logger &logger =Logger::GetInstance();\
logger.SetLogLevel(INFO);\
char c[1024]={0};\
snprintf(c,1024,logmsgformat,##__VA_ARGS__);\
logger.Log(c);\
}while (0);
#define LOG_ERROR(logmsgformat, ...)\
do\
{\
Logger &logger =Logger::GetInstance();\
logger.SetLogLevel(ERROR);\
char c[1024]={0};\
snprintf(c,1024,logmsgformat,##__VA_ARGS__);\
logger.Log(c);\
}while (0);
- 在宏内部,获取logger的实例
- 设置日志级别为info;
- 创建一个长度为1024的char数组c,使用snprintf函数将格式化字符串(logmsgformat) 和可变参数(va_args)写入这个数组中;
- 调用logger的log函数将日志消息写入日志文件中。