简易版RPC框架实现

RPC框架:核心概念与使用价值(新手易懂版)

RPC框架是分布式系统中的核心概念------简单来说,RPC(远程过程调用)框架是让你像调用本地函数一样调用「另一台服务器上的函数」的工具,无需关心网络通信、数据序列化等底层细节。

一、先搞懂:什么是RPC框架?

1. 核心定义(人话版)

  • 本地调用 :你在Java代码里调用 Math.max(1,2),或在Python里调用 list.append(3),都是「进程内」的函数调用,直接在内存中完成;
  • 远程调用:你的代码部署在服务器A,需要调用服务器B上的「用户余额查询函数」,这就是「跨机器」的远程调用;
  • RPC框架 :封装了远程调用的所有底层细节(网络传输、数据格式转换、异常处理等),让你写 userService.getBalance(1001) 时,感觉就像调用本地函数一样,完全感知不到「跨机器」的存在。

2. RPC的核心特征

特征 解释
透明性 调用远程函数 ≈ 调用本地函数,开发者无需关注网络、序列化等底层逻辑
跨语言/跨平台 比如Java服务可以调用Python服务,Windows服务器可以调用Linux服务器
高性能 相比HTTP接口(如RESTful),RPC通常采用更高效的序列化协议和通信模型
可扩展性 内置负载均衡、服务发现、熔断降级等分布式必备能力

3. RPC框架的工作流程(简化版)

用「客户端调用服务器B的余额查询函数」举例,RPC框架会自动完成以下步骤:

yaml 复制代码
graph TD
    A[客户端代码调用 getBalance(1001)] --> B[RPC客户端SDK]
    B --> C[序列化参数:把1001转成二进制字节流]
    C --> D[网络通信:通过TCP/UDP把字节流发给服务器B]
    D --> E[RPC服务器端SDK]
    E --> F[反序列化:把字节流转回1001]
    F --> G[调用服务器B的本地getBalance函数]
    G --> H[得到结果:比如99元]
    H --> I[序列化结果:99转字节流]
    I --> J[网络回传:把结果发给客户端]
    J --> K[反序列化结果:字节流转99]
    K --> L[客户端代码拿到99元]

整个过程中,开发者只需要写 getBalance(1001) 和处理返回值,其余步骤全由RPC框架自动完成。

二、什么时候不用RPC框架?

RPC不是万能的,以下场景优先用HTTP接口(如RESTful):

  • 跨平台开放接口:比如给第三方开发者提供的接口(如微信支付API),HTTP+JSON更通用,无需对方接入你的RPC SDK;
  • 简单的小项目:单机/少量服务的项目,远程调用少,没必要引入复杂的RPC框架;
  • 需要浏览器访问的接口:浏览器只能发起HTTP请求,无法直接调用RPC接口。

三、主流RPC框架举例

不同语言/场景有不同的主流框架,核心功能类似:

框架 语言/特点
Dubbo 阿里开源,Java为主,国内微服务主流框架,适配Spring生态
gRPC Google开源,跨语言(C++/Java/Python/Go等),基于HTTP/2+Protobuf
Thrift Facebook开源,跨语言,支持多种传输协议和序列化协议
Spring Cloud OpenFeign Java,基于HTTP的伪RPC(本质是HTTP封装),适配Spring Cloud生态
Motan 微博开源,Java/Go,支持多注册中心、跨语言

总结(核心要点)

  1. RPC框架的本质:封装远程调用的底层细节,让「跨机器调用函数」像「本地调用」一样简单;
  2. 使用RPC的核心原因
    • 提效:避免手写网络、序列化等重复代码;
    • 稳性:内置服务发现、熔断、负载均衡等分布式能力;
    • 高性能:更优的通信/序列化协议,适配高并发;
    • 适配微服务:支撑大规模服务间的远程调用;
  3. RPC vs HTTP:RPC适合内部微服务调用,HTTP适合开放接口/浏览器访问;
  4. 核心价值:让开发者聚焦业务逻辑,而非分布式底层细节。

简单记:RPC框架是分布式系统的「远程调用管家」,帮你搞定跨机器调用的所有脏活累活。

下面用一张图来说下其执行流程

  1. 消费者(客户端)发起调用

    • 消费者 :业务代码所在的客户端,它只需要调用 orderService.order(...) 方法,完全感知不到这是一个远程调用。
    • 代理对象 orderService:这是RPC框架生成的动态代理,它会拦截这个调用请求,把本地调用转换成RPC框架能处理的远程请求。
  2. RPC框架客户端处理

    • 请求客户端(HTTP/其他):代理对象将调用信息(接口名、方法名、参数)交给请求客户端,由它负责网络通信。
    • 序列化/反序列化:在发送请求前,请求客户端会把调用信息序列化为二进制字节流(比如用JSON、Protobuf),以便在网络中高效传输。
    • 网络发送:请求客户端通过HTTP或其他协议(如TCP),将序列化后的请求发送给服务端的Web服务器。
  3. RPC框架服务端处理

    • Web服务器:监听并接收客户端的请求,然后将请求转发给「请求处理器」。
    • 请求处理器:接收请求后,先对字节流进行反序列化,还原出原始的调用信息(接口名、方法名、参数)。
    • 本地服务注册器 :根据接口名 orderService,在注册器中找到对应的本地业务实现类(服务提供者的真实代码)。
  4. 服务提供者执行方法

    • 提供者(orderService接口/order方法) :本地服务注册器定位到实现类后,调用其 order 方法执行真实的业务逻辑(比如创建订单、查询数据)。

代码后续会上传到github,这里先记录下项目结构与模块划分,方便大家理解。

项目结构与模块划分

这个简易RPC框架项目分为四个主要模块,各模块职责明确,分工协作:

1. 核心框架模块(feng-rpc-easy)

这是RPC框架的核心实现,包含以下子模块:

1.1 数据模型层(model)
  • RpcRequest.java:RPC请求数据结构,包含服务名、方法名、参数类型和参数值
  • RpcResponse.java:RPC响应数据结构,包含响应数据、响应消息、异常信息和数据类型
1.2 序列化层(serializer)
  • Serializer.java:序列化接口,定义了序列化和反序列化方法
  • JDKSerializer.java:JDK序列化实现,使用ObjectOutputStream和ObjectInputStream进行序列化和反序列化
1.3 服务注册层(registry)
  • LocalRegistry.java:本地服务注册表,使用ConcurrentHashMap存储服务名和对应的实现类
1.4 代理层(proxy)
  • ServiceProxy.java:动态代理实现,实现InvocationHandler接口,负责将方法调用转换为RPC请求
  • ServiceProxyFactory.java:代理工厂,用于创建服务代理实例
1.5 服务器层(server)
  • HttpServer.java:HTTP服务器接口,定义了启动服务器的方法
  • VertxHttpServer.java:使用Vert.x实现的HTTP服务器
  • HttpServerHandler.java:HTTP请求处理器,负责处理RPC请求的核心逻辑

2. 公共模块(example-common)

定义了客户端和服务端共享的数据模型和服务接口:

  • User.java:用户数据模型
  • UserService.java:用户服务接口

3. 服务提供者模块(example-provider)

实现了服务接口并提供服务:

  • UserServiceImpl.java:UserService接口的实现类
  • EasyProviderExample.java:服务提供者启动类,负责注册服务和启动服务器

4. 服务消费者模块(example-consumer)

调用远程服务:

  • UserServiceProxy.java:静态代理实现(已被动态代理替代)
  • EasyConsumerExample.java:服务消费者启动类,负责获取服务代理并调用服务

核心实现原理

1. 服务注册与发现

服务提供者在启动时,通过LocalRegistry.register()方法将服务接口名和实现类注册到本地注册表中。服务消费者通过服务接口名从注册表中获取服务实现类。

2. 序列化与反序列化

使用JDK自带的序列化机制,将Java对象转换为字节数组进行网络传输,接收方再将字节数组转换回Java对象。

3. 动态代理

客户端使用JDK动态代理生成服务代理实例,当调用代理方法时,代理会将方法调用转换为RpcRequest对象,然后通过HTTP发送到服务端。

4. HTTP通信

使用Vert.x实现的HTTP服务器处理客户端请求,服务端接收到请求后,反序列化得到RpcRequest对象,然后通过反射调用对应的服务方法,最后将结果序列化为RpcResponse对象返回给客户端。

工作流程

服务提供者

  1. 实现公共接口(如UserServiceImpl实现UserService接口)
  2. 将服务注册到本地注册表:LocalRegistry.register(UserService.class.getName(), UserServiceImpl.class)
  3. 启动HTTP服务器:vertxHttpServer.doStart(8080)

服务消费者

  1. 获取服务代理:UserService userService = ServiceProxyFactory.getProxy(UserService.class)
  2. 调用代理方法:User newUser = userService.getUser(user)
  3. 代理将方法调用转换为RpcRequest对象
  4. 序列化RpcRequest并发送HTTP请求:HttpRequest.post("http://localhost:8080").body(bodyBytes).execute()
  5. 接收响应并反序列化为RpcResponse对象
  6. 返回结果给调用者:return (User) rpcResponse.getData()

服务端处理

  1. 接收HTTP请求:HttpServerHandler.handle()方法
  2. 反序列化请求体得到RpcRequest对象
  3. 从本地注册表获取服务实现类:LocalRegistry.get(serviceName)
  4. 通过反射调用服务方法:method.invoke(clazz.newInstance(), rpcRequest.getArgs())
  5. 创建RpcResponse对象并设置结果
  6. 序列化RpcResponse并返回响应:doResponse(request, rpcResponse, serializer)

实现注意事项

1. 服务名一致性

服务注册和调用必须使用相同的服务名,否则会导致服务查找失败。在这个项目中,服务名使用接口的全限定名(如UserService.class.getName())。

2. 序列化方式统一

客户端和服务端必须使用相同的序列化方式,否则会导致反序列化失败。这个项目中使用JDK序列化,要求所有传输的对象都实现Serializable接口。

3. 异常处理

目前的异常处理比较简单,只是打印异常信息,没有完善的异常传递机制。在实际应用中,需要将服务端的异常正确传递给客户端。

4. 服务发现机制

当前使用的是本地注册表,只适用于单机环境。在分布式环境中,需要使用远程注册中心(如ZooKeeper、Nacos等)。

5. 负载均衡

当前没有实现负载均衡机制,客户端只能调用固定地址的服务。在实际应用中,需要实现负载均衡来分发请求。

6. 配置化

端口、地址等信息目前是硬编码的,在实际应用中,应该通过配置文件或配置中心进行配置。

7. 线程安全

本地注册表使用ConcurrentHashMap保证线程安全,适合多线程环境下使用。

8. 资源释放

在序列化和反序列化过程中,需要注意关闭流资源,避免资源泄漏。

相关推荐
任聪聪2 小时前
《蜉蝣文明》文明收割培养皿与更高空间维度入场卷。
网络·人工智能·深度学习
二哈哈黄2 小时前
握手协议打拍
网络
阿豪学编程3 小时前
【Linux】网络基础
网络
Anthony_2313 小时前
五、交换技术与VLAN
服务器·网络·网络协议·http·https·udp·信息与通信
梁洪飞3 小时前
使用rockchip sdk提供的uboot调通网络
linux·网络·arm开发·嵌入式硬件·arm
fanruitian3 小时前
k8s 创建service 暴漏集群ip
服务器·网络·kubernetes
Ar呐3 小时前
HCIP-Datacom-Core Technology~OSPF特殊区域及其他特性
网络
yingzicat3 小时前
华为和华三交换机和路由器时间配置
网络·华为
橘颂TA4 小时前
【Linux 网络】从理论到实践:IP 协议的报头分析与分段技术详解
linux·运维·服务器·网络·tcp/ip