03-Thrift进阶

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()
}

客户端目前只有两种初始化方法,NewMathServiceClientFactoryNewMathServiceClientProtocol,之前使用第一种,但是这里只能使用第二种。
坑: 官方示例代码中的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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值