本文是对RPC框架的非常简单的实现,参考了何人听我楚狂声的博客,十分感谢!这篇博客在我看来是保姆文,好多java基础知识顺便进行回顾,还存在很多不足,后续会进行更新优化(可能)。
基本原理
存在一个接口,客户端想要调用接口中方法的具体实现,但是由于客户端不存在该接口的实现类,只有服务端存在该接口的实现类,也就是服务端提供的服务,所以只能向服务器端发出请求,服务器接收到客户端请求,解析处理后调用服务器本地的实现类对象,然后把调用结果返回客户端。
存在的知识点
- 接口实现: 接口是客户端和服务器端都存在的公共接口,但是只有服务端存在实现类
- 客户端需要向服务器传输的信息: 用户名和id,因此单独定义一个类作为接口实现类的输入参数,包含上述信息。同时这部分用户信息需要从客户端传输到服务端,所以需要序列化。
- 序列化:本例中的所有序列化都采用实现Serializable接口实现。
- 网络编程,也就是客户端和服务端的通信,本例使用socket通信
- 反射:在调用服务端实现类方法时需要根据客户端传入请求信息反射调用。因为客户端调用的方法和参数不定(本例没有体现出来),所以不能提前在程序中静态写好直接new服务端实现类对象然后调用方法,而是采用动态的反射机制。
- 建造者模式:设计模式之一
- 抽象类及其实现子类,函数重写
- 泛型
- 枚举:枚举类型使用方法,需要对枚举中的参数进行构造器构造
- 多态
- 强制类型转换:本例中考虑到类型通用,需要地方返回值类型设置为Object,某些地方需要强制类型转换。
- 动态代理,由于客户端没有实现类,所以动态代理代理的是公用接口,调用接口方法,把调用信息传递给服务器,经由接口实现类方法接收处理,把结果返回客户端,才是真正实现客户端调用实现类方法。
- 线程池
demo实现步骤
一、基础接口和实现类定义
- 创建一个公共接口,本例中是Remote_game_interface接口,存在一个playerinfo方法
public interface Remote_game_interface {
// 创建一个接口,在客户端没有实现类,在服务端存在实现类
String playerinfo(Player player);
}
- 在客户端创建一个用户信息类,作为服务器端接口实现类中方法的输入参数,需要进行序列化,本例中为Player类,继承Serializable接口,下面代码由于篇幅忽略了set和get方法。
import java.io.Serializable;
// 创建一个序列化的类,用来从客户端到服务端的传输,这个类是客户端可以实例化的类
public class Player implements Serializable {
private int id;
private String name;
public Player(int id,String name) {
this.id = id;
this.name = name;
}
}
- 在服务端创建一个接口的实现类,本例中是Play_info_find类,playinfo方法的参数是2中传输的player对象
public class Player_info_find implements Remote_game_interface {
@Override
public String playerinfo(Player player) {
// 使用客户端传递过来的参数
System.out.println("查一查id为"+player.getId()+"名字为"+player.getName()+"的玩家,查封");
// 反馈给客户端的信息
return "告诉你好消息:官网查封该外挂玩家";
}
}
二、 客户端定义
客户端主要作用是创建调用服务端实现类对象的方法的请求并且发送请求,同时需要接收服务端的回复消息。
- 客户端想要调用服务端实现类对象的方法,需要知道四个属性:服务端实现类对象的所属类/所属接口,被调用的方法名,需要的参数,所需参数的Class对象(反射调用需要)。由于请求需要在客户端和服务端传输,因此需要序列化。本例中客户端请求为RpcClientRequest类,下面代码由于篇幅忽略了set和get方法。
import java.io.Serializable;
// 客户端向服务端发送的调用接口方法请求,服务端使用这里面的参数来进行反射调用方法
public class RpcClientRequest implements Serializable {
private String interfacename; //想调用的接口名
private String methodname; // 想调用的接口方法名
private Object[] parameters; //调用参数
private Class<?>[] paramTypes; // 参数Class对象(未定)
}
-
定义了请求类之后,需要考虑如何实现。本例采用建造者builder模式,好处就是实现过程中可以自定义请求类中属性set的顺序,同时可以增加自定义操作(本例未体现)。建造过程中只需要传入请求的类型和内容,具体的复杂的内容赋值实现和其他的操作(本例未体现)不被考虑,在实际建造类中实现。
2.1 首先需要建立一个抽象类,定义建造请求类对象中所需要的实现方法。本例中是Builder类
public abstract class Builder { public abstract Builder setInterfacename(String msg); public abstract Builder setMethodname(String method); public abstract Builder setParacters(Object[] parameters); public abstract Builder setParTypes(Class<?>[] paramTypes); // 返回建造后的客户端请求对象 public abstract RpcClientRequest build(); }
2.2 建立一个抽象类的具体实现类,重写2.1中抽象类的方法,请求类对象的具体实现步骤在该类定义。本例中是trueBuilder类
public class TrueBuilder extends Builder { RpcClientRequest request = new RpcClientRequest(); @Override public Builder setInterfacename(String msg) { request.setInterfacename(msg); return this; } @Override public Builder setMethodname(String method) { request.setMethodname(method); return this; } @Override public Builder setParacters(Object[] parameters) { request.setParameters(parameters); return this; } @Override public Builder setParTypes(Class<?>[] paramTypes) { request.setParamTypes(paramTypes); return this; } @Override public RpcClientRequest build(){ return request; } }
-
定义客户端类,主要作用是实现socket通信,传递请求,接收服务端回复。本例中是RpcClient类,方法是sendrequest。
// 客户端,主要的作用是发送请求和接收数据
public class RpcClient {
public Object sendrequest(RpcClientRequest request,String host,int port){
try {
// 发送请求
Socket socket = new Socket(host,port);
ObjectOutputStream objos = new ObjectOutputStream(socket.getOutputStream());
ObjectInputStream objis = new ObjectInputStream(socket.getInputStream());
objos.writeObject(request);
objos.flush();
// 返回服务端返回的数据
return objis.readObject();
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
return null;
}
}
}
实现socket通信需要知道服务端的ip和端口。通过ObjectInputStream/ObjectOutputStream来对形成二进制流读取/写入到socket的输入输出流进行 客户端和服务器端的信息传输,注意flush()和close()方法的区别。
- 由于客户端没有接口的实现类,所以想要调用实现类方法,需要创建一个客户端的动态代理,代理公用接口,动态代理负责初始化客户端请求,实例化客户端类并发送请求,同时接收服务器反馈消息。(假定客户端也存在实现类且想要实现动态代理的话,就在invoke函数中调用实现类的方法。),本例客户端动态代理类为RpcClientProxy类,继承了InvocationHandler接口。
public class RpcClientProxy implements InvocationHandler {
// 服务端的ip和端口
private String host;
private int port;
public RpcClientProxy(String host,int port){
this.host = host;
this.port = port;
}
// 创建预设接口的动态代理,因为客户端没有接口的具体实现,所以参数是接口的Class对象
//方法一
public <T> T getProxy(Class<T> target){
return (T)Proxy.newProxyInstance(target.getClassLoader(),new Class<?>[]{target},this);
}
//方法二
// public <T> T getProxy(T target){
// return (T)Proxy.newProxyInstance(target.getClass().getClassLoader(),target.getClass().getInterfaces(),this);
// }
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("动态代理开始创建客户端请求。。。");
RpcClientRequest request = new TrueBuilder().setInterfacename(method.getDeclaringClass().getName())
.setMethodname(method.getName())
.setParacters(args)
.setParTypes(method.getParameterTypes())
.build();
RpcClient rpcClient = new RpcClient();
try{
Object receieve = rpcClient.sendrequest(request,host,port);
System.out.println("动态代理向服务端发送客户端请求。。。");
Response response = ((Response)receieve);
System.out.println("状态码为:"+response.getStatusCode());
System.out.println(response.getMsg());
return response.getData();
}catch (Exception e){
System.out.println("服务器响应失败");
return null;
}
}
}
其中创建动态代理的方法有两种,方法一的参数是接口的Class对象,方法二的参数是接口实现类的对象,但是由于实现类的在服务端,动态代理代理的是公用接口,所以本例采用方法一。
其中
target.getClassLoader() //是动态代理类的类加载器
new Class<?>[]{target} //是代理类实际代理的接口,这里传入的就是接口对象,这个语句的意思是:创建一个未知类型的数组 Class<?>[],里面只有一个元素(本例中代理类只代理一个接口),就是Class<T> target。
this //指代的是InvocationHandler对象,就是对生成动态代理类对象进行处理的对象,通俗点就是动态代理对象想要调用实现类方法时,实际调用哪个类的invoke方法。
invoke方法是真正进行代理类处理的函数,注意这里动态代理对象代理的是公用接口,所以客户端实际调用的是接口的方法,再传递到服务端实际类的方法处理返回才是真正调用实际类方法。invoke的参数分别是根据getProxy()方法创建的对象、调用的实现类方法(反射)、方法的参数。首先是用2.2中的TrueBuilder类来建造客户端发送请求,其中请求的具体信息采用反射机制取得。
接下来创建3中客户端对象,发送客户端请求,最后需要接收服务端响应,所以需要创建一个服务端的响应类。
- 在服务器端创建一个响应类,因为需要在客户端和服务器端传递,所以要序列化,继承Serializable接口。具备状态码、状态信息、响应信息等属性,服务器响应成功/失败后会对该类进行处理,赋值上述属性。本例中为Response类。代码如下,由于篇幅忽略了set和get方法。
public class Response<T> implements Serializable {
private Integer statusCode; // 状态码
// 补充信息
private String msg;
// 回复的服务端响应数据
private T data;
public void setStatusCode(Integer statusCode) {
this.statusCode = statusCode;
}
public Response<T> success(T data){
// 分别设置响应成功时的属性和信息
this.setStatusCode(StatusCode.Success.getCode());
this.setMsg(StatusCode.Success.getMsg());
this.setData(data);
return this;
}
public Response<T> fail(){
// 分别设置响应失败时的属性和信息
this.setStatusCode(StatusCode.Fail.getCode());
this.setMsg(StatusCode.Fail.getMsg());
return this;
}
}
同时需要预先定义一个枚举状态数组
public enum StatusCode{
Success(0125,"响应成功,服务器发回响应数据。。。"),
Fail(555,"响应失败,无回复");
private final int code;
private final String msg;
StatusCode(int code, String msg) {
this.code = code;
this.msg = msg;
}
public int getCode() {
return code;
}
public String getMsg() {
return msg;
}
}
至此完成客户端的定义
三、 服务端定义
服务器端的主要作用就是创建一个线程池,建立socket通信,如果存在客户端发送信息,就开启一个线程。线程的作用就是处理客户端发送过来的二进制流,反序列化为客户端请求信息,针对服务端实现类对象通过反射机制调用方法,根据调用成功/失败向服务端响应类对象赋值,并发送回客户端。
- 首先定义服务端类,建立一个线程池;需要建立socket通信,端口要和客户端中连接端口匹配。本例中是RpcServer类,具体代码如下
public class RpcServer {
private final ExecutorService threadPool;
// 初始化创建一个线程池
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);
}
// 建立网络通信,传入对象是接口的实现类(也就是服务端提供的服务) 和 网络端口
public void connect(Object service, int port){
try(ServerSocket serverSocket = new ServerSocket(port);) {
System.out.println("服务器正在创建连接。。");
// 接收socket通信
Socket socket ;
while((socket= serverSocket.accept())!=null){
System.out.println("服务器和客户端通信成功!对方ip为:"+socket.getInetAddress());
threadPool.execute(new WorkThread(service,socket));
}
threadPool.shutdown();
} catch (IOException e) {
System.out.println("服务器和客户端连接失败。。。");
e.printStackTrace();
}
}
}
其中ThreadPoolExecutor是ExecutorService的子类的子类,多态。线程池创建一个线程, 需要的参数是当前的网络通信socket和服务端提供的实现类服务。
- 接下来就是定义具体处理客户端请求的线程类,在重写的run()方法中实现调用方法并发送,本例中为WorkThread类,继承Runnable接口,代码如下
public class WorkThread implements Runnable{
private Object service;
private Socket socket;
public WorkThread(Object service, Socket socket) {
this.service = service;
this.socket = socket;
}
@Override
public void run(){
try {
ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());
ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());
// 接收到请求,注意这里有一个强制类型转换
RpcClientRequest request = (RpcClientRequest) objectInputStream.readObject();
// 反射调用,先加载实现类的Class对象,参数是方法名和类型Class对象
Method method = service.getClass().getMethod(request.getMethodname(),request.getParamTypes());
// 参数是方法作用对象(服务端的实现类服务),以及参数
Object result = method.invoke(service,request.getParameters());
System.out.println("服务器生成响应");
//设置响应信息
Response response = new Response().success(result);
// 发送响应
objectOutputStream.writeObject(response);
// 刷新缓冲区
objectOutputStream.flush();
} catch (IOException | ClassNotFoundException | NoSuchMethodException | IllegalAccessException |InvocationTargetException e) {
e.printStackTrace();
}
}
}
工作线程类需要两个参数,分别是服务端实现类对象(用于反射调用方法),以及服务端和客户端的当前网络socket通信,用于接收处理客户端输入流信息。反序列化接收就是使用objectInputStream.readObject()方法接收socket.getInputStream()输入流。
在反射调用之后,新建一个响应信息类对象并调用Response().success()进行赋值,同样序列化发送给客户端。
至此,完成了服务端的定义。
四、 测试
-
服务端实现
在服务端需要创建一个接口实现类对象用于反射调用,接下来需要创建一个服务器类对象,调用connect方法,参数是创建的实现类对象和服务器端口。代码如下
public class TestServer { public static void main(String[] args) { Remote_game_interface Tx_net = new Player_info_find(); // 接口实现类对象 RpcServer rpcServer = new RpcServer(); //服务器 rpcServer.connect(Tx_net,9000); } }
-
客户端实现
首先要创建客户端要发送给客户端的信息,所以新建一个player类对象,写入玩家信息以供服务器端查询。接下来创建一个RpcClientProxy类对象,标明socket通信的服务器端IP和端口,并调用getProxy()方法,传入此参数是Remote_game_interface接口的Class对象,产生一个客户端代理共用接口的动态代理对象。接下来该动态代理对象调用接口中的playerinfo()方法,传入参数是创建的player对象,至此完成了向服务器端传输待查询玩家信息并调用查询方法,接收查询结果的流程。代码如下
public class TestClient { public static void main(String[] args) { //创建代理 端口要和服务器端口匹配 RpcClientProxy rpcClientProxy = new RpcClientProxy("Localhost",9000); // 方式一: 传入要代理对象的接口的Class对象 Remote_game_interface client_proxy = rpcClientProxy.getProxy(Remote_game_interface.class); // 方式二:传入实际要代理的对象(本例不用,因为实现类不在客户端) //Remote_game_interface client_proxy = rpcClientProxy.getProxy(new Player_info_find() ); // 创建要查询的玩家 就是客户端向服务器传输的信息 Player player = new Player(2508,"倪粑粑"); // 使用客户端的代理调用服务器端的方法,信息由player提供 String result = client_proxy.playerinfo(player); // 输出官网回复结果 System.out.println("官网回复为:"+result); } }
-
结果
客户端:
动态代理开始创建客户端请求。。。
动态代理向服务端发送客户端请求。。。
状态码为:85
响应成功,服务器发回响应数据。。。
官网回复为:告诉你好消息:官网查封该外挂玩家
服务端:
服务器正在创建连接。。
服务器和客户端通信成功!对方ip为:/127.0.0.1
查一查id为2508名字为倪粑粑的玩家,查封
服务器生成响应