TeamTalk原始碼分析(四) —— 伺服器端db_proxy_server原始碼分析

analogous_love發表於2017-05-17

從這篇文章開始,我將詳細地分析TeamTalk伺服器端每一個服務的原始碼和架構設計。這篇從db_proxy_server開始:

db_proxy_server是TeamTalk伺服器端最後端的程式,它連線著關係型資料庫mysql和nosql記憶體資料庫redis。其位置在整個服務架構中如圖所示:



我們從db_proxy_server的main()函式開始,main()函式其實就是做了以下初始化工作,我整理成如下偽碼:

int main()
{
	//1. 初始化redis連線
	
	//2. 初始化mysql連線
	
	//3. 啟動任務佇列,用於處理任務
	
	//4. 啟動從mysql同步資料到redis工作
	
	//5. 在埠10600上啟動偵聽,監聽新連線
	
	//6. 主執行緒進入迴圈,監聽新連線的到來以及出來新連線上的資料收發
}

下面,我們將一一介紹以上步驟。

一、初始化redis連線

CacheManager* pCacheManager = CacheManager::getInstance();

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;
}

int CacheManager::Init()
{
	CConfigFileReader config_file("dbproxyserver.conf");

    //CacheInstances=unread,group_set,token,sync,group_member
	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);
		//printf("%s", pool_name);
		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;
}

在pCachePool->Init()中是實際連線redis的動作:

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;
}

pConn->Init()呼叫如下:

int CacheConn::Init()
{
	if (m_pContext) {
		return 0;
	}

	// 4s 嘗試重連一次
	uint64_t cur_time = (uint64_t)time(NULL);
	if (cur_time < m_last_connect_time + 4) {
		return 1;
	}

	m_last_connect_time = cur_time;

	// 200ms超時
	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;
	}
}


層級關係是這樣的:
CacheManager的成員變數m_cache_pool_map儲存了配置檔案配置的redis快取池,這是一個map物件,key是快取池的名字,value是快取池CachePool物件的指標。

map<string, CachePool*>	m_cache_pool_map;
dbproxyserver.conf目前配置瞭如下幾個redis快取池:

CacheInstances=unread,group_set,token,sync,group_member

每一個快取池物件CachePool的成員變數m_free_list中儲存著若干個與redis的連線物件,具體是多少個,根據配置檔案來配置。m_free_list定義:

list<CacheConn*>	m_free_list;

這些與redis連線物件後面會介紹在何處使用。


二、初始化mysql連線

CDBManager* pDBManager = CDBManager::getInstance();

CDBManager* CDBManager::getInstance()
{
	if (!s_db_manager) 
    {
		s_db_manager = new CDBManager();
		if (s_db_manager->Init()) {
			delete s_db_manager;
			s_db_manager = NULL;
		}
	}

	return s_db_manager;
}

int CDBManager::Init()
{
	CConfigFileReader config_file("dbproxyserver.conf");
    //DBInstances=teamtalk_master,teamtalk_slave
	char* db_instances = config_file.GetConfigName("DBInstances");

	if (!db_instances) {
		log("not configure DBInstances");
		return 1;
	}

	char host[64];
	char port[64];
	char dbname[64];
	char username[64];
	char password[64];
    char maxconncnt[64];
	CStrExplode instances_name(db_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(dbname, 64, "%s_dbname", pool_name);
		snprintf(username, 64, "%s_username", pool_name);
		snprintf(password, 64, "%s_password", pool_name);
        snprintf(maxconncnt, 64, "%s_maxconncnt", pool_name);

		char* db_host = config_file.GetConfigName(host);
		char* str_db_port = config_file.GetConfigName(port);
		char* db_dbname = config_file.GetConfigName(dbname);
		char* db_username = config_file.GetConfigName(username);
		char* db_password = config_file.GetConfigName(password);
        char* str_maxconncnt = config_file.GetConfigName(maxconncnt);

		if (!db_host || !str_db_port || !db_dbname || !db_username || !db_password || !str_maxconncnt) {
			log("not configure db instance: %s", pool_name);
			return 2;
		}

		int db_port = atoi(str_db_port);
        int db_maxconncnt = atoi(str_maxconncnt);
		CDBPool* pDBPool = new CDBPool(pool_name, db_host, db_port, db_username, db_password, db_dbname, db_maxconncnt);
		if (pDBPool->Init()) 
        {
			log("init db instance failed: %s", pool_name);
			return 3;
		}
		m_dbpool_map.insert(make_pair(pool_name, pDBPool));
	}

	return 0;
}

同理pDBPool->Init()中是實際連線mysql程式碼:

int CDBPool::Init()
{
	for (int i = 0; i < m_db_cur_conn_cnt; i++) {
		CDBConn* pDBConn = new CDBConn(this);
		int ret = pDBConn->Init();
		if (ret) {
			delete pDBConn;
			return ret;
		}

		m_free_list.push_back(pDBConn);
	}

	log("db pool: %s, size: %d", m_pool_name.c_str(), (int)m_free_list.size());
	return 0;
}


int CDBConn::Init()
{
	m_mysql = mysql_init(NULL);
	if (!m_mysql) {
		log("mysql_init failed");
		return 1;
	}

	my_bool reconnect = true;
	mysql_options(m_mysql, MYSQL_OPT_RECONNECT, &reconnect);
	mysql_options(m_mysql, MYSQL_SET_CHARSET_NAME, "utf8mb4");

	if (!mysql_real_connect(m_mysql, m_pDBPool->GetDBServerIP(), m_pDBPool->GetUsername(), ""/*m_pDBPool->GetPasswrod()*/,
			m_pDBPool->GetDBName(), m_pDBPool->GetDBServerPort(), NULL, 0)) {
		log("mysql_real_connect failed: %s", mysql_error(m_mysql));
		return 2;
	}

	return 0;
}

與redis連線物件類似,CDBManager的成員物件m_dbpool_map儲存了mysql連線池,這也是一個stl map,key是池子的名字,value是連線池的物件CDBPool指標。配置檔案中總共配置了名稱為主從兩個mysql連線池。

DBInstances=teamtalk_master,teamtalk_slave

連線池物件CDBPool用一個成員變數儲存自己的若干個mysql連線:
list<CDBConn*>	m_free_list;        //實際儲存mysql連線的容器
具體每個連線池有多少個mysql連線,根據配置檔案得到,這裡主從兩個庫都是16個。

這些mysql連線的用途後面介紹。


三、啟動任務佇列,用於處理任務

初始化一:建立執行緒處理任務佇列中的任務

init_proxy_conn(thread_num);

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);
}


執行緒數量根據配置檔案得到。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();

		// put wait in while cause there can be spurious wake up (due to signal/ENITR)
		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++;
		//log("%d have the execute %d task\n", m_thread_idx, m_task_cnt);
	}
}

可以看到工作執行緒一直在等待一個條件變數,當向任務佇列中新增任務時,條件變數被喚醒:

void CWorkerThread::PushTask(CTask* pTask)
{
	m_thread_notify.Lock();
	m_task_list.push_back(pTask);
	m_thread_notify.Signal();
	m_thread_notify.Unlock();
}

任務佇列的用途,下文會介紹。


初始化二:將各個任務id與對應的處理函式繫結起來:

s_handler_map = CHandlerMap::getInstance();

CHandlerMap* CHandlerMap::getInstance()
{
	if (!s_handler_instance) {
		s_handler_instance = new CHandlerMap();
		s_handler_instance->Init();
	}

	return s_handler_instance;
}

void CHandlerMap::Init()
{
    //DB_PROXY是名稱空間,不是類名
	// Login validate
	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));
    
    // recent session
    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));
    
    // users
    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));

    
    // message content
    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));
    
    // device token
    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));
    
    //push 推送設定
    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));
    
    
    // group
    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));

    
    // file
    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));
}


四、啟動從mysql同步資料到redis工作

CSyncCenter::getInstance()->init();
CSyncCenter::getInstance()->startSync();


CSyncCenter::getInstance()->init()是獲得上次同步的資料位置,接下來同步從這個位置開始。

/*
 * 初始化函式,從cache裡面載入上次同步的時間資訊等
 */
void CSyncCenter::init()
{
    // Load total update time
    CacheManager* pCacheManager = CacheManager::getInstance();
    // increase message count
    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");
    }
}


CSyncCenter::getInstance()->startSync();新開啟一個執行緒進行同步工作:

/**
 *  開啟內網資料同步以及群組聊天記錄同步
 */
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()如下:

/**
 *  同步群組聊天資訊
 *
 *  @param arg NULL
 *
 *  @return NULL
 */
void* CSyncCenter::doSyncGroupChat(void* arg)
{
    m_bSyncGroupChatRuning = true;
    CDBManager* pDBManager = CDBManager::getInstance();
    map<uint32_t, uint32_t> mapChangedGroup;
    do{
        mapChangedGroup.clear();
        CDBConn* pDBConn = pDBManager->GetDBConn("teamtalk_slave");
        if(pDBConn)
        {
            string strSql = "select id, lastChated from IMGroup where status=0 and lastChated >=" + int2string(m_pInstance->getLastUpdateGroup());
            CResultSet* pResult = pDBConn->ExecuteQuery(strSql.c_str());
            if(pResult)
            {
                while (pResult->Next()) {
                    uint32_t nGroupId = pResult->GetInt("id");
                    uint32_t nLastChat = pResult->GetInt("lastChated");
                    if(nLastChat != 0)
                    {
                        mapChangedGroup[nGroupId] = nLastChat;
                    }
                }
                delete pResult;
            }
            pDBManager->RelDBConn(pDBConn);
        }
        else
        {
            log("no db connection for teamtalk_slave");
        }
        m_pInstance->updateLastUpdateGroup(time(NULL));
        for (auto it=mapChangedGroup.begin(); it!=mapChangedGroup.end(); ++it)
        {
            uint32_t nGroupId =it->first;
            list<uint32_t> lsUsers;
            uint32_t nUpdate = it->second;
            CGroupModel::getInstance()->getGroupUser(nGroupId, lsUsers);
            for (auto it1=lsUsers.begin(); it1!=lsUsers.end(); ++it1)
            {
                uint32_t nUserId = *it1;
                uint32_t nSessionId = INVALID_VALUE;
                nSessionId = CSessionModel::getInstance()->getSessionId(nUserId, nGroupId, IM::BaseDefine::SESSION_TYPE_GROUP, true);
                if(nSessionId != INVALID_VALUE)
                {
                    CSessionModel::getInstance()->updateSession(nSessionId, nUpdate);
                }
                else
                {
                    CSessionModel::getInstance()->addSession(nUserId, nGroupId, IM::BaseDefine::SESSION_TYPE_GROUP);
                }
            }
        }
//    } while (!m_pInstance->m_pCondSync->waitTime(5*1000));
    } while (m_pInstance->m_bSyncGroupChatWaitting && !(m_pInstance->m_pCondGroupChat->waitTime(5*1000)));
//    } while(m_pInstance->m_bSyncGroupChatWaitting);
    m_bSyncGroupChatRuning = false;
    return NULL;
}

可以看到流程就是先用sql從mysql取出資料,再用“sql”寫到redis中去。操作mysql和redis時,並沒有新建新連線,而是使用上文介紹的連線池和快取池中已有的連線。我們上文說了,每個池中都有若干個連線,那使用哪個連線呢?由於儲存mysql的連線是一個list物件,所以預設從list的頭部取一個可用的。如果當前沒有空閒連線可用,則新建一個:

CDBConn* CDBPool::GetDBConn()
{
	m_free_notify.Lock();

	while (m_free_list.empty()) {
		if (m_db_cur_conn_cnt >= m_db_max_conn_cnt) {
			m_free_notify.Wait();
		} else {
			CDBConn* pDBConn = new CDBConn(this);
			int ret = pDBConn->Init();
			if (ret) {
				log("Init DBConnecton failed");
				delete pDBConn;
				m_free_notify.Unlock();
				return NULL;
			} else {
				m_free_list.push_back(pDBConn);
				m_db_cur_conn_cnt++;
				log("new db connection: %s, conn_cnt: %d", m_pool_name.c_str(), m_db_cur_conn_cnt);
			}
		}
	}

	CDBConn* pConn = m_free_list.front();
	m_free_list.pop_front();

	m_free_notify.Unlock();

	return pConn;
}

分配redis和mysql的一模一樣,這裡程式碼就不貼了。


五、在埠10600上啟動偵聽,監聽新連線

CStrExplode listen_ip_list(listen_ip, ';');
    for (uint32_t i = 0; i < listen_ip_list.GetItemCnt(); i++) {
        ret = netlib_listen(listen_ip_list.GetItem(i), listen_port, proxy_serv_callback, NULL);
        if (ret == NETLIB_ERROR)
            return ret;
    }


netlib_listen()建立CBaseSocket物件,並將回撥函式指標proxy_serv_callback儲存在CBaseSocket物件中。
int netlib_listen(
		const char*	server_ip, 
		uint16_t	port,
		callback_t	callback,
		void*		callback_data)
{
	CBaseSocket* pSocket = new CBaseSocket();
	if (!pSocket)
		return NETLIB_ERROR;

	int ret =  pSocket->Listen(server_ip, port, callback, callback_data);
	if (ret == NETLIB_ERROR)
		delete pSocket;
	return ret;
}


pSocket->Listen()是實際呼叫bind()和listen()函式建立偵聽的地方。

int CBaseSocket::Listen(const char* server_ip, uint16_t port, callback_t callback, void* callback_data)
{
	m_local_ip = server_ip;
	m_local_port = port;
	m_callback = callback;
	m_callback_data = callback_data;

	m_socket = socket(AF_INET, SOCK_STREAM, 0);
	if (m_socket == INVALID_SOCKET)
	{
		printf("socket failed, err_code=%d\n", _GetErrorCode());
		return NETLIB_ERROR;
	}

	_SetReuseAddr(m_socket);
	_SetNonblock(m_socket);

	sockaddr_in serv_addr;
	_SetAddr(server_ip, port, &serv_addr);
    int ret = ::bind(m_socket, (sockaddr*)&serv_addr, sizeof(serv_addr));
	if (ret == SOCKET_ERROR)
	{
		log("bind failed, err_code=%d", _GetErrorCode());
		closesocket(m_socket);
		return NETLIB_ERROR;
	}

	ret = listen(m_socket, 64);
	if (ret == SOCKET_ERROR)
	{
		log("listen failed, err_code=%d", _GetErrorCode());
		closesocket(m_socket);
		return NETLIB_ERROR;
	}

	m_state = SOCKET_STATE_LISTENING;

	log("CBaseSocket::Listen on %s:%d", server_ip, port);

	AddBaseSocket(this);
	CEventDispatch::Instance()->AddEvent(m_socket, SOCKET_READ | SOCKET_EXCEP);
	return NETLIB_OK;
}
這個函式有大量的細節需要注意:

1. socket被設定成非阻塞模式;

2. 將繫結的地址設定成reuse(具體原因,我在《伺服器程式設計心得》系列已經介紹過)

3. 將socket的狀態設定成SOCKET_STATE_LISTENING,這個狀態將偵聽的socket與普通客戶端連線的socket區別開來。

4.AddBaseSocket(this);將socket控制程式碼和對應的CBaseSocket放到一個全域性物件中管理起來。

typedef hash_map<net_handle_t, CBaseSocket*> SocketMap;
SocketMap	g_socket_map;

void AddBaseSocket(CBaseSocket* pSocket)
{
	g_socket_map.insert(make_pair((net_handle_t)pSocket->GetSocket(), pSocket));
}
之所以不用map而用hash_map是因為STL的map底層是用紅黑樹實現的,查詢時間複雜度是log(n),而hash_map底層是用hash表儲存的,查詢時間複雜度是O(1)。後面會介紹將在這個hash_map中查詢所有的socket。

5.目前只關注socket的讀和異常事件,偵聽socket可讀意味著有新連線到來,異常就意味著偵聽出錯。對於伺服器程式一般要關閉或重啟服務。


六、主執行緒進入迴圈,監聽新連線的到來以及出來新連線上的資料收發

netlib_eventloop(10)

10是超時時間,用於select()函式的呼叫:

void netlib_eventloop(uint32_t wait_timeout)
{
	CEventDispatch::Instance()->StartDispatch(wait_timeout);
}

void CEventDispatch::StartDispatch(uint32_t wait_timeout)
{
	fd_set read_set, write_set, excep_set;
	timeval timeout;
	timeout.tv_sec = 0;
	timeout.tv_usec = wait_timeout * 1000;	// 10 millisecond

    if(running)
        return;
    running = true;
    
    while (running)
	{
		_CheckTimer();
        _CheckLoop();

		if (!m_read_set.fd_count && !m_write_set.fd_count && !m_excep_set.fd_count)
		{
			Sleep(MIN_TIMER_DURATION);
			continue;
		}

		m_lock.lock();
		memcpy(&read_set, &m_read_set, sizeof(fd_set));
		memcpy(&write_set, &m_write_set, sizeof(fd_set));
		memcpy(&excep_set, &m_excep_set, sizeof(fd_set));
		m_lock.unlock();

		int nfds = select(0, &read_set, &write_set, &excep_set, &timeout);

		if (nfds == SOCKET_ERROR)
		{
			log("select failed, error code: %d", GetLastError());
			Sleep(MIN_TIMER_DURATION);
			continue;			// select again
		}

		if (nfds == 0)
		{
			continue;
		}

		for (u_int i = 0; i < read_set.fd_count; i++)
		{
			//log("select return read count=%d\n", read_set.fd_count);
			SOCKET fd = read_set.fd_array[i];
			CBaseSocket* pSocket = FindBaseSocket((net_handle_t)fd);
			if (pSocket)
			{
				pSocket->OnRead();
				pSocket->ReleaseRef();
			}
		}

		for (u_int i = 0; i < write_set.fd_count; i++)
		{
			//log("select return write count=%d\n", write_set.fd_count);
			SOCKET fd = write_set.fd_array[i];
			CBaseSocket* pSocket = FindBaseSocket((net_handle_t)fd);
			if (pSocket)
			{
				pSocket->OnWrite();
				pSocket->ReleaseRef();
			}
		}

		for (u_int i = 0; i < excep_set.fd_count; i++)
		{
			//log("select return exception count=%d\n", excep_set.fd_count);
			SOCKET fd = excep_set.fd_array[i];
			CBaseSocket* pSocket = FindBaseSocket((net_handle_t)fd);
			if (pSocket)
			{
				pSocket->OnClose();
				pSocket->ReleaseRef();
			}
		}

	}
}


這個函式是整個服務程式的動力和訊息泵。我把它簡化成如下偽碼來重點介紹一下:


while(退出條件)
{
	//1. 遍歷定時器佇列,檢測是否有定時器事件到期,有則執行定時器的回撥函式
	
	//2. 遍歷其他任務佇列,檢測是否有其他任務需要執行,有,執行之
	
	//3. 檢測socket集合,分離可讀、可寫和異常事件
	
	//4. 處理socket可讀事件
	
	//5. 處理socket可寫事件
	
	//6. 處理socket異常事件
}

我們先不說1、2兩點,當程式初始化後,socket集合中,也只有一個socket,就是上文中說的偵聽socket。當有新連線來的時候,該socket被檢測到可讀。執行

for (u_int i = 0; i < read_set.fd_count; i++)
{
	//log("select return read count=%d\n", read_set.fd_count);
	SOCKET fd = read_set.fd_array[i];
	CBaseSocket* pSocket = FindBaseSocket((net_handle_t)fd);
	if (pSocket)
	{
		pSocket->OnRead();
		pSocket->ReleaseRef();
	}
}


FindBaseSocket()就是在上文提到的socket集合map中通過控制程式碼查詢socket:

CBaseSocket* FindBaseSocket(net_handle_t fd)
{
	CBaseSocket* pSocket = NULL;
	SocketMap::iterator iter = g_socket_map.find(fd);
	if (iter != g_socket_map.end())
	{
		pSocket = iter->second;
		pSocket->AddRef();
	}

	return pSocket;
}

接著執行pSocket->OnRead():

void CBaseSocket::OnRead()
{
	if (m_state == SOCKET_STATE_LISTENING)
	{
		_AcceptNewSocket();
	}
	else
	{
		u_long avail = 0;
		if ( (ioctlsocket(m_socket, FIONREAD, &avail) == SOCKET_ERROR) || (avail == 0) )
		{
			m_callback(m_callback_data, NETLIB_MSG_CLOSE, (net_handle_t)m_socket, NULL);
		}
		else
		{
			m_callback(m_callback_data, NETLIB_MSG_READ, (net_handle_t)m_socket, NULL);
		}
	}
}

因為是偵聽socket,其狀態被設定成SOCKET_STATE_LISTENING(上文介紹了)。接著就接受新連線。

void CBaseSocket::_AcceptNewSocket()
{
	SOCKET fd = 0;
	sockaddr_in peer_addr;
	socklen_t addr_len = sizeof(sockaddr_in);
	char ip_str[64];
	while ( (fd = accept(m_socket, (sockaddr*)&peer_addr, &addr_len)) != INVALID_SOCKET )
	{
		CBaseSocket* pSocket = new CBaseSocket();
		uint32_t ip = ntohl(peer_addr.sin_addr.s_addr);
		uint16_t port = ntohs(peer_addr.sin_port);

		snprintf(ip_str, sizeof(ip_str), "%d.%d.%d.%d", ip >> 24, (ip >> 16) & 0xFF, (ip >> 8) & 0xFF, ip & 0xFF);

		log("AcceptNewSocket, socket=%d from %s:%d\n", fd, ip_str, port);

		pSocket->SetSocket(fd);
		pSocket->SetCallback(m_callback);
		pSocket->SetCallbackData(m_callback_data);
		pSocket->SetState(SOCKET_STATE_CONNECTED);
		pSocket->SetRemoteIP(ip_str);
		pSocket->SetRemotePort(port);

		_SetNoDelay(fd);
		_SetNonblock(fd);
		AddBaseSocket(pSocket);
		CEventDispatch::Instance()->AddEvent(fd, SOCKET_READ | SOCKET_EXCEP);
		m_callback(m_callback_data, NETLIB_MSG_CONNECT, (net_handle_t)fd, NULL);
	}
}

接收新連線,需要注意以下事項:

1. 產生一個新的socket和對應的CBaseSocket物件。

2. 該socket和對應的CBaseSocket物件和偵聽socket一樣也被加入全域性g_socket_map中進行管理。

3. 新socket同樣被設定成非阻塞的。

4. 禁用該socket的nagle演算法(_SetNoDelay(fd);)。

5. 關注該socket的讀和異常事件(CEventDispatch::Instance()->AddEvent(fd, SOCKET_READ | SOCKET_EXCEP);)。

6. 將socket的狀態設定成SOCKET_STATE_CONNECTED。

7. 呼叫偵聽socket的的回撥函式m_callback(m_callback_data, NETLIB_MSG_CONNECT, (net_handle_t)fd, NULL),並傳入訊息型別是NETLIB_MSG_CONNECT。

這個回撥函式在上面初始化偵聽函式設定的,指向函式proxy_serv_callback。呼叫程式碼如下:

void proxy_serv_callback(void* callback_data, uint8_t msg, uint32_t handle, void* pParam)
{
    if (msg == NETLIB_MSG_CONNECT)
    {
        CProxyConn* pConn = new CProxyConn();
        pConn->OnConnect(handle);
    }
    else
    {
        log("!!!error msg: %d", msg);
    }
}


接著呼叫CProxyConn的OnConnect()函式:

void CProxyConn::OnConnect(net_handle_t handle)
{
	m_handle = handle;

	g_proxy_conn_map.insert(make_pair(handle, this));

	netlib_option(handle, NETLIB_OPT_SET_CALLBACK, (void*)imconn_callback);
	netlib_option(handle, NETLIB_OPT_SET_CALLBACK_DATA, (void*)&g_proxy_conn_map);
	netlib_option(handle, NETLIB_OPT_GET_REMOTE_IP, (void*)&m_peer_ip);
	netlib_option(handle, NETLIB_OPT_GET_REMOTE_PORT, (void*)&m_peer_port);

	log("connect from %s:%d, handle=%d", m_peer_ip.c_str(), m_peer_port, m_handle);
}


注意!這裡,已經悄悄地將該新socket的回撥函式由proxy_serv_callback偷偷地換成了imconn_callback。同時,將該連線物件放入一個全域性map g_proxy_conn_map中:

typedef hash_map<net_handle_t, CImConn*> ConnMap_t;

static ConnMap_t g_proxy_conn_map;
同樣,該map的key是socket控制程式碼,value是連線物件基類的指標。


至此,對於偵聽socket,如果socket可讀,則接收新連線,並置換其預設OnRead的回撥函式為imconn_callback;而對於新socket,如果socket可讀,則會呼叫imconn_callback。

我們接著看新socket可讀的處理流程:

void CBaseSocket::OnRead()
{
	if (m_state == SOCKET_STATE_LISTENING)
	{
		_AcceptNewSocket();
	}
	else
	{
		u_long avail = 0;
		if ( (ioctlsocket(m_socket, FIONREAD, &avail) == SOCKET_ERROR) || (avail == 0) )
		{
			m_callback(m_callback_data, NETLIB_MSG_CLOSE, (net_handle_t)m_socket, NULL);
		}
		else
		{
			m_callback(m_callback_data, NETLIB_MSG_READ, (net_handle_t)m_socket, NULL);
		}
	}
}

上述OnRead函式會走else分支,先呼叫ioctlsocket獲得可讀的資料位元組數。如果出錯或者位元組數為0,則以訊息NETLIB_MSG_CLOSE呼叫回撥函式imconn_callback,

反之,以訊息NETLIB_MSG_READ呼叫回撥函式imconn_callback。

void imconn_callback(void* callback_data, uint8_t msg, uint32_t handle, void* pParam)
{
	NOTUSED_ARG(handle);
	NOTUSED_ARG(pParam);

	if (!callback_data)
		return;

	ConnMap_t* conn_map = (ConnMap_t*)callback_data;
	CImConn* pConn = FindImConn(conn_map, handle);
	if (!pConn)
		return;

	//log("msg=%d, handle=%d ", msg, handle);

	switch (msg)
	{
	case NETLIB_MSG_CONFIRM:
		pConn->OnConfirm();
		break;
	case NETLIB_MSG_READ:
		pConn->OnRead();
		break;
	case NETLIB_MSG_WRITE:
		pConn->OnWrite();
		break;
	case NETLIB_MSG_CLOSE:
		pConn->OnClose();
		break;
	default:
		log("!!!imconn_callback error msg: %d ", msg);
		break;
	}

	pConn->ReleaseRef();
}


出錯訊息NETLIB_MSG_CLOSE沒啥好看的,無非是關閉連線。我們來看NETLIB_MSG_READ訊息,會呼叫pConn->OnRead(),pConn是一個CImConn指標,但根據上文介紹我們知道,其實際是一個CImConn的子類CProxyConn物件:

class CProxyConn : public CImConn {

所以pConn->OnRead()實際會呼叫CProxyConn的OnRead():

void CProxyConn::OnRead()
{
	for (;;) {
		uint32_t free_buf_len = m_in_buf.GetAllocSize() - m_in_buf.GetWriteOffset();
		if (free_buf_len < READ_BUF_SIZE)
			m_in_buf.Extend(READ_BUF_SIZE);

		int ret = netlib_recv(m_handle, m_in_buf.GetBuffer() + m_in_buf.GetWriteOffset(), READ_BUF_SIZE);
		if (ret <= 0)
			break;

		m_recv_bytes += ret;
		m_in_buf.IncWriteOffset(ret);
		m_last_recv_tick = get_tick_count();
	}

	uint32_t pdu_len = 0;
    try {
        while ( CImPdu::IsPduAvailable(m_in_buf.GetBuffer(), m_in_buf.GetWriteOffset(), pdu_len) ) {
            HandlePduBuf(m_in_buf.GetBuffer(), pdu_len);
            m_in_buf.Read(NULL, pdu_len);
        }
    } catch (CPduException& ex) {
        log("!!!catch exception, err_code=%u, err_msg=%s, close the connection ",
            ex.GetErrorCode(), ex.GetErrorMsg());
        OnClose();
    }
	
}

CImConn實際是代表一個連線,即每一個連線都有這樣一個物件。具體被分化成它的各個子物件,如CProxyConn。每一個連線CImConn都存在一個讀緩衝區和寫緩衝區,讀緩衝區用於存放從網路上收取的資料,寫緩衝區用於存放即將發到網路中資料。CProxyConn::OnRead()先檢測該物件的讀緩衝區中還有多少可用空間,如果可用空間小於當前收到的位元組數目,則將該讀緩衝區的大小擴充套件到需要的大小READ_BUF_SIZE。接著收到的資料放入讀緩衝區中。並記錄下這次收取資料的時間到m_last_recv_tick變數中。接著開始解包,即呼叫CImPdu::IsPduAvailable()從讀取緩衝區中取出資料處理,先判斷現有資料是否大於一個包頭的大小,如果不大於,退出。如果大於一個包頭的大小,則接著根據包頭中的資訊得到整個包的大小:

bool CImPdu::IsPduAvailable(uchar_t* buf, uint32_t len, uint32_t& pdu_len)
{
	if (len < IM_PDU_HEADER_LEN)
		return false;

	pdu_len = CByteStream::ReadUint32(buf);
	if (pdu_len > len)
	{
		//log("pdu_len=%d, len=%d\n", pdu_len, len);
		return false;
	}
    
    if(0 == pdu_len)
    {
        throw CPduException(1, "pdu_len is 0");
    }

	return true;
}

得到包的大小就可以正式處理包了,呼叫HandlePduBuf(m_in_buf.GetBuffer(), pdu_len);

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());
    }
}

包的資料結構是CImPdu(Im 即Instant Message即時通訊軟體的意思,teamtalk本來就是一款即時通訊,pdu,Protocol Data Unit 協議資料單元,通俗的說就是一個包單位),該資料結構分為包頭和包體兩部分。類CImPdu的兩個成員變數:

CSimpleBuffer	m_buf;
PduHeader_t      m_pdu_header;

分別表示包頭和包體,包頭的定義PduHeader_t如下:

typedef struct {
    uint32_t 	length;		  // the whole pdu length
    uint16_t 	version;	  // pdu version number
    uint16_t	flag;		  // not used
    uint16_t	service_id;	  //
    uint16_t	command_id;	  //
    uint16_t	seq_num;     // 包序號
    uint16_t    reversed;    // 保留
} PduHeader_t;

通過包頭的command_id就知道該包是什麼資料了。接著根據對應的命令號呼叫在程式初始化階段繫結的包處理函式:

pdu_handler_t handler = s_handler_map->GetHandler(pPdu->GetCommandId());

執行處理函式不是直接呼叫該函式,而是包裝成一個任務放入前面介紹的任務佇列中:

du_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());
    }

前面介紹過,處理任務的執行緒可能有多個,那麼到底將任務加入到哪個工作執行緒呢?這裡採取的策略是隨機分配:

void CThreadPool::AddTask(CTask* pTask)
{
	/*
	 * select a random thread to push task
	 * we can also select a thread that has less task to do
	 * but that will scan the whole thread list and use thread lock to get each task size
	 */
	uint32_t thread_idx = random() % m_worker_size;
	m_worker_list[thread_idx].PushTask(pTask);
}

當然需要注意的是。如果資料包是心跳包的話,就直接不處理了。因為心跳包只是來保活通訊的,與具體業務無關:

if (pPdu->GetCommandId() == IM::BaseDefine::CID_OTHER_HEARTBEAT) {
        return;
    }

該包處理完成以後,將該包的資料從連線的讀緩衝區移除:

m_in_buf.Read(NULL, pdu_len);

接著繼續處理下一個包,因為收來的資料可能不夠一個包大小,也可能是多個包的大小,所以要放在一個迴圈裡面解包處理,直到讀緩衝區中無資料或資料不夠一個包的大小。

我們將這個流程抽象出來,這個流程也是現在所有網路通訊庫都要做的工作:

while(退出條件)
{
	//1. 檢測非偵聽socket可讀
	
	//2. 處理可讀事件
	
	//3. 檢測可讀取的位元組數,出錯就關閉,不出錯,將收取的位元組放入連線的讀緩衝區
	
	//迴圈做以下處理
	//4. 檢測可讀緩衝區資料大小是否大於等於一個包頭大小: 否,資料不夠一個包,跳出該迴圈;
	//	  是,從包頭中得到一個包體的大小,檢測讀緩衝區是否夠一個包頭+包體的大小;否,資料不夠一個包,跳出迴圈
	//	  是,解包,根據包命令號,處理該包資料,可以產生一個任務,丟入任務佇列。
	//	  從可讀緩衝區中移除剛才處理的包資料的位元組數目。
	//	  繼續第4步。
}

當加入任務後,任務佇列執行緒被喚醒,從任務佇列的頭部拿出該任務執行。這個上文介紹過了。


到此,本文還沒有完,因為上文只介紹了從客戶端收取資料,然後解包。並沒有介紹解完包,呼叫處理函式處理後如何應答客戶端。下面以一個登入資料包的應答來敘述這個應答流程。登入任務從任務佇列中取出來後,執行如下函式:

void CHandlerMap::Init()
{
    //DB_PROXY是名稱空間,不是類名
	// Login validate
	m_handler_map.insert(make_pair(uint32_t(CID_OTHER_VALIDATE_REQ), DB_PROXY::doLogin));

void doLogin(CImPdu* pPdu, uint32_t conn_uuid)
{
    
    CImPdu* pPduResp = new CImPdu;
    
    IM::Server::IMValidateReq msg;
    IM::Server::IMValidateRsp msgResp;
    if(msg.ParseFromArray(pPdu->GetBodyData(), pPdu->GetBodyLength()))
    {
        
        string strDomain = msg.user_name();
        string strPass = msg.password();
        
        msgResp.set_user_name(strDomain);
        msgResp.set_attach_data(msg.attach_data());
        
        do
        {
            CAutoLock cAutoLock(&g_cLimitLock);
            list<uint32_t>& lsErrorTime = g_hmLimits[strDomain];
            uint32_t tmNow = time(NULL);
            
            //清理超過30分鐘的錯誤時間點記錄
            /*
             清理放在這裡還是放在密碼錯誤後新增的時候呢?
             放在這裡,每次都要遍歷,會有一點點效能的損失。
             放在後面,可能會造成30分鐘之前有10次錯的,但是本次是對的就沒辦法再訪問了。
             */
            auto itTime=lsErrorTime.begin();
            for(; itTime!=lsErrorTime.end();++itTime)
            {
                if(tmNow - *itTime > 30*60)
                {
                    break;
                }
            }
            if(itTime != lsErrorTime.end())
            {
                lsErrorTime.erase(itTime, lsErrorTime.end());
            }
            
            // 判斷30分鐘內密碼錯誤次數是否大於10
            if(lsErrorTime.size() > 10)
            {
                itTime = lsErrorTime.begin();
                if(tmNow - *itTime <= 30*60)
                {
                    msgResp.set_result_code(6);
                    msgResp.set_result_string("使用者名稱/密碼錯誤次數太多");
                    pPduResp->SetPBMsg(&msgResp);
                    pPduResp->SetSeqNum(pPdu->GetSeqNum());
                    pPduResp->SetServiceId(IM::BaseDefine::SID_OTHER);
                    pPduResp->SetCommandId(IM::BaseDefine::CID_OTHER_VALIDATE_RSP);
                    CProxyConn::AddResponsePdu(conn_uuid, pPduResp);
                    return ;
                }
            }
        } while(false);
        
        log("%s request login.", strDomain.c_str());
        
        
        
        IM::BaseDefine::UserInfo cUser;
        
        if(g_loginStrategy.doLogin(strDomain, strPass, cUser))
        {
            IM::BaseDefine::UserInfo* pUser = msgResp.mutable_user_info();
            pUser->set_user_id(cUser.user_id());
            pUser->set_user_gender(cUser.user_gender());
            pUser->set_department_id(cUser.department_id());
            pUser->set_user_nick_name(cUser.user_nick_name());
            pUser->set_user_domain(cUser.user_domain());
            pUser->set_avatar_url(cUser.avatar_url());
            
            pUser->set_email(cUser.email());
            pUser->set_user_tel(cUser.user_tel());
            pUser->set_user_real_name(cUser.user_real_name());
            pUser->set_status(0);

            pUser->set_sign_info(cUser.sign_info());
           
            msgResp.set_result_code(0);
            msgResp.set_result_string("成功");
            
            //如果登陸成功,則清除錯誤嘗試限制
            CAutoLock cAutoLock(&g_cLimitLock);
            list<uint32_t>& lsErrorTime = g_hmLimits[strDomain];
            lsErrorTime.clear();
        }
        else
        {
            //密碼錯誤,記錄一次登陸失敗
            uint32_t tmCurrent = time(NULL);
            CAutoLock cAutoLock(&g_cLimitLock);
            list<uint32_t>& lsErrorTime = g_hmLimits[strDomain];
            lsErrorTime.push_front(tmCurrent);
            
            log("get result false");
            msgResp.set_result_code(1);
            msgResp.set_result_string("使用者名稱/密碼錯誤");
        }
    }
    else
    {
        msgResp.set_result_code(2);
        msgResp.set_result_string("服務端內部錯誤");
    }
    
    
    pPduResp->SetPBMsg(&msgResp);
    pPduResp->SetSeqNum(pPdu->GetSeqNum());
    pPduResp->SetServiceId(IM::BaseDefine::SID_OTHER);
    pPduResp->SetCommandId(IM::BaseDefine::CID_OTHER_VALIDATE_RSP);
    CProxyConn::AddResponsePdu(conn_uuid, pPduResp);
}

這段程式碼有點複雜,下面分析之:

首先,將登入請求包資料通過函式引數(第一個引數)傳入進來,其次是連線物件的id。前面已經介紹過了,每一個新的socket不僅對應一個CBaseSocket物件,同時也對應一個連線物件CImConn(可能會被具體化成對應的子類,如CProxyConn)。這些連線物件被放在另外一個全域性map g_proxy_conn_map裡面進行管理。

      通過包資料,我們能得到登入的使用者名稱和密碼等資訊。接著檢測30分鐘之內,嘗試登入的次數,如果30分鐘之內密碼錯誤次數超過10此。則不允許登入。組成一個提示“使用者名稱或密碼錯誤此時太多”的包:

msgResp.set_result_code(6);
msgResp.set_result_string("使用者名稱/密碼錯誤次數太多");
pPduResp->SetPBMsg(&msgResp);
pPduResp->SetSeqNum(pPdu->GetSeqNum());
pPduResp->SetServiceId(IM::BaseDefine::SID_OTHER);
pPduResp->SetCommandId(IM::BaseDefine::CID_OTHER_VALIDATE_RSP);
CProxyConn::AddResponsePdu(conn_uuid, pPduResp);


如果不存在這種情況,則接著呼叫g_loginStrategy.doLogin(strDomain, strPass, cUser)連線資料庫進行使用者名稱和密碼校驗:

bool CInterLoginStrategy::doLogin(const std::string &strName, const std::string &strPass, IM::BaseDefine::UserInfo& user)
{
    bool bRet = false;
    CDBManager* pDBManger = CDBManager::getInstance();
    CDBConn* pDBConn = pDBManger->GetDBConn("teamtalk_slave");
    if (pDBConn) {
        string strSql = "select * from IMUser where name='" + strName + "' and status=0";
        CResultSet* pResultSet = pDBConn->ExecuteQuery(strSql.c_str());
        if(pResultSet)
        {
            string strResult, strSalt;
            uint32_t nId, nGender, nDeptId, nStatus;
            string strNick, strAvatar, strEmail, strRealName, strTel, strDomain,strSignInfo;
            while (pResultSet->Next()) {
                nId = pResultSet->GetInt("id");
                strResult = pResultSet->GetString("password");
                strSalt = pResultSet->GetString("salt");
                
                strNick = pResultSet->GetString("nick");
                nGender = pResultSet->GetInt("sex");
                strRealName = pResultSet->GetString("name");
                strDomain = pResultSet->GetString("domain");
                strTel = pResultSet->GetString("phone");
                strEmail = pResultSet->GetString("email");
                strAvatar = pResultSet->GetString("avatar");
                nDeptId = pResultSet->GetInt("departId");
                nStatus = pResultSet->GetInt("status");
                strSignInfo = pResultSet->GetString("sign_info");

            }

            string strInPass = strPass + strSalt;
            char szMd5[33];
            CMd5::MD5_Calculate(strInPass.c_str(), strInPass.length(), szMd5);
            string strOutPass(szMd5);
            //去掉密碼校驗
            //if(strOutPass == strResult)
            {
                bRet = true;
                user.set_user_id(nId);
                user.set_user_nick_name(strNick);
                user.set_user_gender(nGender);
                user.set_user_real_name(strRealName);
                user.set_user_domain(strDomain);
                user.set_user_tel(strTel);
                user.set_email(strEmail);
                user.set_avatar_url(strAvatar);
                user.set_department_id(nDeptId);
                user.set_status(nStatus);
  	        user.set_sign_info(strSignInfo);

            }
            delete  pResultSet;
        }
        pDBManger->RelDBConn(pDBConn);
    }
    return bRet;
}


這裡也需要一個mysql連線,這個連線的分配方式在前面介紹過了。即在連線池中隨機拿一個,如果池中不存在,則新建一個。用完還回去:

pDBManger->RelDBConn(pDBConn);

接著通過使用者名稱從資料庫中取出該使用者資訊,如果記錄存在,則接著校驗密碼。密碼在資料庫裡面的儲存形式是:密碼+使用者的salt值 組成的字串的md5值。密碼如果也校驗正確,組裝成一個正確應答資料包(附上命令號、序列號、提示資訊等):

pPduResp->SetPBMsg(&msgResp);
    pPduResp->SetSeqNum(pPdu->GetSeqNum());
    pPduResp->SetServiceId(IM::BaseDefine::SID_OTHER);
    pPduResp->SetCommandId(IM::BaseDefine::CID_OTHER_VALIDATE_RSP);
    CProxyConn::AddResponsePdu(conn_uuid, pPduResp);

現在不管登入成功與否,登入應答包也已經組裝好了。接下來,就是如何發出去了?上述程式碼最後一行:

CProxyConn::AddResponsePdu(conn_uuid, pPduResp)

其呼叫如下:

void CProxyConn::AddResponsePdu(uint32_t conn_uuid, CImPdu* pPdu)
{
	ResponsePdu_t* pResp = new ResponsePdu_t;
	pResp->conn_uuid = conn_uuid;
	pResp->pPdu = pPdu;

	s_list_lock.lock();
	s_response_pdu_list.push_back(pResp);
	s_list_lock.unlock();
}
我們這裡並沒有直接將應答資料包通過連線物件CProxyConn發出去。因為直接發出去,未必能發出去。這會導致程式阻塞。(原因是:對方的tcp視窗太小,導致tcp視窗太小的常見原因是:對方無法收包或不及時收包,資料積壓在對方網路協議棧裡面)。我們這裡是將應答資料包放入連線物件的一個應答連結串列s_response_pdu_list中。這是一個stl list容器:

static list<ResponsePdu_t*>	s_response_pdu_list;	// 主執行緒傳送回覆訊息

那麼,包在這個連結串列中,何時被髮出去呢?我們在介紹該服務的訊息泵時介紹到如下流程:

while(退出條件)
{
	//1. 遍歷定時器佇列,檢測是否有定時器事件到期,有則執行定時器的回撥函式
	
	//2. 遍歷其他任務佇列,檢測是否有其他任務需要執行,有,執行之
	
	//3. 檢測socket集合,分離可讀、可寫和異常事件
	
	//4. 處理socket可讀事件
	
	//5. 處理socket可寫事件
	
	//6. 處理socket異常事件
}


注意第2步:遍歷其他任務佇列,檢測是否有其他任務需要執行,有,執行之。我們來看看這步具體做了什麼。

在main函式裡面初始化任務佇列執行緒時,同時也建立了一個其他任務:

init_proxy_conn(thread_num);

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);
}

注意程式碼netlib_add_loop(proxy_loop_callback, NULL);該行加入了一個其他任務到其他任務佇列。這樣在主執行緒的訊息泵中:2. 遍歷其他任務佇列,檢測是否有其他任務需要執行,有,執行之。

_CheckLoop();

void CEventDispatch::_CheckLoop()
{
    for (list<TimerItem*>::iterator it = m_loop_list.begin(); it != m_loop_list.end(); it++) {
        TimerItem* pItem = *it;
        pItem->callback(pItem->user_data, NETLIB_MSG_LOOP, 0, NULL);
    }
}

其他任務的回撥函式目前只有一個,就是上面設定的proxy_loop_callback:

void proxy_loop_callback(void* callback_data, uint8_t msg, uint32_t handle, void* pParam)
{
	CProxyConn::SendResponsePduList();
}

void CProxyConn::SendResponsePduList()
{
	s_list_lock.lock();
	while (!s_response_pdu_list.empty()) {
		ResponsePdu_t* pResp = s_response_pdu_list.front();
		s_response_pdu_list.pop_front();
		s_list_lock.unlock();

		CProxyConn* pConn = get_proxy_conn_by_uuid(pResp->conn_uuid);
		if (pConn) {
			if (pResp->pPdu) {
				pConn->SendPdu(pResp->pPdu);
			} else {
				log("close connection uuid=%d by parse pdu error\b", pResp->conn_uuid);
				pConn->Close();
			}
		}

		if (pResp->pPdu)
			delete pResp->pPdu;
		delete pResp;

		s_list_lock.lock();
	}

	s_list_lock.unlock();
}

看到這裡,你應該明白了。原來應答資料包在這裡從list中取出來。然後呼叫pConn->SendPdu(pResp->pPdu)“發出去”。這裡需要解釋兩個問題:第一個就是一般伺服器端會有多個連線物件,那麼如何定位某個應答資料包對應的連線物件呢?這裡就通過資料包本身的conn_uuid來確定:

CProxyConn* pConn = get_proxy_conn_by_uuid(pResp->conn_uuid);

CProxyConn* get_proxy_conn_by_uuid(uint32_t uuid)
{
	CProxyConn* pConn = NULL;
	UserMap_t::iterator it = g_uuid_conn_map.find(uuid);
	if (it != g_uuid_conn_map.end()) {
		pConn = (CProxyConn *)it->second;
	}

	return pConn;
}

全域性物件g_uuid_conn_map裡面存的是uuid與連線物件的對應關係。這個關係何時存入到這個全域性g_uuid_conn_map物件的呢?在CProxyConn的建構函式中:

CProxyConn::CProxyConn()
{
	m_uuid = ++CProxyConn::s_uuid_alloctor;
	if (m_uuid == 0) {
		m_uuid = ++CProxyConn::s_uuid_alloctor;
	}

	g_uuid_conn_map.insert(make_pair(m_uuid, this));
}

這個uuid的基數是一個CProxyConn的靜態變數:

static uint32_t	s_uuid_alloctor;

預設是0:

uint32_t CProxyConn::s_uuid_alloctor = 0;


以後每產生一個新連線物件CProxyConn,在此基礎上遞增,因為沒有用鎖保護,所以只能在一個執行緒裡面呼叫。而CProxyConn正好就是在主執行緒裡面產生的,前面介紹過了,再次貼一下程式碼吧:

void proxy_serv_callback(void* callback_data, uint8_t msg, uint32_t handle, void* pParam)
{
    if (msg == NETLIB_MSG_CONNECT)
    {
        CProxyConn* pConn = new CProxyConn();
        pConn->OnConnect(handle);
    }
    else
    {
        log("!!!error msg: %d", msg);
    }
}

這樣uuid和連線物件CProxyConn還有CBaseSocket這三者的關係就唯一繫結了。

接著說,通過uuid獲得對應資料包的連線物件後,呼叫其方法pConn->SendPdu(pResp->pPdu); “發出去”?但是,還是不行,因為這還沒有解決上文提出的該連線上對端的tcp視窗太小導致資料發不出的問題。所以pConn->SendPdu()方法中一定不是呼叫send函式直接傳送資料:

int SendPdu(CImPdu* pPdu) { return Send(pPdu->GetBuffer(), pPdu->GetLength()); }

實際上是呼叫其基類CImConn類的Send方法,傳送資料的時候,先記錄一下傳送資料的時間:m_last_send_tick = get_tick_count();

int CImConn::Send(void* data, int len)
{
    m_last_send_tick = get_tick_count();
    //	++g_send_pkt_cnt;

    if (m_busy)
    {
        m_out_buf.Write(data, len);
        return len;
    }

    int offset = 0;
    int remain = len;
    while (remain > 0) {
        int send_size = remain;
        if (send_size > NETLIB_MAX_SOCKET_BUF_SIZE) {
            send_size = NETLIB_MAX_SOCKET_BUF_SIZE;
        }

        int ret = netlib_send(m_handle, (char*)data + offset, send_size);
        if (ret <= 0) {
            ret = 0;
            break;
        }

        offset += ret;
        remain -= ret;
    }

    if (remain > 0)
    {
        m_out_buf.Write((char*)data + offset, remain);
        m_busy = true;
        log("send busy, remain=%d ", m_out_buf.GetWriteOffset());
    }
    else
    {
        OnWriteCompelete();
    }

    return len;
}

注意這段程式碼,也是特別的講究:

先試著呼叫底層send方法去傳送,能發多少是多少,剩下發不完的,寫入該連線的傳送緩衝區中,並將忙碌標誌m_busy置位(設定為ture)。反之,如果資料一次性傳送完成,則呼叫資料傳送完成函式OnWriteComplete(),這個函式目前為空,即不做任何事情。

int ret = netlib_send(m_handle, (char*)data + offset , send_size);


int netlib_send(net_handle_t handle, void* buf, int len)
{
	CBaseSocket* pSocket = FindBaseSocket(handle);
	if (!pSocket)
	{
		return NETLIB_ERROR;
	}
	int ret = pSocket->Send(buf, len);
	pSocket->ReleaseRef();
	return ret;
}


上面的程式碼通過socket控制程式碼找到具體的CBaseSocket物件。接著呼叫CBaseSocket::Send()方法:


int CBaseSocket::Send(void* buf, int len)
{
	if (m_state != SOCKET_STATE_CONNECTED)
		return NETLIB_ERROR;

	int ret = send(m_socket, (char*)buf, len, 0);
	if (ret == SOCKET_ERROR)
	{
		int err_code = _GetErrorCode();
		if (_IsBlock(err_code))
		{
#if ((defined _WIN32) || (defined __APPLE__))
			CEventDispatch::Instance()->AddEvent(m_socket, SOCKET_WRITE);
#endif
			ret = 0;
			//log("socket send block fd=%d", m_socket);
		}
		else
		{
			log("!!!send failed, error code: %d", err_code);
		}
	}

	return ret;
}

該方法傳送指定長度的資料,因為socket在建立的時候被設定成非阻塞的(上文介紹過)。所以,如果傳送不了,底層send函式會立刻返回,並返回錯誤碼EINPROGRESS(EWOULDBLOCK),表明對端tcp視窗太小,當前已經無法發出去:

bool CBaseSocket::_IsBlock(int error_code)
{
#ifdef _WIN32
	return ( (error_code == WSAEINPROGRESS) || (error_code == WSAEWOULDBLOCK) );
#else
	return ( (error_code == EINPROGRESS) || (error_code == EWOULDBLOCK) );
#endif
}

這個時候,我們再設定關注該socket的可寫事件。這樣,下次對端tcp視窗大小增大時,本端的socket可寫時,我們就能接著傳送資料了。會在服務的訊息泵中檢測可寫事件,接著呼叫CBaseSocket::OnWrite(), 該函式首先移除該socket的可寫事件(這裡為啥只有win32平臺和mac機器移除可寫事件,linux平臺不需要嗎?個人覺得是程式作者的疏忽)。
void CBaseSocket::OnWrite()
{
#if ((defined _WIN32) || (defined __APPLE__))
	CEventDispatch::Instance()->RemoveEvent(m_socket, SOCKET_WRITE);
#endif

	if (m_state == SOCKET_STATE_CONNECTING)
	{
		int error = 0;
		socklen_t len = sizeof(error);
#ifdef _WIN32

		getsockopt(m_socket, SOL_SOCKET, SO_ERROR, (char*)&error, &len);
#else
		getsockopt(m_socket, SOL_SOCKET, SO_ERROR, (void*)&error, &len);
#endif
		if (error) {
			m_callback(m_callback_data, NETLIB_MSG_CLOSE, (net_handle_t)m_socket, NULL);
		} else {
			m_state = SOCKET_STATE_CONNECTED;
			m_callback(m_callback_data, NETLIB_MSG_CONFIRM, (net_handle_t)m_socket, NULL);
		}
	}
	else
	{
		m_callback(m_callback_data, NETLIB_MSG_WRITE, (net_handle_t)m_socket, NULL);
	}
}

走else分支,呼叫設定的回撥函式imconn_callback:

void imconn_callback(void* callback_data, uint8_t msg, uint32_t handle, void* pParam)
{
	NOTUSED_ARG(handle);
	NOTUSED_ARG(pParam);

	if (!callback_data)
		return;

	ConnMap_t* conn_map = (ConnMap_t*)callback_data;
	CImConn* pConn = FindImConn(conn_map, handle);
	if (!pConn)
		return;

	//log("msg=%d, handle=%d ", msg, handle);

	switch (msg)
	{
	case NETLIB_MSG_CONFIRM:
		pConn->OnConfirm();
		break;
	case NETLIB_MSG_READ:
		pConn->OnRead();
		break;
	case NETLIB_MSG_WRITE:
		pConn->OnWrite();
		break;
	case NETLIB_MSG_CLOSE:
		pConn->OnClose();
		break;
	default:
		log("!!!imconn_callback error msg: %d ", msg);
		break;
	}

	pConn->ReleaseRef();
}

因為這次傳入的訊息是NETLIB_MSG_WRITE,所以走pConn->OnWrite分支,接著由於多型呼叫CImConn的子類CProxyConn的OnWrite()函式,但由於子類CProxyConn並沒有改寫OnWrite()方法,所以呼叫CImConn的OnWrite():

void CImConn::OnWrite()
{
	if (!m_busy)
		return;

	while (m_out_buf.GetWriteOffset() > 0) {
		int send_size = m_out_buf.GetWriteOffset();
		if (send_size > NETLIB_MAX_SOCKET_BUF_SIZE) {
			send_size = NETLIB_MAX_SOCKET_BUF_SIZE;
		}

		int ret = netlib_send(m_handle, m_out_buf.GetBuffer(), send_size);
		if (ret <= 0) {
			ret = 0;
			break;
		}

		m_out_buf.Read(NULL, ret);
	}

	if (m_out_buf.GetWriteOffset() == 0) {
		m_busy = false;
	}

	log("onWrite, remain=%d ", m_out_buf.GetWriteOffset());
}

接著繼續從寫緩衝區取出資料繼續傳送,如果還是隻能傳送出去,繼續監聽該socket可寫事件,每次傳送出去多少,就從寫緩衝區中移除該部分位元組。如果全部傳送完了。將忙碌標誌m_busy清零(false)。

至此,應答資料包的流程也介紹完了。我們來總結下該流程:

//1. 主訊息泵檢測到有其他任務需要做,做之。

//2. 該任務是從全域性的連結串列中取出應答包資料,找到對應的連線物件,然後嘗試直接發出去;

//3. 如果發不出,則將該資料存入該連線的傳送緩衝區(寫緩衝區),並監聽該連線的socket可寫事件。

//4. 下次該socket觸發可寫事件時,接著傳送該連線的寫緩衝區中剩餘的資料。如此迴圈直到所有資料都傳送成功。

//5. 取消監聽該socket可寫事件,以避免無資料的情況下觸發寫事件(該事件大多數情況下很頻繁)

上面的流程從第2步到第5步也是主流網路庫的發資料的邏輯。總而言之,就是說,先試著傳送資料,如果發不出去,存起來,監聽可寫事件,下次觸發可寫事件後接著發。一直到資料全部發出去後,移除監聽可寫事件。通常只要可寫事件是不斷會觸發的,所以預設不監聽可寫事件,只有資料發不出的時候才會監聽可寫事件。這個原則,千萬要記住。


最後一個問題,是關於心跳包的,即db_proxy_server是如何傳送心跳包的:

程式初始化的時候,註冊一個定時器函式:

init_proxy_conn(thread_num);

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);
}

最後一行:return netlib_register_timer(proxy_timer_callback, NULL, 1000);

然後在訊息泵裡面檢測定時器:

_CheckTimer();

void CEventDispatch::_CheckTimer()
{
	uint64_t curr_tick = get_tick_count();
	list<TimerItem*>::iterator it;

	for (it = m_timer_list.begin(); it != m_timer_list.end(); )
	{
		TimerItem* pItem = *it;
		it++;		// iterator maybe deleted in the callback, so we should increment it before callback
		if (curr_tick >= pItem->next_tick)
		{
			pItem->next_tick += pItem->interval;
			pItem->callback(pItem->user_data, NETLIB_MSG_TIMER, 0, NULL);
		}
	}
}

uint64_t get_tick_count()
{
#ifdef _WIN32
	LARGE_INTEGER liCounter; 
	LARGE_INTEGER liCurrent;

	if (!QueryPerformanceFrequency(&liCounter))
		return GetTickCount();

	QueryPerformanceCounter(&liCurrent);
	return (uint64_t)(liCurrent.QuadPart * 1000 / liCounter.QuadPart);
#else
	struct timeval tval;
	uint64_t ret_tick;

	gettimeofday(&tval, NULL);

	ret_tick = tval.tv_sec * 1000L + tval.tv_usec / 1000L;
	return ret_tick;
#endif
}


由上面的函式可以看出來定時器的單位是毫秒,當定時器時間到了後,呼叫回撥函式proxy_timer_callback:

void proxy_timer_callback(void* callback_data, uint8_t msg, uint32_t handle, void* pParam)
{
	uint64_t cur_time = get_tick_count();
	for (ConnMap_t::iterator it = g_proxy_conn_map.begin(); it != g_proxy_conn_map.end(); ) {
		ConnMap_t::iterator it_old = it;
		it++;

		CProxyConn* pConn = (CProxyConn*)it_old->second;
		pConn->OnTimer(cur_time);
	}
}

void CProxyConn::OnTimer(uint64_t curr_tick)
{
	if (curr_tick > m_last_send_tick + SERVER_HEARTBEAT_INTERVAL) {
        
        CImPdu cPdu;
        IM::Other::IMHeartBeat msg;
        cPdu.SetPBMsg(&msg);
        cPdu.SetServiceId(IM::BaseDefine::SID_OTHER);
        cPdu.SetCommandId(IM::BaseDefine::CID_OTHER_HEARTBEAT);
		SendPdu(&cPdu);
	}

	if (curr_tick > m_last_recv_tick + SERVER_TIMEOUT) {
		log("proxy connection timeout %s:%d", m_peer_ip.c_str(), m_peer_port);
		Close();
	}
}


m_last_send_tick是上一次傳送資料的時間,我們上文中介紹過,如果當前時間距上一次傳送資料的時間已經超過了指定的時間間隔,則傳送一個心跳包(這裡的時間間隔是5000毫秒)。


m_last_recv_tick是上一次收取資料的時間,我們上文也介紹過,如果當前時間舉例上一次接收時間已經超過了指定的時間間隔(相當於一段時間內,對端沒有給當前服務傳送任何資料),這個時候就關閉該連線(這裡設定的時間間隔是30000毫秒,也就是30秒)。


這種心跳包機制特別值得推崇,也是常見的心跳包策略。


至此,db_proxy_server的框架和原理也就介紹完了。剩下的就是一些業務邏輯了。如果你感興趣,可以自己檢視對應的命令號繫結的處理函式的處理流程。


文中如果有說錯的地方,歡迎提出留言指正。QQ:906106643

zhangyl 2017.05.17 


如果您對伺服器開發技術感興趣,可以關注我的微信公眾號『高效能伺服器開發』,這個微信公眾號致力於將伺服器開發技術通俗化、平民化,讓伺服器開發技術不再神祕,其中整理了將伺服器開發需要掌握的一些基礎技術歸納整理,既有基礎理論部分,也有實戰部分。









相關文章