实现自己的RPC框架

前言

RPC(Remote Procedure Call)—远程过程调用,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。直白点说它可以让你调用远程接口,就像调用本地方法一样,要实现这个简易版功能要用到socket编程JDK动态代理反射等。

dubbo就是阿里实现的RPC微服务的一种框架了,这篇记录一下自己的第一版RPC框架。代码上传至github,链接

思路

假设有两台服务Local和Remote,Local上有一个接口HelloService,里面有一个SayHi的方法。为了实现接口微服务化,在Remote上实现该接口的实现HelloServiceImpl。

现在想要实现的功能就是Local调用SayHi的方法时,得到Remote上一样的实现。我一开始的想法,只要在Local端把对应的接口名发过去,然后Remote通过该接口名,把该接口的实现通过反射生成,然后再传回去即可。如下图。

你想输入的替代文字

当然上面的想法是错误的,因为通过反射和Socket把对应的实现类不能完成对应的功能,假设Remote实现类中要用到Remote上的资源,而你的Local却没有。所以我们不能把这个实现类传回来,而是要把对应的结果传回去。正确的想法是:

1.客服端将想要调用的类名、方法名、方法的参数利用socket发送请求到服务端。
2.服务端根据这些参数选择对应接口,然后返回执行完毕的结果。    
3.客服端得到服务端返回的结果,做出相应的操作。

核心思想

现在来看第一步,把类名、方法名、参数等传到服务端,这个是针对所有的方法。第一想法就是为每一个方法加上Socket传输。但是如果方法有几百几千个,难道都要我们去手动加吗!所以这里想到面向切面编程,利用JDK的动态代理来增强我们的类方法。JDK的动态代理,主要是要实现一个InvocationHandler接口的类,在ivoke方法中加强原有的方法。

第二步,服务端需要根据前面的参数,利用反射生成对应的实现类,然后把执行的结果返回回去。实现效果图:

你想输入的替代文字

核心代码

项目结构:

你想输入的替代文字

HelloService

1
2
3
public interface HelloService {
String sayHi(String name);
}

HelloServiceImpl

1
2
3
4
5
6
//需要实现Serializable接口,序列化要使用
public class HelloServiceImpl implements HelloService,Serializable{
public String sayHi(String name) {
return "hi"+name;
}
}

MyInvokeHandler核心代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
socket = new Socket();
socket.connect(new InetSocketAddress("localhost", 8088));
output = new ObjectOutputStream(socket.getOutputStream());
System.out.println("Client 传递信息中...");

output.writeUTF(target.getName());
output.writeUTF(method.getName());
output.writeObject(method.getParameterTypes());
output.writeObject(args);

input = new ObjectInputStream(socket.getInputStream());
Object o = input.readObject();
return o;
}

server核心代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public void run()  {
/**
* 获得类名(className)以及方法名(methodName),通过类名和方法列表我们可以得到一个method,
* 想要执行这个method还要有调用method参数值。
*/
ServerSocket server = new ServerSocket();
server.bind(new InetSocketAddress(8088));
System.out.println("Server 等待客服端的链接...");

objectInputStream = new ObjectInputStream(client.getInputStream());
System.out.println("Server 等待接受...");
String className = objectInputStream.readUTF();
String methodName = objectInputStream.readUTF();
Class<?>[] parameterTypes = (Class<?>[]) objectInputStream.readObject();
Object[] arguments = (Object[]) objectInputStream.readObject();

//根据名字把实现接口的方法类返回回去
System.out.println("Server 收到"+className);
Class serverClass = serviceRegistry.get(className);
Method method = serverClass.getMethod(methodName,parameterTypes);
Object result = method.invoke(serverClass.newInstance(),arguments);
//给client的响应
System.out.println("Server 回应消息...");
objectOutputStream = new ObjectOutputStream(client.getOutputStream());
objectOutputStream.writeObject(result);
}

ServerStart ,先启动ServerStart,然后再启动RPCClient.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ServerStart {
public static void main(String args[]) throws Exception{
new Thread(new Runnable() {
@Override
public void run() {
try {
Server server = new Server();
server.register(HelloService.class, HelloServiceImpl.class);
server.start();
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
}).start();
}
}

RPCClient:

1
2
3
4
5
6
7
8
public static void main(String args[]) throws Exception{
MyInvokeHandler invokeHandler = new MyInvokeHandler(HelloService.class);
HelloService helloService = (HelloService) Proxy.newProxyInstance(HelloService.class.getClassLoader(),
new Class<?>[]{HelloService.class},invokeHandler);
String result = helloService.sayHi("123") ;
System.out.println(result);

}

注意的地方

1
2
output.writeUTF(target.getName());
output.flush();

当我们只把方法名传过去的时候,会发现服务端被readUTF阻塞住,这个是因为数据量比较小,还在缓冲区,只要调用flush方法就会缓存区中的数据写入到套接字接受法。我们上面因为调用了writeObject,所以不用显示调用flush也可以。

1
2
HelloService helloService = (HelloService) Proxy.newProxyInstance(HelloService.class.getClassLoader(),
new Class<?>[]{HelloService.class},invokeHandler);

newProxyInstance中的第二个参数需要传入接口类数组,刚好HelloService是一个接口,只要数组化即可。平常这个参数传入HelloServiceImpl.class.getInterfaces()。

缺点与改进

第一版我们使用的BIO对性能有比较大的影响,每一个链接都需要一个线程支持。第二版将利用NIO来改进。