在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函數將日志消息寫入日志文件中。