2023-07-29
原文作者:说好不能打脸 原文地址:https://yinwj.blog.csdn.net/article/details/49453303

1、概述

通过上一篇文章《架构设计:系统间通信(10)——RPC的基本概念》的介绍,相信读者已经理解了基本的RPC概念。为了加深这个理解,后面几篇文章我将详细讲解一款典型的RPC规范的实现Apache Thrift。Apache Thrift的介绍一共分为三篇文章,上篇讲解Apache Thrift的基本使用;中篇讲解Apache Thrift的工作原理(主要围绕Apache Thrift使用的消息格式封装、支持的网络IO模型和它的客户端请求处理方式);下篇对Apache Thrift的不足进行分析,并基于Apache Thrift实现一个自己设计的RPC服务治理的管理方案。这样对我们后续理解Dubbo的服务治理方式会有很好的帮助作用。

2、基本知识

Thrift最初由facebook开发用做系统内各语言之间的RPC框架 。2007年由facebook贡献到apache基金 ,08年5月进入apache孵化器 ,称为Apache Thrift。和其他RPC实现相比,Apache Thrift主要的有点是:支持的语言多(C++, Java, Python, PHP, Ruby, Erlang, Perl, Haskell, C#, Cocoa, Smalltalk等多种语言)、并发性能高(还记得上篇文章中,我们提到的影响RPC性能的几个关键点吗?)。

为了支持多种语言,Apache Thrift有一套自己的接口定义语言,并且通过Apache Thrift的代码生成程序,能够生成各种编程语言的代码。这样是保证各种语言进行通讯的前提条件。为了能够实现简单的Apache Thrift实例,首先我们就需要讲解一下Apache Thrift的IDL。

2-1、Thrift代码生成程序安装

如果您是在windows环境下运行进行Apache Thrift的试验,那么您无需安装任何工具,直接下载Apache Thrift在windows下的代码生成程序http://www.apache.org/dyn/closer.cgi?path=/thrift/0.9.3/thrift-0.9.3.exe(在这篇文章写作时,使用的是Apache Thrift的0.9.3版本);如果您运行在Linux系统下,那么下载http://www.apache.org/dyn/closer.cgi?path=/thrift/0.9.3/thrift-0.9.3.tar.gz,并进行编译、安装(过程很简单,这里就不再赘述了)。安装后记得添加运行位置到环境变量中。

2-2、IDL格式概要

以下是一个简单的IDL文件定义:

    # 命名空间的定义 注意‘java’的关键字
    namespace java testThrift.iface
    
    # 结构体定义
    struct Request {
        1:required string paramJSON;
        2:required string serviceName;
    }
    
    # 另一个结构体定义
    struct Reponse {
        1:required  RESCODE responeCode;
        2:required  string responseJSON;
    }
    
    # 异常描述定义
    exception ServiceException {
        1:required EXCCODE exceptionCode;
        2:required string exceptionMess;
    }
    
    # 枚举定义
    enum RESCODE {
        _200=200;
        _500=500;
        _400=400;
    }
    
    # 另一个枚举
    enum EXCCODE {
        PARAMNOTFOUND = 2001;
        SERVICENOTFOUND = 2002;
    }
    
    # 服务定义
    service HelloWorldService {
        Reponse send(1:Request request) throws (1:ServiceException e);
    }

以上IDL文件是可以直接用来生成各种语言的代码的。下面给出常用的各种不同语言的代码生成命令:

    # 生成java
    thrift-0.9.3 -gen java ./demoHello.thrift
    
    # 生成c++
    thrift-0.9.3 -gen cpp ./demoHello.thrift
    
    # 生成php
    thrift-0.9.3 -gen php ./demoHello.thrift
    
    # 生成node.js
    thrift-0.9.3 -gen js:node ./demoHello.thrift
    
    # 生成c#
    thrift-0.9.3 -gen csharp ./demoHello.thrift
    
    # 您可以通过以下命令查看生成命令的格式
    thrift-0.9.3 -help

2-2-1、基本类型

基本类型就是:不管哪一种语言,都支持的数据形式表现。Apache Thrift中支持以下几种基本类型:

  • bool: 布尔值 (true or false), one byte
  • byte: 有符号字节
  • i16: 16位有符号整型
  • i32: 32位有符号整型
  • i64: 64位有符号整型
  • double: 64位浮点型
  • string: 字符串/字符数组
  • binary: 二进制数据(在java中表现为java.nio.ByteBuffer)

2-2-2、struct结构

在面向对象语言中,表现为“类定义”;在弱类型语言、动态语言中,表现为“结构/结构体”。定义格式如下:

    struct <结构体名称> {
            <序号>:[字段性质] <字段类型> <字段名称> [= <默认值>] [;|,]
    }

实例:

    struct Request {
        1:required binary paramJSON;
        2:required string serviceName
        3:optional i32 field1 = 0;
        4:optional i64 field2,
        5: list<map<string , string>> fields3
    }
  • 结构体名称:可以按照您的业务需求,给定不同的名称(区分大小写)。但是要注意,一组IDL定义文件中结构体名称不能重复,且不能使用IDL已经占用的关键字(例如required 、struct 等单词)。
  • 序号:序号非常重要。正整数,按照顺序排列使用。这个属性在Apache Thrift进行序列化的时候被使用。
  • 字段性质:包括两种关键字:required 和 optional,如果您不指定,那么系统会默认为required。required表示这个字段必须有值,并且Apache Thrift在进行序列化时,这个字段都会被序列化;optional表示这个字段不一定有值,且Apache Thrift在进行序列化时,这个字段只有有值的情况下才会被序列化。
  • 字段类型:在struct中,字段类型可以是某一个基础类型,也可以是某一个之前定义好的struct,还可以是某种Apache Thrift支持的容器(set、map、list),还可以是定义好的枚举。字段的类型是必须指定的。
  • 字段名称:字段名称区分大小写,不能重复,且不能使用IDL已经占用的关键字(例如required 、struct 等单词)。
  • 默认值:您可以为某一个字段指定默认值(也可以不指定)。
  • 结束符:在struct中,支持两种结束符,您可以使用“;”或者“,”。当然您也可以不使用结束符(Apache Thrift代码生成程序,会自己识别到)

2-2-3、containers集合/容器

Apache Thrift支持三种类型的容器,容器在各种编程语言中普遍存在:

  • list< T >:有序列表(JAVA中表现为ArrayList),T可以是某种基础类型,也可以是某一个之前定义好的struct,还可以是某种Apache Thrift支持的容器(set、map、list),还可以是定义好的枚举。有序列表中的元素允许重复。
  • set< T >:无序元素集合(JAVA中表现为HashSet),T可以是某种基础类型,也可以是某一个之前定义好的struct,还可以是某种Apache Thrift支持的容器(set、map、list),还可以是定义好的枚举。无序元素集合中的元素不允许重复,一旦重复后一个元素将覆盖前一个元素。
  • map

2-2-4、enmu枚举

    enum <枚举名称> {
            <枚举字段名> = <枚举值>[;|,]
    }

示例如下:

    enum RESCODE {
        _200=200;
        _500=500;
        _400=400;
    }

2-2-5、常量定义

Apache Thrift允许定义常量。常量的关键字为“const”,常量的类型可以是Apache Thrift的基础类型,也可以是某一个之前定义好的struct,还可以是某种Apache Thrift支持的容器(set、map、list),还可以是定义好的枚举。示例如下:

    const i32 MY_INT_CONST = 111111; 
    
    const i64 MY_LONG_CONST = 11111122222222333333334444444;
    
    const RESCODE MY_RESCODE = RESCODE._200;

2-2-6、exception 异常

Apache Thrift的exception,主要在定义服务接口时使用。其定义方式类似于struct(您可以理解成,把struct关键字换成exception关键字即可),示例如下:

    exception ServiceException {
        1:required EXCCODE exceptionCode;
        2:required string exceptionMess;
    }

2-2-7、service 服务接口

Apache Thrift中最重要的IDL定义之一。在后续的代码生成阶段,通过IDL定义的这些服务将构成Apache Thrift客户端调用Apache Thrift服务端的基本远端过程。service服务接口的定义形式如下所示:

    service <服务名称> {
        <void | 返回指类型> <服务方法名>([<入参序号>:[required | optional] <参数类型> <参数名> ...]) [throws ([<异常序号>:[required | optional] <异常类型> <异常参数名>...])]
    }
  • 服务名称:服务名可以按照您的业务需求自行制定,注意服务名是区分大小写的。IDL中服务名称只有两个限制,就是不能重复使用相同的名称,不能使用IDL已经占用的关键字(例如required 、struct 等单词)。
  • 返回值类型:如果这个调用方法没有返回类型,那么可以关键字“void”; 可以是Apache Thrift的基础类型,也可以是某一个之前定义好的struct,还可以是某种Apache Thrift支持的容器(set、map、list),还可以是定义好的枚举。
  • 服务方法名:服务方法名可以根据您的业务需求自定制定,注意区分大小写。在同一个服务中, 不能重复使用一个服务方法名命名多个方法(一定要注意) ,不能使用IDL已经占用的关键字。
  • 服务方法参数:<入参序号>:[required | optional] <参数类型> <参数名>。注意和struct中的字段定义相似,可以指定required或者optional;如果不指定则系统默认为required 。如果一个服务方法中有多个参数名,那么这些参数名称不能重复。
  • 服务方法异常:throws ([<异常序号>:[required | optional] <异常类型> <异常参数名>。throws关键字是服务方法异常定义的开始点。在throws关键字后面,可以定义1个或者多个不同的异常类型。

Apache Thrift服务定义的示例如下:

    service HelloWorldService {
        Reponse send(1:Request request) throws (1:ServiceException e);
    }

2-2-8、namespace命名空间

Apache Thrift支持为不同语言制定不同的命名空间:

    namespace java testThrift.iface
    
    namespace php testThrift.iface
    
    namespace cpp testThrift.iface

2-2-9、注释

Apache Thrift 支持多种风格的注释。这是为了适应不同语言背景的开发者:

    /*
    * 注释方式1:
    **/
    
    // 注释方式2
    
    # 注释方式3

2-2-10、include关键字

如果您的整个工程中有多个IDL定义文件(IDL定义文件的文件名可以随便取)。那么您可以使用include关键字,在IDL定义文件A中,引入一个其他的IDL文件:

    include "other.thrift"

请注意,一定使用双引号(不要用成中文的双引号咯),并且不使用“;”或者“,”结束符。

以上就是IDL基本的语法了,由于篇幅原因不可能把每种语法、每一个细节都讲到,但是以上的语法要点已经足够您编辑一个适应业务的,灵活的IDL定义了。如果您需要了解更详细的Thrift IDL语法,可以参考官方文档的讲述:http://thrift.apache.org/docs/idl

2-3、最简单的Thrift代码

  • 定义Thrift中业务接口HelloWorldService.Iface的实现:
    package testThrift.impl;
    
    import org.apache.commons.logging.Log;
    import org.apache.commons.logging.LogFactory;
    import org.apache.thrift.TException;
    
    import testThrift.iface.HelloWorldService.Iface;
    import testThrift.iface.RESCODE;
    import testThrift.iface.Reponse;
    import testThrift.iface.Request;
    
    /**
     * 我们定义了一个HelloWorldService.Iface接口的具体实现。<br>
     * 注意,这个父级接口:HelloWorldService.Iface,是由thrift的代码生成工具生成的<br>
     * 要运行这段代码,请导入maven-log4j的支持。否则修改LOGGER.info方法
     * @author yinwenjie
     */
    public class HelloWorldServiceImpl implements Iface {
        /**
         * 日志
         */
        private static final Log LOGGER = LogFactory.getLog(HelloWorldServiceImpl.class);
    
        /**
         * 在接口定义中,只有一个方法需要实现。<br>
         * HelloWorldServiceImpl.send(Request request) throws TException <br>
         * 您可以理解成这个接口的方法接受客户端的一个Request对象,并且在处理完成后向客户端返回一个Reponse对象<br>
         * Request对象和Reponse对象都是由IDL定义的结构,并通过“代码生成工具”生成相应的JAVA代码。
         */
        @Override
        public Reponse send(Request request) throws TException {
            /*
             * 这里就是进行具体的业务处理了。
             * */
            String json = request.getParamJSON();
            String serviceName = request.getServiceName();
            HelloWorldServiceImpl.LOGGER.info("得到的json:" + json + " ;得到的serviceName: " + serviceName);
    
            // 构造返回信息
            Reponse response = new Reponse();
            response.setResponeCode(RESCODE._200);
            response.setResponseJSON("{\"user\":\"yinwenjie\"}");
            return response;
        } 
    }

各位可以看到,上面一段代码中具体业务和过程和普通的业务代码没有任何区别。 甚至这段代码的实现都不知道自己将被Apache Thrift框架中的客户端调用

  • 然后我们开始书写Apache Thrift的服务器端代码:
    package testThrift.man;
    
    import java.util.concurrent.Executors;
    
    import org.apache.commons.logging.Log;
    import org.apache.commons.logging.LogFactory;
    import org.apache.log4j.BasicConfigurator;
    import org.apache.thrift.TProcessor;
    import org.apache.thrift.protocol.TBinaryProtocol;
    import org.apache.thrift.server.TThreadPoolServer;
    import org.apache.thrift.server.TThreadPoolServer.Args;
    import org.apache.thrift.transport.TServerSocket;
    
    import testThrift.iface.HelloWorldService;
    import testThrift.iface.HelloWorldService.Iface;
    import testThrift.impl.HelloWorldServiceImpl;
    
    public class HelloBoServerDemo {
    
        static {
            BasicConfigurator.configure();
        }
    
        /**
         * 日志
         */
        private static final Log LOGGER =LogFactory.getLog(HelloBoServerDemo.class);
    
        public static final int SERVER_PORT = 9111;
    
        public void startServer() {
            try {
                HelloBoServerDemo.LOGGER.info("看到这句就说明thrift服务端准备工作 ....");
    
                // 服务执行控制器(只要是调度服务的具体实现该如何运行)
                TProcessor tprocessor = new HelloWorldService.Processor<Iface>(new HelloWorldServiceImpl());
    
                // 基于阻塞式同步IO模型的Thrift服务,正式生产环境不建议用这个
                TServerSocket serverTransport = new TServerSocket(HelloBoServerDemo.SERVER_PORT);
    
                // 为这个服务器设置对应的IO网络模型、设置使用的消息格式封装、设置线程池参数
                Args tArgs = new Args(serverTransport);
                tArgs.processor(tprocessor);
                tArgs.protocolFactory(new TBinaryProtocol.Factory());
                tArgs.executorService(Executors.newFixedThreadPool(100));
    
                // 启动这个thrift服务
                TThreadPoolServer server = new TThreadPoolServer(tArgs);
                server.serve();
            } catch (Exception e) {
                HelloBoServerDemo.LOGGER.error(e);
            }
        }
    
        /**
         * @param args
         */
        public static void main(String[] args) {
            HelloBoServerDemo server = new HelloBoServerDemo();
            server.startServer();
        }
    }

以上的代码有几点需要说明:

  1. TBinaryProtocol:这个类代码Apache Thrift特有的一种二进制描述格式。它的特点是传输单位数据量所使用的传输量更少。Apache Thrift还支持多种数据格式,例如我们熟悉的JSON格式。后文我们将详细介绍Apache Thrift中的数据格式。
  2. tArgs.executorService():是不是觉得这个executorService很熟悉,是的这个就是JAVA JDK 1.5+ 后java.util.concurrent包提供的异步任务调度服务接口,Java标准线程池ThreadPoolExecutor就是它的一个实现。
  3. server.serve(),由于是使用的同步阻塞式网络IO模型,所以这个应用程序的主线程执行到这句话以后就会保持阻塞状态了。不过下层网络状态不出现错误,这个线程就会一直停在这里。

另外,同HelloWorldServiceImpl 类中的代码,请使用Log4j。如果您的测试工程里面没有Log4j,请改用System.out。

  • 接下来我们进行最简单的Apache Thrift Client的代码编写:
    package testThrift.client;
    
    import org.apache.commons.logging.Log;
    import org.apache.commons.logging.LogFactory;
    import org.apache.log4j.BasicConfigurator;
    import org.apache.thrift.protocol.TBinaryProtocol;
    import org.apache.thrift.protocol.TProtocol;
    import org.apache.thrift.transport.TSocket;
    
    import testThrift.iface.HelloWorldService;
    import testThrift.iface.Reponse;
    import testThrift.iface.Request;
    
    /**
     * 同样是基于同步阻塞模型的thrift client。
     * @author yinwenjie
     */
    public class HelloClient {
    
        static {
            BasicConfigurator.configure();
        }
    
        /**
         * 日志
         */
        private static final Log LOGGER = LogFactory.getLog(HelloClient.class);
    
        public static final void main(String[] args) throws Exception {
            // 服务器所在的IP和端口
            TSocket transport = new TSocket("127.0.0.1", 9111);
            TProtocol protocol = new TBinaryProtocol(transport);
    
            // 准备调用参数
            Request request = new Request("{\"param\":\"field1\"}", "\\mySerivce\\queryService");
            HelloWorldService.Client client = new HelloWorldService.Client(protocol);
    
            // 准备传输
            transport.open();
            // 正式调用接口
            Reponse reponse = client.send(request);
            // 一定要记住关闭
            transport.close();
    
            HelloClient.LOGGER.info("response = " + reponse);
        }
    }
  • Thrift客户端所使用的网络IO模型,必须要与Thrift服务器端所使用的网络IO模型一致。也就是说服务器端如果使用的是阻塞式同步IO模型,那么客户端就必须使用阻塞式同步IO模型。
  • Thrift客户端所使用的消息封装格式,必须要与Thrift服务器端所使用的消息封装格式一直。也就是说服务器端如果使用的是二进制流的消息格式TBinaryProtocol,那么客户端就必须同样使用二进制刘的消息格式TBinaryProtocol。
  • 其它的代码要么就是由IDL定义并由Thrift的代码生成工具生成;要么就不是重要的代码,所以为了节约篇幅就没有必要再贴出来了。以下是运行效果。
  • 服务器端运行效果

202307292150369571.png

  • 服务器端收到客户端请求后,取出线程池中的线程进行运行

202307292150405662.png

请注意服务器端在收到客户端请求后的运行方式:取出一条线程池中的线程,并且运行这个服务接口的具体实现。接下来我们马上介绍Apache Thrift的工作细节。

(接下文)

阅读全文