【橘子分布式】Thrift RPC(编程篇)

发布于:2025-07-16 ⋅ 阅读:(17) ⋅ 点赞:(0)

一、简介

之前我们研究了一下thrift的一些知识,我们知道他是一个rpc框架,他作为rpc自然是提供了客户端到服务端的访问以及两端数据传输的消息序列化,消息的协议解析和传输,所以我们今天就来了解一下他是如何实现这些功能,并且如何在实际代码中使用。
我们需要搭建环境。

1. 安装Thrift 作用:把IDL语言描述的接口内容,生成对应编程语言的代码,简化开发。我们已经介绍了在mac如何使用brew安装了。
2. 引入依赖    作用:引入thrift针对于某一种编程语言的封装 (网络通信 协议【序列化】)

二、结构和模块

1、Thrift核心对象

1. TTransport
   作用:底层封装了rpc的网络通信能力,具体实现有下面三种
   TSocket 阻塞IO通信
   TNonblockdingTransport 非阻塞网络通信
   TFramedTransport 加入了封帧的操作 (压缩后 数据边界问题)  
2. TProtocol
   作用:底层封装了rpc的关于协议的处理 (序列化方式)
   TBinayProtocol 二进制进行序列化
   TCompactProtocol 压缩方式 处理二进制 
   TJSONProtocol  JSON进行序列化
 这里需要说明一下,当我们的序列化方式走了TCompactProtocol压缩处理的时候,你最好使用TFramedTransport来处理网络通信,因为一旦数据开启了压缩,那就可能传输很大量的数据,数据的模样和原始的不同,此时可能因为缓冲区大小等等出现半包粘包问题。需要解决封帧。其他的方式一般没啥,因为rpc数据量不会很大,一般没有半包粘包,具体问题具体分析吧。而且你能看到他提供多种协议,并不是用的http协议这个最常见的,这个没啥说的,高性能的网络协议,一般都不用http,http的优势是简单通用。
3. TProcessor
   作用:进行业务处理,把通信数据 和 业务功能整合在一起。换句话说我们的业务代码就在这里,其实就是把数据拿过来了,开始业务处理了。
4. TServer 服务端
	服务端就是用上面的三个组件,组合起来实现具体的服务端逻辑,根据不同的实现有异步的有同步的等等,并且发布服务出去。

2、项目结构

我们在编码之前需要明确一点,就是作为rpc(Remote Procedure Call),远程调用必然存在一个服务的提供者和一个服务的调用者。所以我们必须有的两个模块是rpc-server和rpc-client。
在实际开发的时候,我们比如说服务调用者和提供者可能有相同的数据比如User这个类,那你不能在两边各自都写一个吧,肯定是写一份维护就好了。所以我们还需要一个rpc-common的模块,用来声明那些公用的类,包括一些公用的实体类,还有发起rpc调用的service代理类等等。
于是我们就抽象出了三个模块。

1. thrift-client  代表的是服务的调用者,客户端要想本地方法那样调用远端方法,底层必然是代理。
2. thrift-server  代表的是服务的提供者
	2.1. 实现服务接口 :idl语言生成的
	2.2. 创建服务端代码
3. thrift-common  RPC编程共有的内容 1,实体类型 2,服务接口
	3.1. 通过IDL语言 定义 client与服务端 共用的数据类型 和 服务接口 
	3.2. client server端需要引入 common模块

我们这里是以同一个工程下的模块化的形式搭建项目,实际上你也可以单独创建不同的工程。

3、引入依赖

我们这里是一个项目多个模块,所以我直接在父级项目引入了。

<modules>
    <module>thrift-client</module>
    <module>thrift-server</module>
    <module>thrift-common</module>
</modules>

<properties>
    <thrift.version>0.22.0</thrift.version>
    <javax.annotation.version>1.3.2</javax.annotation.version>
    <slf4j-api.version>1.7.32</slf4j-api.version>
    <logback-classic.version>1.2.9</logback-classic.version>
    <junit.version>3.8.1</junit.version>

    <maven.compiler.source>11</maven.compiler.source>
    <maven.compiler.target>11</maven.compiler.target>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<dependencies>
	<!-- 引入thrift针对于某一种编程语言的封装,java就是maven管理jar,你要是用py开发,那就是pip (网络通信 协议【序列化】)-->
    <dependency>
        <groupId>org.apache.thrift</groupId>
        <artifactId>libthrift</artifactId>
        <version>${thrift.version}</version>
    </dependency>

    <dependency>
        <groupId>javax.annotation</groupId>
        <artifactId>javax.annotation-api</artifactId>
        <version>${javax.annotation.version}</version>
    </dependency>

    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-api</artifactId>
        <version>${slf4j-api.version}</version>
    </dependency>

    <dependency>
        <groupId>ch.qos.logback</groupId>
        <artifactId>logback-classic</artifactId>
        <version>${logback-classic.version}</version>
    </dependency>

    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>${junit.version}</version>
        <scope>test</scope>
    </dependency>

</dependencies>

4、构建thrift文件

此时我们需要构建一些用来编程的代码。首先我们的实体类和service要放在common里面,然后client和server引用common即可。我们实现一个保存用户User和根据用户名查看用户的实现。
因为我们是java开发,所以我们其实目标就是一个User类,和一个service类,service里面提供void save(User user)和User getUserByName(String name)这样的东西。所以我们就基于这个构建IDL,然后用thrift翻译出来java代码即可。根据我们昨天对于IDL的语法梳理,可以这么编写.thrift文件。
名称为rpc-thrift.thrift

# 用来构建rpc的thrift文件

namespace java com.levi
struct User {
    1: required string name,
    2: required string password
}
service UserService {
    /**
     * 注册用户
     * @param user 用户信息
     * @return 注册结果
     */
    void registerUser(1: User user)

    /**
     * 获取用户信息
     * @param name 用户名称
     * @return 用户信息
     */
    User getUser(1: string name)
}

然后我们进入到这个文件的目录,使用如下命令:

thrift --gen java rpc-thrift.thrift

生成如下结果:
在这里插入图片描述
我们把这些转移到类目录下即可,然后删除这些即可。
在这里插入图片描述
然后我们maven install,再把common这个依赖给client和server模块引入即可。

<dependencies>
    <dependency>
        <groupId>com.levi</groupId>
        <artifactId>thrift-common</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
</dependencies>

此时我们就完成了对应的项目结构搭建,下面我们来实现业务。

三、业务实现

1、服务端实现

package com.levi.service;

import com.levi.User;
import com.levi.UserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 注意这里实现的接口不是UserService,而是UserService.Iface,这是thrift生成的接口
 * 按照thrift的规范,每个接口都有一个对应的Iface接口,用于封装接口的方法,方便调用
 */
public class UserServiceImpl implements UserService.Iface{

    Logger logger = LoggerFactory.getLogger(UserServiceImpl.class);

    @Override
    public void registerUser(User user) {
        logger.info("register user:{}",user);
    }

    @Override
    public User getUser(String name) {
        logger.info("get user:{}",name);
        return new User(name,"123456");
    }
}

有了业务类之后,就启动服务端。

package com.levi;

import com.levi.service.UserServiceImpl;
import org.apache.thrift.protocol.TBinaryProtocol;
import org.apache.thrift.server.TSimpleServer;
import org.apache.thrift.transport.TServerSocket;

public class TestServer {
    public static void main(String[] args) {
        // 创建服务端,监听端口8888,其实就是对应的TTransport组件,通信功能在此封装
        try (TServerSocket tServerSocket = new TServerSocket(8888);){
            // 协议工厂,用于封装协议,比如二进制协议、JSON协议等,此处使用二进制协议,其实对应的就是TProtocol组件,协议功能在此封装
            TBinaryProtocol.Factory protocolFactory = new TBinaryProtocol.Factory();
            // 处理器工厂,用于封装处理器,比如UserService.Processor,其实对应的就是TProcessor组件,处理器功能在此封装。绑定我们的业务逻辑。
            UserService.Processor processor = new UserService.Processor(new UserServiceImpl());
            // 封装参数
            TSimpleServer.Args arg = new TSimpleServer.Args(tServerSocket).protocolFactory(protocolFactory).processor(processor);
            // 构建服务端起动器,其实对应的就是TServer组件,服务端功能在此封装,发布服务出去,等待客户端连接
            TSimpleServer tSimpleServer = new TSimpleServer(arg);
            tSimpleServer.serve();
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

2、客户端实现

package com.levi;

import org.apache.thrift.protocol.TBinaryProtocol;
import org.apache.thrift.protocol.TProtocol;
import org.apache.thrift.transport.TSocket;
import org.apache.thrift.transport.TTransport;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class TestClient {
    private static Logger logger = LoggerFactory.getLogger(TestClient.class);

    public static void main(String[] args) {
        // 创建服务端,监听端口8888,其实就是对应的TTransport组件,通信功能在此封装
        try (TTransport tTransport = new TSocket("localhost", 8888);){
            // 协议工厂,用于封装协议,比如二进制协议、JSON协议等,此处使用二进制协议,其实对应的就是TProtocol组件,协议功能在此封装,和服务端保持一致
            TProtocol tProtocol = new TBinaryProtocol(tTransport);
            // rpc的本质,就是像调用本地服务一样调用远程服务,此处就是一个体现,但是本质都是代理模式,只不过这个代理thrift封装了
            UserService.Client userService = new UserService.Client(tProtocol);
            tTransport.open();
            User user = userService.getUser("levi");
            logger.info("client get user is {}",user);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

启动服务端之后开始监听,然后启动客户端。输出如下:

client get user is User(name:levi, password:123456)

没毛病。

四、重构

实际上开发的时候,我们一般是有些业务公用的,可能RPC调用在用其他的业务,所以最好是封装隔离开,我们一般是在RPC这里直接调用,实在没得用再写。
于是我们把thrift的这个实现改为UserServiceImplRPC

package com.levi.service;

import com.levi.User;
import com.levi.UserService;

/**
 * 注意这里实现的接口不是UserService,而是UserService.Iface,这是thrift生成的接口
 * 按照thrift的规范,每个接口都有一个对应的Iface接口,用于封装接口的方法,方便调用
 */
public class UserServiceImplRPC implements UserService.Iface{

    @Override
    public void registerUser(User user) {
        UserServiceImpl userService = new UserServiceImpl();
        userService.registerUser(user);
    }

    @Override
    public User getUser(String name) {
        UserServiceImpl userService = new UserServiceImpl();
        return userService.getUser(name);
    }
}

常规的业务放在UserServiceImpl

package com.levi.service;

import com.levi.User;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 注意这里实现的接口不是UserService,而是UserService.Iface,这是thrift生成的接口
 * 按照thrift的规范,每个接口都有一个对应的Iface接口,用于封装接口的方法,方便调用
 */
public class UserServiceImpl {

    Logger logger = LoggerFactory.getLogger(UserServiceImpl.class);

    public void registerUser(User user) {
        logger.info("register user:{}",user);
    }

    public User getUser(String name) {
        logger.info("get user:{}",name);
        return new User(name,"123456");
    }
}

这样方便维护也比较清晰。


网站公告

今日签到

点亮在社区的每一天
去签到