1.RPC远程过程调用
是一种用于分布式系统的通信机制。
它允许一个计算机程序在另一个地址空间上执行过程或函数,就像本地调用一样。
在 RPC 中,客户端通过调用远程服务的接口(通常是类似于本地接口的方式)来触发远程服务器上相应的过程或方法。
RPC 框架需要负责处理远程调用的细节,包括通信协议的选择、参数的序列化和反序列化、网络传输、错误处理等。
2.框架实现
服务端实现接口的具体方法。
远程过程调用中,启动服务器后,客户端可以代码可以更改,只要传的参数与服务端的方法吻合,服务器便不用重新启动。即服务器的内容不会因为客户端的逻辑结构改变而改变。所以,以后可以将常用的方法堆到服务器上,各种分支版本可以在客户端调用时重新实现重新写。
服务器类Server:
package com.rpc.server;
import com.rpc.server.lib.RpcInterface;
import java.io.*;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
/**
* 服务器类
* 负责提供服务,如果有客户端进行调用,提供对应服务执行操作,返回对应的结果给服务端
* 关键词:反射 invoke函数中动态参数列表
*/
public class Server {
public static void main(String[] args) throws IOException, InstantiationException, IllegalAccessException, ClassNotFoundException {
// 启动时初始化调用列表 用哈希表查找O(1)时间
//第一个是方法名,第二个是RPC接口类型,即方法表
HashMap<String, RpcInterface> methodMap = new HashMap<>();
/**
* 用哈希表初始化调用列表
* 读取方法表以及配置文档
* 解析文档中的方法名与类名
* 利用反射根据类名加载类实例,获取并用哈希表存储类方法
*
* 服务时,读取客户端传过来的方法名,判断存在与否
* 如果存在,则反射获取该类以及该类中的方法
* 调用方法,得到返回结果
* 将结果返回给客户端,完成服务
*/
// methodMap.put("sayHello", new HelloWorldImp());
// methodMap.put("call", new CallImp());
/**
* 模拟Spring Boot扫描包的功能
* 1.获取包路径
* 2.扫描指定包下的所有类:使用Java的反射机制,扫描指定包下的所有类,找到所有带有特定注解(比如@Service或者@Component)的类,并将它们实例化存储在一个容器中。
* 3.初始化调用列表 根据扫描的结果初始化调用列表,将找到的类实例与其对应的方法名存储在map中
*/
// 读取方法表 配置文档(替换上面两句话) 实现后,只需要维护好config中的methods.txt
File file = new File("config/methods.txt");
FileReader fileReader = new FileReader(file);
//利用BR 每次读取一行
BufferedReader br = new BufferedReader(fileReader);
String listStr = null;
while ((listStr = br.readLine()) != null) {
System.out.println(listStr);
// 解析单行数据 空格分隔 [0]方法名 [1]类名
String[] methodMsg = listStr.split(" ");
String methodName = methodMsg[0];
String className = methodMsg[1];
// 反射获取类对象
Class<?> aClass = Class.forName(className);
// 加载类实例
Object object = aClass.newInstance();// 实例化
// 存储方法名-类实例
methodMap.put(methodName, (RpcInterface) object);
// System.out.println(listStr);
}
//启动服务
ServerSocket serverSocket = new ServerSocket(12345);
System.out.println("Server started");
while (true) {
try (Socket socket = serverSocket.accept();
ObjectInputStream input = new ObjectInputStream(socket.getInputStream());
ObjectOutputStream output = new ObjectOutputStream(socket.getOutputStream())) {
// 1.读第一句话 方法名
String methodName = input.readUTF();
// 2.判断是否有此方法
if (methodMap.containsKey(methodName)) {
// 3.根据方法名 获取实例(RPC接口类型对象)【需要调用这个对象的方法】
RpcInterface rpcInterface = methodMap.get(methodName);
// 4.反射类 【需要获取此对象的所有方法】
Class rpcClass = rpcInterface.getClass();
// 5.加载方法 【这个类当中的所有方法】
Method[] methods = rpcClass.getMethods();
// 6.读第二句话 参数
String name = input.readUTF();
// 7.调用方法 获取返回结果 {此处为动态参数列表}
Object obj = methods[0].invoke(rpcInterface, name);
// 8.发送结果给调用者
output.writeUTF(obj.toString());
output.flush();
}
// if ("sayHello".equals(methodName)) {
读第二句话 参数
// String name = input.readUTF();
// String res = new HelloWorldImp().sayHello(name);
// output.writeUTF(res);
// output.flush();
// }
} catch (IOException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}
}
package com.rpc.server.imp;
import com.rpc.server.lib.Call;
public class CallImp implements Call {
@Override
public String call(String name) {
return name + "Call~~";
}
}
package com.rpc.clinet;
import com.rpc.clinet.lib.HelloWorld;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.Socket;
// 客户端实现
// 客户端 自己不会有具体的实现逻辑 需要服务端上的方法来执行
public class HelloWorldImp implements HelloWorld {
@Override
public String sayHello(String name) {
String response = null;
try (Socket socket = new Socket("localhost", 12345);
ObjectOutputStream output = new ObjectOutputStream(socket.getOutputStream());
ObjectInputStream input = new ObjectInputStream(socket.getInputStream())) {
output.writeUTF("sayHello");
output.writeUTF(name);
output.flush();
response = input.readUTF();
System.out.println(response);
} catch (IOException e) {
e.printStackTrace();
}
return response;
}
}
客户端类Client:
package com.rpc.clinet;
import com.sun.jdi.event.ThreadStartEvent;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.Socket;
/**
* 客户端类
* 负责写逻辑结构,传入参数,调用对应服务端的函数
* 得到返回结果
*/
public class Client {
public static void main(String[] args) {
for (int i = 0; i < 50; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// 调用服务器的方法,传入参数
new HelloWorldImp().sayHello("AJava");
// 调用服务器的方法,传入参数,获得返回结果
String res = new CallImp().call("张伟");
}
}
}
public class CallImp implements Call {
@Override
public String call(String name) {
String response = null;
try (Socket socket = new Socket("localhost", 12345);
ObjectOutputStream output = new ObjectOutputStream(socket.getOutputStream());
ObjectInputStream input = new ObjectInputStream(socket.getInputStream())) {
output.writeUTF("call");
output.writeUTF(name);
output.flush();
response = input.readUTF();
System.out.println(response);
} catch (IOException e) {
e.printStackTrace();
}
return response;
}
}
// 客户端 自己不会有具体的实现逻辑 需要服务端上的方法来执行
public class HelloWorldImp implements HelloWorld {
@Override
public String sayHello(String name) {
String response = null;
try (Socket socket = new Socket("localhost", 12345);
ObjectOutputStream output = new ObjectOutputStream(socket.getOutputStream());
ObjectInputStream input = new ObjectInputStream(socket.getInputStream())) {
output.writeUTF("sayHello");
output.writeUTF(name);
output.flush();
response = input.readUTF();
System.out.println(response);
} catch (IOException e) {
e.printStackTrace();
}
return response;
}
}
3.RPC远程过程调用中,客户端重新修改接口的内容时,为什么服务器不用重新启动?
这由 RPC 框架的特性和工作原理决定。
-
动态代理:很多 RPC 框架基于动态代理实现远程调用。客户端编写的代码中通常使用了框架提供的客户端 Stub 或代理类;当客户端代码发生变化时,只需要重新生成代理类即可,而不需要影响服务端的代码。
-
基于接口定义的调用:RPC 框架通常是基于接口定义的调用,客户端调用远程服务时是基于接口定义的,当客户端代码修改了接口的内容时,这并不会对服务端产生影响,只有在接口定义发生变化时服务端才需要修改并重新启动。
-
服务注册与发现:大多数 RPC 框架支持服务注册与发现机制,客户端可以通过服务注册中心动态获取服务地址,这意味着即使服务端的地址发生变化,客户端也不需要重新启动;只需要更新服务注册中心的信息即可。
4.如果在 RPC 调用中,客户端和服务端同时修改了接口定义,会导致什么问题?如何解决?
如果客户端和服务端同时修改了接口定义,会导致接口不一致的问题,导致 RPC 调用失败或者产生错误的结果。因为客户端和服务端不再满足相同的协议,无法正常进行通信。
为了解决这个问题,可以考虑以下几种方法:
-
版本控制:在 RPC 接口定义中引入版本控制,每次接口发生变化时,都更新接口的版本号,这样就可以同时保留新老接口供不同的客户端使用。客户端和服务端可以在通信时通过版本号来判断使用哪个版本的接口。
-
兼容性设计:在进行接口修改时,尽量保持向后兼容性。对于已有的接口或数据结构,尽量不要删除或者修改已有的字段、方法等,而是新增加字段或方法。这样可以确保旧版的客户端仍能够与新版的服务端进行通信,同时新版的客户端也能够与旧版的服务端进行通信。
-
强制更新:当接口发生不兼容的改变时,可以采取强制更新的方式,要求所有的客户端和服务端必须一起升级到新的接口定义,这样可以避免不一致性的问题。
-
自动生成工具:为了避免人为的错误,可以使用自动生成的工具来生成客户端和服务端的接口定义。这样可以确保客户端和服务端使用的是一致的接口定义,降低手工修改接口的风险。
5.序列化
序列化(Serialization)是指将对象的状态转换为可以存储或传输的格式的过程。
序列化的功能主要包括以下几点:
-
数据传输:可以将对象转换为可以在网络上传输的数据格式。可以在不同系统之间传递数据,从而实现分布式系统和远程调用。
-
数据持久化:可以将对象保存到磁盘或其他持久存储介质中,使得对象状态可以在稍后重新加载和恢复。这对于持久化应用程序状态非常有用,如缓存、数据库存储等。
-
对象克隆:通过序列化,可以实现对象的深复制。这意味着可以创建原始对象状态的一份精确拷贝,而不是简单的引用或浅克隆。
6.反序列化
反序列化是序列化的逆过程,它将序列化后的数据重新转换为对象的状态。
反序列化的功能包括以下几点:
-
数据还原:将序列化后的数据还原为原始对象的状态,包括对象的属性、字段等。这样可以从持久存储介质中读取对象的状态并将其重新创建为内存中的对象。
-
远程调用:在远程调用中,序列化后的数据被发送到远程计算机,在接收端进行反序列化操作,将数据还原成对象,并执行相应的调用操作。
-
对象恢复:在一些场景下,需要将序列化后的对象进行反序列化,以便进行对象状态的恢复和复原。
-
涉及到从存储介质中读取序列化后的数据,或者接收从远程计算机传输过来的数据,并将其还原为对象的状态。
import java.io.*;
// 消息类型
class GreetRequest implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
public GreetRequest(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
public class SerializationExample {
public static void main(String[] args) {
try {
// 1.创建 GreetRequest 对象
GreetRequest request = new GreetRequest("Alice");
// 2.将对象序列化为字节数组
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);
objectOutputStream.writeObject(request);
byte[] serializedRequest = outputStream.toByteArray();
// 3.从字节数组反序列化为 GreetRequest 对象
ByteArrayInputStream inputStream = new ByteArrayInputStream(serializedRequest);
ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
GreetRequest deserializedRequest = (GreetRequest)objectInputStream.readObject();
// 4.输出反序列化后的消息内容
System.out.println("Deserialized request name: " + deserializedRequest.getName());
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
7.反射
反射(Reflection)是一种编程语言特性,允许程序在运行时获取对象的类型信息(比如类名、属性、方法等)并对它们进行操作。反射使得程序能够动态地调用对象的方法、访问和修改属性,以及创建新的对象实例。
使用 Java 中的反射机制也可以实现消息的序列化和反序列化。通过反射,我们可以在运行时动态地获取类的属性和方法信息,从而实现对对象的序列化和反序列化操作。
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
class GreetRequest {
private String name;
public GreetRequest() {
}
public GreetRequest(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
// 使用反射机制将对象序列化为 Map
public Map<String, Object> serialize() throws IllegalAccessException {
Map<String, Object> map = new HashMap<>();
Class<?> clazz = this.getClass();
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
field.setAccessible(true);
map.put(field.getName(), field.get(this));
}
return map;
}
// 使用反射机制将 Map 反序列化为对象
public void deserialize(Map<String, Object> map) throws IllegalAccessException {
Class<?> clazz = this.getClass();
for (Field field : clazz.getDeclaredFields()) {
if (map.containsKey(field.getName())) {
field.setAccessible(true);
field.set(this, map.get(field.getName()));
}
}
}
}
public class ReflectionSerializationExample {
public static void main(String[] args) throws IllegalAccessException {
GreetRequest request = new GreetRequest("Alice");
// 将对象序列化为 Map
Map<String, Object> serializedData = request.serialize();
// 输出序列化后的数据
System.out.println("Serialized data: " + serializedData);
// 将 Map 反序列化为对象
GreetRequest deserializedRequest = new GreetRequest();
deserializedRequest.deserialize(serializedData);
// 输出反序列化后的消息内容
System.out.println("Deserialized request name: " + deserializedRequest.getName());
}
}