thrift网络通讯架构解析与实例验证

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/lijinqi1987/article/details/77896140

Thrift实现了C/S模式,通过代码生成工具将接口定义文件生成服务器端和客户端代码(可以为不同语言),从而实现服务端和客户端跨语言的支持。用户在Thirft描述文件中声明自己的服务,这些服务经过编译后会生成相应语言的代码文件,然后用户实现服务(客户端调用服务,服务器端提服务)便可以了。

一、Thrift Rpc整体架构

Thrift的协议栈如图:


在Client和Server的最顶层都是用户自定义的处理逻辑,也就是说用户只需要编写用户逻辑,就可以完成整套的RPC调用流程。用户逻辑的下一层是Thrift自动生成的代码,这些代码主要用于结构化数据的解析,发送和接收,同时服务器端的自动生成代码中还包含了RPC请求的转发(Client的A调用转发到Server A函数进行处理)

● 底层IO模块,负责实际的数据传输,包括Socket,文件,或者压缩数据流等。

● TTransport负责以字节流方式发送和接收Message,是底层IO模块在Thrift框架中的实现,每一个底层IO模块都会有一个对应TTransport来负责Thrift的字节流(Byte Stream)数据在该IO模块上的传输。例如TSocket对应Socket传输,TFileTransport对应文件传输。

● TProtocol主要负责结构化数据组装成Message,或者从Message结构中读出结构化数据。TProtocol将一个有类型的数据转化为字节流以交给TTransport进行传输,或者从TTransport中读取一定长度的字节数据转化为特定类型的数据。如int32会被TBinaryProtocol Encode为一个四字节的字节数据,或者TBinaryProtocol从TTransport中取出四个字节的数据Decode为int32。

● TServer负责接收Client的请求,并将请求转发到Processor进行处理。TServer主要任务就是高效的接受Client的请求,特别是在高并发请求的情况下快速完成请求。

● Processor(或者TProcessor)负责对Client的请求做出相应,包括RPC请求转发,调用参数解析和用户逻辑调用,返回值写回等处理步骤。Processor是服务器端从Thrift框架转入用户逻辑的关键流程。Processor同时也负责向Message结构中写入数据或者读出数据。


二、数据传输方式(TTransport)

  • TSocket : 阻塞式socker;
  • THttpTransport : 采用HTTP传输协议进行数据传输;
  • TFramedTransport : 以frame为单位进行传输,非阻塞式服务中使用;
  • TFileTransport : 以文件形式进行传输;
  • TMemoryTransport : 将内存用于I/O. java实现时内部实际使用了简单的ByteArrayOutputStream;
  • TZlibTransport : 使用zlib进行压缩, 与其他传输方式联合使用。当前无java实现;
  • TBufferedTransport : 对某个transport对象操作的数据进行buffer,即从buffer中读取数据进行传输,或将数据直接写入到buffer
Thrift实现中,一个关键的设计选择就是将传输层从代码生成层解耦。从根本上,生成的Thrift代码只需要知道如何读和写数据。数据的源和目的地无关紧要,可以使一个socket,一段共享内存,或本地磁盘上的一个文件。TTransport(Thrift transport)接口支持以下方法:
open    Opens the tranpsort
close    Closes the tranport
isOpen  Indicates whether the transport is open
read    Reads from the transport
write   Writes to the transport
flush   Forces any pending writes
除以上的TTransport接口外,还有一个TServerTransport接口,用来接受或创建原始传输对象。它的接口如下:
open   Opens the transport
listen   Begins listening for connections
accept  Returns a new client transport
close   Closes the transport

三、传输协议(TProtocol)

thrift做到很好的让用户在服务器端与客户端选择对应的传输协议,总体上一般为2种传输协议:二进制或者文本,如果想要节省带宽可以采用二进制的协议,如果希望方便抓包、调试则可以选择文本协议,用户可用根据自己的项目需求选择对应的协议。

  • TCompactProtocol : 紧凑的、高效的二进制传输协议;
  • TBinaryProtocol : 基于二进制传输的协议,使用方法与TCompactProtocol 相同
  • TJSONProtocol : 使用json格式编码传输协议
  • TDebugProtocol : 使用易懂的可读的文本格式,以便于debug
TCompactProtocol 高效的编码方式,使用了类似于ProtocolBuffer的Variable-Length Quantity (VLQ) 编码方式,主要思路是对整数采用可变长度,同时尽量利用没有使用Bit。对于一个int32并不保证一定是4个字节编码,实际中可能是1个字节,也可能是5个字节,但最多是五个字节。TCompactProtocol并不保证一定是最优的,但多数情况下都会比TBinaryProtocol性能要更好。
TProtocol接口非常直接,它根本上支持两件事: 1) 双向有序的消息传递; 2) 基本类型、容器及结构体的编码。
writeMessageBegin(name, type, seq)
writeMessageEnd()
writeStructBegin(name)
writeStructEnd()
writeFieldBegin(name, type, id)
writeFieldEnd()
writeFieldStop()
writeMapBegin(ktype, vtype, size)
writeMapEnd()
writeListBegin(etype, size)
writeListEnd()
writeSetBegin(etype, size)
writeSetEnd()
writeBool(bool)
writeByte(byte)
writeI16(i16)
writeI32(i32)
writeI64(i64)
writeDouble(double)
writeString(string)
name, type, seq = readMessageBegin()
readMessageEnd()
name = readStructBegin()
readStructEnd()
name, type, id = readFieldBegin()
readFieldEnd()
k, v, size = readMapBegin()
readMapEnd()
etype, size = readListBegin()
readListEnd()
etype, size = readSetBegin()
readSetEnd()
bool = readBool()
byte = readByte()
i16 = readI16()
i32 = readI32()
i64 = readI64()
double = readDouble()
string = readString()
注意到每个write函数有且仅有一个相应的read方法。WriteFieldStop()异常是一个特殊的方法,标志一个结构的结束。读一个结构的过程是readFieldBegin()直到遇到stop域,然后readStructEnd()。生成的代码依靠这个调用顺序,来确保一个协议编码器所写的每一件事,都可被一个相应的协议解码器读取。 这组功能在设计上更加注重健壮性,而非必要性。例如,writeStructEnd()不是严格必需的,因为一个结构体的结束可用stop域表示。

四、服务器网络模型(TServer)

  • TSimpleServer : 简单的单线程网络模型, 同时只能服务一个client,通常是结合TSocket用于测试;
  • TThreadedServer : 多线程网络模型,使用阻塞式IO,为每个请求创建一个线程;
  • TThreadPoolServer : 线程池网络模型,使用阻塞式IO,将每个请求都加入到线程池中;
  • TNonblockingServer : 多线程服务模型,使用非阻塞式IO(需使用TFramedTransport数据传输方式);
Thrift 使用 libevent 作为服务的事件驱动器, libevent 其实就是 epoll更高级的封装而已(在linux下是epoll)。处理大量更新的话,主要是在TThreadedServer和TNonblockingServer中进行选择。TNonblockingServer能够使用少量线程处理大量并发连接,但是延迟较高;TThreadedServer的延迟较低。实际中,TThreadedServer的吞吐量可能会比TNonblockingServer高,但是TThreadedServer的CPU占用要比TNonblockingServer高很多。

TServer对象通常如下工作:

1)使用TServerTransport获得一个TTransport
2)使用TTransportFactory,可选地将原始传输转换为一个适合的应用传输(典型的是使用TBufferedTransportFactory)
3)使用TProtocolFactory,为TTransport创建一个输入和输出
4)调用TProcessor对象的process()方法
Thrift中定义一个server的方法如下:
TSimpleServer server(
    boost::make_shared<CalculatorProcessor>(boost::make_shared<CalculatorHandler>()),
    boost::make_shared<TServerSocket>(9090),
    boost::make_shared<TBufferedTransportFactory>(),
    boost::make_shared<TBinaryProtocolFactory>());

  TThreadedServer server(
    boost::make_shared<CalculatorProcessorFactory>(boost::make_shared<CalculatorCloneFactory>()),
    boost::make_shared<TServerSocket>(9090), //port
    boost::make_shared<TBufferedTransportFactory>(),
    boost::make_shared<TBinaryProtocolFactory>());


  const int workerCount = 4;//线程池容量
  boost::shared_ptr<ThreadManager> threadManager = ThreadManager::newSimpleThreadManager(workerCount);
  threadManager->threadFactory(boost::make_shared<PlatformThreadFactory>());
  threadManager->start();
  TThreadPoolServer server(
    boost::make_shared<CalculatorProcessorFactory>(boost::make_shared<CalculatorCloneFactory>()),
    boost::make_shared<TServerSocket>(9090),
    boost::make_shared<TBufferedTransportFactory>(),
    boost::make_shared<TBinaryProtocolFactory>(),
    threadManager);

  TNonBlockingServer server(
    boost::make_shared<CalculatorProcessorFactory>(boost::make_shared<CalculatorCloneFactory>()),
    boost::make_shared<TServerSocket>(9090),
    boost::make_shared<TFramedTransportFactory>(),
    boost::make_shared<TBinaryProtocolFactory>(),
    threadManager);

  server.serve();//启动server

五、TProcessor/Processor

Processor是由Thrift生成的TProcessor的子类,主要对TServer中一次请求的 InputProtocol和OutputTProtocol进行操作,也就是从InputProtocol中读出Client的请求数据,向OutputProtcol中写入用户逻辑的返回值。Processor是TServer从Thrift框架转到用户逻辑的关键流程。同时TProcessor.process是一个非常关键的处理函数,因为Client所有的RPC调用都会经过该函数处理并转发。
Thrift在生成Processor的时候,会遵守一些命名规则,可以参考 Thrift Generator部分的介绍。
TProcessor对于一次RPC调用的处理过程可以概括为:
1、TServer接收到RPC请求之后,调用TProcessor.process进行处理
2、TProcessor.process首先调用TTransport.readMessageBegin接口,读出RPC调用的名称和RPC调用类型。如果RPC调用类型是RPC Call,则调用TProcessor.process_fn继续处理,对于未知的RPC调用类型,则抛出异常。
3、TProcessor.process_fn根据RPC调用名称到自己的processMap中查找对应的RPC处理函数。如果存在对应的RPC处理函数,则调用该处理函数继续进行请求响应。不存在则抛出异常。
a)在这一步调用的处理函数,并不是最终的用户逻辑。而是对用户逻辑的一个包装。
b)processMap是一个标准的std::map。Key为RPC名称。Value是对应的RPC处理函数的函数指针。 processMap的初始化是在Processor初始化的时候进行的。Thrift虽然没有提供对processMap做修改的API,但是仍可以通过继承TProcessor来实现运行时对processMap进行修改,以达到打开或关闭某些RPC调用的目的。
4、RPC处理函数是RPC请求处理的最后一个步骤,它主要完成以下三个步骤:
a) 调用RPC请求参数的解析类,从TProtocol中读入数据完成参数解析。不管RPC调用的参数有多少个,Thrift都会将参数放到一个Struct中去。Thrift会检查读出参数的字段ID和字段类型是否与要求的参数匹配。对于不符合要求的参数都会跳过。这样,RPC接口发生变化之后,旧的处理函数在不做修改的情况,可以通过跳过不认识的参数,来继续提供服务。进而在RPC框架中提供了接口的多Version支持。
b) 参数解析完成之后,调用用户逻辑,完成真正的请求响应。
c) 用户逻辑的返回值使用返回值打包类进行打包,写入TProtocol。

六、ThriftClient

ThriftClient跟TProcessor一样都主要操作InputProtocol和OutputProtocol,不同的是ThritClient将RPC调用分为Send和receive两个步骤。
1、Send步骤,将用户的调用参数作为一个整体的Struct写入TProcotol,并发送到TServer。
2、Send结束之后,ThriftClient便立刻进入Receive状态等待TServer的相应。对于TServer返回的响应,使用返回值解析类进行返回值解析,完成RPC调用。


七、Thrift通讯实例

1、编写thrift接口文件student.thrift 
struct Student{
 1: i32 sno,
 2: string sname,
 3: bool ssex,
 4: i16 sage,
}
service Serv{
 i32 put(1: Student s),
}

2、根据.thrift文件生成代码文件
thrift -r --gen cpp student.thrift
在目录下生成了gen-cpp文件夹,内有文件 Serv.cpp  Serv.h  Serv_server.skeleton.cpp  student_constants.cpp  student_constants.h  student_types.cpp  student_types.h,
文件Serv_server.skeleton.cpp是一个简单的server端文件,可以以此为参考来编写代码

3、编写服务端代码server.cpp
#include <concurrency/ThreadManager.h> //zml
#include <concurrency/PosixThreadFactory.h> //zml
#include "gen-cpp/Serv.h"
#include <protocol/TBinaryProtocol.h>
#include <server/TSimpleServer.h>
#include <transport/TServerSocket.h>
#include <transport/TBufferTransports.h>
#include <server/TNonblockingServer.h> //zml    
using namespace ::apache::thrift;
using namespace ::apache::thrift::protocol;
using namespace ::apache::thrift::transport;
using namespace ::apache::thrift::server;    
using namespace::apache::thrift::concurrency; //zml        
using boost::shared_ptr;  

#define THREAD_NUM 2
const int g_port = 9090;
class ServHandler : virtual public ServIf {
 public:
  ServHandler() {
  // Your initialization goes here
  }
  int32_t put(const Student& s) {
  // Your implementation goes here
  printf("put student.sno=%d\n", s.sno);
  return s.sno;
  }
};
int thrift_server_run()
{
  //创建thrift server
  shared_ptr<ServHandler> handler(new ServHandler());
  shared_ptr<TProcessor> processor(new ServProcessor(handler));
  shared_ptr<TProtocolFactory> protocolFactory(new TBinaryProtocolFactory());
  
  shared_ptr<ThreadManager> threadManager = ThreadManager::newSimpleThreadManager(THREAD_NUM);
  shared_ptr<PosixThreadFactory> threadFactory = shared_ptr<PosixThreadFactory> (new PosixThreadFactory()); //PosixThreadFactory可以自定义(继承于ThreadFactory)
  threadManager->threadFactory(threadFactory);
  threadManager->start();   
  TNonblockingServer server(processor, protocolFactory, g_port, threadManager);
  try {
    server.serve();
  }
  catch(TException e) {
    printf("Server.serve() failed\n");
    exit(-1);
  }
  return 0;
}
int main(int argc, char **argv) {
  thrift_server_run();
  while(1) {
    sleep(10);
  }
  return 0;
}

4、编写服务端代码client.cpp
#include "gen-cpp/Serv.h"  // 替换成你的.h  
#include <transport/TSocket.h>
#include <transport/TBufferTransports.h>
#include <protocol/TBinaryProtocol.h>
using namespace apache::thrift;
using namespace apache::thrift::protocol;
using namespace apache::thrift::transport;
using boost::shared_ptr;
int main()
{
	boost::shared_ptr<TSocket> socket(new TSocket("localhost", 9090));
	
	//对接nonblockingServer时必须的,对普通server端时用boost::shared_ptr<TTransport> transport(new TBufferedTransport(socket));
	boost::shared_ptr<TTransport> transport(new TFramedTransport(socket)); 
	
	boost::shared_ptr<TProtocol> protocol(new TBinaryProtocol(transport));
	ServClient client(protocol);
	
	//设置发送、接收、连接超时
	socket->setConnTimeout(2000);
	socket->setRecvTimeout(2000);
	socket->setSendTimeout(2000);
	
	transport->open();
	
	//insert your code here  
	Student stu;
	stu.sno = 1;
	stu.sname = "zml";
	stu.ssex = 0;
	stu.sage = 25;
	int ret = client.put(stu);
	printf("client put ret=%d\n", ret);
	
	transport->close();
	return 0;
}

5、编译执行
g++ -g -DHAVE_NETINET_IN_H -I/usr/local/include/thrift -L/usr/local/lib/ gen-cpp/Serv.cpp gen-cpp/student_types.cpp gen-cpp/student_constants.cpp client.cpp -o client -lpthread -lthrift -lrt
g++ -g -DHAVE_NETINET_IN_H -I. -I/usr/local/include/thrift -L/usr/local/lib gen-cpp/Serv.cpp gen-cpp/student_types.cpp gen-cpp/student_constants.cpp server.cpp -o server -lthriftnb -levent -lthrift -lrt

开启服务端,并且执行客户端,服务端打印:
lijinqi@ubuntu:~/test/thrift$ ./server
Thrift: Thu Sep 21 16:31:13 2017 TNonblockingServer: Serving on port 9090, 1 io threads.
Thrift: Thu Sep 21 16:31:13 2017 TNonblockingServer: using libevent 2.0.21-stable method epoll
Thrift: Thu Sep 21 16:31:13 2017 TNonblocking: IO thread #0 registered for listen.
Thrift: Thu Sep 21 16:31:13 2017 TNonblocking: IO thread #0 registered for notify.
Thrift: Thu Sep 21 16:31:13 2017 TNonblockingServer: IO thread #0 entering loop...
put student.sno=1

客户端打印
lijinqi@ubuntu:~/test/thrift$ ./client 
client put ret=1


参考:
http://cpper.info/2016/03/17/Thrift-Reserch.html
http://zheming.wang/blog/2014/08/28/94D1F945-40EC-45E4-ABAF-3B32DFFE4043/
https://my.oschina.net/zmlblog/blog/177245

展开阅读全文

没有更多推荐了,返回首页