本部分 前接《服务端各种关键类简洁》。
初始化流程:
在Application::main中
给_epollServer绑定完Adapter和setHandle之后,会执行,start全部Handle线程;给每个网络线程单独 createEpoll()..
在createEpoll中会:
1、创建 TC_BufferPool 网络线程buffer链表内存池。
此内存池 块的大小范围配在
/tars/application/server<poolminblocksize> ->/tars/application/server<poolmaxblocksize>之间,代码默认分别是1k->8M。
另外设置TC_BufferPool 占用的内存上限 /tars/application/server<poolmaxbytes>,代码默认为64M
2、创建TC_Epoller _epoller。最大链接数设置成 10240。并添加以下socket的实践监听:
_shutdown 所用socket; _notify所用socket;Adapter绑定的 _listeners所用socket;
3、设置管理的连接链表_list 的 最大链接数。在tcp情况下为 _listeners的Adapter支持的最大链接数总和。数值不大于 1<<22。。在这里面执行,预分配空间等操作,不细述
网络线程loop流程 NetThread::run()部分流程解析
这个线程,是通过TC_Epoller对象的wait来实现频率控制的。wait超时时间是2s.
1、检查_list连接链表中超时.这个检查频率至少要间隔1s。将超时链接关闭,并从_list中删除。如果是tcp链接,还要构建一个tagRecvData,isClosed=true 通知业务该链接关闭。其中,链接超时时间不小于2s。来源是在TC_Endpoint中的超时字段,在此默认是3s。但实际用的,一般是模板配置文件中Adapter部分配的 "endpoint=tcp -h localip.tars.com -p 18193 -t 60000"..此处 -t部分的参数值。。比如此处是60000ms.也就是1分钟。
如果空链接检测机制开关打开,关闭超时的空链接。空连接超时时间代码默认是2s,实际用的是/tars/application/server<emptyconntimeout>中配置的,配置默认是3s.此配置经常不配。
2、循环epoll事件。处理下面请求:
ET_LISTEN ->NetThread::accept()
ET_NOTIFY ->NetThread::processPipe()
ET_NET ->NetThread::processNet().
其中 accept()流程比较简单。accept监听的Adapter对应_listeners收到的connect请求,建立对应的链接,设置好参数之后,添加到_list中,并在_epoller中添加对应socket的EPOLLIN | EPOLLOUT监听。
processPipe的流程也比较简单,从_sbuffer中拿数据SendData。
如果SendData.cmd== 'c' 关闭SendData.uid对应的链接.
如果SendData.cmd== 's' 往SendData.ip,SendData.port中发SendData.buffer的数据。
那么这个'c','s'的SendData分别是啥时候会生成过来的呢?
对于'c'。查证下来,是由_pEpollServer->close(uid,fd)后,关闭指定的uid链接
对于's'。查下来,最终调用_pEpollServer->send()实现。而这个地方,就Handle::sendResponse()会调用。最终查代码发现,其实调用来源就是TarsCurrent::sendResponse()。而这个函数是用于 协议的发送响应数据 。啥意思呢?其实就是tars的rpc流程中,给调用客户端发送 执行函数 返回值及返回的数据。
重点业务流程是 processNet
ET_NET值是0.也就是默认没特意设过ev.data.u64值的epoll_event事件.这个流程里,有很多很重要的链接状态管理细节,不过太繁琐就不细述。
1、链接状态判断。有异常则关闭此链接。
2、如果是 EPOLLIN 事件。收数据包vRecvData,当收到的vRecvData非空时。
正常流程,调用BindAdapter::insertRecvQueue()。将vRecvData塞到链接绑定的Adapter的接收数据队列 _rbuffer.push_front()队列尾中。
如果此Adapter过载,给vRecvData中每个Data的isOverload = true;并调用BindAdapter::insertRecvQueue()塞入_rbuffer.push_front()队列头..
注意.BindAdapter::insertRecvQueue()加入队列后,会通知_handleGroup->monitor.notify()业务线程组去处理。。
如果_rbuffer 队列满,丢弃此接收数据,直接返回。这里我比较惊讶的事,也不记录错误日志,也不统计丢弃总数?
队列过载长度变量是_iQueueCapacity。 此值代码默认是10*1024个,当_rbuffer队列长度<=_iQueueCapacity/2时未过载;在_iQueueCapacity/2 -> _iQueueCapacity之间时候为过载;大于等于_iQueueCapacity时队列满需丢包。
3、如果是EPOLLOUT事件。
这个好像没啥好说的。将缓存的_sendbuffer,各种数据组织后,调用writev,发送给对应的socket。当发送的总数据长度超过8k时,会记日志。另外,如果配了BackPacketBuffLimit, 并且发送长度超过iBackPacketBuffLimit,则会返回错误。iBackPacketBuffLimit配在 /tars/application/server<BackPacketBuffLimit>",一般默认是不配的。
_sendbuffer中数据的来源,这个地方感觉有点奇怪,貌似只有一个调用之处,就是在前面processPipe的's'模式中,调用_pEpollServer->send()时候,最终会调用到NetThread::sendBuffer(cptr,buff,ip,port);这里会调用到Connection::send(buffer, ip, port,byEpollOut=false)模式。
Connection::send函数有两种功能。byEpollOut==true时候,执行上面描述中的将_sendbuffer的数据发送出去;byEpollOut==false时候,先尝试发送buff内容,如果发送失败表示IO满等异常时,将参数中带入的buff数据打包成TC_Slice塞入_sendbuffer中,缓存待发,这种写法看起来应该是发送保护,当发送失败时,通过epoll,缓存数据到_sendbuffer中待后续epoll触发再去发送。
这个EPOLLOUT并不需要自己去触发,当调用::send, 发送直到eagain(表示socketbuffer不可写), 当可写时, 内核会通知你(触发EPOLLOUT事件) 。另外此事件,可以通过epoll_ctl mod触发。
4、刷新_list中对应uid的链接的超时时间。超时时间为前面部分介绍的.
业务线程loop流程 ServantHandle::run()部分流程解析
注意,在看这部分流程时候,会有很多操作Servant对象的操作。可以直接将Servant这些部分默认成是平常所用的Imp类。方便理解
在上面流程中。BindAdapter::insertRecvQueue()会将网络线程中收到的数据塞入_rbuffer中。。
下面的流程就走到业务线程中了。业务线程,ServantHandle::public TC_EpollServer::Handle其实只用看ServantHandle::run()。不用管TC_EpollServer::Handle类了。
在业务线程中,先调用initialize()。实际上是给所有Servant对象绑定上本线程,并调用他们的initialize
后面流程,有两种模式:普通模式,及协程模式。协程模式开关是在 /tars/application/server<opencoroutine> 默认是没开,其实还是开着好一点。
普通模式时:
一、执行startHandle()..此函数默认是空实现的虚函数.此时线程已经启动, 可用于进入具体处理前调用
二、执行handleImp()循环读取数据并处理。
1、通过_handleGroup->monitor这个TC_ThreadLock实现 当_rbuffer中没数据时,可以等待超时,超时时长_iWaitTime为100ms;当insertRecvQueue()往_rbuffer中新增数据时,会触发_handleGroup->monitor.notify()。
2、在跳出lock之后,先轮询各个Adapter上报心跳。如果与上次心跳时间差大于10s。则执行下面上报心跳流程。
设置 本次心跳的时间。
给NodeServer发TARS_KEEPALIVE.
给PropertyReport 上报连接数比率;有队列时候,上报队列长度.
3、处理异步回调队列 handleAsyncResponse().此函数流程如下.
轮询全部Servant对象
1]循环从_asyncResponseQueue 异步响应队列的队头取出数据resp。
if (resp->response.iRet == TARSSERVERSUCCESS) { //作为客户端访问其他server时,成功返回 pServantPtr->doResponse(resp); } else if (resp->pObjectProxy == NULL){ //作为客户端访问其他server时,如果resp没有找到request,则响应该接口 pServantPtr->doResponseNoRequest(resp); } else { //作为客户端访问其他server时,返回其他异常的响应接口 pServantPtr->doResponseException(resp); }
看了下_asyncResponseQueue这个东东,貌似只有在ServantCallback 单线程全异步中的callback对象这个类的 onDispatch 中才有添加。但ServantCallback 此类未看到在哪应用,暂时还不懂其用法。看代码doResponse(),doResponseNoRequest(),doResponseException()都得在Servant类中自己去实现。完全看不懂系列。
2]再是处理业务附加的自有消息
pServantPtr->doCustomMessage(true);
pServantPtr->doCustomMessage();
doCustomMessage()也得自己去实现。不带参数的是为了兼容老版本。尽量用带参数的函数。
此函数的功能是 每次handle被唤醒后都会调用,业务可以通过在其他线程中调用handle的notify实现在主线程中处理自有数据的功能,比如定时器任务或自有网络的异步响应
4、处理用户自有数据 handleCustomMessage()
实现内容感觉跟上面重复了。遍历所有servant。
pServantPtr->doCustomMessage(true);
pServantPtr->doCustomMessage();
不再 叙述。 话说为啥这里要多次执行处理用户自有数据呢??完全看不懂系列
5、轮询所有的Adapter。
调用BindAdapter::waitForRecvQueue()从_rbuffer取队首数据 stRecvData。
这里让人蛋疼的是,每次取数据之后,都会执行。。
重复性流程。上报心跳 (与上面流程所用的一模一样);handleAsyncResponse(与上面流程所用的一模一样);handleCustomMessage(只是参数变false)了;这3个重复的流程。我觉得这么设计的原因是服务端正常跑起来后,极有可能_rbuffer是会源源不断会调用过来的,如果不在这个小循环里去执行上面3个流程,那可能这3个流程中的功能就没机会执行了。
其它流程如下:
//数据已超载 overload if (stRecvData.isOverload) { handleOverload(stRecvData); } //关闭连接的通知消息 else if (stRecvData.isClosed) { handleClose(stRecvData); } //数据在队列中已经超时了 //_iQueueTimeout代码默认至少是3s。数值是配在Adapter的<queuetimeout>中一般配60s else if ( (now - stRecvData.recvTimeStamp) > (int64_t)adapter->getQueueTimeout()) { handleTimeout(stRecvData); } else { //正常处理 handle(stRecvData); }
handleOverload()函数内容:
根据stRecvData创建对应的TarsCurrentPtr current = createCurrent(stRecvData)
如果此Adapter是tars协议,调用current->sendResponse(TARSSERVEROVERLOAD);也就是给请求端发请求过载的消息包。返回值是-9
handleClose()函数内容:
创建TarsCurrentPtr current = createCloseCurrent(stRecvData);这里的TarsCurrent有点特殊。
查找到对应的Servant。调用pServantPtr->doClose(current );客户端关闭连接时的处理..doClose函数是Imp类自己去实现。
handleClose()函数内容:
根据stRecvData创建对应的TarsCurrentPtr current = createCurrent(stRecvData)
给PropertyReport上报超时数目一条
如果此Adapter是tars协议,调用current->sendResponse(TARSSERVERQUEUETIMEOUT);也就是给请求端发请求超时的消息包。返回值是-6
handle()函数内容:
根据stRecvData创建对应的TarsCurrentPtr current = createCurrent(stRecvData)
如果是tars协议。走handleTarsProtocol (current);非tars协议走handleNoTarsProtocol(current)
其中handleTarsProtocol()函数内容为:
检查set调用合法性,不合法 流程返回;
处理染色 processDye()。其实就是打印指定的log。此流程有空再介绍。
如果启用了tracking.则processTracking()。处理tracking信息。此流程有空再介绍
查找current 对应servant。若未查到,sendResponse(TARSSERVERNOSERVANTERR)。-4
执行 ret = pServantPtr->dispatch(current, buffer);若出异常,异常catch部分设置对应的异常code到ret 及 生成对应的log字符串sResultDesc。
如果调用需要回包给客户端.current->sendResponse(ret, buffer, TarsCurrent::TARS_STATUS(), sResultDesc);
handleNoTarsProtocol()函数内容为:
这个函数就更简单了。其实就是找到current 对应servant后。
执行 ret = pServantPtr->dispatch(current, buffer); 用current->sendResponse()返回对应返回值。。
上面已经用过很多TarsCurrent::sendResponse了。
TarsCurrent::sendResponse()函数的流程如下:
单次调用类型直接返回
如果协议不为TUP (普通tars协议)。生成ResponsePacket response对象。将返回值ret 及获取的参数内容buffer 等信息填上 .
如果为TUP协议,多个tup::UniAttribute<>模式对buffer内容编码。生成的数据还是response中
将response序列化到 TarsOutputStream<BufferWriter> os中.再将os转成字符串s;
再调用TarsCurrent中绑定的ServantHandle指针。 ServantHandlePtr->sendResponse(s)。将s发送出去。后面过程就是上面介绍的 processNet()函数 's' 模式的流程了。
Servant::dispatch()的内容:
此函数其实是个中转函数.
if (current->getFuncName() == "tars_ping"){ //tars_ping 是测试消息。此协议比较奇怪,源码中我只找到, //在NodeServer启动时候会往自己<local>的AdminObj发。也就是发给自己? ret = TARSSERVERSUCCESS; } else if (!current->getBindAdapter()->isTarsProtocol()){ //非tars协议 TC_LockT<TC_ThreadRecMutex> lock(*this);//完全看不懂系列。此处为啥要加锁? ret = doRequest(current, buffer); } else { //tars协议 TC_LockT<TC_ThreadRecMutex> lock(*this); ret = onDispatch(current, buffer); }
如果是非tars协议。可以参考下框架部分的《QueryPropertyServer》的写法,其实就是在Imp类中实现 doRequest(current, buffer)去处理逻辑
如果是tars协议。则会调用到Imp类的 onDispatch()类中。这个类就有意思了。。
cpp部分,比如代码中带有的示例.AdminReg.tars通过框架提供的工具将AdminReg.tars文件生成的AdminReg.h文件中,有一个类class AdminReg : public tars::Servant。
而我们写代码的Imp类是要继承于此类的。
class AdminRegistryImp: public AdminReg。
在AdminReg类中。默认定义有
int onDispatch(tars::TarsCurrentPtr _current, vector<char> &_sResponseBuffer)。
在AdminRegistryImp类中,一般我们不会去重写这个函数,当然如果需要对收到的调用函数进行特殊处理,就重写此函数,在此处做文章。
默认生成的AdminReg::onDispatch()函数中,把AdminReg.tars中定义的所有的接口名都排个序放在一个数组中,当被调用请求上来时,通过_current.getFuncName()从数组中查到对应的函数index.switch过对应函数去执行。
在默认生成的swtich-case对应的函数块中,实现了:
从_current中解包出参数;
调用此子类Imp中实现的同名函数,将参数传入。
如果有返回值,打包好返回值和返回参数到tars::TarsOutputStream<tars::BufferWriter>中,然后传给vector<char> &_sResponseBuffer中传出。
onDispatch流程到处完毕。
通过前面TarsCurrent::sendResponse()介绍可以发现,真正发回给调用客户端的返回结果,经过了两次 TarsOutputStream<tars::BufferWriter>的打包??这个设计的有点奇怪。。完全看不懂系列
另外通过观察 AdminReg.h文件,还发现对应每个.tars中定义的接口同名函数,还默认生成了一个对应的async_response_函数。比如:
addTaskReq()接口有个 async_response_addTaskReq()。
这种 async_response_ 系列函数又是做啥用的呢?实际上这个是用于做嵌套调用的实现。
看了下 这个代码在AdminRegistry服务中有应用,比如batchPatch中,需要去异步调用NodePrx。
NodePrxCallbackPtr callback = new PatchProCallbackImp(reqPro, NodePrx, defaultTime, current);
NodePrx->tars_set_timeout(timeout)->async_patchPro(callback, reqPro);
在PatchProCallbackImp的实现中,有
virtual void callback_patchPro(tars::Int32 ret, const std::string& result);
virtual void callback_patchPro_exception(tars::Int32 ret);
这两个函数。在async_patchPro执行成功时,会调用到callback_patchPro,失败时,会调用到callback_patchPro_exception。
在callback_patchPro和callback_patchPro_exception里面,会调用AdminReg::async_response_batchPatch().给调用请求客户端回结果。
说白了,async_response_类函数是这种 服务A->服务B ->服务C。服务B异步请求服务C的结果,返回给服务A 这类嵌套请求异步调用的一种包装写法。
三、执行stopHandle()..此函数默认是空实现的虚函数.线程马上要退出时调用。基本上不会用到。
协程模式:
协程模式的写法。。跟普通模式类似。也是
startHandle(); 消息处理业务;stopHandle();不同在于消息处理业务部分。
首先每个ServantHandle线程 创建一个协程管理器。
CoroutineScheduler _coroSched; _coroSched->createCoroutine(std::bind(&ServantHandle::handleRequest, this)); while (!getEpollServer()->isTerminate()) { _coroSched->tars_run(); }
协程管理器会配置 协程池大小iPoolSize 和 协程栈大小iStackSize。
其中iStackSize代码默认是ServerConfig::CoroutineStackSize=131072。此数值是可配的,配置在/tars/application/server<coroutinestack>中,配置默认没配。
另iPoolSize 的取值就复杂了:
size_t iPoolSize = (ServerConfig::CoroutineMemSize > ServerConfig::CoroutineStackSize) ? (ServerConfig::CoroutineMemSize / (ServerConfig::CoroutineStackSize * iThreadNum) ) : 1; if(iPoolSize < 1) iPoolSize = 1; _coroSched->init(iPoolSize, iStackSize);
其中默认ServerConfig::CoroutineMemSize=1073741824。这个值也是可配的,配在/tars/application/server<coroutinememsize>,配置默认没配。iThreadNum是默认业务线程数默认是10个。 那么如果全是默认值,算下来iPoolSize=819。。
此协程管理器 在与本线程及ServantHandle::handleRequest()绑定上.线程就交给协程循环去了。
协程管理器实现的细节,在协程部分介绍。。 可以直白的理解成,协程循环调用ServantHandle::handleRequest()。
那么重要的流程就是ServantHandle::handleRequest()函数的实现了:
这个流程的实现,跟普通模式中的基本handleImp一致。
不同之处只是:
当 _activeCoroQueue中有内容要处理,需先yield()到这些协程里面。
当处理正常请求数据时,就是对等普通模式的 handle(stRecvData)函数,此处会_coroSched->createCoroutine(std::bind(&ServantHandle::handleRecvData, this, stRecvData));创建一个新协程去处理。其中handleRecvData(stRecvData)与ServantHandle::handle()基本一致,再此,不再多说。