teamtalk的conn框架简介及netlib线程安全问题
2019獨角獸企業(yè)重金招聘Python工程師標準>>>
最近把teamtalk的conn_map改成了智能指針,但改了總要多方面試試有沒有問題,總不能編譯通過,能正常啟動就萬事大吉了。所以就寫了一個shell client客戶端來進行功能的測試。
tt的官方上一次發(fā)布版本里有一個test目錄,里面寫了一個簡易的測試客戶端。不過這個test根本不可用,因為不是代碼寫的有錯誤,就是功能缺失,所以我只好親自動手重做了。
在做的過程中總是發(fā)現(xiàn)client發(fā)起的connect偶爾會有連接不上,這個偶爾的概率非常低,但既然發(fā)生了,那肯定是有問題的。于是就查啊查啊。。。
test的測試客戶端相當于是一個命令行shell的客戶端,也就是沒有圖形界面,你的功能通過在終端上輸入命令來完成。目前我僅實現(xiàn)了注冊和登陸。未來打算把聊天等各種功能也做了,這樣就差不多相當于實現(xiàn)了一個命令行式的客戶端。有人也許會問,TT有windows,mac,ios,android全平臺客戶端,做個命令行的客戶端有什么用?當然有用了,測試功能方便啊,你不用考慮折騰界面就能把各種功能給測了。未來添加功能也方便寫測試,比如我現(xiàn)在新增一個注冊功能,在這個命令行上面輸入reg xx oo,那么一個用戶名叫xx的用戶便以密碼為oo注冊進了數(shù)據(jù)庫。對命令的解析可比做界面的事件響應函數(shù)方便多了。
好了,現(xiàn)在問題來了,shell命令的輸入是需要一個死循環(huán)來反復等待用戶輸入的,而tt的異步網(wǎng)絡框架又需要另一個死循環(huán),如果兩個死循環(huán)放在同一線程里顯然不行,所以就把接受用戶輸入的死循環(huán)放到了另一個線程里面。那么當用戶輸入reg xx oo時,將這條命令解析出用戶名和密碼,并開始啟動注冊流程,這一切都是在另一個線程里做的。
注冊流程是怎么樣的呢?這里先講一講TT的conn框架。
TT的底層異步網(wǎng)絡庫是將socket和epoll封裝成一個netlib庫,你要做的任何有關異步網(wǎng)絡的操作都是通過調用netlib來實現(xiàn)的。但netlib只是一個原始的對tcp報文發(fā)送接收的異步庫,你要做即時通訊,還需要在此基礎上實現(xiàn)一套通訊協(xié)議,并且封裝一組接口來完成對這些協(xié)議的操作。
于是TT就定義了一個叫
CImConn的類,這個類定義在imconn.h里面。
為了方便大家閱讀,這里摘入部分代碼
class CImConn : public CRefObject { public:CImConn();virtual ~CImConn();int Send(void* data, int len);virtual void OnRead();virtual void OnWrite();bool IsBusy() { return m_busy; }int SendPdu(CImPdu* pPdu) { return Send(pPdu->GetBuffer(), pPdu->GetLength()); }virtual void OnConnect(net_handle_t handle) { m_handle = handle; }virtual void OnConfirm(){}virtual void OnClose(){}virtual void OnTimer(uint64_t){}virtual void OnWriteCompelete(){}virtual void HandlePdu(CImPdu*){}
這里為了方便你理解,做個類比,如果你做過android開發(fā),想一下每次你寫一個應用的最常用的流程是什么樣的?定義一個類繼承Activity,然后override里面的onCreate等xx方法,是不是很相似?當然如果你沒有安卓開發(fā)經(jīng)驗,類比一下ios吧,ios也是這樣的,如果ios也沒做過,那也沒關系,繼續(xù)往下看。
這里CImConn其實就是留給你繼承的,當你繼承后,請實現(xiàn)里面對應的成員函數(shù)。
如果你做的是服務端,那么需要實現(xiàn)OnConnect來響應用戶的接入,如果是客戶端,就需要OnConfirm來定義連接上服務器后的操作。其他幾個接口服務端和客戶端是通用的。
所以,看完這里你就會理解msg_server目錄下為什么有DBServConn,FileServConn, LoginServConn, RouteServConn, PushServConn以及MsgConn。
前面幾個都是消息服務器主動向其他幾個服務器發(fā)起的客戶端連接,最后一個是消息服務器自己的服務端Conn,用來等待用戶接入,所以需要實現(xiàn)OnConnect函數(shù)。
而login_server里面的HttpConn和LoginConn含義也顯而易見了,一個是用來響應http請求的,另一個是響應消息服務器login信息登記請求的。其他幾個服務器里的conn也以此類推。
之前對TT感到很凌亂的朋友是不是突然感覺自己頓悟了?感謝我吧。
另外一個疑問,TT的imconn框架是如何把這個CImConn和netlib連接起來的?
這里以DBServConn為例做一個解釋,看代碼
void CDBServConn::Connect(const char* server_ip, uint16_t server_port, uint32_t serv_idx) {log("Connecting to DB Storage Server %s:%d ", server_ip, server_port);m_serv_idx = serv_idx;m_handle = netlib_connect(server_ip, server_port, imconn_callback, (void*)&g_db_server_conn_map);if (m_handle != NETLIB_INVALID_HANDLE) {g_db_server_conn_map.insert(make_pair(m_handle, this));} }
這兩個參數(shù)就是連接imconn和netlib的關鍵。g_db_server_conn_map是定義在CDBServConn里的一個static全局map映射表,用來保存什么呢?下面一句
g_db_server_conn_map.insert(make_pair(m_handle, this)) 很明顯,這個映射表保存了每次連接的socket句柄(m_handle)和imconn對象(this)的映射關系。
當TT底層的事件分發(fā)器產生事件后,便會調用imconn_callback,里面有一個FindImConn會反查到對應的Conn,然后再調用Conn對象的OnConfirm等函數(shù),這些函數(shù)就是你之前繼承CImConn自己實現(xiàn)的。運行時多態(tài)有木有?是不是覺得TT的框架做的還挺不錯的。conn對象的OnRead其實是最重要的一個函數(shù),因為你的業(yè)務代碼都將在這里面自行實現(xiàn)。
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); //這里將會通過socket句柄反查到對于的imconnif (!pConn)return;//log("msg=%d, handle=%d ", msg, handle);switch (msg){case NETLIB_MSG_CONFIRM:pConn->OnConfirm(); //connect連接成功后會調此pConn的OnConfirm()函數(shù)break;case NETLIB_MSG_READ:pConn->OnRead(); //業(yè)務代碼會在這里面執(zhí)行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(); }看看OnRead代碼,里面有一個HandlePdu
void CImConn::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();}CImPdu* pPdu = NULL;try{while ( ( pPdu = CImPdu::ReadPdu(m_in_buf.GetBuffer(), m_in_buf.GetWriteOffset()) ) ){uint32_t pdu_len = pPdu->GetLength();HandlePdu(pPdu); //這里面將會完成各種業(yè)務代碼m_in_buf.Read(NULL, pdu_len);delete pPdu;pPdu = NULL; // ++g_recv_pkt_cnt;}} catch (CPduException& ex) {log("!!!catch exception, sid=%u, cid=%u, err_code=%u, err_msg=%s, close the connection ",ex.GetServiceId(), ex.GetCommandId(), ex.GetErrorCode(), ex.GetErrorMsg());if (pPdu) {delete pPdu;pPdu = NULL;}OnClose();} }
TT的conn框架簡介就到此為止了,其實還有很多細節(jié)需要你自己去摳代碼,慢慢來。
現(xiàn)在回到一開始說的在另一個線程里發(fā)起注冊流程,你應該會很清楚整個過程是怎么做的了,其實就是繼承CImConn,然后在里面發(fā)起連接和接受連接處理。這里摘一段我代碼
net_handle_t CClientConn::Connect(const char* ip, uint16_t port, uint32_t idx) {m_handle = netlib_connect(ip, port, imconn_callback_sp, (void*)&s_client_conn_map);log("connect handle %d", m_handle);if (m_handle != NETLIB_INVALID_HANDLE) {log("in invalid %d", m_handle);s_client_conn_map.insert(make_pair(m_handle, shared_from_this()));//這里!!!}return m_handle; }
這個操作是在子線程里進行的,所以netlib_connect會把imconn_callback_sp加入到底層事件分發(fā)器里進行監(jiān)聽,而事件分發(fā)器是在主線程里運行的一個循環(huán),這個循環(huán)會在socket文件句柄發(fā)生讀寫事件后對你加入的函數(shù)進行回調。所以netlib_connect會里面把imconn_callback加入主線程的監(jiān)聽器,主線程一旦監(jiān)聽到事件發(fā)生就會立刻調用此函數(shù),而此函數(shù)里的
CImConn* pConn = FindImConn(conn_map, handle);conn_map是在netlib_connect后insert的,所以就有可能出現(xiàn)FindImConn時,conn_map里面還沒有來得及insert這對關系,也就造成了偶爾會發(fā)生connect后沒有繼續(xù)調用后續(xù)的OnConfirm函數(shù),而你跑到服務端看,connect確實成功的奇怪現(xiàn)象。多線程真要命啊。。。
那么如何解決這個問題呢?這個不是本文的要講的,各位有興趣請自行考慮解決的方法,這里友情提示,加鎖是沒有用的。
轉載于:https://my.oschina.net/u/877397/blog/486617
總結
以上是生活随笔為你收集整理的teamtalk的conn框架简介及netlib线程安全问题的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 开源中国 OsChina Android
- 下一篇: openstack 安装