《电子技术应用》
您所在的位置:首页 > 通信与网络 > 设计应用 > 网络库Tbnet及其应用分析
网络库Tbnet及其应用分析
2017年微型机与应用第1期
李艳1,2,张玲3,胡术1,2,李璞1,2,潘倩1,2
1. 四川大学 计算机学院,四川 成都 610064;2. 四川大学 国家空管自动化系统技术重点实验室,四川 成都610064;3. 四川大学 计算机基础教学实验中心,四川 成都 610064
摘要: Tbnet广泛应用于TFS、Tair等开源分布式系统,不同于仅提供数据传输的传统网络库,该库为客户端/服务端提供交互式通信,即客户端发出请求,服务端接收、处理并予以回应,适用于分布式系统开发。Tbnet采用对象语义进行类的设计并大量使用类的继承,实际使用时需继承IPacketHandler、IServerAdaptor等接口类,重写类中虚函数。Tbnet内部由一个处理事件的输入/输出线程、一个超时检查线程及工作线程池组成,I/O线程与工作线程间采用单生产者多消费者模式[1]共享加锁报文列表。本文分析了Tbnet主要类及其消息通信,对比分析了分布式产品使用Tbnet的差异性。
Abstract:
Key words :

  李艳1,2,张玲3,胡术1,2,李璞1,2,潘倩1,2

  (1. 四川大学 计算机学院,四川 成都 610064;2. 四川大学 国家空管自动化系统技术重点实验室,四川 成都610064;3. 四川大学 计算机基础教学实验中心,四川 成都 610064)

       摘要:Tbnet采用生产者消费者队列模型,具有附带回应的报文发送机制,对外提供类库型的接口,应用具有多样性。研究了淘宝开源网络库Tbnet的核心设计实现、多样化使用,内容包括Tbnet主要类及其类间关系,客户端与服务端间的连接通信过程,以OceanBase早期版本为代表的淘宝分布式产品对Tbnet使用的分析,以及该库向Windows平台的移植工作。

  关键词输入/输出线程工作线程;生产者-消费者

  中图分类号:TP393.1文献标识码:ADOI: 10.19358/j.issn.1674-7720.2017.01.020

  引用格式:李艳,张玲,胡术,等. 网络库Tbnet及其应用分析[J].微型机与应用,2017,36(1):66-68,72.

0引言

  Tbnet广泛应用于TFS、Tair等开源分布式系统,不同于仅提供数据传输的传统网络库,该库为客户端/服务端提供交互式通信,即客户端发出请求,服务端接收、处理并予以回应,适用于分布式系统开发。Tbnet采用对象语义进行类的设计并大量使用类的继承,实际使用时需继承IPacketHandler、IServerAdaptor等接口类,重写类中虚函数。Tbnet内部由一个处理事件的输入/输出线程、一个超时检查线程及工作线程池组成,I/O线程与工作线程间采用单生产者多消费者模式[1]共享加锁报文列表。本文分析了Tbnet主要类及其消息通信,对比分析了分布式产品使用Tbnet的差异性。

1内部实现解析

  Tbnet对用户提供库类型接口,其使用方式呈多样化。通过不同方式调用Transport类对象,可运行为客户端或服务端。为与多个服务端连接通信,客户端主线程调用ConnectionManager类指针与由本机IP地址及其端口号转换的无符号64位整型ipport提供的serverId标识的服务端建立连接。客户端与服务端分别继承IPacketHandler、采用对象适配器模式[2]的IServerAdaptor类,分别重写handlePacket()实现报文处理流程。因继承的类接口不同,服务端在I/O线程或工作线程处理报文,客户端则在I/O线程处理报文。

  1.1主要类

  1.1.1线程相关类

  Tbnet采用I/O线程+工作线程池的模型[3],其线程的实现主要通过Runnable、DefaultRunnable以及Channel类完成,其中DefaultRunnable继承自Runnable,内部组合CThread类。Transport类继承自Runnable,创建一个I/O线程和一个超时检查线程,线程内部包含至多一个eventLoop处理事件;而继承自DefaultRunnable的PacketQueueThread类则创建工作线程池。

  1.1.2IOComponent相关类

  Socket类封装套接字及其操作函数,SocketEvent类封装epoll机制[4],而IOComponent类包含Socket类指针,设置SocketEvent类指针、引用计数及回调函数等。TCPAcceptor类继承自IOComponent,完成类似Acceptor接收器功能,服务端根据TCPAcceptor类指针触发读事件执行accept()生成已连接套接字的Socket来创建TCPComponent类指针。TCPComponent继承自IOComponent类,TCPComponent根据init函数传入的参数来判断选择客户端或服务端,作为客户端发起非阻塞连接,服务端则代表连接成功。

  1.1.3Channel相关类

  为跟踪各报文交互式通信,Tbnet使用Channel进行抽象,通过对客户端Packet设置信道ID,服务端回应Packet也使用该ID,可确保请求与回应一一对应。客户端处理回应报文的IPacketHandler类指针定义在Channel,实现了客户端发送和接受处理报文的异步化,Channel机制也为用户带来更多可能性。因定义在主机间正在使用的Channel数量有限,为避免不断申请Channel造成内存碎片,客户端发送请求报文需从ChannelPool中获取一个空闲Channel,客户端收到回应报文后将该Channel取出并放回ChannelPool。

  1.1.4Transport相关类

  Transport类封装监听与连接,创建Socket、IOComponent类指针,设置Socket超时检查。主线程回调Socket设置IP地址、端口等信息;回调IOComponent创建套接字、设置套接字选项以及监听或连接。因TCPComponent内部创建了用于报文收发的TCPConnection类指针,Transport类创建的I/O线程在EPollSocketEvent类对象的驱动下,回调IOComponent进行事件处理。超时线程遍历检查存储在vector中的内部正在使用的超时IOComponent类指针,再检查内部发生错误已移入删除列表的超时或引用计数≤ -10的IOComponent类指针。

  1.1.5ConnectionManager相关类

  ConnectionManager类包含Transport类指针,客户端首次与指定服务端建立TCP连接时采用长连接复用的ConnectionManager类指针,调用Transport类指针建立非阻塞连接,将key为对应服务端序号(serverId)、value为主机间连接的Connection类指针插入map;待双方再次连接,客户端主线程不再调用Transport建立TCP连接,而是在map中根据serverId查找Connection类指针,通过该Connection将Packet压入发送队列,等待I/O线程触发写事件。

  1.2对象生命周期管理设计

  Tbnet中类继承、类间相互操作,导致较多类对象的创建与回收不在自身类中。主线程创建Socket类指针,并根据Socket创建IOComponent类指针。待I/O线程不再调用Socket及IOComponent时,主线程IOComponent类的析构函数回收Socket,Transport类的析构函数回收在用链表及删除链表中的IOComponent指针对象。服务端主线程创建的TCPAcceptor类指针,客户端主线程以及服务端I/O线程分别创建的TCPComponent类指针,主机I/O线程通过原子计数修改这些类指针的引用计数,待引用计数≤-10时,回收该指针。TCPComponent内部创建TCPConnection类指针,其析构函数回收该类指针。

  1.3消息通信

  客户端与服务端使用单I/O线程+多线程模型即半同步半异步模式[5],主机间通信数据流程如图1所示。

001.jpg

  客户端主线程使用工厂类IPacketFactory,根据数据包类型创建Packet类指针,设置Packet超时时间,将分配的Channel信道ID设置到Packet头部,再将Packet依次压入加锁的发送队列,见步骤1~2;I/O线程将Packet从发送队列移至临时队列,依次取出Packet并调用IPacketStreamer类指针将Packet二进制转换,Packet组装后放入输出缓存,直至临时队列为空或输出缓存可写空间尚未超过阈值,使用send函数发送请求报文直至输出缓存为空或发送次数超过10次,见步骤4。服务端I/O线程使用recv函数接收请求报文至空间足够的输入缓存,调用IPacketStreamer类指针解析报文,获取packet并放入加锁的报文队列,见步骤6;工作线程从报文队列取出并处理Packet,接着回调Connection将设置了超时时间、信道ID的回应Packet压入发送队列,见步骤7~8;服务端I/O线程触发写事件,从发送队列中取出Packet,组装并发送该回应报文,见步骤9;客户端I/O线程触发读事件,接收并处理报文,见步骤10。

2应用分析

  相较于传统的提供收发功能的网络库,面向对象的Tbnet不仅在使用上存在较大差异且不能直接使用,开发人员需继承多个接口类,重写成员函数,还需将其他类如Transport实例化并通过编码有机结合,方可实现通信功能。基于Tbnet的实现呈多样化,本节将介绍两个典型的应用:Tbnet自带示例代码和OceanBaseV0.3中的应用。

  2.1示例代码

  Tbnet提供一个简单的报文通信反射示例[6],包含客户端EChoClient与服务端EChoServer两个进程。客户端/服务端主线程使用Transport对象建立TCP连接、开启I/O线程及超时检查线程、等待线程结束[7]。示例服务端无工作线程池,主机间消息通信流程如图2所示。

 

002.jpg

  客户端主线程分配内存,创建字符串类型的Packet类指针,将附带信道ID的请求Packet压入加锁的发送队列,见步骤1~2;I/O线程从发送队列中移出并组装Packet,发送请求报文,见步骤4;服务端I/O线程接收并解析请求报文获取请求Packet,将请求Packet内容直接复制拷贝到回应Packet并设置对应信道ID,再将其压入发送队列,组装Packet并发送回应报文,见步骤6~7;客户端I/O线程接收回应报文,确认收发数据包数目是否相同,接收完毕关闭I/O线程,销毁I/O组件,删除回应数据包,见步骤8。

  2.2OceanBase中的使用

  OceanBase[8]作为分布式关系数据库,其0.4之前版本均使用Tbnet,除常规使用,通过添加WaitObject等待对象机制,客户端可在主线程阻塞等待回应报文。OceanBase采用I/O线程+工作线程池模型[9],主机间网络通信数据流程如图3所示。

 

003.jpg

  客户端主线程创建含有回应报文序号(seq_id)的WaitObject类指针,见步骤1;数据包设置附有seq_id的Channel信道ID并将其压入加锁发送队列,WaitObject在主线程阻塞等待回应报文,见步骤2~3;客户端I/O线程向服务端发送从发送队列移出并组装的请求报文,见步骤5;服务端I/O线程接收并解析报文获取请求Packet,将其放入加锁的报文列表,见步骤6;服务端工作线程获取报文并处理,处理完成后工作线程将带有seq_id的回应Packet压入发送队列,见步骤7~8;服务端I/O线程组装并发送回应报文,见步骤9;客户端I/O线程接收回应报文并处理,若报文带有seq_id,则从map中查找对应WaitObject并将回应报文放入其中,利用条件变量唤醒阻塞等待的主线程;客户端主线程从WaitObject中获取回应报文,见步骤10~11。

3向Windows平台的移植实现

  Tbnet基于Linux平台实现,为便于使用,需向Windows平台移植。移植主要思路:(1)Tbnet依赖淘宝另一开源实现Tbsys,即对系统级函数的封装,跨平台移植需先替换Tbsys中的平台相关部分;(2)替换Tbnet中I/O复用模型调用的epoll机制。

  3.1Tbsys的移植

  开源分布式协调系统Zookeeper的C语言客户端的“winport.cpp”文件[10],其包含基于Windows函数实现的常见POSIX语义的线程函数,包括线程创建、回收、互斥锁、自旋锁、读写锁、条件变量等[11],也加载了socket动态链接库。该实现相对较轻量,满足Tbnet移植要求,极大地减少了移植工作量。应用于IOComponent类指针的引用计数通过Tbsys原子计数实现,Tbsys使用AT&T嵌入式汇编实现32位整数的原子操作。在Windows平台,需使用Interlocked系列函数来替换Tbsys中atomic类的相关函数,以此实现自动增加、减少及比较替换功能。

  3.2事件处理机制

  Tbnet中EPollSocketEvent类采用epoll机制实现I/O复用模型,移植采用select机制,通过继承SocketEvent实现。epoll机制返回事件触发的套接字,epoll_ctl()关注事件,epoll_wait()获取激活事件;select机制获取被触发的套接字个数后,需遍历整个套接字数组查找事件触发的套接字。移植中,为操作方便,封装的SelectSocketEvent类内部包含key为Socket类指针、value为Socket关联的IOComponent类指针的map,考虑到跨线程操作map,使用互斥锁机制。select处理流程调整为:(1)遍历map将套接字描述符关注事件加入读描述符集、写描述符集;(2)有限时间内执行select函数,再次遍历map,将map中迭代器指向的已触发事件的value设置到根据标识判断触发了读或写事件的IOEvent类指针的成员变量IOComponent类指针中,返回触发事件数目。

4结论

  作为支持交互式通信的网络库,Tbnet使用时需注意:一个Transport仅有一个I/O线程,对于连接多、吞吐量大的应用存在瓶颈[12];因服务端使用工作线程池处理请求报文,导致报文的处理顺序、回应报文的发送顺序及其接收顺序不一致,对同一客户端发出的报文也存在相同情况,该乱序会导致应用层面问题,如先进先出;服务端对请求报文的处理既可在I/O线程也可在工作线程中进行,客户端则在I/O线程中处理回应报文,这种将应用层的处理放入线程的做法,通常存在线程不安全问题,需附加队列等处理机制。

参考文献

  [1] BEVERIDGE J. Win32 多线程程序设计[M]. 侯捷,译.武汉:华中科技大学出版社,2002.

  [2] (美)沙洛韦,(美)特罗特.设计模式精解[M]. 熊节,译.北京:清华大学出版社,2004.

  [3] 陈硕. Linux多线程服务端编程:使用muduo C++网络库[M]. 北京:电子工业出版社,2013.

  [4]  STEVENS W R, FENNER B,RUDOFF A M. Unix网络编程卷1:套接字联网API(第3版)[M]. 北京:人民邮电出版社,2015.

  [5] SCHMIDT D C,STAL M, ROHNERT H, et al.面向模式的软件体系结构 卷2:用于并发和网络化对象的模式[M]. 张志祥,任雄伟,肖斌,等,译.北京:机械工业出版社,2003.

  [6] Mao Qi.Tbnet开源[EB/OL]. (2013-11-xx)[20151225].http://code.taobao.org/svn/tbcommonutils/.

  [7] STEVENS W R. UNIX网络编程卷2:进程间通信(第2版)[M]. 杨继张,译.北京:人民邮电出版社,2010.

  [8] OceanBase开源[EB/OL]. (2014-04-xx)[20160402].http://code.taobao.org/svn/OceanBase/.

  [9]黄贵,庄明强. OceanBase分布式存储引擎[J]. 华东师范大学学报(自然科学版),2014(5):164173.

  [10] The Apache Software Foundation. Zookeeper开源[EB/OL].(2016-01-xx)[20160502].http://wwwus.apache.org/dist/zookeeper/.

  [11] BUTENHOF D R. POSIX多线程程序设计 [M]. 丁磊,曾刚,译.北京:中国电力出版社,2003.

  [12] 杨传辉. 大规模分布式存储系统:原理解析和架构实战[M]. 北京:机械工业出版社,2013.


此内容为AET网站原创,未经授权禁止转载。