RPC 详解

一、简介

RPC(Remote Procedure Call,远程过程调用)是一种计算机通信协议,允许程序在不同的计算机上执行过程或服务。RPC 使得开发者能够像调用本地函数一样调用远程服务,简化了网络编程的复杂性。使得开发者能够专注于业务逻辑,而不必过多关注底层的网络通信细节。

二、工作原理

  1. 客户端调用:客户端程序调用一个本地的代理(Stub),这个代理的功能是模拟远程过程的调用。
  2. 参数打包:代理将调用的参数打包(序列化),将其转换为可以通过网络传输的格式。这一过程通常称为"编码"或"序列化"。
  3. 发送请求:代理通过网络将请求发送到远程服务器。这个请求包含了要调用的函数名和参数。
  4. 服务器接收请求:远程服务器上的代理(Stub)接收到请求后,进行解码(反序列化),将参数转换回原始格式。
  5. 执行过程:服务器的代理调用实际的服务端过程,并将结果返回给代理。
  6. 结果返回:服务器的代理将结果打包并发送回客户端。
  7. 客户端接收结果:客户端的代理接收到结果后进行解码,并将结果返回给原始调用的客户端程序。

三、优缺点

优点:

  • 透明性:开发者可以像调用本地函数一样调用远程服务,简化了分布式系统的开发。
  • 语言无关性:RPC 可以在不同的编程语言之间进行通信,只要双方遵循相同的协议。
  • 高效性:通过使用序列化和网络传输,RPC 可以高效地进行远程调用。

缺点:

  • 网络延迟:由于涉及网络通信,RPC 调用的延迟通常高于本地调用。
  • 错误处理:网络问题可能导致调用失败,开发者需要处理这些异常情况。
  • 安全性:远程调用可能面临安全风险,需要采取适当的安全措施。

四、常见RPC框架

一、gRPC

gRPC(Google Remote Procedure Call)是一个高性能、开源和通用的远程过程调用(RPC)框架,由Google开发。它基于HTTP/2协议,支持多种编程语言,旨在简化微服务之间的通信。

  • 语言支持:多种编程语言,包括(C++, Java, Python, Go, C#, Node.js)等。
  • 主要特性:
    • 高性能:使用HTTP/2,支持多路复用、流控和头压缩,能够提高网络效率和降低延迟。
    • IDL(接口定义语言):使用Protocol Buffers(protobuf)作为接口定义语言,允许开发者定义服务和消息格式,具有良好的跨语言兼容性。
    • 流式传输:支持四种类型的服务方法:单向 RPC、服务器流式 RPC、客户端流式 RPC 和双向流式 RPC,适合不同的应用场景。
    • 负载均衡和故障恢复:内置了负载均衡和故障恢复机制,能够提高系统的可用性和可靠性。
    • 安全性:支持TLS加密,确保数据在传输过程中的安全性。
示例代码:

下面使用gRPC实现一个Java客户端调用Python服务端的示例,包括服务定义、服务实现和客户端调用。主要有以下几个步骤:

  1. 定义gRPC服务:使用Protocol Buffers(.proto文件)定义服务和消息。
  2. 生成代码:使用protoc编译器生成Java和Python代码。
  3. 实现Python服务端:编写Python代码来实现gRPC服务。
  4. 实现Java客户端:编写Java代码来调用Python服务。
(1)定义 gRPC 服务

首先,需要定义一个 gRPC 服务。创建一个 .proto 文件,例如:example.proto:

bash 复制代码
syntax = "proto3";

package example;

// 定义请求消息
message HelloRequest {
  string name = 1;
}

// 定义响应消息
message HelloResponse {
  string message = 1;
}

// 定义服务
service Greeter {
  rpc SayHello(HelloRequest) returns (HelloResponse);
}
(2)生成代码

使用protoc编译器生成Java和Python代码。确保已经安装了protoc和相应的gRPC插件。

  • 生成Python代码

    bash 复制代码
    python -m grpc_tools.protoc -I. --python_out=./python_out --grpc_python_out=./python_out helloworld.proto
    
    -I.:指定 proto 文件的搜索路径。
    --python_out=./python_out:指定生成的 Python 代码输出目录。
    --grpc_python_out=./python_out:指定生成的 gRPC Python 代码输出目录。
  • 生成Java代码

    bash 复制代码
    protoc --java_out=./java_out --grpc_out=./java_out --plugin=protoc-gen-grpc-java=path/to/protoc-gen-grpc-java  helloworld.proto
    
    --java_out=./java_out:指定生成的 Java 代码输出目录。
    --grpc_out=./java_out:指定生成的 gRPC 代码输出目录。
    --plugin=protoc-gen-grpc-java=path/to/protoc-gen-grpc-java:指定 gRPC Java 插件的路径。

生成的代码将会在指定的输出目录中。对于 Java,通常会生成以下文件:

  • GreeterGrpc.java
  • HelloRequest.java
  • HelloResponse.java

对于 Python,通常会生成以下文件:

  • example_pb2.py
  • example_pb2_grpc.py
(3)实现Python服务端

在 Python 中,使用生成的代码来实现 gRPC 服务,创建一个名为server.py的文件,内容如下:

bash 复制代码
import grpc
from concurrent import futures
import time

# 导入生成的python代码
import example_pb2
import example_pb2_grpc

// 定义一个ExampleService类,继承自 example_pb2_grpc.ExampleServiceServicer,这意味着它实现了 gRPC 服务的基本功能
class ExampleService(example_pb2_grpc.ExampleServiceServicer):
	/**
 	 * SayHello 是一个 RPC 方法,接收两个参数:
 	 * request: 包含客户端发送的数据,通常是一个包含请求字段的对象。
 	 * context: 提供与请求相关的上下文信息,例如元数据、取消请求等。
 	 * 方法返回一个 HelloResponse 对象,该对象是通过 example_pb2 模块定义的。
  	 */
    def SayHello(self, request, context):
        return example_pb2.HelloResponse(message=f"Hello, {request.name}!")

def serve():
	// 创建一个 gRPC 服务器,使用线程池执行器,最大工作线程数为 10
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
    // 将 ExampleService 的实现添加到服务器
    example_pb2_grpc.add_ExampleServiceServicer_to_server(ExampleService(), server)
    // 服务器监听所有可用的网络接口,端口为 50051
    server.add_insecure_port('[::]:50051')
    // 启动服务器
    server.start()
    print("Server is running on port 50051...")
    try:
        while True:
        	# 运行一天
            time.sleep(86400)  
    except KeyboardInterrupt:
    	# 停止服务器
        server.stop(0)

if __name__ == '__main__':
    serve()
(4)实现Java客户端

在Java中,使用生成的代码来实现客户端,创建一个名为Client.java的文件,内容如下:

bash 复制代码
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;

# 导入生成的Java代码
import example.ExampleServiceGrpc;
import example.HelloRequest;
import example.HelloResponse;

public class Client {
    public static void main(String[] args) {
    	// 创建一个 gRPC 管道,连接到本地的 50051 端口
        ManagedChannel channel = ManagedChannelBuilder.forAddress("localhost", 50051)
                .usePlaintext()  // 使用明文传输,不加密
                .build();  // 构建管道
		
		// 创建一个阻塞式的存根,用于调用 ExampleService 服务
        ExampleServiceGrpc.ExampleServiceBlockingStub stub = ExampleServiceGrpc.newBlockingStub(channel);
        
        // 构建请求
        HelloRequest request = HelloRequest.newBuilder().setName("World").build();
        // 通过存根发送请求并接收响应
        HelloResponse response = stub.sayHello(request);
        // 打印从服务器接收到的响应消息
        System.out.println("Response from server: " + response.getMessage());
        
        // 关闭管道
        channel.shutdown();
    }
}
二、Thrift

Thrift 是一个开源的跨语言服务开发框架,最初由 Facebook 开发,旨在简化不同编程语言之间的服务调用。

  • 语言支持:多种编程语言,包括(Java、C++、Python、PHP、Ruby、Go)等。
  • 主要特性:
    • 高效的序列化:Thrift 提供了高效的二进制序列化机制,能够快速地将数据结构转换为字节流,适合高性能的网络通信。
    • 灵活的接口定义:Thrift 使用一种简单的接口定义语言(IDL)来定义服务和数据结构,开发者可以通过 Thrift 编译器生成相应语言的代码。
    • 多种传输和协议:Thrift 支持多种传输方式(如 TCP、HTTP)和协议(如二进制协议、JSON 协议),可以根据需求选择合适的组合。
    • 服务治理:Thrift 提供了服务注册和发现的功能,方便管理和调用分布式服务。
示例代码:

下面使用Thrift实现一个Java客户端调用Python gRPC服务端的示例。主要涉及以下几个步骤,包括定义服务、生成代码、实现服务端和客户端。

(1)定义服务

首先,创建一个IDL(Interface Definition Language)文件,主要用于定义服务接口和数据结构,以便在不同编程语言之间进行高效的远程过程调用(RPC)。例如下面创建的example.thrift文件中主要包含以下几部分:

  • 命名空间:使用 namespace 关键字定义不同编程语言的命名空间。
  • 结构体:使用 struct 定义数据结构,字段使用 类型 名称 的格式,并且每个字段都有一个唯一的 ID(数字)。
  • 服务:使用 service 定义服务接口,服务中的每个方法可以定义输入参数和返回值。
bash 复制代码
namespace py example  // Python 命名空间
namespace java example  // Java 命名空间

// 定义一个简单的结构
struct User {
  1: i32 id,          // 用户 ID
  2: string name,     // 用户名
  3: string email     // 用户邮箱
}

// 定义一个服务
service ExampleService {
  // 创建用户
  void createUser(1: User user),
  
  // 获取用户信息
  User getUser(1: i32 id),
  
  // 更新用户信息
  void updateUser(1: User user),
  
  // 删除用户
  void deleteUser(1: i32 id)
}
(2)生成代码

使用Thrift编译器生成Java和Python代码,生成Python和Java的代码,分别在 gen-py 和 gen-java 目录下。在已经安装了Thrift编译器的情况下,可以使用以下命令:

bash 复制代码
thrift --gen py example.thrift
thrift --gen java example.thrift
(3)实现Python服务端

在Python中实现服务端。创建一个文件 server.py

bash 复制代码
from thrift import Thrift
from thrift.transport import TSocket
from thrift.transport import TTransport
from thrift.protocol import TBinaryProtocol
from example import ExampleService
from example.ttypes import User

class ExampleServiceHandler:
    def __init__(self):
        self.users = {}

    def createUser(self, user):
        self.users[user.id] = user

    def getUser(self, id):
        return self.users.get(id)

    def updateUser(self, user):
        if user.id in self.users:
            self.users[user.id] = user

    def deleteUser(self, id):
        if id in self.users:
            del self.users[id]

if __name__ == '__main__':
    handler = ExampleServiceHandler()
    processor = ExampleService.Processor(handler)
    transport = TSocket.TServerSocket(host='127.0.0.1', port=9090)
    tfactory = TTransport.TBufferedTransportFactory()
    pfactory = TBinaryProtocol.TBinaryProtocolFactory()

    server = TServer.TSimpleServer(processor, transport, tfactory, pfactory)
    print("Starting the server...")
    server.serve()
(4)实现Java客户端

在Java中实现客户端。创建一个文件 Client.java:

bash 复制代码
import org.apache.thrift.TException;
import org.apache.thrift.transport.TSocket;
import org.apache.thrift.transport.TTransport;
import org.apache.thrift.protocol.TBinaryProtocol;
import example.ExampleService;
import example.User;

public class ExampleClient {
    public static void main(String[] args) {
        TTransport transport = new TSocket("127.0.0.1", 9090);
        try {
            transport.open();
            TBinaryProtocol protocol = new TBinaryProtocol(transport);
            ExampleService.Client client = new ExampleService.Client(protocol);

            // 创建用户
            User user = new User();
            user.setId(1);
            user.setName("Alice");
            user.setEmail("alice@example.com");
            client.createUser(user);

            // 获取用户信息
            User retrievedUser = client.getUser(1);
            System.out.println("Retrieved User: " + retrievedUser.getName());

            // 更新用户信息
            user.setEmail("alice_new@example.com");
            client.updateUser(user);

            // 删除用户
            client.deleteUser(1);

        } catch (TException x) {
            x.printStackTrace();
        } finally {
            transport.close();
        }
    }
}
三、Dubbo

Dubbo 是一个开源的高性能 Java RPC 框架,最初由阿里巴巴开发。它主要用于构建分布式服务,支持服务的注册、发现、调用和负载均衡等功能。Dubbo 以其高效、灵活和可扩展性而受到广泛欢迎,尤其是在微服务架构中。

  • 语言支持:Java
  • 主要特性:
    • 高性能:Dubbo 采用了高效的网络通信协议,能够处理大量的并发请求。
    • 服务治理:提供服务注册与发现、负载均衡、容错、限流等功能,帮助开发者管理微服务。
    • 多协议支持:支持多种协议(如 Dubbo、HTTP、REST、gRPC 等),可以根据需求选择合适的协议。
    • 扩展性:支持 SPI(Service Provider Interface)机制,允许用户自定义扩展。
    • 监控与管理:提供监控和管理工具,帮助开发者实时监控服务的状态和性能。
  • 组件
    • Provider:提供服务的应用。
    • Consumer:调用服务的应用。
    • Registry:服务注册中心,负责服务的注册与发现。
    • Monitor:监控中心,收集服务调用的统计信息。
示例代码:

通过将服务提供者和消费者同时注册到nacos注册中心后,直接在消费者服务中掉用提供者服务的接口。主要包含以下几步:

(1)环境准备
  • 一个能正常启动的提供者服务。
  • 一个能正常启动的消费者服务。
  • 一个能正常启动的Nacos 服务器。
(2)配置提供者服务
  1. 首先在提供者服务的 pom.xml 中添加以下依赖:

    bash 复制代码
    <dependencies>
    	<!-- Dubbo 依赖 -->
    	<dependency>
        	<groupId>org.apache.dubbo</groupId>
        	<artifactId>dubbo</artifactId>
        	<version>3.0.0</version> <!-- 请根据需要选择版本 -->
    	</dependency>
    	<dependency>
        	<groupId>org.apache.dubbo</groupId>
        	<artifactId>dubbo-spring-boot-starter</artifactId>
        	<version>3.0.0</version>
    	</dependency>
    
    	<!-- Nacos 依赖 -->
    	<dependency>
        	<groupId>com.alibaba.cloud</groupId>
        	<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        	<version>2.2.0</version> <!-- 请根据需要选择版本 -->
    	</dependency>
    	<dependency>
        	<groupId>com.alibaba.cloud</groupId>
        	<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        	<version>2.2.0</version>
    	</dependency>
    </dependencies>
  2. 然后在提供者服务的 src/main/resources/application.properties 中添加 Nacos 的配置:

    bash 复制代码
    spring.application.name=dubbo-demo
    spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
    spring.cloud.nacos.config.server-addr=127.0.0.1:8848
  3. 在提供者服务中创建一个服务接口,例如 HelloService:

    bash 复制代码
    package com.example.service;
    
    public interface HelloService {
    	String sayHello(String name);
    }
  4. 实现服务接口,创建接口的实现类,使用@DubboService注解来定义这个服务是dubbo服务:

    bash 复制代码
    package com.example.service.impl;
    
    import com.example.service.HelloService;
    import org.apache.dubbo.config.annotation.DubboService;
    
    @DubboService
    public class HelloServiceImpl implements HelloService {
    	@Override
    	public String sayHello(String name) {
        	return "Hello, " + name;
    	}
    }
(3)配置消费者服务
  1. 首先在消费者服务的 pom.xml 中添加以下依赖:

    bash 复制代码
    <dependencies>
    	<!-- Dubbo 依赖 -->
    	<dependency>
        	<groupId>org.apache.dubbo</groupId>
        	<artifactId>dubbo</artifactId>
        	<version>3.0.0</version> <!-- 请根据需要选择版本 -->
    	</dependency>
    	<dependency>
        	<groupId>org.apache.dubbo</groupId>
        	<artifactId>dubbo-spring-boot-starter</artifactId>
        	<version>3.0.0</version>
    	</dependency>
    
    	<!-- Nacos 依赖 -->
    	<dependency>
        	<groupId>com.alibaba.cloud</groupId>
        	<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        	<version>2.2.0</version> <!-- 请根据需要选择版本 -->
    	</dependency>
    	<dependency>
        	<groupId>com.alibaba.cloud</groupId>
        	<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        	<version>2.2.0</version>
    	</dependency>
    </dependencies>
  2. 然后在消费者服务的 src/main/resources/application.properties 中添加 Nacos 的配置:

    bash 复制代码
    spring.application.name=dubbo-consumer
    spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
  3. 使用 Dubbo 进行 RPC 调用,通过创建一个消费者类,并使用@DubboReference注解自动注入远程服务的代理对象来调用提供者服务的service:

    bash 复制代码
    import org.apache.dubbo.config.annotation.DubboReference;
    import com.example.service.HelloService;
    import org.springframework.stereotype.Component;
    
    @Component
    public class HelloConsumer {
    
    	@DubboReference // Dubbo 的注解,标识这是一个服务消费者
    	private HelloService helloService;
    
    	public void sayHello(String name) {
        	String result = helloService.sayHello(name);
        	System.out.println(result);
    	}
    }
四、Spring Cloud

Spring Cloud 是一组工具和框架,旨在帮助开发者构建分布式系统,特别是微服务架构。在 Spring Cloud 中实现 RPC 调用通常可以通过使用 Spring Cloud OpenFeign 或 Spring Cloud Ribbon + Spring Cloud Eureka 来完成。

  • 语言支持:Java
  • 主要特性:
    • 服务注册与发现:Spring Cloud 提供了 Eureka 作为服务注册与发现的解决方案,并结合 Ribbon 或 Spring Cloud LoadBalancer,客户端可以在多个服务实例之间进行负载均衡。
    • 服务调用:通过Spring Cloud Feign 是一个声明式的 Web 服务客户端,可以简化 HTTP 请求的编写。传统的 RESTful 服务调用方式,可以使用RestTemplate 来进行 HTTP 请求。
    • 容错处理:Hystrix提供了熔断器模式的实现,能够在服务调用失败时快速返回默认值,防止服务雪崩。而Resilience4j作为 Hystrix 的替代品,提供了更轻量级的熔断、限流和重试机制。
    • API 网关:Spring Cloud Gateway提供了一个简单的 API 网关解决方案,可以路由请求到不同的微服务,并支持过滤器、负载均衡等功能。
    • 配置管理:Spring Cloud Config提供了集中式的配置管理,支持动态刷新配置,方便微服务的配置管理。
    • 链路追踪:Sleuth 和 Zipkin提供了分布式追踪的能力,可以追踪请求在微服务之间的流转,帮助开发者分析性能瓶颈和故障。
    • 安全性:Spring Security: 可以与 Spring Cloud 结合,提供安全认证和授权机制,保护微服务的访问。
    • 消息驱动:Spring Cloud Stream提供了基于消息中间件的微服务通信方式,支持多种消息中间件(如 RabbitMQ、Kafka),实现异步消息传递。
    • 监控与管理:Spring Boot Actuator: 提供了监控和管理微服务的功能,可以查看服务的健康状态、指标等。
示例代码:

下面使用 Spring Cloud OpenFeign 实现rpc跨服务调用:

(1)添加依赖

在 pom.xml 中添加 OpenFeign 的依赖。

bash 复制代码
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
(2)启用 Feign 客户端

在主应用类上添加 @EnableFeignClients 注解。

bash 复制代码
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
@EnableFeignClients
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
(3)定义 Feign 客户端

创建一个接口并使用 @FeignClient 注解。

bash 复制代码
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

@FeignClient(name = "service-name") // service-name 是要调用的服务名称
public interface MyFeignClient {
    @GetMapping("/api/resource/{id}") // 指定调用服务的接口地址
    Resource getResourceById(@PathVariable("id") Long id);
}
(4)使用 Feign 客户端

在服务中注入并使用 Feign 客户端。通过调用客户端接口中的方法,就会调用到该方法@GetMapping注解中对应的远程服务的接口。

bash 复制代码
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class MyService {
    @Autowired
    private MyFeignClient myFeignClient;

    public Resource fetchResource(Long id) {
        return myFeignClient.getResourceById(id);
    }
}
相关推荐
丁劲犇2 小时前
让 Win10 上网本 Debug 模式 QUDPSocket 信号&槽 收发不丢包的方法总结
网络·windows·qt·网络协议·udp·qudpsocket·丢包
泰山小张只吃荷园2 小时前
期末复习-计算机网络篇
java·网络·网络协议·计算机网络·面试
drebander4 小时前
使用 Netty 实现 RPC 通信框架
网络协议·rpc·netty
泗水长流4 小时前
1.网络知识-IP与子网掩码的关系及计算实例
网络·网络协议·tcp/ip
搬砖的果果6 小时前
数据采集,如何选择适合自己的HTTP代理?
网络·网络协议·http
davidson14716 小时前
http网络服务-swift-Alamofire
网络协议·http·swift
乌南竹6 小时前
四十五:HTTP/2特性概述
网络·网络协议·http
码农客栈6 小时前
qt QCommandLineParser详解
qt
去往火星6 小时前
windows下Qt5自动编译配置QtMqtt环境
开发语言·qt
键盘会跳舞7 小时前
【Qt项目实战】使用脚本拓展CPP应用程序(3)——从外部控制Lua脚本中的循环中断
qt·lua