前言
用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个。