本文轉載自:http://blog.csdn.net/hailong0715/article/details/53418594
特根據理解繪製業務圖如下:
線程圖:
DB_PROXY_Server數據庫代理是TeamTalk
TTServer中負責與數據庫交互的代理服務器,在DB server中負責承載TeamTalk所有業務層面和邏輯層面的數據入庫和持久化等服務,是TT_Server中比較重要的一環,在設計中採用了很多實用的技術,比如池化技術,數據庫代理,單例模式等,收益匪淺,下面對TealTalk的DB_Server作個詳細分析,以封面是自己閱讀代碼後的學習總結,一方面可以給其他學習TT_Server的人提供參考。
可以通過查看DB_Server的配置文件dbproxyserver.conf文件,DB_SERVER主要分爲以下幾個部分:
1、TeamTalk_Matser 採用了MySQL數據庫
2、TeamTalk_Slave MYSQL數據庫
3、unread 未讀信息實例 Redis 數據庫
4、group_set 羣組設置實例 redis數據庫
5、token 實例 redis數據庫
6、sync實例 redis數據庫 同步功能
7、group_member redis 數據庫
每個數據庫實例都會預先打開於數據庫的兩個鏈接,不需要在每次使用的時候再打開,使用結束後釋放,節省了數據打開和釋放需要的時間和資源,在當前可用連接數不夠的情況下再新增一個數據庫鏈接,動態調節DB_Server的負載,同時限定了每個實例的最大可用連接數,由於系統資源是有限的,當業務比較繁忙時不能無限制創建新的連接,避免耗盡系統資源,這種場景下,當沒有可用連接的時候,新的業務請求必須等待,等待可用的連接,然後再執行相應的業務操作。
DB_Server採用了多線程,在DB_Server啓動的時候預先分配了配置文件中指定的線程數,用來處理具體的數據庫請求,當一個請求到達DB_Server時,DB_Server將該請求封裝成DB相關的額任務類,然後隨機加入到預先啓動的線程的任務列表中,有線程回調函數不停執行具體的任務請求,者就是整個DB_Serve的設計思路。
下面來看代碼的流程:
1、CacheManager的初始化
db_proxy_server的main函數中,依次從配置文件中讀取各個實例的名稱以及初始DB實例個數,最大連接數等信息。
首先獲取CacheManager的對象,在獲取對象的同時對該對象以及該對象管理的資源進行了初始化。
-
CacheManager* pCacheManager = CacheManager::getInstance();
需要明確的是,cacheManager維護了一個map,map<string, CachePool*>m_cache_pool_map;這個map的key值就是每個配置文件中每個CacheInstances的名稱,對應給每一個cache實例維護了一個連接池CachePool, 在CachePool中有維護了一個list<CacheConn*>m_free_list;,這個list保存了對應cache實例的連接,CacheConn是對每個DB連接的封裝。這個類中維護了對應數據庫連接的一些基本信息,redis數據的話保存的是數據庫連接上下文redisContext,以及操作數據庫的Set
和Get方法。
創建CacheManager的對象
-
CacheManager* CacheManager::getInstance()
-
{
-
if (!s_cache_manager) {
-
s_cache_manager = new CacheManager();
-
if (s_cache_manager->Init()) {
-
delete s_cache_manager;
-
s_cache_manager = NULL;
-
}
-
}
-
-
return s_cache_manager;
-
}
調用CacheManager的Init的方法,初始化CacheManager管理的資源,在這裏就是讀取配置文件,根據配置文件中的CacheInstance名稱創建對應的實例,並創建相應的連接池對象,將其插入到Manager管理的Map中。
-
int CacheManager::Init()
-
{
-
CConfigFileReader config_file("dbproxyserver.conf");
-
-
char* cache_instances = config_file.GetConfigName("CacheInstances");
-
if (!cache_instances) {
-
log("not configure CacheIntance");
-
return 1;
-
}
-
-
char host[64];
-
char port[64];
-
char db[64];
-
char maxconncnt[64];
-
CStrExplode instances_name(cache_instances, ',');
-
for (uint32_t i = 0; i < instances_name.GetItemCnt(); i++) {
-
char* pool_name = instances_name.GetItem(i);
-
-
snprintf(host, 64, "%s_host", pool_name);
-
snprintf(port, 64, "%s_port", pool_name);
-
snprintf(db, 64, "%s_db", pool_name);
-
snprintf(maxconncnt, 64, "%s_maxconncnt", pool_name);
-
-
char* cache_host = config_file.GetConfigName(host);
-
char* str_cache_port = config_file.GetConfigName(port);
-
char* str_cache_db = config_file.GetConfigName(db);
-
char* str_max_conn_cnt = config_file.GetConfigName(maxconncnt);
-
if (!cache_host || !str_cache_port || !str_cache_db || !str_max_conn_cnt) {
-
log("not configure cache instance: %s", pool_name);
-
return 2;
-
}
-
-
CachePool* pCachePool = new CachePool(pool_name, cache_host, atoi(str_cache_port),
-
atoi(str_cache_db), atoi(str_max_conn_cnt));
-
if (pCachePool->Init()) {
-
log("Init cache pool failed");
-
return 3;
-
}
-
-
m_cache_pool_map.insert(make_pair(pool_name, pCachePool));
-
}
-
-
return 0;
-
}
根據在配置文件中定義的instanceNane_DB的值創建CacheConn對象,調用其init函數,並將其插入到CachePool管理的空閒鏈表中。
-
int CachePool::Init()
-
{
-
for (int i = 0; i < m_cur_conn_cnt; i++) {
-
CacheConn* pConn = new CacheConn(this);
-
if (pConn->Init()) {
-
delete pConn;
-
return 1;
-
}
-
-
m_free_list.push_back(pConn);
-
}
-
-
log("cache pool: %s, list size: %lu", m_pool_name.c_str(), m_free_list.size());
-
return 0;
-
}
調用CacheConn的Init函數,創建數據庫的連接。
-
-
-
-
int CacheConn::Init()
-
{
-
if (m_pContext) {
-
return 0;
-
}
-
-
-
uint64_t cur_time = (uint64_t)time(NULL);
-
if (cur_time < m_last_connect_time + 4) {
-
return 1;
-
}
-
-
m_last_connect_time = cur_time;
-
-
-
struct timeval timeout = {0, 200000};
-
m_pContext = redisConnectWithTimeout(m_pCachePool->GetServerIP(), m_pCachePool->GetServerPort(), timeout);
-
if (!m_pContext || m_pContext->err) {
-
if (m_pContext) {
-
log("redisConnect failed: %s", m_pContext->errstr);
-
redisFree(m_pContext);
-
m_pContext = NULL;
-
} else {
-
log("redisConnect failed");
-
}
-
-
return 1;
-
}
-
-
redisReply* reply = (redisReply *)redisCommand(m_pContext, "SELECT %d", m_pCachePool->GetDBNum());
-
if (reply && (reply->type == REDIS_REPLY_STATUS) && (strncmp(reply->str, "OK", 2) == 0)) {
-
freeReplyObject(reply);
-
return 0;
-
} else {
-
log("select cache db failed");
-
return 2;
-
}
-
}
上述流程就完成了所有CacheInstance的初始化,並創建了數據的連接池,後續所有對數據庫的操作都通過調用CashManager::GetCacheConn方法來獲取數據庫的連接,從cachePool管理的free_list中出隊列,如果隊列爲空,則創建新的連接或者等待,使用完連接後加入free_list中,供下次使用。
2、CDBManager的初始化
CDBManager的初始化流程與CacheManager的初始化流程類似,唯一不同的是CDBManager保存的CDBConn是到MYSQL數據庫的連接。其他過程類似,理解上面CacheManager的流程就很容易理解該部分流程,這裏不再重複敘述了。
3、資源對象初始化
完成DB相關資源的對象初始化,這些類都用了單例模式,因此初始化這些對象都只需要調用自身的GetInstance方法,
-
puts("db init success");
-
-
if (!CAudioModel::getInstance()) {
-
return -1;
-
}
-
-
if (!CGroupMessageModel::getInstance()) {
-
return -1;
-
}
-
-
if (!CGroupModel::getInstance()) {
-
return -1;
-
}
-
-
if (!CMessageModel::getInstance()) {
-
return -1;
-
}
-
-
if (!CSessionModel::getInstance()) {
-
return -1;
-
}
-
-
if(!CRelationModel::getInstance())
-
{
-
return -1;
-
}
-
-
if (!CUserModel::getInstance()) {
-
return -1;
-
}
-
-
if (!CFileModel::getInstance()) {
-
return -1;
-
}
4、工作線程池初始化,任務執行回調初始化。
這個部分可謂是db_proxy_server中重要的一環,所有的DB任務都是通過從工作線程池中的線程通過獲取對應命令ID的回調函數來執行所有的數據庫操作任務,因此理解了這部分差不多就理解了db_proxy_server的一半,下面介紹這部分流程。
-
int init_proxy_conn(uint32_t thread_num)
-
{
-
s_handler_map = CHandlerMap::getInstance();
-
g_thread_pool.Init(thread_num);
-
-
netlib_add_loop(proxy_loop_callback, NULL);
-
-
signal(SIGTERM, sig_handler);
-
-
return netlib_register_timer(proxy_timer_callback, NULL, 1000);
-
}
該部分的初始化時在main函數中調用全局函數init_proxy_conn函數完成的,該函數的一個入參thread_num指定了工作線程池的線程數量。
(1)首先獲取CHandlerMap的對象,在獲取對象的同時完成了所有回調函數的註冊。
-
CHandlerMap* CHandlerMap::getInstance()
-
{
-
if (!s_handler_instance) {
-
s_handler_instance = new CHandlerMap();
-
s_handler_instance->Init();
-
}
-
-
return s_handler_instance;
-
}
-
void CHandlerMap::Init()
-
{
-
-
m_handler_map.insert(make_pair(uint32_t(CID_OTHER_VALIDATE_REQ), DB_PROXY::doLogin));
-
m_handler_map.insert(make_pair(uint32_t(CID_LOGIN_REQ_PUSH_SHIELD), DB_PROXY::doPushShield));
-
m_handler_map.insert(make_pair(uint32_t(CID_LOGIN_REQ_QUERY_PUSH_SHIELD), DB_PROXY::doQueryPushShield));
-
-
-
m_handler_map.insert(make_pair(uint32_t(CID_BUDDY_LIST_RECENT_CONTACT_SESSION_REQUEST), DB_PROXY::getRecentSession));
-
m_handler_map.insert(make_pair(uint32_t(CID_BUDDY_LIST_REMOVE_SESSION_REQ), DB_PROXY::deleteRecentSession));
-
-
-
m_handler_map.insert(make_pair(uint32_t(CID_BUDDY_LIST_USER_INFO_REQUEST), DB_PROXY::getUserInfo));
-
m_handler_map.insert(make_pair(uint32_t(CID_BUDDY_LIST_ALL_USER_REQUEST), DB_PROXY::getChangedUser));
-
m_handler_map.insert(make_pair(uint32_t(CID_BUDDY_LIST_DEPARTMENT_REQUEST), DB_PROXY::getChgedDepart));
-
m_handler_map.insert(make_pair(uint32_t(CID_BUDDY_LIST_CHANGE_SIGN_INFO_REQUEST), DB_PROXY::changeUserSignInfo));
-
-
-
-
m_handler_map.insert(make_pair(uint32_t(CID_MSG_DATA), DB_PROXY::sendMessage));
-
m_handler_map.insert(make_pair(uint32_t(CID_MSG_LIST_REQUEST), DB_PROXY::getMessage));
-
m_handler_map.insert(make_pair(uint32_t(CID_MSG_UNREAD_CNT_REQUEST), DB_PROXY::getUnreadMsgCounter));
-
m_handler_map.insert(make_pair(uint32_t(CID_MSG_READ_ACK), DB_PROXY::clearUnreadMsgCounter));
-
m_handler_map.insert(make_pair(uint32_t(CID_MSG_GET_BY_MSG_ID_REQ), DB_PROXY::getMessageById));
-
m_handler_map.insert(make_pair(uint32_t(CID_MSG_GET_LATEST_MSG_ID_REQ), DB_PROXY::getLatestMsgId));
-
-
-
m_handler_map.insert(make_pair(uint32_t(CID_LOGIN_REQ_DEVICETOKEN), DB_PROXY::setDevicesToken));
-
m_handler_map.insert(make_pair(uint32_t(CID_OTHER_GET_DEVICE_TOKEN_REQ), DB_PROXY::getDevicesToken));
-
-
-
m_handler_map.insert(make_pair(uint32_t(CID_GROUP_SHIELD_GROUP_REQUEST), DB_PROXY::setGroupPush));
-
m_handler_map.insert(make_pair(uint32_t(CID_OTHER_GET_SHIELD_REQ), DB_PROXY::getGroupPush));
-
-
-
-
m_handler_map.insert(make_pair(uint32_t(CID_GROUP_NORMAL_LIST_REQUEST), DB_PROXY::getNormalGroupList));
-
m_handler_map.insert(make_pair(uint32_t(CID_GROUP_INFO_REQUEST), DB_PROXY::getGroupInfo));
-
m_handler_map.insert(make_pair(uint32_t(CID_GROUP_CREATE_REQUEST), DB_PROXY::createGroup));
-
m_handler_map.insert(make_pair(uint32_t(CID_GROUP_CHANGE_MEMBER_REQUEST), DB_PROXY::modifyMember));
-
-
-
-
m_handler_map.insert(make_pair(uint32_t(CID_FILE_HAS_OFFLINE_REQ), DB_PROXY::hasOfflineFile));
-
m_handler_map.insert(make_pair(uint32_t(CID_FILE_ADD_OFFLINE_REQ), DB_PROXY::addOfflineFile));
-
m_handler_map.insert(make_pair(uint32_t(CID_FILE_DEL_OFFLINE_REQ), DB_PROXY::delOfflineFile));
-
-
}
在ChandleMap註冊數據庫請求的CommandID對應的回調函數,在處理接收的請求時,根據CmdID查詢CHandleMap,獲取回調函數處理請求。
(2)初始化工作線程池
g_thread_pool.Init(thread_num);這行代碼根據傳入的參數創建了指定數量的線程,每個工作線程中都維護了一個任務隊列,在有數據請求來的時候,系統隨機將任務添加到線程的任務隊列中,線程一次執行自己任務隊列中的任務。
-
int CThreadPool::Init(uint32_t worker_size)
-
{
-
m_worker_size = worker_size;
-
m_worker_list = new CWorkerThread [m_worker_size];
-
if (!m_worker_list) {
-
return 1;
-
}
-
-
for (uint32_t i = 0; i < m_worker_size; i++) {
-
m_worker_list[i].SetThreadIdx(i);
-
m_worker_list[i].Start();
-
}
-
-
return 0;
-
}
-
void CWorkerThread::Start()
-
{
-
(void)pthread_create(&m_thread_id, NULL, StartRoutine, this);
-
}
線程回調
-
void* CWorkerThread::StartRoutine(void* arg)
-
{
-
CWorkerThread* pThread = (CWorkerThread*)arg;
-
-
pThread->Execute();
-
-
return NULL;
-
}
處理任務
-
void CWorkerThread::Execute()
-
{
-
while (true) {
-
m_thread_notify.Lock();
-
-
-
while (m_task_list.empty()) {
-
m_thread_notify.Wait();
-
}
-
-
CTask* pTask = m_task_list.front();
-
m_task_list.pop_front();
-
m_thread_notify.Unlock();
-
-
pTask->run();
-
-
delete pTask;
-
-
m_task_cnt++;
-
-
}
-
}
(3)環回檢查,超時檢測處理異常中斷
-
netlib_add_loop(proxy_loop_callback, NULL);
-
-
signal(SIGTERM, sig_handler);
-
-
return netlib_register_timer(proxy_timer_callback, NULL, 1000);
DB操作中的相應並不是來一個請求發送一個響應,CProxyConn中維護了一個static list<ResponsePdu_t*>s_response_pdu_list,將需要發送的相應PDU插入到該list中,在db_proxy_server的時間分發器中,每次循環都會調用CheckLoop函數,根據註冊的proxy_loop_callback回調發送響應信息。
-
<span style="white-space:pre"> </span>signal(SIGTERM, sig_handler);
註冊程序異常終止時的信號處理函數,處理函數異常退出時的一些清理工作,發送未發送的數據,停止接受請求等
-
static void sig_handler(int sig_no)
-
{
-
if (sig_no == SIGTERM) {
-
log("receive SIGTERM, prepare for exit");
-
CImPdu cPdu;
-
IM::Server::IMStopReceivePacket msg;
-
msg.set_result(0);
-
cPdu.SetPBMsg(&msg);
-
cPdu.SetServiceId(IM::BaseDefine::SID_OTHER);
-
cPdu.SetCommandId(IM::BaseDefine::CID_OTHER_STOP_RECV_PACKET);
-
for (ConnMap_t::iterator it = g_proxy_conn_map.begin(); it != g_proxy_conn_map.end(); it++) {
-
CProxyConn* pConn = (CProxyConn*)it->second;
-
pConn->SendPdu(&cPdu);
-
}
-
-
-
CSyncCenter::getInstance()->stopSync();
-
-
-
netlib_register_timer(exit_callback, NULL, 4000);
-
}
-
}
netlib_register_timer(proxy_timer_callback, NULL, 1000)註冊心跳,1000tick到發送一個心跳包,防止客戶端掉線時佔用系統資源,及時清理對應客戶端的連接符等系統資源。
5、同步聊天記錄等信息
-
CSyncCenter::getInstance()->init();
-
CSyncCenter::getInstance()->startSync();
-
void CSyncCenter::init()
-
{
-
-
CacheManager* pCacheManager = CacheManager::getInstance();
-
-
CacheConn* pCacheConn = pCacheManager->GetCacheConn("unread");
-
if (pCacheConn)
-
{
-
string strTotalUpdate = pCacheConn->get("total_user_updated");
-
-
string strLastUpdateGroup = pCacheConn->get("last_update_group");
-
pCacheManager->RelCacheConn(pCacheConn);
-
if(strTotalUpdate != "")
-
{
-
m_nLastUpdate = string2int(strTotalUpdate);
-
}
-
else
-
{
-
updateTotalUpdate(time(NULL));
-
}
-
if(strLastUpdateGroup.empty())
-
{
-
m_nLastUpdateGroup = string2int(strLastUpdateGroup);
-
}
-
else
-
{
-
updateLastUpdateGroup(time(NULL));
-
}
-
}
-
else
-
{
-
log("no cache connection to get total_user_updated");
-
}
-
}
創建線程並開始同步
-
void CSyncCenter::startSync()
-
{
-
#ifdef _WIN32
-
(void)CreateThread(NULL, 0, doSyncGroupChat, NULL, 0, &m_nGroupChatThreadId);
-
#else
-
(void)pthread_create(&m_nGroupChatThreadId, NULL, doSyncGroupChat, NULL);
-
#endif
-
}
由於主線程執行需要執行其他任務,爲了避免主線程耗時,同步聊天記錄,羣主信息功能在新創建的線程中執行,節省主線程啓動時間;所有同步操作均在線程回調函數中完
成doSyncGroupChat.
6、事件分發,主線程助理主流程,接受客戶連接,處理客戶請求
所有TT_SERVER的網絡處理統一在事件分發起中執行,包括從socket中接收連接,讀寫數據,處理異常
-
void CEventDispatch::StartDispatch(uint32_t wait_timeout)
-
{
-
struct epoll_event events[1024];
-
int nfds = 0;
-
-
if(running)
-
return;
-
running = true;
-
-
while (running)
-
{
-
nfds = epoll_wait(m_epfd, events, 1024, wait_timeout);
-
for (int i = 0; i < nfds; i++)
-
{
-
int ev_fd = events[i].data.fd;
-
CBaseSocket* pSocket = FindBaseSocket(ev_fd);
-
if (!pSocket)
-
continue;
-
-
-
#ifdef EPOLLRDHUP
-
if (events[i].events & EPOLLRDHUP)
-
{
-
-
pSocket->OnClose();
-
}
-
#endif
-
-
-
if (events[i].events & EPOLLIN)
-
{
-
-
pSocket->OnRead();
-
}
-
-
if (events[i].events & EPOLLOUT)
-
{
-
-
pSocket->OnWrite();
-
}
-
-
if (events[i].events & (EPOLLPRI | EPOLLERR | EPOLLHUP))
-
{
-
-
pSocket->OnClose();
-
}
-
-
pSocket->ReleaseRef();
-
}
-
-
_CheckTimer();
-
_CheckLoop();
-
}
-
}
在事件分發中需要詳解的是從socket中讀取客戶請求數據並解析PDU數據包,處理客戶請求,回覆效應,其他socket連接等部分在之前的文章中已經敘述過了,這裏就不再重複了,下面主要陳述db_proxy_server中處理客戶請求的流程。
在db_proxy_server中接收到用戶的請求數據後都會在如下的函數中解析請求數據。
-
void CProxyConn::HandlePduBuf(uchar_t* pdu_buf, uint32_t pdu_len)
-
{
-
CImPdu* pPdu = NULL;
-
pPdu = CImPdu::ReadPdu(pdu_buf, pdu_len);
-
if (pPdu->GetCommandId() == IM::BaseDefine::CID_OTHER_HEARTBEAT) {
-
return;
-
}
-
-
pdu_handler_t handler = s_handler_map->GetHandler(pPdu->GetCommandId());
-
-
if (handler) {
-
CTask* pTask = new CProxyTask(m_uuid, handler, pPdu);
-
g_thread_pool.AddTask(pTask);
-
} else {
-
log("no handler for packet type: %d", pPdu->GetCommandId());
-
}
-
}
解析接收到的PDU數據包,根據CommandId判斷是否是心跳包,如果是心跳包直接丟失,不是心跳包則在CHandlerMap中獲取對應CommandId的回調,創建一個新的CProxyTask任務類,將任務類隨機的加入到工作線程的任務隊列中。
-
void CThreadPool::AddTask(CTask* pTask)
-
{
-
-
-
-
-
-
uint32_t thread_idx = random() % m_worker_size;
-
m_worker_list[thread_idx].PushTask(pTask);
-
}
線程在處理任務的時候會調用相應任務類的Run方法,處理任務
-
void CProxyTask::run()
-
{
-
-
if (!m_pPdu) {
-
-
CProxyConn::AddResponsePdu(m_conn_uuid, NULL);
-
} else {
-
if (m_pdu_handler) {
-
m_pdu_handler(m_pPdu, m_conn_uuid);
-
}
-
}
-
}
至此db_proxy_server的整個初始化流程和處理流程都介紹完畢。在整個db server中核心思想就是池化技術,(DB連接池,工作線程池);整個核心流程是創建線程池,根據命令ID註冊對應的處理回調,在線程回調函數中處理任務隊列,依次取出任務,根據註冊的回調處理任務請求,迴響應。
鑑於本人理解有限,在行文的過程中可能存在一些理解或者描述錯誤的地方,請各位看官指正,TT_SERVER詳解系列,是本人學習teamtalk源碼的一些理解和心得,看源碼主要看的是設計方法,處理思維,在不斷學習中進步。