当前位置 博文首页 > 青石路:RPC 是通信协议吗 ?→ 我们来看下它的演进过程

    青石路:RPC 是通信协议吗 ?→ 我们来看下它的演进过程

    作者:青石路 时间:2021-01-18 12:08

    开心一刻

      一实习小护士给我挂针,拿着针在我胳膊上扎了好几针也没找到血管

      但这位小姑娘真镇定啊,表情严肃认真,势有不扎到血管不罢休的意思

      十几针之后,我忍着剧痛,带着敬畏的表情问小护士:你这针法跟容嬷嬷学的么?

    写在前面

      单机应用中的方法调用很简单,直接调用就行,像这样

      因为调用方与被调用方在一个进程内

      随着业务的发展,单机应用会越来越力不从心,势必会引入分布式来解决单机的问题,那么调用方如何调用另一台机器上的方法呢 ?

      这就涉及到分布式通信方式,从单机走向分布式,产生了很多通信方式

      而 RPC 就是实现远程方法调用的方式之一;说 RPC 不是协议,可能很多小伙伴难以置信,以为我在骗你们

      看着你们这一身腱子肉,我哪敢骗你们;只要你们把下面的看完,骗没骗你们,你们自己说了算

    RPC 的演进过程

      先说明一下,下文中的示例虽然是 Java 代码实现的,但原理是通用的,重点是理解其中的原理

      第一版

        两台机器之间进行交互,那么肯定离不开网络通信协议,TCP / IP 也就成了绕不开的点,所以先辈们最初想到的方法就是通过 TCP / IP 来实现远程方法的调用

        而操作系统是没有直接暴露 TCP / IP 接口的,而是通过 Socket 抽象了 TCP / IP 接口,所以我们可以通过 Socket 来实现最初版的远程方法调用

        完整示例代码:rpc-01,核心代码如下

        Server:

    package com.qsl.rpc;
    
    import com.qsl.rpc.entity.User;
    import com.qsl.rpc.server.UserServiceImpl;
    import com.qsl.rpc.service.IUserService;
    
    import java.io.DataInputStream;
    import java.io.DataOutputStream;
    import java.io.InputStream;
    import java.io.OutputStream;
    import java.net.ServerSocket;
    import java.net.Socket;
    
    /**
     * @author 青石路
     * @date 2021/1/16 19:49
     */
    public class Server {
        private static boolean is_running = true;
    
        public static void main(String[] args) throws Exception {
            ServerSocket serverSocket = new ServerSocket(8888);
            while (is_running) {
                System.out.println("等待 client 连接");
                Socket client = serverSocket.accept();
                System.out.println("获取到 client...");
                handle(client);
                client.close();
            }
            serverSocket.close();
        }
    
        private static void handle(Socket client) throws Exception {
            InputStream in = client.getInputStream();
            OutputStream out = client.getOutputStream();
            DataInputStream dis = new DataInputStream(in);
            DataOutputStream dos = new DataOutputStream(out);
    
            // 从 socket 读取参数
            int id = dis.readInt();
            System.out.println("id = " + id);
    
            // 查询本地数据
            IUserService userService = new UserServiceImpl();
            User user = userService.getUserById(id);
    
            // 往 socket 写响应值
            dos.writeInt(user.getId());
            dos.writeUTF(user.getName());
            dos.flush();
    
            dis.close();
            dos.close();
        }
    }
    View Code

        Client:

    package com.qsl.rpc;
    
    import com.qsl.rpc.entity.User;
    
    import java.io.DataInputStream;
    import java.io.DataOutputStream;
    import java.net.Socket;
    
    /**
     * @author 青石路
     * @date 2021/1/16 19:49
     */
    public class Client {
    
        public static void main(String[] args) throws Exception {
            Socket s = new Socket("127.0.0.1", 8888);
    
            // 网络传输数据
            // 往 socket 写请求参数
            DataOutputStream dos = new DataOutputStream(s.getOutputStream());
            dos.writeInt(18);
            // 从 socket 读响应值
            DataInputStream  dis = new DataInputStream(s.getInputStream());
            int id = dis.readInt();
            String name = dis.readUTF();
            // 将响应值封装成 User 对象
            User user = new User(id, name);
            dos.close();
            dis.close();
            s.close();
    
            // 进行业务处理
            System.out.println(user);
        }
    }
    View Code

        代码很简单,就是一个简单的 Socket 通信;如果看不懂,那就需要去补充下 Socket 和 IO 的知识

        测试结果如下

        可以看到 Client 与 Server 之间是可以进行通信的;但是,这种方式非常麻烦,有太多缺点,最明显的一个就是

          Client 端业务代码 与 网络传输代码 混合在一起,没有明确的模块划分

          如果有多个开发者同时进行 Client 开发,那么他们都需要知道 Socket、IO

      第二版

        针对第一版的缺点,演进出了这一版,引进 Stub (早期的叫法,不用深究,理解成代理就行)实现 Client 端网络传输代码的封装

        完整示例代码:rpc-02,改动部分如下

        Stub:

    package com.qsl.rpc;
    
    import com.qsl.rpc.entity.User;
    
    import java.io.DataInputStream;
    import java.io.DataOutputStream;
    import java.net.Socket;
    
    /**
     * 相当于一个静态代理,封装了网络数据传输
     * @author 青石路
     * @date 2021/1/17 9:38
     */
    public class Stub {
    
        public User getUserById(Integer id) throws Exception {
            Socket s = new Socket("127.0.0.1", 8888);
    
            // 网络传输数据
            // 往 socket 写请求参数
            DataOutputStream dos = new DataOutputStream(s.getOutputStream());
            dos.writeInt(id);
            // 从 socket 读响应值
            DataInputStream dis = new DataInputStream(s.getInputStream());
            int userId = dis.readInt();
            String name = dis.readUTF();
            // 将响应值封装成 User 对象
            User user = new User(userId, name);
            dos.close();
            dis.close();
            s.close();
    
            return user;
        }
    }
    View Code

        Client:

    package com.qsl.rpc;
    
    import com.qsl.rpc.entity.User;
    
    /**
     * @author 青石路
     * @date 2021/1/16 19:49
     */
    public class Client {
    
        public static void main(String[] args) throws Exception {
    
            // 不再关注网络传输
            Stub stub = new Stub();
            User user = stub.getUserById(18);
    
            // 进行业务处理
            System.out.println(user);
        }
    }
    View Code

        Client 不再关注网络数据传输,一心关注业务代码就好

        有小伙伴可能就杠上了:这不就是把网络传输代码移了个位置嘛,这也算改进?

        迭代开发是一个逐步完善的过程,而这也算是一个改进哦

        但这一版还是有很多缺点,最明显的一个就是

          Stub 只能代理 IUserService 的一个方法 getUserById ,局限性太大,不够通用

          如果想在 IUserService 新增一个方法: getUserByName ,那么需要在 Stub 中新增对应的方法,Server 端也需要做对应的修改来支持

      第三版

        第二版中的 Stub 代理功能太弱了,那有没有什么方式可以增强 Stub 的代理功能了?

        前面的 Stub 相当于是一个静态代理,所以功能有限,那静态代理的增强版是什么了,没错,就是:动态代理

        不熟悉动态代理的小伙伴,一定要先弄懂动态代理:设计模式之代理,手动实现动态代理,揭秘原理实现

        JDK 有动态代理的 API,我们就用它来实现

        完整示例代码:rpc-03,相较于第二版,改动比较大,大家需要仔细看

        Server:

    package com.qsl.rpc;
    
    import com.qsl.rpc.entity.User;
    import com.qsl.rpc.server.UserServiceImpl;
    import com.qsl.rpc.service.IUserService;
    
    import java.io.InputStream;
    import java.io.ObjectInputStream;
    import java.io.ObjectOutputStream;
    import java.io.OutputStream;
    import java.lang.reflect.Method;
    import java.net.ServerSocket;
    import java.net.Socket;
    
    /**
     * @author 青石路
     * @date 2021/1/16 19:49
     */
    public class Server {
        private static boolean is_running = true;
    
        public static void main(String[] args) throws Exception {
            ServerSocket serverSocket = new ServerSocket(8888);
            while (is_running) {
                System.out.println("等待 client 连接");
                Socket client = serverSocket.accept();
                System.out.println("获取到 client...");
                handle(client);
                client.close();
            }
            serverSocket.close();
        }
    
        private static void handle(Socket client) throws Exception {
            InputStream in = client.getInputStream();
            OutputStream out = client.getOutputStream();
            ObjectInputStream ois = new ObjectInputStream(in);
            ObjectOutputStream oos = new ObjectOutputStream(out);
    
            // 获取方法名、方法的参数类型、方法的参数值
            String methodName = ois.readUTF();
            Class[] parameterTypes = (Class[]) ois.readObject();
            Object[] args = (Object[]) ois.readObject();
    
            IUserService userService = new UserServiceImpl();
            Method method = userService.getClass().getMethod(methodName, parameterTypes);
            User user = (User) method.invoke(userService, args);
    
            // 往 socket 写响应值;直接写可序列化对象(实现 Serializable 接口)
            oos.writeObject(user);
            oos.flush();
    
            ois.close();
            oos.close();
        }
    }
    View Code

        Stub:

    package com.qsl.rpc;
    
    import com.qsl.rpc.entity.User;
    import com.qsl.rpc.service.IUserService;
    
    import java.io.ObjectInputStream;
    import java.io.ObjectOutputStream;
    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.Method;
    import java.lang.reflect.Proxy;
    import java.net.Socket;
    
    /**
     * 动态代理,封装了网络数据传输
     * @author 青石路
     * @date 2021/1/17 9:38
     */
    public class Stub {
    
        public static IUserService getStub() {
            Object obj = Proxy.newProxyInstance(IUserService.class.getClassLoader(),
                    new Class[]{IUserService.class}, new NetInvocationHandler());
            return (IUserService)obj;
        }
    
        static class NetInvocationHandler implements InvocationHandler {
    
            /**
             *
             * @param proxy
             * @param method
             * @param args
             * @return
             * @throws Throwable
             */
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                Socket s = new Socket("127.0.0.1", 8888);
    
                // 网络传输数据
                ObjectOutputStream oos = new ObjectOutputStream(s.getOutputStream());
                // 传输方法名、方法参数类型、方法参数值;可能会有方法重载,所以要传参数列表
                oos.writeUTF(method.getName());
                Class[] parameterTypes = method.getParameterTypes();
                oos.writeObject(parameterTypes);
                oos.writeObject(args);
    
                // 从 socket 读响应值
                ObjectInputStream ois = new ObjectInputStream(s.getInputStream());
                User user = (User) ois.readObject();
    
                oos.close();
                ois.close();
                s.close();
    
                return user;
            }
        }
    }
    View Code

        Client:

    package com.qsl.rpc;
    
    import com.qsl.rpc.entity.User;
    import com.qsl.rpc.service.IUserService;
    
    /**
     * @author 青石路
     * @date 2021/1/16 19:49
     */
    public class Client {
    
        public static void main(String[] args) throws Exception {
    
            IUserService userService = Stub.getStub();
            //User user = userService.getUserById(23);
    
            User user = userService.getUserByName("李小龙");
            // 进行业务处理
            System.out.println(user);
        }
    }
    View Code

        我们来看下效果

        此时, IUserService 接口的方法都能被代理了,即使它新增接口, Stub 不用做任何修改也能代理上

        另外, Server 端的响应值改成了对象,而不是单个属性逐一返回,那么无论