手动实现一个RPC框架 (一):RPC的介绍

article/2025/10/2 13:51:18

手动实现RPC框架

  最近在备战22年暑假实习的招聘,由于之前也没有实习的经验,所以在项目经验这方面也比较缺乏。在跟着B站尚硅谷的课程学习完微服务和分布式组件的内容后又跟着写了尚医通的微服务实战项目。尚医通项目中有使用到OpenFeign和FeignClient来远程调用其他模块的服务。

  在跟着写完后,总感觉自己有一种囫囵吞枣的感觉。在看了许多前辈的面试项目准备和经验后。我也想尝试自己手动写一个RPC框架,考虑到可以加深自身对分布式环境中远程调用服务的理解,也相对来说比较新,而且工程量不算很大,但是得益是很不错的。


目录

手动实现RPC框架

前言

正文

一、RPC是什么?

二、RPC调用的简单实现

三、传输的规则

 四、客户端实现——动态代理

五、服务端的实现——反射调用

六、测试

七、总结


前言

本系列文章是基于CSDN博主 ”何人听我楚狂声“ 《一起写个Dubbo》和GitHub作者 “Java Guide” 《手动搭建RPC》系列文章来作为参考并且进行实现的。大家如果可以直接去二位前辈的博客或者GitHub中进行学习,肯定会有更好的收获和理解,本人这里只作为学习记录,无论是能力还是理解都比不上二位前辈。

博客地址:(1条消息) 何人听我楚狂声的博客_CSDN博客-java,一起写个数据库,一起写个Dubbo领域博主

 Snailclimb/guide-rpc-framework: A custom RPC framework implemented by Netty+Kyro+Zookeeper.(一款基于 Netty+Kyro+Zookeeper 实现的自定义 RPC 框架-附详细实现过程和相关教程。) (github.com)


正文

一、RPC是什么?

RPC(Remote Procedure Call Protocol)远程过程调用协议,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。简言之,RPC使得程序能够像访问本地系统资源一样,去访问远端系统资源。比较关键的一些方面包括:通讯协议、序列化、资源(接口)描述、服务框架、性能、语言支持等。

RPC是一种远程过程调用协议,而我们的RPC框架,例如Dubbo,就是封装了这个协议然后进行实现的。

一个RPC框架要进行使用应该要具有如下的组件(功能)

本图来源于Java Guide(超级详细的图,一个很厉害的作者大大~)

 首先从整体层次来看,一个RPC协议的框架应该具有三个层面。分别是

 服务的注册中心,请求服务的客户端,提供服务的服务端。

 简而言之,就是服务端需要把提供的服务注册到注册中心,而客户端可以发现注册中心的服务,它调用服务端的服务的时候,相当于调用它自己本地的方法,客户端是有服务的接口的,然而服务的实现类,只在服务端拥有。所以客户端的请求会通过网络传输去服务端调用接口和实现类,然后将执行的结果再返回给客户端。

关于这三个层面,其实细分的话,又可以分为以下几个部分,每一部分完成各自的任务。

1.客户端(上面提到了,客户端发起请求,调用远程方法)

2.客户端存根(存放服务端地址信息,将客户端的请求参数数据信息打包成网络消息,再通过网络传输发送给服务端)作为一个代理类。

3.网络传输 通过网络传输,把我们调用的远程接口中的参数传输给服务端,这样服务端的接口实现类才能进行处理,在处理完成之后,还要通过网络传输的方式把返回的结果发送回来。网络传输一般有原生的Soket方式,还有现在常用的Netty。

4.服务端 提供服务的一方,有远程接口和实现类。

5.服务端存根 接收客户端发送过来的请求消息并进行解析,然后再调用服务端的方法进行处理

这样的步骤中存在许多相关的问题,如下

1、如何确定客户端和服务端之间的通信协议?

2、如何更高效地进行网络通信?

3、服务端提供的服务如何暴露给客户端?

4、客户端如何发现这些暴露的服务?

5、如何更高效地对请求对象和响应结果进行序列化和反序列化操作?

这些问题,我们在后续完成整个框架的过程中会进行解答,首先我跟着声哥的步骤一样,我们默认客户端已经直到服务端的地址,那我们首先就只需要安排一下客户端和服务端的接口,以及服务端独有的实现类就行了。

这里说明一下,由于声哥已经把项目开源到了Github上,所以教程里貌似没有搭建环境的步骤,当然对于很多人来说也没有必要展现完整的步骤,但是对于我这种小白来说,我觉得更加详细一些,可能可以帮助到更多像我一样还什么都不懂的人吧。。。

这里附上声哥Github的地址,大家也可以自行拉取然后再理解。

https://github.com/cn-guoziyang/my-rpc-framework

二、RPC调用的简单实现

首先 搭建不同的模块,由于这里暂时只作为测试使用,我把接口的实现类写进rpc-server模块中,而客户端调用的接口和接口处理数据的实体类写到了rpc-api模块中。这里搭建的模块是Maven工程模块。

 然后我们正式编写代码

首先是接口

public interface HelloService {String sayHello(HelloObject helloObject);
}

接口操作数据的实体类

 注意啦,在网络传输的过程中,实体类都需要实现Serializable接口,代表可序列化,

序列化作用:

  • 提供一种简单又可扩展的对象保存恢复机制。
  • 对于远程调用,能方便对对象进行编码和解码,就像实现对象直接传输。
  • 可以将对象持久化到介质中,就像实现对象直接存储。
  • 允许对象自定义外部存储的格式。

更多详细关于Serizlizable的内容,大家可以去这篇文章中详细阅读。

谈谈实现Serializable接口的作用和必要性 - 简书

@Data
@NoArgsConstructor
@AllArgsConstructor
public class HelloObject implements Serializable {private Integer id;private String message;}

接口的实现类

这里为了成功引入HelloService接口,需要在rpc-server模块的pom.xml文件中引入rpc-api模块

@Service
public class HelloServiceImpl implements HelloService{private static final Logger logger = LoggerFactory.getLogger(HelloServiceImpl.class);@Overridepublic String sayHello(HelloObject helloObject) {logger.info("接收到消息:{}", helloObject.getMessage());return "这是Hello的Impl1方法";}
}

这里的@Service注解使用的并不是Spring为我们封装的注解,而是声哥自定义的注解,但是起的作用实际上跟Spring的注解是一样的,就是表示该类是个服务提供类,标注在远程接口的实现类上

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Service {public String name() default "";}

三、传输的规则

我们直到,客户端有了服务的接口,服务端有了服务的实现类。。。那我们可以想象一下,在一次请求中,服务端需要哪些信息才能够确定它要去调用哪个实现类呢?

首先肯定要知道接口的名字,以及调用的接口中的方法名,另外再确定方法的参数值和参数类型,这样就可以确保服务端能够唯一确认一个实现类中的方法进行处理。

所以我们可以模拟一个我们自己写的传输规则,也就是我们的PRC请求过程中,我们的请求是遵循这个格式的。

由于请求格式这些东西肯定是通用的模块,所以我们再建立一个rpc-common模块,用于存放实体对象、工具类等公用类。

这里使用了Lombok的@Data注解和@Builder注解,@Data注解自动生成getter和setter,toString方法,而@Builder注解帮我们使用了建造者模式,有兴趣的可以去设计模式中了解,简单来说就是可以让我们通过链式编程的方式在创建对象的时候进行实例化。

@Data
@Builder
public class RpcRequest implements Serializable {/*** 待调用接口名称*/private String interfaceName;/*** 待调用方法名称*/private String methodName;/*** 调用方法的参数*/private Object[] parameters;/*** 调用方法的参数类型*/private Class<?>[] paramTypes;
}

有了RPC的Request,那我们服务的过程中不可能只有请求,我们还会有响应,所以我们还需要有一个RpcResponse,用于封装响应的信息。

@Data
public class RpcResponse<T> implements Serializable {/*** 响应状态码*/private Integer statusCode;/*** 响应状态补充信息*/private String message;/*** 响应数据*/private T data;public static <T> RpcResponse<T> success(T data) {RpcResponse<T> response = new RpcResponse<>();response.setStatusCode(ResponseCode.SUCCESS.getCode());response.setData(data);return response;}public static <T> RpcResponse<T> fail(ResponseCode code) {RpcResponse<T> response = new RpcResponse<>();response.setStatusCode(code.getCode());response.setMessage(code.getMessage());return response;}
}

 既然要用到返回的Code,那我们就再定义一个枚举类。

@AllArgsConstructor
@Getter
public enum ResponseCode {SUCCESS(200, "调用方法成功"),FAIL(500, "调用方法失败"),METHOD_NOT_FOUND(500, "未找到指定方法"),CLASS_NOT_FOUND(500, "未找到指定类");private final int code;private final String message;}

 四、客户端实现——动态代理

那么,我们在拥有了客户端的接口,以及服务端的实现类,并且我们自定义了服务端如何匹配对应的实体类后,我们应该思考,由于在客户端这一侧我们并没有接口的具体实现类,就没有办法直接生成实例对象。这时,我们可以通过动态代理的方式生成实例,并且调用方法时生成需要的RpcRequest对象并且发送给服务端。

这里我们采用JDK动态代理,代理类是需要实现InvocationHandler接口的。

public class RpcClientProxy implements InvocationHandler {private String host;private int port;public RpcClientProxy(String host, int port) {this.host = host;this.port = port;}@SuppressWarnings("unchecked")public <T> T getProxy(Class<T> clazz) {return (T) Proxy.newProxyInstance(clazz.getClassLoader(), new Class<?>[]{clazz}, this);}
}

这里生成代理对象的参数,host和port分别对应主机的ip地址和端口号,因为我们需要通过地址和端口号才能找到服务端主机并且去使用里面的服务,使用getProxy()方法来生成代理对象。

InvocationHandler接口需要实现invoke()方法,来指明代理对象的方法被调用时的动作。在这里,我们显然就需要生成一个RpcRequest对象,发送出去,然后返回从服务端接收到的结果即可

@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {RpcRequest rpcRequest = RpcRequest.builder().interfaceName(method.getDeclaringClass().getName()).methodName(method.getName()).parameters(args).paramTypes(method.getParameterTypes()).build();RpcClient rpcClient = new RpcClient();return ((RpcResponse) rpcClient.sendRequest(rpcRequest, host, port)).getData();}

 在这里,生成了RpcRequest对象后,我们使用一个RpcClient来发送这个请求,并且通过getData方法来获取响应的数据。

public class RpcClient {private static final Logger logger = LoggerFactory.getLogger(RpcClient.class);public Object sendRequest(RpcRequest rpcRequest, String host, int port) {try (Socket socket = new Socket(host, port)) {ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());objectOutputStream.writeObject(rpcRequest);objectOutputStream.flush();return objectInputStream.readObject();} catch (IOException | ClassNotFoundException e) {logger.error("调用时有错误发生:", e);return null;}}
}

这里的实现方式就是直接使用Java的序列化方式(通过实现Serizlizable),创建一个Socket,利用Socket进行传输,获取ObjectOutputStream对象,然后把需要发送的对象传进去即可,接收时获取ObjectInputStream对象,readObject()方法就可以获得一个返回的对象。 

五、服务端的实现——反射调用

在我们前面完成了远程调用的接口,实现类,远程调用封装的对线,传输规则等等,最后就只需要完成服务端进行功能实现就可以实现一个简单的远程调用了,这里服务端是通过反射来进行调用的

主要流程就是使用一个ServerSocket监听某个端口,循环接收连接请求,如果发来了请求就创建一个线程,在新线程中处理调用。这里创建线程采用线程池的方式。

public class RpcServer {private final ExecutorService threadPool;private static final Logger logger = LoggerFactory.getLogger(RpcServer.class);public RpcServer() {int corePoolSize = 5;int maximumPoolSize = 50;long keepAliveTime = 60;BlockingQueue<Runnable> workingQueue = new ArrayBlockingQueue<>(100);ThreadFactory threadFactory = Executors.defaultThreadFactory();threadPool = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit.SECONDS, workingQueue, threadFactory);}}

然后在RpcServer里面对外提供一个接口的调用服务,添加register方法,在注册完一个服务后立刻开始监听。

public void register(Object service, int port) {try (ServerSocket serverSocket = new ServerSocket(port)) {logger.info("服务器正在启动...");Socket socket;while((socket = serverSocket.accept()) != null) {logger.info("客户端连接!Ip为:" + socket.getInetAddress());threadPool.execute(new WorkerThread(socket, service));}} catch (IOException e) {logger.error("连接时有错误发生:", e);}}

这里向工作线程WorkerThread传入了socket和用于服务端实例service。

WorkerThread实现了Runnable接口,用于接收RpcRequest对象,解析并且调用,生成RpcResponse对象并传输回去。run方法如下:

@Overridepublic void run() {try (ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream())) {RpcRequest rpcRequest = (RpcRequest) objectInputStream.readObject();Method method = service.getClass().getMethod(rpcRequest.getMethodName(), rpcRequest.getParamTypes());Object returnObject = method.invoke(service, rpcRequest.getParameters());objectOutputStream.writeObject(RpcResponse.success(returnObject));objectOutputStream.flush();} catch (IOException | ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {logger.error("调用或发送时有错误发生:", e);}}

其中,通过class.getMethod方法,传入方法名和方法参数类型即可获得Method对象。如果你上面RpcRequest中使用String数组来存储方法参数类型的话,这里你就需要通过反射生成对应的Class数组了。通过method.invoke方法,传入对象实例和参数,即可调用并且获得返回值。
 

六、测试

我们已经在上面已经实现了一个HelloService的实现类了,现在我们只需要创建一个RpcServer并且把这个实现类注册进去就行了

public class TestServer {public static void main(String[] args) {HelloService helloService = new HelloServiceImpl();RpcServer rpcServer = new RpcServer();rpcServer.register(helloService, 9000);}
}

服务端开放在9000端口。

客户端方面,我们需要通过动态代理,生成代理对象,并且调用,动态代理会自动帮我们向服务端发送请求的

public class TestClient {public static void main(String[] args) {RpcClientProxy proxy = new RpcClientProxy("127.0.0.1", 9000);HelloService helloService = proxy.getProxy(HelloService.class);HelloObject object = new HelloObject(12, "This is a message");String res = helloService.hello(object);System.out.println(res);}
}

 创建一个HelloObject对象来作为传递的参数,然后启动服务端,再启动客户端。

服务端输出

服务器正在启动...

客户端连接!Ip为:127.0.0.1

接收到:This is a message

客户端输出

这是调用的返回值,id=12 


七、总结

最后,总结一下这次测试的RPC全过程。

1.首先,客户端接收到请求,然后以调用本地方法的方式调用远程服务。

2.客户端根接收到调用后,通过代理对象,将方法,参数等信息封装成能够在网络中传输的消息体(RpcRequest)要记得实现序列化接口。

3.客户端找到远程服务的地址,将消息体(RpcRequest)发送给服务端根。

4.服务端根进行反序列化操作,把消息体转换成RpcRequest对象,并且根据转换成的RpcRequest对象中的参数(方法名,实现类名,方法参数值,参数类型)等等去调用服务端的方法。

5.服务端进行方法的业务逻辑处理,在处理完毕后,返回处理结果(RpcResponse对象)组装成能在网络传输的消息体给服务端根。

6.服务端根再把处理结果进行序列化。发送给客户端。

7.客户端根接收到消息体,进行反序列化操作,变回RpcResponse对象,然后给客户端去进行处理

流程大家可以看下图


http://chatgpt.dhexx.cn/article/vTf7yVs6.shtml

相关文章

[转]php中流行的rpc框架有哪些?

什么是rpc框架 先回答第一个问题&#xff1a;什么是RPC框架&#xff1f; 如果用一句话概括RPC就是&#xff1a;远程调用框架&#xff08;Remote Procedure Call&#xff09; 那什么是远程调用&#xff1f; 通常我们调用一个php中的方法&#xff0c;比如这样一个函数方法: loc…

中间件 rpc是什么?php中流行的中间件rpc框架有哪些

rpc是什么&#xff1f;php中流行的rpc框架有哪些。 更好的排版&#xff1a;https://www.zybuluo.com/phper/note/76641 什么是rpc框架 先回答第一个问题&#xff1a;什么是RPC框架&#xff1f; 如果用一句话概括RPC就是&#xff1a;远程调用框架&#xff08;Remote Procedure C…

rpc是什么?php中流行的rpc框架有哪些?

什么是rpc框架 先回答第一个问题&#xff1a;什么是RPC框架&#xff1f;如果用一句话概括RPC就是&#xff1a;远程调用框架&#xff08;Remote Procedure Call&#xff09; 那什么是远程调用&#xff1f; 通常我们调用一个php中的方法&#xff0c;比如这样一个函数方法: localA…

常用的RPC框架

为什么要使用RPC&#xff1f; RPC&#xff08;remote procedure call&#xff09;是指远程过程调用&#xff0c;比如两台服务器A和B&#xff0c;A服务器上部署一个应用&#xff0c;B服务器上部署一个应用&#xff0c;A服务器上的应用想调用B服务器上的应用提供的接口&#xff0…

Go语言 - RPC框架

1.什么是RPC RPC - Remote Procedure Calls 远程函数调用 相当于本地将参数上传到云端&#xff0c;云端根据形参计算返回结果&#xff0c;并返还给本地。 2.RPC需要解决的问题 函数映射 数据转换成字节流 网络传输 3.一次RPC的完整过程 IDL文件&#xff1a;通过一种中立…

主流的RPC框架有哪些

RPC是远程过程调用的简称&#xff0c;广泛应用在大规模分布式应用中&#xff0c;作用是有助于系统的垂直拆分&#xff0c;使系统更易拓展。Java中的RPC框架比较多&#xff0c;各有特色&#xff0c;广泛使用的有RMI、Hessian、Dubbo等。RPC还有一个特点就是能够跨语言。 1、RMI&…

简单使用iPhone自带视频播放器

利用苹果自带的视频播放器播放视频 在调用方法前&#xff0c;我们需要包含头文件 #import <MediaPlayer/MediaPlayer.h> 然后调用系统的方法&#xff0c;来实现视频播放。只需简单几步即可 1.获取要播放的视频的路径 NSString *path [[NSBundle mainBundle]pathForR…

iOS 音视频录制之播放视频,AVPlayer可播放本地视频和在线视频

文章目录 在开发中&#xff0c;单纯使用AVPlayer类是无法显示视频的&#xff0c;要将视频层添加至AVPlayerLayer中&#xff0c;这样才能将视频显示出来&#xff0c;所以先在ViewController的interface中添加以下属性 property (nonatomic ,strong) AVPlayer *player; property …

【iOS】视频播放之AVPlayer

【iOS】视频播放之AVPlayer iOS平台使用播放视频&#xff0c;可用的选项一般有这四个&#xff0c;他们各自的作用和功能如下&#xff1a; 使用环境优点缺点AVPlayerViewControllerAVKit简单易用不可定制MPMoviePlayerControllerMediaPlayer简单易用不可定制IJKPlayerIJKMedi…

【iOS】AVPlayer 播放音视频

1、常见的音视频播放器 iOS开发中不可避免地会遇到音视频播放方面的需求。 常用的音频播放器有 AVAudioPlayer、AVPlayer 等。不同的是&#xff0c;AVAudioPlayer 只支持本地音频的播放&#xff0c;而 AVPlayer 既支持本地音频播放&#xff0c;也支持网络音频播放。 常用的视…

【iOS】AVPlayer 视频播放

视频播放器的类别 iOS开发中不可避免地会遇到音视频播放方面的需求。 常用的音频播放器有 AVAudioPlayer、AVPlayer 等。不同的是&#xff0c;AVAudioPlayer 只支持本地音频的播放&#xff0c;而 AVPlayer 既支持本地音频播放&#xff0c;也支持网络音频播放。 常用的视频播放…

iOS音视频播放-AVPlayer简单使用

按公司需求需要对音频文件进行后台播放,借此机会对音频播放做了个总结.主要针对 AVPlayer 进行详细说明. iOS 各播放器比较 名称使用环境优点确点System Sound ServicesAVFoundationC语言的底层写法&#xff0c;节省内存支持的格式有限&#xff0c;音量无法通过音量键控制&…

iOS音视频播放指南(二)

1. 让你的App支持画中画 画中画指可以让视频在小窗中播放,可以一边看视频一边刷知乎 你可以使用AVPlayerViewController或者AVPictureInPictureController来实现画中画播放。 其中AVPictureInPictureController支持你自定义一些播放控件 在支持画中画播放之前,确保你按照iOS音视…

iOS音视频播放指南(一)

1. 简介 苹果目前提供两个框架用来处理音视频播放 1.AVFoundation AVFoundation用于播放、处理音视频。可以通过结构图看到AVFoundation位于UIKit之下,很好理解AVFoundation并不提供用户界面,你可以自己自己构建用户界面来控制媒体的播放处理等功能。 但是苹果更推荐使用AVKit来…

iOS视频播放的基本方法

本文总结了iOS中最常见的视频播放方法&#xff0c;不同的方法都各具特点&#xff0c;我希望能够总结它们的不同&#xff0c;方便在开发中选择合适的技术方案。 Apple为我们提供了多种方法来实现视频播放&#xff0c;包括MPMoviePlayerController&#xff0c;MPMoviePlayerView…

【计算机系统1】4 Nim游戏

目录 目的与要求 内容与方法 步骤与过程 程序总体设计 核心数据结构及算法流程 核心代码 调试过程 界面展示子程序DISPLAY&#xff08;嵌套&#xff1a;球数展示子程序PUTBALL&#xff09; 游戏子程序GAME&#xff08;嵌套&#xff1a;单人每轮子程序PLAY&#xff09; 结论或体…

java nim游戏_LeetCode 292. Nim游戏

题目描述&#xff1a; 你和你的朋友&#xff0c;两个人一起玩 Nim游戏&#xff1a;桌子上有一堆石头&#xff0c;每次你们轮流拿掉 1 - 3 块石头。 拿掉最后一块石头的人就是获胜者。你作为先手。 你们是聪明人&#xff0c;每一步都是最优解。 编写一个函数&#xff0c;来判断你…

Nim游戏、3的幂、4的幂

&#x1f345; Java学习路线&#xff1a;Java学习路线 &#x1f345; 简介&#xff1a;Java领域优质创作者&#x1f3c6;、CSDN哪吒公众号作者✌ 、Java架构师奋斗者&#x1f4aa; &#x1f345; 百日刷题计划&#xff1a;第 12 / 100 天。 &#x1f345; 扫描主页左侧二维码&a…

【数论】博弈论 —— nim游戏

知识点 一 . nim游戏的数学定义 Nim游戏是博弈论中最经典的模型&#xff0c;它又有着十分简单的规则和无比优美的结论 。 Nim游戏是组合游戏(Combinatorial Games)的一种&#xff0c;准确来说&#xff0c;属于“Impartial Combinatorial Games”&#xff08;以下简称ICG&#…

【模板题】几种常见的Nim游戏(博弈论)

一、AcWing 891. Nim游戏 【题目描述】 给定 n n n堆石子&#xff0c;两位玩家轮流操作&#xff0c;每次操作可以从任意一堆石子中拿走任意数量的石子&#xff08;可以拿完&#xff0c;但不能不拿&#xff09;&#xff0c;最后无法进行操作的人视为失败。 问如果两人都采用最优…