本项目所有代码可见:https://github.com/weiyu-zeng/SimpleRPC
前言
在simpleRPC-01中,我们实现了仅仅能用的RPC,但是调用只能调用server中某一个确定的方法,如果有很多方法需要调用怎么办?
因此需要把调用请求抽象出来,记为RPCRequest。
同样的,调用的返回对象也需要解耦,我们不需要知道对象是User还是其他。我们把调用返回抽象出来,记为RPCResponse。
我们不希望每次调用都要重新写host,port和调用方法,因此也需要抽象。
实现
项目创建
创建名为simpleRPC-02的module
老样子,把名为com.rpc的package创建好,然后创建client,common,server,service四个package:
依赖配置
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>SimpleRPC</artifactId>
<groupId>org.example</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>simpleRPC-02</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>
common
User.java
package com.rpc.common;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* @author zwy
*
* 定义简单User信息。
*/
@Builder
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User implements Serializable {
// 客户端和服务端共有的
private Integer id;
private String userName;
private Boolean sex;
}
我们定义抽象的RPC调用请求
RPCRequest.java
package com.rpc.common;
import lombok.Builder;
import lombok.Data;
import java.io.Serializable;
/**
* 客户端请求的抽象RPCRequest(接口名,方法名,参数,参数类型)
*
*/
@Data
@Builder
public class RPCRequest implements Serializable {
// 服务类(接口)名,客户端只知道接口名,在服务端中用接口名指向实现类
private String interfaceName;
// 方法名
private String methodName;
// 参数列表
private Object[] params;
// 参数类型
private Class<?>[] paramsTypes;
}
在上个例子中,我们的Request仅仅只发送了一个id参数过去,这显然是不合理的, 因为服务端不会只有一个服务一个方法,因此只传递参数服务端不会知道调用那个方法
因此一个RPC请求中,client发送应该是需要调用的Service接口名,方法名,参数,参数类型这样服务端就能根据这些信息根据反射调用相应的方法使用java自带的序列化方式(实现接口)
RPCResponse.java
定义了服务器端给客户端回应的抽象 RPCResponse,包含两个部分:
1.状态信息:状态码int code,状态信息String message
2.具体数据:Object data
此外还包含success方法:将RPCResponse对象的code状态码初始化为200,data初始化为传入的data,后返回RPCResponse,还包含fail方法,将RPCResponse对象的code初始化为500,将状态信息message初始化为"服务器发生错误",后返回RPCResponse对象上个例子中response传输的是User对象,显然在一个应用中我们不可能只传输一种类型的数据
由此我们将传输对象抽象成为Object RPC需要经过网络传输,有可能失败,类似于http,引入状态码和状态信息表示服务调用成功还是失败
package com.rpc.common;
import lombok.Builder;
import lombok.Data;
import java.io.Serializable;
/**
* 定义了服务器端给客户端回应的抽象 RPCResponse,包含两个部分:
* 1.状态信息:状态码int code,状态信息String message
* 2.具体数据:Object data
* 此外还包含success方法:将RPCResponse对象的code状态码初始化为200,data初始化为传入的data,后返回RPCResponse
* 还包含fail方法,将RPCResponse对象的code初始化为500,将状态信息message初始化为"服务器发生错误",后返回RPCResponse对象
*
* 上个例子中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();
}
}
service
创建服务接口 UserService.java
package com.rpc.service;
import com.rpc.common.User;
/**
* @author zwy
*
* 服务器端提供服务的方法的接口
*/
public interface UserService {
// 客户端通过这个接口调用服务端的实现类
User getUserByUserId(Integer id);
// 给这个服务增加一个功能
Integer insertUserId(User user);
}
服务实现类:UserServiceImpl.java
package com.rpc.service;
import com.rpc.common.User;
import java.util.Random;
import java.util.UUID;
/**
* @author zwy
*
* 服务器端提供服务的方法
* 1.getUserByUserId方法:接收一个id,返回一个User对象,提供属于这个ID(Integer)的User,
* User中包含他的ID(Integer),名字Name(String)和性别sex(Boolean)。
* 2.insertUserId:打印成功插入数据的信息(模拟数据库插入数据的情况)
*/
public class UserServiceImpl implements UserService {
@Override
public User getUserByUserId(Integer id) {
System.out.println("客户端查询了"+id+"的用户");
// 模拟从数据库中取用户的行为
Random random = new Random();
User user = User.builder()
.userName(UUID.randomUUID().toString())
.id(id)
.sex(random.nextBoolean()).build();
return user;
}
@Override
public Integer insertUserId(User user) {
System.out.println("插入数据成功: " + user);
return 1;
}
}
到此为止我们把需要的实例,request和response定义好了。之后对于server和client的设计我们将用到动态代理和反射。
client
我们先定义底层Client逻辑:IOClient.java
这里负责底层与服务器端的通信,发送的Request,接受的是Response对象客户端发起一次请求调用,Socket建立连接,发起请求Request,得到相应Response,这里的request是封装好的(上层进行封装),不同的service需要进行不同的封装,客户端只知道Service接口,需要一层动态代理根据反射封装不同的Service
package com.rpc.client;
import com.rpc.common.RPCRequest;
import com.rpc.common.RPCResponse;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.Socket;
/**
* @author zwy
*
* IO Client:底层的通信
* 通过Socket和输出流把 RPCRequest 传给服务器端,接收到服务器端传来的 RPCResponse,返回这个 RPCResponse
*
*/
public class IOClient {
public static RPCResponse sendRequest(String host, int port, RPCRequest request) throws IOException, ClassNotFoundException {
// 老样子,创建Socket对象,定义host和port
Socket socket = new Socket(host, port);
// 定义输入输出流对象
ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());
ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());
System.out.println("request: " + request);
// 输出流写入request对对象,刷新输出流
objectOutputStream.writeObject(request);
objectOutputStream.flush();
// 通过输入流的readObject方法,得到服务器端传来的RPCResponse,并返回RPCResponse对象
RPCResponse response = (RPCResponse) objectInputStream.readObject();
return response;
}
}
然后我们编写代理类:ClientProxy.java
它实现了InvocationHandler接口
package com.rpc.client;
import com.rpc.common.RPCRequest;
import com.rpc.common.RPCResponse;
import lombok.AllArgsConstructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
/**
* @author zwy
*
* 客户端代理:把动态代理封装request对象
*
* '@AllArgsConstructor':它是lombok中的注解。使用后添加一个构造函数,该构造函数含有所有已声明字段属性参数
* (这也就是为什么ClientProxy明明没定义构造函数,但RPCClient还可以再创建ClientProxy时,
* 通过构造函数传参给 host 和 port。)
*/
@AllArgsConstructor
public class ClientProxy implements InvocationHandler {
private String host;
private int port;
/**
* 动态代理,每一次代理对象调用方法,会经过此方法增强(反射获取request对象,socket发送至客户端)
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 构建RPCRequest对象,初始化其中的四个重要参数,使用了lombok中的builder。
// 初始化interfaceName。初始化methodName,初始化params,,初始化paramsTypes
RPCRequest request = RPCRequest.builder()
.interfaceName(method.getDeclaringClass().getName())
.methodName(method.getName())
.params(args)
.paramsTypes(method.getParameterTypes())
.build();
// 调用IOClient,通过输入输出流进行request的数据传输,并返回服务器端传来的response
RPCResponse response = IOClient.sendRequest(host, port, request);
System.out.println("response: " + response);
return response.getData(); // 获取RPCResponse中的目标数据(因为RPCResponse中除了目标数据,还有状态码和状态信息这些非目标数据)
}
/**
* 传入Client需要的服务的class反射对象
*/
<T> T getProxy(Class<T> clazz) {
// 传入目标接口的类加载器,目标接口,和InvocationHandler(的实现类,也就是本类,this),生成动态代理类实例
Object o = Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{clazz}, this);
return (T)o;
}
}
java动态代理机制中有两个重要的类和接口InvocationHandler(接口)和Proxy(类):也是实现动态代理的核心,InvocationHandler接口:是proxy代理实例的调用处理程序实现的一个接口,每一个proxy代理实例都有一个关联的调用处理程序,在代理实例调用方法时,方法调用被编码分派到调用处理程序的invoke方法。每一个动态代理类的调用处理程序都必须实现InvocationHandler接口,并且每个代理类的实例都关联到了实现该接口的动态代理类调用处理程序中,当我们通过动态代理对象调用一个方法时候,这个方法的调用 就会被转发到实现InvocationHandler接口类的invoke方法来调用
Proxy:该类用于动态生成代理类,只需传入目标接口、目标接口的类加载器以及InvocationHandler便可为目标接口生成代理类及代理对象
- Proxy.newProxyInstance:该方法用于为指定类装载器、一组接口及调用处理器生成动态代理类实例
最后我们编写RPC客户端,也就是用户操作的界面:RPCClient.java
package com.rpc.client;
import com.rpc.common.User;
import com.rpc.service.UserService;
/**
* @author zwy
*
* RPC客户端:调用服务器端的方法
*/
public class RPCClient {
public static void main(String[] args) {
// 初始化主机名ip和端口号port
ClientProxy clientProxy = new ClientProxy("127.0.0.1", 8899);
UserService proxy = clientProxy.getProxy(UserService.class); // 反射获得代理
// 服务的方法1:通过id获取User
User userByUserId = proxy.getUserByUserId(10);
System.out.println("从服务器端得到的user为:" + userByUserId);
System.out.println();
// 服务的方法2:(假装)插入一个User数据
User user = User.builder().userName("张三").id(100).sex(true).build();
Integer integer = proxy.insertUserId(user);
System.out.println("向服务器端插入数据" + integer);
}
}
server
现在写服务端server:RPCServer.java
package com.rpc.server;
import com.rpc.common.RPCRequest;
import com.rpc.common.RPCResponse;
import com.rpc.service.UserServiceImpl;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.ServerSocket;
import java.net.Socket;
/**
* @author zwy
*
* RPC server:接受/解析request,封装,发送response
*
* getClass方法:返回Object的运行时类
* Class.getMethod(String name, Class<?>... parameterTypes):返回Method对象,方法的作用是获得对象所声明的公开方法
* 该方法的第一个参数name是要获得方法的名字,第二个参数parameterTypes是按声明顺序标识该方法形参类型。
* java.lang.reflect.Method.invoke(Object receiver, Object... args):返回Object对象,方法来反射调用一个方法,
* 当然一般只用于正常情况下无法直接访问的方法(比如:private 的方法,或者无法或者该类的对象)。
* 方法第一个参数是方法属于的对象(如果是静态方法,则可以直接传 null),第二个可变参数是该方法的参数
*/
public class RPCServer {
public static void main(String[] args) throws IOException {
// 初始化(客户端Client)需要的服务:UserServiceImpl
UserServiceImpl userService = new UserServiceImpl();
// 创建ServerSocket对象,端口号要和Client一致
ServerSocket serverSocket = new ServerSocket(8899);
System.out.println("服务器启动!");
// BIO的方式监听Socket,监听到之后返回Socket对象
while (true) {
Socket socket = serverSocket.accept();
// 监听到连接之后,开启一个线程来处理
new Thread(new Runnable() {
@Override
public void run() {
try {
// socket对象的获取输入输出流作为targat,初始化输入输出流
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());
// 把得到的invoke对象写入response的success方法中,写入输出流(传给客户端),刷新输出流
oos.writeObject(RPCResponse.success(invoke));
oos.flush();
} catch (IOException | ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
}
}
}).start();
}
}
}
到此RPC改造完成
文件结构
文件结构如下:
运行
先运行RPCServer.java
然后运行RPCClient.java
我们可以看到,Client调用的两个service,都成功打印了信息,说明RPC功能运行成功。