前言
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方法中加强原有的方法。
第二步,服务端需要根据前面的参数,利用反射生成对应的实现类,然后把执行的结果返回回去。实现效果图:

核心代码
项目结构:

HelloService1
2
3public interface HelloService {
    String sayHi(String name);
}
HelloServiceImpl1
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来改进。