Thrift源码学习(一)

1. 前言

前段时间接手的优惠券的项目是以Thrift RPC的形式对外提供服务的,这是第一次真正接触并使用Thrift,在此之前对于Thrift的了解仅限于"听过"。作为一个喜欢刨根究底的程序员,周末花了两天时间好好地研究了下Thrift的实现(仅限于Thrift的Java SDK,对于Thrift Compiler,下次有时间在研究),这系列博客就是自己学习的总结吧。

2. What

Thrift是一种语言无关的RPC框架,它通过一个中间语言IDL(接口定义语言),在扩展名为.thrift的文件中定义服务类型和服务接口,然后通过Thrift Compiler来生成特定语言的代码。生成的代码中实现了RPC网络层,传输层,协议层,用户只需要实现自定义的接口即可。

Thrift包含一套完整的栈来创建客户端和服务端程序,其协议栈架构如下图所示:
thrift协议栈

顶层是用户自己实现的业务逻辑,第二层是由Thrift生成的代码,主要包含数据的读取,解析和发送。

第三层则是协议层,主要定义数据的传输格式。Thrift目前支持如下几种协议:

  • TBinaryProtocol:一种简单的二进制格式,简单,但没有为空间效率而优化。比文本协议处理起来更快,但更难于调试。
  • TCompactProtocol:更紧凑的二进制格式,处理起来通常同样高效。
  • TDebugProtocol:一种人类可读的文本格式,用来协助调试。
  • TDenseProtocol:与TCompactProtocol类似,将传输数据的元信息剥离。
  • TJSONProtocol:使用JSON对数据编码。
  • TSimpleJSONProtocol:一种只写协议,它不能被Thrift解析,因为它使用JSON时丢弃了元数据。适合用脚本语言来解析。

第四层为传输层,定义数据传输方式,目前支持一下几种传输协议:

  • TFileTransport:该传输协议会写文件。
  • TFramedTransport:当使用一个非阻塞服务器时,要求使用这个传输协议。它按帧来发送数据,其中每一帧的开头是长度信息。
  • TMemoryTransport:使用存储器映射输入输出。(Java的实现使用了一个简单的ByteArrayOutputStream。)
  • TSocket:使用阻塞的套接字I/O来传输。
  • TZlibTransport:用zlib执行压缩。用于连接另一个传输协议。

除此之外,Thrift还提供众多的服务器,包括

  • TSimpleServer:单线程服务器,采用标准的阻塞I/O,一次只能接收和处理一个socker连接,效率比较低,主要用于演示Thrift的工作过程,在实际开发过程中很少用到它。
  • TThreadPoolServer:多线程服务器,它使用非阻塞I/O,主线程负责阻塞式监听“监听socket”中是否有新socket到来,业务处理交由一个线程池来处理。
  • TNonblockingServer:单线程服务器,使用NIO模式, 借助Channel/Selector机制, 采用IO事件模型来处理。
  • THsHaServer:THsHaServer继承TNonblockingServer,引入了线程池去处理, 其模型把读写任务放到线程池去处理。
  • TThreadedSelectorServer:TThreadedSelectorServer是对以上NonblockingServer的扩充, 其分离了Accept和Read/Write的Selector线程, 同时引入Worker工作线程池. 它也是种Half-sync/Half-async的服务模型。

3. How

关于如何使用thrift,网上一大堆教程,这里就不介绍了,直接上代码。
首先是在.thrift文件中定义服务,代码如下:

namespace java com.beautyboss.slogen.idl

typedef i32 int
typedef i64 long

struct HelloWorldRequest {
    1:required int age
    2:required string name
    3:optional string gender
}

struct HelloWorldResponse {
    1:required bool success
    2:required long code
    3:optional string message
    4:required string response
}

service HelloWorldService {
    HelloWorldResponse hello(1:HelloWorldRequest request)
}

在编译成java文件之后会生成HelloWorldRequest.java、HelloWorldResponse.java和HelloWorldService.java三个文件。

接下来实现HelloWorldService.Iface接口,代码如下:

package com.beautyboss.slogen.server.processor;

import com.beautyboss.slogen.idl.HelloWorldRequest;
import com.beautyboss.slogen.idl.HelloWorldResponse;
import com.beautyboss.slogen.idl.HelloWorldService;
import org.apache.thrift.TException;

/**
 * Author : Slogen
 * Date   : 2018/9/9
 */
public class HelloWorldProcessor implements HelloWorldService.Iface{
    @Override
    public HelloWorldResponse hello(HelloWorldRequest request) throws TException {
        HelloWorldResponse response = new HelloWorldResponse();
        response.setId(request.getId());
        if(null != request.getWord() && !"".equals(request.getWord())) {
            response.setWord(request.getName() + " ~~~" + request.getWord());
        } else {
            response.setWord(request.getName() + " ~~~ world");
        }
        return response;
    }
}

在实现了业务员逻辑之后,接下来就需要启动server,本文采用最复杂效率最高的TThreadedSelectorServer为例,代码如下:

package com.beautyboss.slogen.server;

import com.beautyboss.slogen.idl.HelloWorldService;
import com.beautyboss.slogen.server.processor.HelloWorldProcessor;
import org.apache.thrift.TProcessor;
import org.apache.thrift.protocol.TBinaryProtocol;
import org.apache.thrift.server.TThreadedSelectorServer;
import org.apache.thrift.transport.TFramedTransport;
import org.apache.thrift.transport.TNonblockingServerSocket;

/**
 * Author : Slogen
 * Date   : 2018/9/9
 */
public class ServerApplication {

    private static final int PORT = 9999;
    public static void main(String[] args) throws Exception {
        // 定义业务处理层processor
        TProcessor processor = new HelloWorldService.Processor<>(new HelloWorldProcessor());
        // transport传输层
        TNonblockingServerSocket transport =  new TNonblockingServerSocket(PORT);
        // server参数
        TThreadedSelectorServer.Args serverArgs = new TThreadedSelectorServer.Args(transport);
        serverArgs.processor(processor);
        serverArgs.transportFactory(new TFramedTransport.Factory());
        // 二进制协议
        serverArgs.protocolFactory(new TBinaryProtocol.Factory());
        // 多线程半同步半异步线程模型
        TThreadedSelectorServer server = new TThreadedSelectorServer(serverArgs);
        System.out.println("server serving...");
        server.serve();
    }
}

接着是启动client,代码如下:

package com.beautyboss.slogen.client;

import com.beautyboss.slogen.idl.HelloWorldRequest;
import com.beautyboss.slogen.idl.HelloWorldResponse;
import com.beautyboss.slogen.idl.HelloWorldService;
import org.apache.thrift.TException;
import org.apache.thrift.protocol.TBinaryProtocol;
import org.apache.thrift.protocol.TProtocol;
import org.apache.thrift.transport.TFramedTransport;
import org.apache.thrift.transport.TSocket;
import org.apache.thrift.transport.TTransport;

/**
 * Author : Slogen
 * Date   : 2018/9/9
 */
public class ClientApplication {

    public static void main(String[] args) throws TException {
        TTransport transport = new TFramedTransport(new TSocket("localhost",9999));
        TProtocol protocol = new TBinaryProtocol(transport);
        HelloWorldService.Client client = new HelloWorldService.Client(protocol);
        transport.open();
        HelloWorldRequest request = new HelloWorldRequest();
        for(int i = 0; i < 100;++i) {
            request.setId(i);
            request.setName("Slogen");
            request.setWord(" test");
            HelloWorldResponse response = client.hello(request);
            System.out.println(response);
        }
    }
}

启动client之后,就会自动连接到server并收发数据。

4. Why

既然Thrift是一种RPC框架,那么在分析它的源码实现之前,先来回顾下RPC的相关知识吧。
所谓的RPC(Remote Producer Call Protocol,远程过程调用协议),指的是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。当服务调用方和服务提供方不在一台服务器上,没办法直接调用,就需要通过网络传达调用的语义和参数,也即需要RPC调用。
RPC结构图

RPC框架需要解决下面的几个问题:

  1. 通讯的问题。底层是采用tcp还是udp?bio还是nio?多线程还是单线程?连接是按需连接,调用结束后就断掉,还是长连接,然后多个远程过程调用共享同一个连接?
  2. 寻址的问题,也就是说,服务提供方应该怎么对外暴露服务,暴露哪些服务?服务调用方应该怎么知道要连接哪个服务器,调用哪个服务?
  3. 序列化问题。网络上只能传输二进制数据,那么RPC请求及相应怎么转换成二进制传输?接收到数据二进制数据之后怎么转换成原始的Java对象?

接下来的几篇文章会详细分析Thrift是如何解决上面几个问题的。

2018-09-09 18:3836
  • andy2018-09-09 21:23

    网络学的棒棒哒!远程服务调用很有用