从零理解rpc

首先,要知道rpc(Remote Procedure Call)只是一种定义,表示的是远程调用。

啥是远程调用?

两台不在一起的电脑A和B,A电脑上存有一些用户数据,b想要查询a上存的用户信息,这个时候,就需要远程调用A暴露出来的接口进行查询。或者说调用A提供的服务进行查询。

两个电脑想要通过网络通信,最重要的:二进制!一切信息在网络中都可以看成bit0、bit1,或者说高电平、低电平。

接下来,看看rpc是怎么一步一步演进的。

先定义一下基本的User结构

import java.io.Serializable;

public class User implements Serializable {


    private Integer id;
    private String name;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }
}

A中还定义了一个接口,其中暴露了一个可以通过id查询User的方法

public interface IUserService {
    public User findUserById(Integer id);
}

v1-最原始的二进制传输

既然网络传输使用的是二进制,那最直接的方法:把需要传输的user信息转换成二进制。
使用的是:ByteArrayOutputStream、ByteArrayInputStream

package rpc_v1;

public class Client {
    public static void main(String[] args) throws IOException {
    	// 写id给server
        Socket socket = new Socket("127.0.0.1",8888);
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        DataOutputStream dos = new DataOutputStream(baos);
        dos.writeInt(123);

        socket.getOutputStream().write(baos.toByteArray());
        socket.getOutputStream().flush();
		
		// 从server接收User信息
        DataInputStream dis = new DataInputStream(socket.getInputStream());
        int id = dis.readInt();
        String name = dis.readUTF();
        User user = new User(id, name);

        System.out.println(user);

        dos.close();
        socket.close();
    }
}

客户端client需要把查询id传递给server,使用ByteArrayOutputStream ,再套一层DataOutputStream,为了方便我们写入各种数据类型。再通过socket.getOutputStream().write(baos.toByteArray()); 把我们的数据通过二进制传递出去啦~

package rpc_v1;

public class Server {

    private static boolean running = true;

    public static void main(String[] args) throws Exception {
        ServerSocket ss = new ServerSocket(8888);
        while (running){
            Socket s = ss.accept();
            process(s);
            s.close();
        }
        ss.close();
    }

    private static void process(Socket s) throws Exception{
        InputStream inputStream = s.getInputStream();
        OutputStream outputStream = s.getOutputStream();
        DataInputStream dataInputStream = new DataInputStream(inputStream);
        DataOutputStream dataOutputStream = new DataOutputStream(outputStream);

		//读client发过来的id
        int id = dataInputStream.readInt();
        // 查找服务
        IUserService service = new UserServiceImpl();
        User user = service.findUserById(id);
		// 写会对象
        dataOutputStream.writeInt(user.getId());
        dataOutputStream.writeUTF(user.getName());
        dataOutputStream.flush();
    }
}

对于server端,

  • 1 读取client发送的id,nt id = dataInputStream.readInt();
  • 2 查找User
  • 3 把这个User对象写给client

这里是最最最基本的方法,把这个对象的每个数据类型写回去!

这种方式的缺点很明显,比如User对象改变一个属性,那么server端的代码都需要改。这种写死的方法,要求client、server都必须了解对象,且每一个方法都需要再写方法。且传输过程代码、业务逻辑代码全部混淆在一起,太糟糕了!

v2-简化客户端,引入stub

v1太糟糕了,那么v2想要的改进点:把client端传输代码和业务代码拆分开。

使用一个代理stub代理client中网络通信的操作。

public class Client {
    public static void main(String[] args) throws IOException {
        Stub stub = new Stub();
        System.out.println(stub.findUserById(123));
    }
}
public class Stub {
    public User findUserById(int i) throws IOException {
        Socket socket = new Socket("127.0.0.1",8888);
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        DataOutputStream dos = new DataOutputStream(baos);
        dos.writeInt(123);

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

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

        System.out.println(user);

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

但这个stub缺点同样很明显,只代理了一个findUserById方法就需要写这么多,有更多的方法,就需要写更多更多的代码,差评!

v3-动态代理生成service

v2虽然把client端网络通信和业务代码分开了,但目前的代理只能代理一个方法。

希望:代理直接给客户端提供封装好的service,这个service有getUserById方法,且代理给client屏蔽掉了网络细节,那我就可以远程访问了!

public class Client {
    public static void main(String[] args) throws  Exception {
        IUserService service = Stub.getStub();
        System.out.println(service.findUserById(123));
    }
}

如上的代码,client直接调用service.findUserById(123)就可以通信了,原本的网络通信呢,被service隐藏了,但它执行findUserById时会给我们加上网络通信的逻辑,无需我们再操心。

(这里其实涉及了代理模式)

当我们在client调用service时,这个类是动态产生的。使用Stub.getStub()实际会调用stub中getStub中定义的invoke方法。

public class Stub {
    public static IUserService getStub(){

        InvocationHandler h = new InvocationHandler() {
            @Override
            public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
                Socket socket = new Socket("127.0.0.1",8888);
				//传给server
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                DataOutputStream dos = new DataOutputStream(baos);
                dos.writeInt(123);

                socket.getOutputStream().write(baos.toByteArray());
                socket.getOutputStream().flush();
				//接收server的信息
                DataInputStream dis = new DataInputStream(socket.getInputStream());
                int id = dis.readInt();
                String name = dis.readUTF();
                User user = new User(id, name);

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

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

InvocationHandler是调用方法时的处理

invoke方法有三个参数

  • proxy :哪个代理
  • method :哪个方法
  • args:哪些输入参数

这个stub的写法好处很明显,当给IUserService添加新的方法时,我们都可以在invoke可统一处理

v4-动态代理,支持多个方法

v3的stub其实存在缺陷,代码:dos.writeInt(123);是写死的。不论你调用的啥方法,传给server的都是123。想用别的方法怎么办呢?继续完善。

stub采用动态代理的思想来设计,

public class Stub {

    public static IUserService getStub(){

        InvocationHandler h = new InvocationHandler(){
            @Override
            public Object invoke(Object o, Method method, Object[] args) throws Throwable {
                Socket socket = new Socket("127.0.0.1",8888);
                // 改动部分
                ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream());

                String methodName = method.getName();   //获取方法名
                Class[] parameterTypes = method.getParameterTypes();  //获取方法参数类型,避免在server端的重载
                out.writeUTF(methodName);  // 代替client把方法名写出去
                out.writeObject(parameterTypes); // 把参数类型写出去
                out.writeObject(args);  // 把参数写出去
                out.flush();


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

                out.close();
                socket.close();
                return user;
            }
        };

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

对我们的服务器端,同样改进。

当我们的client如下时:

public class Client {
    public static void main(String[] args) {
        IUserService service = Stub.getStub();
        System.out.println(service.findUserById(123));
    }
}

通过service调用findUserById时,会调用到stub的invoke,传入的参数method=findUserById。最终通过:方法名》参数类型》参数,这种顺序传递给了server端。

public class Server {

    private static boolean running = true;

    public static void main(String[] args) throws Exception {
        ServerSocket ss = new ServerSocket(8888);
        while (running){
            Socket s = ss.accept();
            process(s);
            s.close();
        }
        ss.close();
    }

    private static void process(Socket s) throws Exception{
        InputStream in = s.getInputStream();
        OutputStream out = s.getOutputStream();

        ObjectInputStream oin = new ObjectInputStream(in);
        DataOutputStream dout = new DataOutputStream(out);

        //改进部分
        String methodName  = oin.readUTF();  // 读入方法名
        Class[] parameterTypes = (Class[]) oin.readObject();  //读参数类型
        Object[] args = (Object[])oin.readObject();  //读参数

        IUserService service = new UserServiceImpl();
        Method method = service.getClass().getMethod(methodName,parameterTypes);  //找到这个方法
        User user = (User)method.invoke(service, args);   // 调用

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

在server端,接收到方法名、参数类型、参数后,通过 IUserService service = new UserServiceImpl()提供一个服务。

这样的改进,可以提供对同一个接口的很多方法支持,但是只能支持一个接口( IUserService service = new UserServiceImpl();),这里写死了。

并且在stub这,回传的对象还是经过了拆解的,如下。

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

v5 返回值用object封装,支持任意类型

针对v4中的缺点,改进措施:读写都以obejct为单位

public class Server {

    private static boolean running = true;

    public static void main(String[] args) throws Exception {
        ServerSocket ss = new ServerSocket(8888);
        while (running){
            Socket s = ss.accept();
            process(s);
            s.close();
        }
        ss.close();
    }

    private static void process(Socket s) throws Exception{
        InputStream in = s.getInputStream();
        OutputStream out = s.getOutputStream();

        ObjectInputStream oin = new ObjectInputStream(in);
        DataOutputStream dout = new DataOutputStream(out);

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

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

        /**
        dout.writeInt(user.getId());
        dout.writeUTF(user.getName());
         **/

        //改进部分
        ObjectOutputStream oos = new ObjectOutputStream(out);
        oos.writeObject(user);
        dout.flush();
    }
}
public class Stub {

    public static IUserService getStub(){

        InvocationHandler h = new InvocationHandler(){
            @Override
            public Object invoke(Object o, Method method, Object[] args) throws Throwable {
                Socket socket = new Socket("127.0.0.1",8888);

                ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream());

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

/**
                DataInputStream in = new DataInputStream(socket.getInputStream());
                int id = in.readInt();
                String name = in.readUTF();
                User user = new User(id, name);
**/
                // 改动后
                ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
                User user = (User)ois.readObject();  // 把返回值封装成了对象

                out.close();
                socket.close();
                return user;
            }
        };

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

到这为止,接口可以任意增减方法,user类可任意增减属性,代码都适用。

v6 支持任意service

v5的局限性还是有,那就是在client端IUserService service = Stub.getStub(),我们只能拿到IUserService,写stub只能把我们生成这个。

//v5-stub中写死了
 Object o = Proxy.newProxyInstance(IUserService.class.getClassLoader(),new Class[]{IUserService.class},h );

我们希望,可以通过该stub拿到任意接口。

其实很简单,只要把希望的service类型传给stub就行了

public class Stub {

    //public static IUserService getStub(){
    public static Object getStub(Class clazz){

        InvocationHandler h = new InvocationHandler(){
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                Socket socket = new Socket("127.0.0.1",8888);

                ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream());

                String clazzName = clazz.getName();  // 先获取一波类名
                String methodName = method.getName();
                Class[] parameterTypes = method.getParameterTypes();

                out.writeUTF(methodName);
                out.writeObject(parameterTypes);
                out.writeObject(args);
                out.flush();

                ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
                //User user = (User)ois.readObject();
                Object o =  ois.readObject();

                out.close();
                socket.close();
                return o;
            }
        };

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

在server端,小修改即可

public class Server {

    private static boolean running = true;

    public static void main(String[] args) throws Exception {
        ServerSocket ss = new ServerSocket(8888);
        while (running){
            Socket s = ss.accept();
            process(s);
            s.close();
        }
        ss.close();
    }

    private static void process(Socket s) throws Exception{
        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();

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

        // 这里其实用spring注入即可
        Class clazz = null;
        clazz = UserServiceImpl.class;    //从服务注册表找到具体的类

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

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

v7

到v6,rpc的主要逻辑已经很ok了。

但是,远程过程调用最底层的逻辑,就是把数据变成二进制在网络中传输。在java中,变成二进制(或者说序列化)的主要方法是实现Serializable接口的类,就可以被序列化。内部逻辑是java端的,这个序列化方法是只支持java的、性能差、系列化后长度又很长。

总而言之,就是不太好用。因此,现在有很多rpc序列化框架。下图,都主要是用来序列化的!!!
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值