很久就想总结一下Thrift的Cpp lib库的学习情况,但是一直没有狠下心来去写。不是没有时间,就是恐怕理解不透彻。一晃几个月过去了,很多东西已经慢慢遗忘。今天,下定决心重新拾起来,边看边总结。这个故事,就从最有意思的Server端lib开始。这里的Server选用TNonBlockingServer,和他搭配的Transport选用TFrameTransport。这一节主要讲述该类包含的几个辅助类。
TNonblockingServer::TConnection::Task
要注意,Thrift有很多类都包括Task内部类,这里提到的Task类是TConnection的内部类。Task是Server端处理业务逻辑的地方,他继承自Runnable,遵守类似Java的线程模型。因此这个类必须实现run()方法。
run()本质是无限循环,每次循环都会首先调用绑在一起的connection对象的serverEventHandler的processContext方法。这个方法的实现由Thrift的使用者编写。如果实现没有实现这个方法,则跳过这一步骤。
然后,执行processor->process方法。processor是业务执行逻辑的封装,相当于Server的进程。process是调用实际的执行函数,即进程的执行体。执行结果将被序列化,存入输出序列(oprot类似的东西),注意,上文提到的Context也会传入这个执行体中。
最后,调用传输层的peek()。如果传输层使用TFrameTransport,那么其行为是查看读缓冲区里面有没有未处理的帧,如果没有,尝试读取下一个请求帧并且存入读缓冲区。如果依然没有可以读取的东西,返回False。
不论process执行发生了问题(可能不一定是故障,输出序列满也是有可能终止的,真正发生的问题会以异常的方式抛出并捕获),还是peek()得不到新的帧请求,都会跳出循环。循环一旦跳出,则通知Server,实现是调用捆绑的connection对象的notifyIOThread()。该方法的作用在于讲connection对象的指针写入Server的通知管道中。因为整个过程都发生在Server的一个进程中,因此传送指针是有意义的。
TNonblockingServer::TConnection
在TNonBlockingServer中负责处理连接管理,即数据的输入和输出。每个TConnection和一个TNonBlockingIOThread关联,通过其可以获得所在Server的句柄。(但是一个TNonBlockingIOThread可能被多个TConnection关联。)同时记录一个套接字连接的基本信息(sockaddr)。套接字信息存放在内部的TSocket类型的属性中。其他重要属性如下:
a. 输入输出分别为inputTransport和outputTransport属性,他们两个都是TMemoryBuffer的对象指针,实质上就是输入缓冲和输出缓冲。factoryInputTransport_和factoryOutputTransport_是对输入缓冲和输出缓冲的封装。这里,应该是封装为TFrameTransport。
b. 在序列化协议方面,inputProtocol_和outputProtocol_两个属性分别是输入和输出的序列化协议。一般使用TBinaryProtocol。不过,也支持自定义的特殊Transport(没有继承TTransport)的上面架设二进制序列化协议,只是需要传入特定的类型给模板。在这里没有传入类型,可见TNonblockingServer只支持继承TTransport的传输协议,实际上,只支持TFrameTransport。
c. connectionContext。如果创建Server的时候指定了serverEventHandler,那么这里新建connectionContext。实现是调用handler的createContext方法。
d. processor。这个调用比较诡异,他传入了三个参数:inputProtocol_、outputProtocol_还有tSocket_,但是下层调用的时候根本就没有使用这三个参数,而是直接调用的TProcessorFactory的getProcessor。这个方法是个纯虚方法,考察得到该类只有一个派生类,TSingletonProcessorFactory,这里的方法调用时候,根本就没有使用到之前传递的三个参数,而是直接返回的processor_。这是一个单例,可以推测,一个TSingletonProcessorFactory只会拥有一个processor,而且注释要求,所有调用这个方法的线程不得同时出现两个。
构造方法会调用init方法,最终完成所有属性的初始化。除了上面这些属性,比较重要的初始化还有两个,appState<--APP_INIT,socketState<--SOCKET_RECV_FRAMING。这两个State主要是用于TConnection以状态机的方式进行工作预设。
TConnection工作的主要方法,是workSocket和transition。前者是收发数据,后者状态迁移。总之,状态机。
workSocket使用socketState_字段记录3个状态:SOCKET_RECV_FRAMING、SOCKET_RECV、SOCKET_SEND。前两个是接收数据,最后一个是发送数据。前两个的状态迁移顺序是SOCKET_RECV_FRAMING--->SOCKET_RECV。
SOCKET_RECV_FRAMING:接收帧头部。这里也可看出TNonBlockingServer和TFrameTransport是严格绑定在一起的。每个帧分为两个部分:帧头部和帧数据。帧头部以字节为单位标明帧数据的长度。由于整个过程是非阻塞的,每次尽力而为的从网络套接字读取字节,如果头部(目前在0.9.0是4Byte)没有读取结束,存起来,下次调用时候继续。直到获取到完整的帧头部,调用transition。(这里不会多读数据。如果对方断开连接,读取的长度会是0,此时关闭套接字。server规定帧数据部分的长度不得超过256MB,这已经是相当大的一个范围了。如果帧头部指明的长度超过了server预设的最大值,则认为是读出了非法的帧格式,也会关闭套接字。)
SOCKET_RECV:接收帧数据。依然是非阻塞,尽力而为的读取。如果获得了完整的帧数据,调用transition。
SOCKET_SEND:发送帧头部和帧数据。因为是非阻塞,所以要尽力而为的发送,并且不会用for循环。如果期间捕获到EPIPE ECONNRESET ENOTCONN信号,那么抛出异常。如果没有捕获到EWOULDBLOCK或者EAGAIN,但是发送的响应是0字节,那么说明发生了错误,依然抛出异常。
transition使用appState_字段记录状态,基本的状态迁移:
+--> APP_INIT -----> APP_READ_FRAME_SIZE ---> APP_READ_REQUEST ---+ | | | | | | +------------------- APP_SEND_RESULT <--- APP_WAIT_TASK <-----+ | | | | +--------------------oneway-------------------------+
APP_INIT:server最开始的状态。设置读写缓冲区等等基本工作。需要设置等待读的标记位,通过调用setFlags(EV_READ|EV_PERSIST)实现,事件的注册是采用libevent库,回调方法是TConnection::eventHandler,由它调用workSocket方法。
APP_READ_FRAME_SIZE:server已经读到了帧长度。调整读缓冲区大小,以适应帧数据接收。因为残缺的帧数据是没有任何意义的。成倍的增加。最后改变appState_。
APP_READ_REQUEST:server已经获得了完整的帧。将输入缓冲区封装为inputTransport结构,并且重置输入缓冲以待将来使用。此时,满足处理请求的条件了。如果后台不是线程池模式,那么立即执行。否则,构造一个Task结构,丢给server调度。调用setIdle,禁止新的读取消息到来。
APP_WAIT_TASK:server已经处理好这个请求,准备回送返回结果。一般的,计算回送帧大小填充到帧首部,然后调用setFlags(EV_WRITE | EV_PERSIST)声明有数据需要写入套接字。但是,特殊的,如果请求是oneway的,无需回送数据。这时,仅仅需要进入APP_INIT的状态即可。
APP_SEND_RESULT:如果设置了每隔若干请求就重新调整输入输出缓冲区大小,则执行。其实对接口比较少的应用意义并不大,只是之前一直是缓冲不够的时候变大,在这里把缓冲区收回来。然后就是执行APP_INIT逻辑了。
APP_CLOSE_CONNECTION:关闭连接,减少活跃Processor计数。不在基本状态机里面。
其他的方法都比较简单,比如close就是关闭套接字,关闭inputTransport和outputTransport,最后调用server_->returnConnection。倒是有个方法,notifyIOThread,调用ioThread_->notify,先标记在这里。