RPC的概念
背景知识
- RPC的基本概念,核心功能
常见的RPC框架
Duboo基本功能
- 远程通讯
- 基于接口方法的透明远程过程调用
- 负载均衡
- 服务注册中心
RPC过程
client 调用远程方法-> request序列化 -> 协议编码 -> 网络传输-> 服务端 -> 反序列化request -> 调用本地方法得到response -> 序列化 ->编码->……
版本迭代过程
目录
从0开始的RPC的迭代过程:
- version0版本:以不到百行的代码完成一个RPC例子
- version1版本:完善通用消息格式(request,response),客户端的动态代理完成对request消息格式的封装
- version2版本:支持服务端暴露多个服务接口, 服务端程序抽象化,规范化
- version3版本:使用高性能网络框架netty的实现网络通信,以及客户端代码的重构
- version4版本:自定义消息格式,支持多种序列化方式(java原生, json…)
- version5版本: 服务器注册与发现的实现,zookeeper作为注册中心
- version6版本: 负载均衡的策略的实现
1.MyRPC版本1
背景知识
- 反射
- 动态代理
本节问题
-
如何使客户端请求远程方法支持多种?
-
如何使服务端返回值的类型多样?
版本升级过程
更新1:定义了一个通用的Request的对象(消息格式)
/**
* 在上个例子中,我们的Request仅仅只发送了一个id参数过去,这显然是不合理的,
* 因为服务端不会只有一个服务一个方法,因此只传递参数不会知道调用那个方法
* 因此一个RPC请求中,client发送应该是需要调用的Service接口名,方法名,参数,参数类型
* 这样服务端就能根据这些信息根据反射调用相应的方法
* 还是使用java自带的序列化方式
*/
@Data
@Builder
public class RPCRequest implements Serializable {
// 服务类名,客户端只知道接口名,在服务端中用接口名指向实现类
private String interfaceName;
// 方法名
private String methodName;
// 参数列表
private Object[] params;
// 参数类型
private Class<?>[] paramsTypes;
}
更新2: 定义了一个通用的Response的对象(消息格式)
/**
* 上个例子中response传输的是User对象,显然在一个应用中我们不可能只传输一种类型的数据
* 由此我们将传输对象抽象成为Object
* Rpc需要经过网络传输,有可能失败,类似于http,引入状态码和状态信息表示服务调用成功还是失败
*/
@Data
@Builder
public class RPCResponse implements Serializable {
// 状态信息
private int code;
private String message;
// 具体数据
private Object data;
public static RPCResponse success(Object data) {
return RPCResponse.builder().code(200).data(data).build();
}
public static RPCResponse fail() {
return RPCResponse.builder().code(500).message("服务器发生错误").build();
}
}
因此在网络传输过程都是request与response格式的数据了,客户端与服务器端就要负责封装与解析以上结构数据
更新3: 服务端接受request请求,并调用request中的对应的方法
public interface UserService {
// 客户端通过这个接口调用服务端的实现类
User getUserByUserId(Integer id);
// 给这个服务增加一个功能
Integer insertUserId(User user);
}
服务端的实现类UserServiceImpl要实现增加的功能
@Override
public Integer insertUserId(User user) {
System.out.println("插入数据成功:"+user);
return 1;
}
服务端接受解析reuqest与封装发送response对象
public class RPCServer {
public static void main(String[] args) {
UserServiceImpl userService = new UserServiceImpl();
try {
ServerSocket serverSocket = new ServerSocket(8899);
System.out.println("服务端启动了");
// BIO的方式监听Socket
while (true){
Socket socket = serverSocket.accept();
// 开启一个线程去处理,这个类负责的功能太复杂,以后代码重构中,这部分功能要分离出来
new Thread(()->{
try {
ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
// 读取客户端传过来的request
RPCRequest request = (RPCRequest) ois.readObject();
// 反射调用对应方法
Method method = userService.getClass().getMethod(request.getMethodName(), request.getParamsTypes());
Object invoke = method.invoke(userService, request.getParams());
// 封装,写入response对象
oos.writeObject(RPCResponse.success(invoke));
oos.flush();
}catch (IOException | ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InvocationTargetException e){
e.printStackTrace();
System.out.println("从IO中读取数据错误");
}
}).start();
}
} catch (IOException e) {
e.printStackTrace();
System.out.println("服务器启动失败");
}
}
}
更新4: 客户端根据不同的Service进行动态代理:
代理对象增强的公共行为:把不同的Service方法封装成统一的Request对象格式,并且建立与Server的通信
- 底层的通信
public class IOClient {
// 这里负责底层与服务端的通信,发送的Request,接受的是Response对象
// 客户端发起一次请求调用,Socket建立连接,发起请求Request,得到响应Response
// 这里的request是封装好的(上层进行封装),不同的service需要进行不同的封装, 客户端只知道Service接口,需要一层动态代理根据反射封装不同的Service
public static RPCResponse sendRequest(String host, int port, RPCRequest request){
try {
Socket socket = new Socket(host, port);
ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());
ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());
System.out.println(request);
objectOutputStream.writeObject(request);
objectOutputStream.flush();
RPCResponse response = (RPCResponse) objectInputStream.readObject();
return response;
} catch (IOException | ClassNotFoundException e) {
System.out.println();
return null;
}
}
}
- 动态代理封装request对象
@AllArgsConstructor
public class ClientProxy implements InvocationHandler {
// 传入参数Service接口的class对象,反射封装成一个request
private String host;
private int port;
// jdk 动态代理, 每一次代理对象调用方法,会经过此方法增强(反射获取request对象,socket发送至客户端)
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// request的构建,使用了lombok中的builder,代码简洁
RPCRequest request = RPCRequest.builder().interfaceName(method.getDeclaringClass().getName())
.methodName(method.getName())
.params(args).paramsTypes(method.getParameterTypes()).build();
// 数据传输
RPCResponse response = IOClient.sendRequest(host, port, request);
//System.out.println(response);
return response.getData();
}
<T>T getProxy(Class<T> clazz){
Object o = Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{clazz}, this);
return (T)o;
}
}
- 客户端调用不同的方法
public class RPCClient {
public static void main(String[] args) {
ClientProxy clientProxy = new ClientProxy("127.0.0.1", 8899);
UserService proxy = clientProxy.getProxy(UserService.class);
// 服务的方法1
User userByUserId = proxy.getUserByUserId(10);
System.out.println("从服务端得到的user为:" + userByUserId);
// 服务的方法2
User user = User.builder().userName("张三").id(100).sex(true).build();
Integer integer = proxy.insertUserId(user);
System.out.println("向服务端插入数据:"+integer);
}
}
结果
总结
- 定义更加通用的消息格式:Request 与Response格式, 从此可能调用不同的方法,与返回各种类型的数据。
- 使用了动态代理进行不同服务方法的Request的封装,
- 客户端更加松耦合,不再与特定的Service,host,port绑定
存在的痛点
- 服务端我们直接绑定的是UserService服务,如果还有其它服务接口暴露呢?(多个服务的注册)
- 服务端以BIO的方式性能是否太低,
- 服务端功能太复杂:监听,处理。需要松耦合