#快速编写Thrift简单接口

来源:互联网 发布:程序员 团队贡献 编辑:程序博客网 时间:2024/06/06 00:55

快速编写Thrift简单接口

公司需求。一个快速学习和开发的经历。也在这里附上我的学习指南。

首先需要结合机器信息安装好Thrift,并且结合官方文档,以入⻔指南为大纲,进行试运行。这里我以Python进
行示范和讲解。

首先复制下来 tutorial.thrift 和 shared.thrift ,并且运行代码: thrift -r –gen py tutorial.thrift 生成对应的模板文件。

然后复制client.py和server.py,在本地进行试运行。

两个脚本的实现在代码的注释中讲解的很详细。这里不再一行行进行分析,只总括全代码进行分析,帮助更好的理解。可以在看了本解释后再去看源代码快速了解,也可先看源代码再看本解释加深了解。

client部分,主体结构在 main() 函数中。通过一系列对协议等模块的初始化及相关设置后,创建了一个客戶端
对象。传输通道开启后,开始实现功能函数。最后结尾需要关闭函数。

server部分大体上也是如此。首先创建一个server对象后,开启一个等待进程,等到客戶端中对server端内描述的函数进行调用,则开始执行相关功能。

总的结构是:在server端对函数进行具体实现的描述,在client对函数进行调用就可以了。

由于Thrift是一种接口描述语言和二进制通讯协议,所以可以被当做远程过程调用(在客戶端和服务端配置相同的IP)框架来使用,也是由Facebook为“大规模跨语言服务开发”而开发的(如Python写服务端Java写客戶端)。

对相关语言熟悉的话看看运行结果和代码,就能大体上明白Thrift是如何进行参数的调用的。如果十分紧急的任务,那么对整个框架的流程认识到这儿就可以了。熟悉一下代码删删改改,就能直接上手。

如果对项目有质量的要求,也不是在现场直接手写代码,那么可以缓缓来。首先需要了解tutorial.thrift中Thrift
的基础语法。常⻅的包括命名空间,变量,服务,数据结构。

这里需要阅读注释,并且查看文档,搜索博客,找到(或者自己创建)最适合你需要的数据结构。在启动服务
中,定义服务的名称也是比较重要的关键字,不要随便乱起。如在Python中,这将是你后面生成的模板代码的项目名称。也是在Python中的模块名称。

Thrift本身足够复杂,所以封装出大量接口后我们能轻易使用。常⻅的开发中,.thrift文件的编写只有这些要点。

然后是对开发的讲解。

首先上图:
这里写图片描述

一步步走完流程后,我们就基本上明白了thrift从客戶端的第一行代码开始,如何一步步创建对象,发起请求,接
受数据,完成处理。

介绍下thrift的四大模块:Transport,Protocol,Process,Server。

Transport

其实是网络通信,现在都是基于TCP/IP,而TCP/IP协议栈由socket来实现,也就是现在的网络通信服务器,最底层都是通过socket的,Thrift也不例外,而在Thrift源码中,则是通过将socket包装成各种Transport来使用。对应的源码目录就是thrift-0.9.0/lib/cpp/src/thrift/transport。大部分和网络数据通信相关的代码都是放在这个目录之下。

Protocol

支持各种语言,是通过一个x.thrift的描述文件来通信。thrift描述文件是各种语言通用的,但是需要通过thrift的代码生成器(比如c++对应的是Thriftthrift‒gencppx.thrift)来生成对应的源代码。那为什么不同的代码可以直接互相调用接口函数呢,其实就是因为制定了同一套协议,就像HTTP协议,你根本不需要知道实现HTTP服务器的是什么语言编写的,只需要遵守HTTP标注来调用即可。所以其实protocol就是transport的上一层。transport负责数据传输,但是要使得程序知道传输的数据具体是什么,还得靠protocol这个组件来对数据进行解析,解析成对应的结构代码,供程序直接调用。

Processor

通过transport和protocol这两层之后,程序已经可以获得对应的数据结构,但是数据结构需要被使用才有价值。
在Thrift里面,就是被processor,比如我们定义了一个描述文件叫foo.thrift内容如下:

service Foo {string  GetName()}

通过thrift‒gencppfoo.thrift之后就会生成gen-cpp目录下面一堆代码。

typedef std::map<std::string,   ProcessFProcessMap  processMap_;...processMap_["GetName"]  =   &FooProcessor

注意到以上三行代码是很有意思的点发,因为我们在foo.thrift文件里面定义了一个函数叫GetName,而在
FooProcessor里面的函数定义是process_GetName,其中使用了一个map将他们对应起来。
其实这就是反射,可以让外界通过字符串类型的函数名来指明调用的函数。从而实现函数的动态调用。但是这样的反射其实非常低效,因为每次map.find需要对比字符串的大小。可以说这也注定Thrift不打算成为一个高性能的RPC服务器框架。到此为止可以大概猜出来每次RPC调用的过程是:
1. 客戶端指定要调用的函数,比如是GetName 。
2. 将该函数名通过 protocol 进行编码,编码后的数据通过 transport 传输给服务端。
3. 服务端接收到数据之后,通过和客戶端一样的 protocol 解码数据。
4. 解码后的数据获得需要调用的函数名,比如是 GetName ,然后通过 FooProcessor的 processMap_ 去找出对应ProcessFunction 来调用。

Server

众所周知,当前最流程的两种高性能服务器编程范式是多线程和异步调用。这里展开讲又是很大的一个话题。这
里简单的说一下:根据高并发编程闻名已久的那篇文章c10k所倡导的来说,多线程当并发数高的时候,内存会成为并发数的瓶颈,而异步编程而没有相关的困扰,是是解决高并发服务的最佳实践。(注意这里说的是解决高并发的最佳实践)我个人的观点是异步编程是把双刃剑。它确实对高并发服务很友好,特别是对于IO密集型的服务。但是对于业务逻辑的开发并不友好。比如Nginx是将异步IO使用得登峰造极的作品,而Node.js则因为异步经常把业务逻辑弄得支离破碎。这两者Thrift都有对应的server类供开发人员使用。
在Thrift中,有以下四种server:

1.TSimpleServer.cpp2.TThreadedServer.cpp3.TThreadPoolServer.cpp4.TNonblockingServer.cpp

第一种TSimpleServer最简单,就是单线程服务器,没什么可说的。第二种和第三种都是多线程模型。这两者的不同点只是在于前者是每个连接进来会新建一个线程去接受该连接。直到对应的连接被关闭,该线程才会被销毁。服务的线程数和连接数相等,有多少个连接就有多少个线程。而后者是连接池的形式,在系统启动的时候就设定好线程数的大小,比如线程数设置为32。每次新的连接过来的时候,就向线程池申请一个线程来处理该连接。直到该连接被释放,该线程才会被回收到线程池。如果线程池被申请到空时,下一次申请则会阻塞,阻塞直到线程池非空(也就是有线程被回收时)。这两者各有利弊,前者的缺点主要是当连接数过大的时候,会把内存撑爆,这就是之前C10K说的并发数太大内存不够用的情况。后者的缺点则是当线程池的线程被用完时,下一次的连接请求则会失败(阻塞住)。所以当使用TThreadPoolServer的时候,如果发现客戶端连接失败了,十有八九都是因为线程池的线程供不应求了。总之,开发者可以针对不同的场景选用不同的服务模型。
TThreadedServer
说说源码细节:有一个成员变量叫serverTransport_,作为服务器的主transport(其实就是主socket)。监听端口【listen】,和接受请求【accept】。这里需要注意的是,这里的serverTransport_其实是个非阻塞socket。非阻塞的过程是借助了poll(不是epoll),来实现,将serverTransport_在poll里面注册,不过呢,注册的时候设置了timeout时间。在thrift-0.9.0里面的超时时间是3seconds。也就是可以理解为其实每次serverTransport_->accept()函数退出时不一定是接受到请求了。也有可能是超时时间到了。具体可以看thrift-0.9.0/lib/cpp/src/thrift/transport/TServerSocket.cpp文件里360行的函数TServerSocket::acceptImpl()的实现过程。所以在TThreadedServer的实现里面,需要用while(stop)轮询进行serverTransport->accept()的调用。这个轮询在没有任何连接请求的时候,每次循环一次的间隔是3s,也就是之前设置的超时时间。而当连接进来的时候,serverTransport_->accept()就会立即返回接受到的新client。然后接下来的过程就是Task上场了。Task就是将transport,protocol,processor包装起来而已。就像上文说的,整个调用的过程从底层往高层就是以此调用transport,protocol,processor来处理请求的。所以直接使用包装它们的Task,将Task绑定到一个线程并启动该线程,再把Task插入任务集合中即可。注意到,之前的while(_stop)轮询退出时,会检测该任务集合,如果任务集合不为空,则会阻塞直到任务集合为空,TThreadedServer的server函数才会退出。细节请看thrift-0.9.0/lib/cpp/src/thrift/server/TThreadedServer.cpp第40行的classTThreadedServer::Task:publicRunnable函数实现。当连接进来的时候,会新建一个Task扔进任务队列。当连接断开的时候,该Task对应的线程会执行完毕,在退出之前会从任务队列中删除该任务。但是当客戶端迟迟不主动断开连接呢?答案是线程就会迟迟不退出,任务队列就会一直保持非空状态。原因在Task的run函数里面,会循环调用thrift-0.9.0/lib/cpp/src/thrift/server/TThreadedServer.cpp71行里面的peek()函数,这个peek()函数是阻塞型函数。功能是窥探客戶端是否有新的函数调用请求,如果没有,则阻塞等待直到客戶端发送函数调用请求。
TThreadPoolServer
按照上文理解了ThreadedServer之后,ThreadPoolServer就没什么好说的了。基本上就是【同理可得】。非阻塞型服务器第四种TNonblockingServer就是传说中的异步服务器模型(非阻塞服务器模型)。在Thrift中使用该模型需要依赖libevent。最后根据自己的需要选择对应模块,你会发现thrift还是十分简单而且友好的。
推荐阅读:
thrift源码剖析
浅谈Thrift内部实现原理