Thrift进阶
在官网和大部分博客中,介绍的都是最简单的Hello World性质的东西。但是实际使用有很多问题考虑在内,比如目前的一个需求:
- Thrift中一个服务对应一个端口,如果需要多个服务,则悲剧了,端口复用问题十分严重。
- 相比较,在ZeroC ICE中,不仅可以做到服务集合,而且端口复用是一个最最基础的用法。
- 下面我们来探究一下,端口复用问题。0.9.1版本之后原生支持。
集中解决三个问题:端口复用、文件续传、双向传输/推送。
参考Url 端口复用
1 端口复用
通过TMultiplexedxxxxx
分别为服务器和客户端提供多服务共用端口的问题。
// 服务器修改, TMultiplexedProcessor
shared_ptr<TMultiplexedProcessor> mprocessor(new TMultiplexedProcessor());
mprocessor->registerProcessor("mathService", processor); // 可以注册多个服务Processor
TNonblockingServer _server(mprocessor, protocolFactory, 9090, threadManager); // 将原始的Processor替换
// 客户端, TMultiplexedProtocol
shared_ptr<TMultiplexedProtocol> mProtocol(new TMultiplexedProtocol(clientProtocol, "mathService")); // 添加一层Protocol
MathServiceClient client(mProtocol); // 修改Protocol
// 如果有多个Service则需要多个mProtocol,对应多个client。但是transport只用打开一次。
如果使用Golang,代码如下:
坑: golang库中的示例代码已经不可用,下面是修改后的!
服务器使用NewTMultiplexedProcessor
, 但是NewTMultiplexedProcessor2
更强大,不会用::>_<::
// 服务器
package main
import (
"fmt"
"mathservice"
"os"
"time"
"git.apache.org/thrift.git/lib/go/thrift"
)
const (
NetWorkAddr = "127.0.0.1:9090"
)
type MyMathService struct {
}
func (this *MyMathService) Add(A int32, B int32) (r int32, err error) {
r = A + B
err = nil
fmt.Println("Add", A, B, "[", time.Now(), "]")
return
}
func main() {
handler := &MyMathService{}
processor := mathservice.NewMathServiceProcessor(handler)
serverTransport, err := thrift.NewTServerSocket(NetWorkAddr)
if err != nil {
fmt.Println("Error!", err)
os.Exit(1)
}
transportFactory := thrift.NewTTransportFactory()
framedtransportFactory := thrift.NewTFramedTransportFactory(transportFactory)
protocolFactory := thrift.NewTBinaryProtocolFactoryDefault()
mProcessor := thrift.NewTMultiplexedProcessor()
mProcessor.RegisterProcessor("mathService", processor)
mProcessor.RegisterProcessor("testService", processor)
server := thrift.NewTSimpleServer4(mProcessor, serverTransport, framedtransportFactory, protocolFactory)
fmt.Println("thrift server in", NetWorkAddr)
server.Serve()
}
客户端目前只有两种初始化方法,NewMathServiceClientFactory
和NewMathServiceClientProtocol
,之前使用第一种,但是这里只能使用第二种。
坑: 官方示例代码中的NewMathServiceClient方法已经不存在!
// 客户端
package main
import (
"mathservice"
"fmt"
"os"
"time"
"git.apache.org/thrift.git/lib/go/thrift"
)
func tt(strName string) {
client_socket, _ := thrift.NewTSocket("127.0.0.1:9090")
//client_transport := thrift.NewTBufferedTransport(client_socket, 512)
client_transport := thrift.NewTFramedTransport(client_socket)
protocol := thrift.NewTBinaryProtocolTransport(client_transport)
mProtocol := thrift.NewTMultiplexedProtocol(protocol, strName)
client := mathservice.NewMathServiceClientProtocol(client_transport, mProtocol, mProtocol)
if err := client_transport.Open(); err != nil {
fmt.Fprintln(os.Stderr, "Error opening socket", err)
os.Exit(1)
}
defer client_transport.Close()
for i := int32(0); i < 100; i++ {
nRet, _ := client.Add(i, i)
fmt.Println(strName, i, "Add", nRet)
time.Sleep(100 * 1000 * 1000)
}
fmt.Println("Over!")
}
func main() {
go tt("mathService") // 并行运行两个客户端
tt("testService")
}
2 文件传输
Thrift的坑很大,比如这里。其实有很多关于文件处理的类,但是没有完善的文档说明,所以,基本没法用。
这里,我们使用一种稍显麻烦,但是比较通用的方案处理。
// 接口定义文件
struct FileChunk{
1:string fileName
2:i64 offset
3:i64 size
4:binary data
}
service FileIface{
i64 uploadFile(1:FileChunk fileChunk)
}
原理:客户端将本地文件分成很多块,每次上传一个小块。(可扩展为断点续传)
瓶颈:每次操作都需要打开关闭文件,这里会成为I/O瓶颈。优化?
优化思路:
1. 文件Open后建立Handle Map,不要每次打开。制作一个超时关闭Handle功能。
2. 增加文件信息查询接口,后续打开可以先查询,后传输。以便断点续传。
3. 服务器增加锁机制,以免读写问题。
4. 服务器针对小文件,增加缓冲模式。提高写性能。
5. 读文件接口同样适用。
具体代码不实现了。
3 推送/双向通信
Thrift默认是被动服务器,即只能处理客户端向服务器主动请求,服务器无法主动向客户端发数据。
如果需要Pub/Sub订阅服务,则最简单是使用MQ一类的单独服务器。
原理原文: http://joelpm.com/2009/04/03/thrift-bidirectional-async-rpc.html
翻译参考: http://blog.csdn.net/qq_27989757/article/details/50725973
Java实现: http://www.w2bc.com/article/189387
虽然这样,但是由于版本和用词等很含糊,也没有C++的版本,所以,在自习研读其他语言的代码后,做出如下修改,并且实验成功!
中心思想是:两端分别使用已经建立好的连接,在客户端模拟服务器,在服务器模拟客户端。
以下代码可能是全球独一份啦!!!!
// msgPush.thrift
service CallbackService{
oneway void Push(1: string msg);
}
// 服务器,支持一对多发送
#include "CallbackService.h"
#include <thrift/protocol/TBinaryProtocol.h>
#include <thrift/server/TThreadPoolServer.h>
#include <thrift/server/TThreadedServer.h>
#include <thrift/transport/TServerSocket.h>
#include <thrift/transport/TBufferTransports.h>
using namespace ::apache::thrift;
using namespace ::apache::thrift::protocol;
using namespace ::apache::thrift::transport;
using namespace ::apache::thrift::server;
using boost::shared_ptr;
void PushThread(const TConnectionInfo& connInfo) {
int nMsgCount = 0;
char buff[128];
shared_ptr<CallbackServiceClient> client(new CallbackServiceClient(connInfo.input, connInfo.output));
while (nMsgCount<100)
{
Sleep(500);
nMsgCount++;
sprintf(buff, "Hello, msg %d!\n", nMsgCount);
// run in thread
printf(buff);
try{
client->Push(buff);
}
catch (TException& e) {
printf("Error: %s\n", e.what());
}
}
printf("Server Send Thread Exit!\n");
}
class MyTProcessorFactory : public CallbackServiceProcessorFactory {
public:
MyTProcessorFactory(const ::boost::shared_ptr< CallbackServiceIfFactory >& handlerFactory) : CallbackServiceProcessorFactory(handlerFactory){
}
boost::shared_ptr<TProcessor> getProcessor(const TConnectionInfo& connInfo) override
{
// 每有一个客户端连接上,就会有一个getProcessor调用,然后单独通信
std::thread* t = new std::thread(PushThread, connInfo);
t->detach();
printf("MyTProcessorFactory getProcessor~\n");
return CallbackServiceProcessorFactory::getProcessor(connInfo);
}
};
int main(int argc, char **argv) {
int port = 9090;
//shared_ptr<CallbackServiceHandler> handler(new CallbackServiceHandler());
shared_ptr<CallbackServiceIfFactory> processorFactory(new CallbackServiceIfSingletonFactory(NULL));
shared_ptr<MyTProcessorFactory> myProcessorFactory(new MyTProcessorFactory(processorFactory));
shared_ptr<TServerTransport> serverTransport(new TServerSocket(port));
shared_ptr<TTransportFactory> transportFactory(new TBufferedTransportFactory());
shared_ptr<TProtocolFactory> protocolFactory(new TBinaryProtocolFactory());
TThreadedServer server(myProcessorFactory, serverTransport, transportFactory, protocolFactory);
server.serve();
return 0;
}
// 客户端
#include "CallbackService.h"
#include <thrift/protocol/TBinaryProtocol.h>
#include <thrift/transport/TSocket.h>
#include <thrift/transport/TBufferTransports.h>
#include <thrift/TProcessor.h>
using namespace ::apache::thrift;
using namespace ::apache::thrift::protocol;
using namespace ::apache::thrift::transport;
using boost::shared_ptr;
class CallbackServiceImply : public CallbackServiceIf {
public:
int msgCount = 0;
void Push(const std::string& msg) {
printf("[CallbackService] Push %d %s\n", msgCount++, msg.c_str());
}
};
void RunRecv(shared_ptr<TProtocol> clientProtocol) {
int nLoopCount = 0;
shared_ptr<CallbackServiceImply> clientCallback(new CallbackServiceImply());
shared_ptr<CallbackServiceProcessor> clientProcessor(new CallbackServiceProcessor(clientCallback));
try
{
while (clientProcessor->process(clientProtocol, clientProtocol, NULL))
{
nLoopCount++;
if ( nLoopCount>10000 )
{
break;
}
}
return;
}
catch (TException* e)
{
printf("Error: %s\n", e->what());
}
}
int main2(int argc, char **argv) {
int port = 9090;
shared_ptr<TTransport> clientSocket(new TSocket("127.0.0.1", port));
shared_ptr<TTransport> clientTransport(new TBufferedTransport(clientSocket));
shared_ptr<TProtocol> clientProtocol(new TBinaryProtocol(clientTransport));
CallbackServiceClient client(clientProtocol);
try
{
clientTransport->open();
printf("Open Remote Transport, wait call it!\n");
// start thread
std::thread* t = new std::thread(RunRecv, clientProtocol);
t->join(); // wait
}
catch (TException& e)
{
printf("ERROR:%s\n", e.what());
}
system("pause");
return 0;
}
在这里,再一次地感受到Thrift
无情地被ZeroC ICE
秒杀了。ZeroC在Pub/Sub和双向通讯已经甩Thrift无数条街,尽管ZeroC已经开源,可惜官方不给力,对新语言的支持跟进速度,以及扩展开发都没有详细的文档,实在是一大遗憾(捶胸顿足,有木有!!)。
优化:完善方案完全可以借鉴ZeroC的做法。这里先说点简单优化:
1. 使用端口复用,然后专门增加一个双向通信的接口。
2. 在getProcessor
中添加Client,对发送的消息队列,通过单独的线程(池)集中一对多发送和Client管理。
3. 增加发送异常处理,处理异常次数,管理Client列表,从而优化发送效率。
4. 整体加心跳?重连?(正规产品还是需要的)
4 感受
C++写这些服务,真的很麻烦,不管是库的配置还是线程、内存管理,明显能感受到巨大的工作量和优化的坑。如果能有Golang类型的并行语言完全替代C++,真的是开发人员的一大福音。
后续会慢慢将1和3重点关注,开发出实际应用的可用项目来。
PS:许多国人很自私,许多已经研究出来的东西不愿意分享,加上国内的大环境,真的是一种悲哀~
5 补充
Golang在网络和并发做的比较好,其他方面真的一般般。这里将双向通信改为Golang版本,自此结束。
服务器的核心思想是重载getProcessor接口,在服务器端通过已经建立的Transport创建通信用客户端,然后用它进行通信。
客户端的核心思想是Open通信后,使用现有的Protocol创建Processor进行Push的数据解析(回调)。
// 服务器
package main
import (
"fmt"
"msgpush"
"os"
"time"
"git.apache.org/thrift.git/lib/go/thrift"
)
func PushThread(trans thrift.TTransport) {
nMsgCount := 0
var buff string
callbackClient := msgpush.NewCallbackServiceClientFactory(trans,
thrift.NewTBinaryProtocolFactoryDefault())
for nMsgCount = 0; nMsgCount < 100; nMsgCount++ {
time.Sleep(500 * time.Millisecond)
buff = fmt.Sprintf("Hello, msg %v!", nMsgCount)
fmt.Println(buff)
callbackClient.Push(buff)
}
fmt.Println("Server Send Thread Exit!\n")
}
// myProcessorFactory 实现 thrift.TProcessorFactory 的 getProcessor接口
type myProcessorFactory struct {
processor thrift.TProcessor
}
func NewmyTProcessorFactory(p thrift.TProcessor) thrift.TProcessorFactory {
return &myProcessorFactory{processor: p}
}
func (p *myProcessorFactory) GetProcessor(trans thrift.TTransport) thrift.TProcessor {
// 每有一个客户端连接上,就会有一个getProcessor调用,然后单独通信
go PushThread(trans)
fmt.Println("MyTProcessorFactory getProcessor~\n")
return p.processor
}
func main() {
fmt.Println("Callback Service start...")
sock, err := thrift.NewTServerSocket("127.0.0.1:9090")
if err != nil {
fmt.Println("Socket Bind Failed!")
os.Exit(0)
}
transFactory := thrift.NewTTransportFactory()
framedTransFactory := thrift.NewTFramedTransportFactory(transFactory)
protocolFactory := thrift.NewTBinaryProtocolFactoryDefault()
processor1 := msgpush.NewCallbackServiceProcessor(nil)
processorFactory := NewmyTProcessorFactory(processor1)
_server := thrift.NewTSimpleServerFactory4(processorFactory, sock,
framedTransFactory, protocolFactory)
_server.Serve()
}
// 客户端
package main
import (
"fmt"
"msgpush"
"os"
"git.apache.org/thrift.git/lib/go/thrift"
)
type MyMsgPushService struct {
msgCount int
}
func (p *MyMsgPushService) Push(msg string) (err error) {
p.msgCount++
fmt.Printf("[Go CallbackService] Push %v %v\n", p.msgCount, msg)
return err
}
func RunRecv(_client *msgpush.CallbackServiceClient) {
var nLoopCount int
clientCallback := &MyMsgPushService{}
clientProcessor := msgpush.NewCallbackServiceProcessor(clientCallback)
for {
clientProcessor.Process(_client.InputProtocol, _client.OutputProtocol)
nLoopCount++
if nLoopCount > 10000 {
break
}
}
fmt.Println("Client RunRecv exit!")
}
func main() {
fmt.Println("Callback Client start...")
sock, err := thrift.NewTSocket("127.0.0.1:9090")
if err != nil {
fmt.Println("Socket connect Failed!")
os.Exit(0)
}
client_transport := thrift.NewTFramedTransport(sock)
protocolFactory := thrift.NewTBinaryProtocolFactoryDefault()
_client := msgpush.NewCallbackServiceClientFactory(client_transport, protocolFactory)
if err := client_transport.Open(); err != nil {
fmt.Println("Error in open Socket")
os.Exit(0)
}
defer client_transport.Close()
// wait
RunRecv(_client)
}
Thrift坑实在是有点多,下一步会尝试和研究Hprose这个框架,最终目的是替代ZeroC。
理想方案:Rust + Hprose。