前言
用tars的客戶端與tars服務端通信非常簡單,因爲框架層已經幫我們隱藏了尋址,協議封裝,通信等細節,我們只需創建一個本地Servant代理,發起一個調用就可以。以官網的HelloServer爲例:
//生成客戶端代理
HelloPrx pPrx = Application::getCommunicator()->stringToProxy<HelloPrx>("Test.HelloServer.HelloObj");
//發起遠程調用
string s = "hello word";
string r;
int ret = pPrx->testHello(s, r);
作爲tars客戶端的第一篇文章,會分兩部分來初步瞭解tars客戶端的實現:
- 先從頂層瞭解一下客戶端的線程模型和客戶端的各個類的職責
- 在熟悉了tars客戶端的一些概念後,再結合代碼看一下客戶端發起rpc的過程
一.客戶端的線程和各個代理概念
1.客戶端線程
tars客戶端rpc在一次調用中主要會有三個不同的線程參與:
- 上圖省略了很多細節,但是已經能夠說明客戶端的一次rpc調用請求的路線:主調線程發起調用,負責把請求序列化,封裝成一個ReqMessage,push到消息隊列中;網絡線程負責處理所有的網絡細節,包括鏈接的建立,請求發送,負載,容災等,在收到響應後,再根據rpc調用方式決定是把響應返回給主調線程還是交由異步處理線程處理
- 網絡線程個數可配置,默認是1;每個網絡線程有自己的一組異步處理線程,個數也可配,默認是3個
2.客戶端主要類
- Communicator:通信器,負責與服務端的通信。
- ServantProxy:服務端Servant的本地代理,通過該類可以訪問服務端的rpc接口。需要注意的是,這裏的Servant是指tars尋址單位的Servant(即App.Server.Servant)
- CommunicatorEpoll:基於epoll的客戶端網絡線程類
- ObjectProxy:Servant在每個客戶端線程的代理
- EndPointManager:管理所有的AdapterProxy節點
- AdapterProxy:服務端每個Adapter(對應着一個ip:port)的本地代理
- Transceiver:負責具體的socket收發
類圖如下:
每個ServantProxy可以有多個ObjectProxy,每個ObjectProxy又可以有多個AdapterProxy,它們的關係如下:
- 黃色部分是ServantA在通信器中的各個代理;灰色部分是ServantB在通信器中的各個代理
- 每個Servant在一個tars進程的通信器中只會有一個代理ServantProxy;Servant和ServantProxy是1:1的關係
- ObjectProxy的個數和網絡線程的個數有關,每個ServantProxy在每個網絡線程中都有一個與之對應的ObjectProxy,例如這裏的網絡線程1和網絡線程2都有1個ObjectProxyA;ServantProxy和ObjectProxy是1:多的關係
- 每個ObjectProxy的EndpointManager裏面又會有該Servant部署過的所有Adapter的代理,例如ServantA部署了ip1和ip2這2個節點,那麼ObjectProxyA裏面就會有AdapterPrxA_ip1和AdapterPrxA_ip2;ObjectProxy和AdapterProxy也是1:多的關係
- 每個AdapterProxy都有一個屬於自己的Transceiver,負責具體的網絡收發工作
在與服務端的通信過程中,這些類都有自己負責的功能。下圖的列子,服務端1個Servant部署兩個節點(ip1,ip2),客戶端通信器配了2個網絡線程:
- ServantProxy負責發起rpc請求,序列化請求並準備好請求包,把請求包推到消息隊列中
- 每個網絡線程中都有一個ObjectProxy與每個ServantProxy對應,負責管理對應的Servant的協議解析器等
- ObjectProxy的EndPointManager根據負載規則和容災規則選出合適的AdaptrProxy,把請求發給服務端
- AdapterProxy的Transceiver負責和服務端建立好TCP鏈接,進行socket收發
- 可以看到,在只有一個ServantProxy,兩個網絡線程,且每個Servant有兩個活躍的Adapter的通信器中,總共會有4個TCP長鏈接與服務端保持鏈接狀態
二.客戶端一次rpc調用過程
1.調用流程
客戶端發起rpc調用的過程,有2條主線,以同步調用方式爲例子:
- 發起調用的線程,在發起請求後阻塞在條件變量上,直到網絡線程通知它解除阻塞;
- 網絡線程負責具有的請求收發工作
整個過程如下圖:
- 每個步驟都寫上了對應的函數,可以根據這些步驟在對應的代碼裏看實現細節
- 網絡線程的io模型和主要邏輯和我們之前講到的服務端網絡線程的很像tars服務端(二):網絡io模型和線程模型,都是基於epoll的io多路複用 + 非阻塞socket;另外,客戶端的網絡線程將epoll的io事件分爲兩類:(1)E_C_NOTIFY:主調線程通知網絡線程(2)E_C_NET:socket讀寫事件
- 整個rpc調用過程中還有很多細節沒體現在上圖:主調線程和網絡線程的交互細節,具體的收發包工作,Adapter節點管理,容載,負載均衡等,會在之後的文章中單獨講到
2.網絡線程的生成和啓動
在發起rpc調用之前,需要生成客戶端代理:HelloPrx pPrx = Application::getCommunicator()->stringToProxy<HelloPrx>
stringToProxy的代碼如下:
template<class T> T stringToProxy(const string& objectName,const string& setName="")
{
T prx = NULL;
stringToProxy<T>(objectName, prx,setName);
return prx;
}
/**
* 生成代理
* @param T
* @param objectName
* @param setName 指定set調用的setid
* @param proxy
*/
template<class T> void stringToProxy(const string& objectName, T& proxy,const string& setName="")
{
ServantProxy * pServantProxy = getServantProxy(objectName,setName);
proxy = (typename T::element_type*)(pServantProxy);
}
stringToProxy最終調用的是通信器的getServantProxy
ServantProxy * Communicator::getServantProxy(const string& objectName,const string& setName)
{
Communicator::initialize();
return _servantProxyFactory->getServantProxy(objectName,setName);
}
如果是第一次調用,則會在Communicator::initialize中創建並啓動網絡線程CommunicatorEpoll:
void Communicator::initialize()
{
TC_LockT<TC_ThreadRecMutex> lock(*this);
if (_initialized)
return;
_initialized = true;
_servantProxyFactory = new ServantProxyFactory(this);
//客戶端網絡線程
_clientThreadNum = TC_Common::strto<size_t>(getProperty("netthread","1"));
if(0 == _clientThreadNum)
{
_clientThreadNum = 1;
}
else if(MAX_CLIENT_THREAD_NUM < _clientThreadNum)
{
_clientThreadNum = MAX_CLIENT_THREAD_NUM;
}
//stat總是有對象, 保證getStat返回的對象總是有效
_statReport = new StatReport(_clientThreadNum);
for(size_t i = 0; i < _clientThreadNum; ++i)
{
_communicatorEpoll[i] = new CommunicatorEpoll(this, i);
_communicatorEpoll[i]->start();
}
.................
}
可以看到網絡線程的個數可以在客戶端的netthread中配置,默認是1個。至此,網絡線程啓動。
然後在網絡線程類CommunicatorEpoll的構造函數中還創建並啓動了異步處理線程:
//異步線程數
_asyncThreadNum = TC_Common::strto<size_t>(pCommunicator->getProperty("asyncthread", "3"));
if(_asyncThreadNum == 0)
{
_asyncThreadNum = 3;
}
if(_asyncThreadNum > MAX_CLIENT_ASYNCTHREAD_NUM)
{
_asyncThreadNum = MAX_CLIENT_ASYNCTHREAD_NUM;
}
........
//創建異步線程
for(size_t i = 0; i < _asyncThreadNum; ++i)
{
_asyncThread[i] = new AsyncProcThread(iAsyncQueueCap);
_asyncThread[i]->start();
}
異步線程個數在客戶端的asyncthread中配置。而且是每個網絡線程分別有一組異步處理線程。所以,如果一個通信器被配置成2個網絡線程,3個異步線程,那麼總的異步線程數爲3*2=6個。