在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函数将日志消息写入日志文件中。