RPC理解

本文是根据B站马士兵所做的学习笔记。
RPC ( Remote Procedure Call ):远程方法调用,它所关注的是分布式通信问题。

一、代码解释

三大组件:Client端、Stub端、Server端
Server端有相应服务对应的接口和实现类,例如IUserService接口和UserServiceImpl实现类

public interface IUserService {
    public User findUserById(Integer id);
}
public class UserServiceImpl implements IUserService {
    public User findUserById(Integer id) {
        return new User(id,"huangying");
    }
}
1.最原始方式:二进制传递

客户端:

public class Client {
    public static void main(String[] args) throws IOException {
        Socket s = new Socket("127.0.0.1", 8888);
        //客户端将消息写出去
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        DataOutputStream dos = new DataOutputStream(baos);
        dos.writeInt(123);//将123转化为二进制格式
        s.getOutputStream().write(baos.toByteArray());
        s.getOutputStream().flush();

        //客户端接收消息
        DataInputStream dis = new DataInputStream(s.getInputStream());
        int id = dis.readInt();
        String name=dis.readUTF();
        User user = new User(id, name);
        System.out.println(user);

        dos.close();
        s.close();
    }
}

服务端:

public class Server {
    private static boolean running=true;
    public static void main(String[] args) throws IOException {
        ServerSocket ss = new ServerSocket(8888);//服务端对接口进行监听
        while(running){
            Socket s = ss.accept();
            process(s);
            s.close();
        }
        ss.close();
    }
    private static void process(Socket s) throws IOException {
        InputStream in = s.getInputStream();
        OutputStream out = s.getOutputStream();
        DataInputStream dis = new DataInputStream(in);
        DataOutputStream dos = new DataOutputStream(out);

        //服务端接收消息
        int id = dis.readInt();
        UserServiceImpl service = new UserServiceImpl();
        User user = service.findUserById(id);

        //服务端向客户端发送消息
        dos.writeInt(user.getId());
        dos.writeUTF(user.getName());
        dos.flush();
    }
}

缺点:这是一种最原始的通信方式,client必须知道传输对象的一切细节;当对象属性改变时代码就要变;传输代码和业务代码混合在一起。

2.引入Stub代理

客户端:

public class Client {
    public static void main(String[] args) throws IOException {
        Stub stub = new Stub();
        System.out.println(stub.findUserById(123));
    }
}

Stub代理:

public class Stub {
    public User findUserById(Integer id) throws IOException {
        Socket s = new Socket("127.0.0.1", 8888);
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        DataOutputStream dos = new DataOutputStream(baos);
        dos.writeInt(123);

        s.getOutputStream().write(baos.toByteArray());
        s.getOutputStream().flush();

        DataInputStream dis = new DataInputStream(s.getInputStream());
        int receiveId = dis.readInt();
        String name=dis.readUTF();
        User user = new User(receiveId, name);

        dos.close();
        s.close();
        return user;
    }
}

优点:引入Stub代理,向用户提供方法,从而隐藏网络传输和业务操作的细节,简化了客户端的操作流程。
缺点:只能代理一个方法:findUserById(),返回的对象仅是User类。 随着业务增多,Stub中会堆积很多的函数。

3.引入动态代理

客户端:

public class Client {
    public static void main(String[] args) {
        IUserService service = Stub.getStub();//不是通过new生成对象,而是生成动态代理对象
        System.out.println(service.findUserById(123));//通过动态代理对象调用方法时,实际上是调用invoke方法
    }
}

服务端:

public class Stub {
    public static IUserService getStub(){
        //对调用的方法进行泛化
        InvocationHandler h=new InvocationHandler(){
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                Socket s = new Socket("127.0.0.1", 8888);
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                DataOutputStream dos = new DataOutputStream(baos);
                dos.writeInt(123);

                s.getOutputStream().write(baos.toByteArray());
                s.getOutputStream().flush();

                DataInputStream dis = new DataInputStream(s.getInputStream());
                int id = dis.readInt();
                String name = dis.readUTF();
                User user = new User(id, name);

                dos.close();
                s.close();
                return user;
            }
        };
        //动态代理对象需要和被代理对象实现同一个接口IUserService
        Object o = Proxy.newProxyInstance(IUserService.class.getClassLoader(), new Class[]{IUserService.class}, h);
        return (IUserService)o;
    }
}

优点:在2中随着业务增多,Stub中会堆积很多的函数。通过引入动态代理,外部使用返回的动态代理对象去调用某个功能时,内部就会调用invoke方法,在Stub中不用写每一个业务函数的细节。
缺点:仅是粗略引入动态代理,在Stub中方法参数是写死的:dos.writeInt(123);,可以对方法进行进一步泛化。

4.动态代理:进一步泛化

Stub端

public class Stub {
    public static IUserService getStub(){
        InvocationHandler h = new InvocationHandler() {
            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());

                String methodName = method.getName();
                Class[] parameterTypes = method.getParameterTypes();
                oos.writeUTF(methodName);//Stub向服务端发送调用方法的方法名
                oos.writeObject(parameterTypes);//Stub向服务端发送调用方法的参数类型(受重载影响)
                oos.writeObject(args);//Stub向服务端发送调用方法的具体参数
                oos.flush();

                DataInputStream dis = new DataInputStream(s.getInputStream());
                int id = dis.readInt();
                String name = dis.readUTF();
                User user = new User(id, name);

                oos.close();
                s.close();
                return user;
            }
        };
        //生成动态代理对象:它与被代理对象实现同一个接口,因此需要的参数有接口的类加载器、接口的类信息
        Object o = Proxy.newProxyInstance(IUserService.class.getClassLoader(), new Class[]{IUserService.class}, h);
        //返回动态代理对象
        return (IUserService)o;
    }
}

Service端

public class Service {
    private static boolean running=true;
    public static void main(String[] args) throws IOException {
        ServerSocket ss = new ServerSocket(8888);//服务端对接口进行监听
        while(running){
            Socket s = ss.accept();
        }
    }
    private static void process(Socket s) throws IOException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        InputStream in = s.getInputStream();
        OutputStream out = s.getOutputStream();
        ObjectInputStream oos = new ObjectInputStream(in);
        DataOutputStream dos = new DataOutputStream(out);

        String methodName = oos.readUTF();//读取客户端调用的方法名
        Class[] parameterTypes=(Class[])oos.readObject();//读取客户端调用的方法的参数类型
        Object[] args=(Object[])oos.readObject();//读取客户端调用的方法的参数


        IUserService service = new UserServiceImpl();
        Method method = service.getClass().getMethod(methodName, parameterTypes);//获得对应的方法
        User user = (User) method.invoke(service, args);

        dos.writeInt(user.getId());
        dos.writeUTF(user.getName());
        dos.flush();
    }
}

优点:Stub端不是向服务端写死参数,而是告知服务端客户的请求(调用方法的名字和参数类型),服务端处理请求,向客户端发送结果。
缺点:服务端分别向客户端发送对象的各个属性,当对象的属性改变时,代码也要相应改变。

5.直接写/读对象

Stub端

public class Stub {
    public static IUserService getStub(){
            InvocationHandler h = new InvocationHandler() {
                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());

                    String methodName = method.getName();
                    Class[] parameterTypes = method.getParameterTypes();
                    oos.writeUTF(methodName);
                    oos.writeObject(parameterTypes);
                    oos.flush();

                    ObjectInputStream ois = new ObjectInputStream(s.getInputStream());
                    User user = (User)ois.readObject();//直接从二进制流中读入一个User对象

                    oos.close();
                    s.close();
                    return user;
                }
            };
            Object o = Proxy.newProxyInstance(IUserService.class.getClassLoader(), new Class[]{IUserService.class}, h);
        return (IUserService)o;
    }
}

Service端

public class Service {
    private static boolean running=true;
    public static void main(String[] args) throws IOException {
        ServerSocket ss = new ServerSocket(8888);//服务端对接口进行监听
        while(running){
            Socket s = ss.accept();
        }
    }
    private static void process(Socket s) throws IOException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        InputStream in = s.getInputStream();
        OutputStream out = s.getOutputStream();
        ObjectInputStream ois = new ObjectInputStream(in);

        String methodName = ois.readUTF();
        Class[] parameterTypes=(Class[])ois.readObject();
        Object[] args=(Object[])ois.readObject();


        IUserService service = new UserServiceImpl();
        Method method = service.getClass().getMethod(methodName, parameterTypes);
        User user = (User) method.invoke(service, args);

        ObjectOutputStream oos = new ObjectOutputStream(out);
        oos.writeObject(user);//直接写出对象
        oos.flush();
    }
}

优点:客户端直接读到对象,服务端直接写出对象。
缺点:

6.对方法接口进行泛化

Client端

public class Client {
    public static void main(String[] args) {
        //对接口进行泛化,返回实现该接口的动态代理类对象
        IUserService service = (IUserService) Stub.getStub(IUserService.class);
        System.out.println(service.findUserById(123));
    }
}

Stub端

public class Stub {
    public static Object getStub(final Class clazz){
        InvocationHandler h=new InvocationHandler(){
            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());

                String clazzName = clazz.getName();//写出接口的名字
                String methodName = method.getName();
                Class[] parameterTypes = method.getParameterTypes();

                oos.writeUTF(clazzName);
                oos.writeUTF(methodName);
                oos.writeObject(parameterTypes);
                oos.writeObject(args);
                oos.flush();

                ObjectInputStream ois = new ObjectInputStream(s.getInputStream());
                Object o = ois.readObject();

                oos.close();
                s.close();
                return o;
            }
        };
        //生成clazz接口对应的动态对象
        Object o = Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{clazz}, h);
        return o;
    }
}

Service端

public class Service {
    private static boolean running=true;
    public static void main(String[] args) throws IOException {
        ServerSocket ss = new ServerSocket(8888);//服务端对接口进行监听
        while(running){
            Socket s = ss.accept();
        }
    }
    private static void process(Socket s) throws IOException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException {
        InputStream in = s.getInputStream();
        OutputStream out = s.getOutputStream();
        ObjectInputStream ois = new ObjectInputStream(in);

        String clazzName=ois.readUTF();
        String methodName = ois.readUTF();
        Class[] parameterTypes=(Class[])ois.readObject();
        Object[] args=(Object[])ois.readObject();

        //根据claaName找到具体的实现类,可以根据Spring注入来解决
        Class clazz=null;
        clazz=UserServiceImpl.class;


        Method method = clazz.getMethod(methodName, parameterTypes);
        Object o = (Object) method.invoke(clazz.newInstance(), args);

        ObjectOutputStream oos = new ObjectOutputStream(out);
        oos.writeObject(o);
        oos.flush();
    }
}

优点:对服务端的服务接口进行泛化,使得它能产生相应服务接口的动态代理对象,所以传入的参数为接口的class对象。