前言
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
3public 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 | public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { |
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
26public 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
16public class ServerStart {
public static void main(String args[]) throws Exception{
new Thread(new Runnable() {
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
8public 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 | output.writeUTF(target.getName()); |
当我们只把方法名传过去的时候,会发现服务端被readUTF阻塞住,这个是因为数据量比较小,还在缓冲区,只要调用flush方法就会缓存区中的数据写入到套接字接受法。我们上面因为调用了writeObject,所以不用显示调用flush也可以。1
2HelloService helloService = (HelloService) Proxy.newProxyInstance(HelloService.class.getClassLoader(),
new Class<?>[]{HelloService.class},invokeHandler);
newProxyInstance中的第二个参数需要传入接口类数组,刚好HelloService是一个接口,只要数组化即可。平常这个参数传入HelloServiceImpl.class.getInterfaces()。
缺点与改进
第一版我们使用的BIO对性能有比较大的影响,每一个链接都需要一个线程支持。第二版将利用NIO来改进。