RPC 原理,Demo 演示,为什么 RPC 用动态代理

RPC 出现背景

现在很少有单机系统,复杂的系统有很多不同的服务组成,不同的服务部署在不同的机器上,包括数据库也是分布式存储的。不可避免地,会出现一个服务器调用另一个服务器的方法的场景。比如,服务器A想查询用户数据库,而提供数据查询的服务在服务器B上,如果A想使用B,就需要把查询请求发给B,B执行查询方法,再返回给A,这其实就是一次远程方法调用过程(Remote Procedure Call,RPC)。

这里有个问题,为什么服务器A查询B的数据,不是一次 http 请求就可以解决了吗?就像 web 浏览器和服务器之间的 http 请求一样,为什么还说是一次远程方法调用?这个问题稍后解释。

我们粗略的画出刚才一次远程方法调用的示意图:

RPC 原理

上述图示看似和一个 http 请求没有区别,别急,我们举一个具体的例子:

有一个客户类 User 代表用户的信息,这些信息存储在服务器 B 上面,服务器B提供查询用户的方法。

假设现在客户端A同样需要一个查询用户的方法 findUserById(),需要这个方法去访问服务器 B,一般的步骤是:

  • A 把参数、接口和方法等信息序列化
  • A 把序列化的信息通过网络发送给 B
  • B 收到信息之后,反序列化解析信息
  • B 根据解析到的信息找到方法,执行得到结果
  • B 把结构序列化通过网络发送给 A
  • A 反序列化解析信息,得到结果

为了完成上述的功能,最直接的做法是在 findUserById() 方法里面写这些序列化、网络请求、反序列化的代码,但是这样做有明显的缺点:

  • 代码复杂,网络请求序列化等方法和业务代码混杂在一起
  • 如果用户类 User 改变,那么代码的序列化等部分都要变
  • 如果有很多地方需求这种远程调用,需要为每一个方法写这一大堆代码
  • 如果远程服务器地址、端口甚至服务改变,需要修改每一个远程调用的方法

看到上述的缺点,可能大家想到的是用一个类把这些网络请求序列化等方法封装起来,让 findUserById() 直接去调用封装的代码:

 findUserById(){
	 xx();// 调用封装方法
 }
 xx(){// 封装方法
 	把 findUserById 序列化
 	网络传输
 	// 服务器 B 执行 findUserById 返回
 	反序列化
 	返回
 }

但是这样,有上述伪代码可以看出,一个封装的方法只能用于 findUserById(),其他远程方法依然还要再写代码,再封装调用,和上面没有本质区别。

一个好的解决方法是用动态代理(关于动态代理,看这里Java 动态代理,invoke() 自动调用原理,invoke() 参数),这样就和方法无关,一系列的方法只需要写一个代理类,修改代码也只需要修改代理类。这其实就是 RPC 的原理:利用动态代理,创建代理类去实现这些细节,把接口(方法)作为参数传递,而不是绑定方法,上述的伪代码改成动态代理可以表示为:

 findUserById(){
	 user = xx(接口);// 得到代理类
	 user.findUserById(参数);// 执行方法,这个方法实际上是 invoke() 里面的方法
 }
 xx(接口){// 封装方法,接口是一个参数
 	invoke(代理类,方法,参数){
	 	把 方法,参数 序列化
	 	网络传输
	 	// 服务器 B 执行 接口方法 返回
	 	反序列化
	 	返回
 	}
 	return 代理类
 	
 }

这样,我们需要远程调用的方法(接口)只是一个参数,全部细节都可以在代理类中实现,并且一个代理类可以处理很多方法,从而把远程访问代码和本地代码解耦,便于项目扩展和维护,非常优雅。

图示可以看的更清楚:

上述过程其实就是一个动态代理的实现,理解了动态代理就很容易理解RPC的原理,所以学习RPC之前一定要理解动态代理。

RPC Demo

这个 Demo 演示了客户端 Client 远程访问 findUserById() 方法的过程。客户端调用代理类,生成代理对象之后执行findUserById()方法,代理类封装了网络序列化等细节,服务器收到网络请求之后去执行返回。

客户端代码:

/**
 * 用户类的接口
 */
public interface IUserService {
    User findUserById(int id);
    
}
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;

/**
 * 代理类,接受一个接口参数,通过反射创建一个代理类,并且封装了远程访问服务器的一系列细节,
 * 此代理类和具体的接口无关,接口只是一个参数
 */
public class Agent {
    public static Object getObject(Class target) {
        Object result = Proxy.newProxyInstance(target.getClassLoader(), new Class[]{target}, new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                Socket s = new Socket("127.0.0.1", 8088);

                ObjectOutputStream objectOutputStream = new ObjectOutputStream(s.getOutputStream());
                String className = target.getName();
                String methodName = method.getName();
                Class[] parametersTypes = method.getParameterTypes();// 获得target的一些参数

                objectOutputStream.writeUTF(className);
                objectOutputStream.writeUTF(methodName);
                objectOutputStream.writeObject(parametersTypes);
                objectOutputStream.writeObject(args);
                objectOutputStream.flush();// 把获得的参数信息写到socket里面,发送给服务器


                ObjectInputStream objectInputStream = new ObjectInputStream(s.getInputStream());
                Object result = objectInputStream.readObject();// 从socket里面读取服务端执行返回的信息
                objectOutputStream.close();
                s.close();
                return result;// 返回结果
            }
        });
        return result;
    }
}

/**
 * 客户端用封装好的动态代理,获得代理类user,执行方法,代理类封装了访问网络的细节。
 */
public class Client {
    public static void main(String[] args) {
        IUserService user = (IUserService)Agent.getObject(IUserService.class);// 获得代理类
        System.out.println(user.findUserById(1));// 执行方法
    }
}

服务器代码:



import java.io.Serializable;
import java.util.Objects;

/**
 * 用户类
 */
public class User implements Serializable, IUserService {
    String userName;
    int userId;
    // 无参构造器
    public User(){

    }

    public User(String userName, int userId) {
        this.userName = userName;
        this.userId = userId;
    }

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public int getUserId() {
        return userId;
    }

    public void setUserId(int userId) {
        this.userId = userId;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        User user = (User) o;
        return userId == user.userId &&
                Objects.equals(userName, user.userName);
    }

    @Override
    public int hashCode() {
        return Objects.hash(userName, userId);
    }

    @Override
    public User findUserById(int id) {
        return new User("user1",id);// 模拟访问数据库
    }

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

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * Server模拟服务器,接受Socket的信息,找到方法,执行方法,并且返回
 */
public class Server {
    public static void main(String[] args) throws Exception {
        ServerSocket server = new ServerSocket(8088);// 用socket模拟客户端访问服务器的方法
        while(true){
            Socket client = server.accept();
            System.out.println(client);
            process(client);// 服务器执行服务,访问方法
            client.close();
            break;
        }


    }
    static void process(Socket socket) throws Exception {
        ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
        ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());

        String className = ois.readUTF();
        String methodName = ois.readUTF();
        Class[] parameterTypes = (Class[]) ois.readObject();
        Object[] parameters = (Object[]) ois.readObject();// 读取客户端发送的信息


        Class myclass = User.class;// 模拟注册表查找或者 Spring 的 bean 注入找到类
        Method method = myclass.getMethod(methodName, parameterTypes);// 获取方法

        Object o = method.invoke(myclass.newInstance(), parameters);// 执行方法
        oos.writeObject(o);
        oos.flush();// 把结果发送到客户端
    }
}

运行的时候,只需要先运行 Server,再运行 Client 就行,代码可以在 Github myrpc 下载。

现在,我们来回答文章开头的问题:

这里需有个问题,为什么服务器A查询B的数据,不是一次 http 请求就可以解决了吗?就像 web 浏览器和服务器之间的 http 请求一样,为什么还说是一次远程方法调用

看了上述的代码演示,可以发现 http 请求只是 RPC 中网络请求的工具,RPC 强调的是用动态代理去实现一个封装的、易修改扩展的远程访问方式,http 请求只是 RPC 实现网络请求的部分。换句话说,RPC 不仅仅是一次网络请求,更类似于一种设计模式。

RPC 需要考虑的问题

上述的 Demo 只是RPC原理的演示,要实现RPC,还需要考虑以下问题:

  • 中间的网络请求所用的网络协议,不止可以使用 http 协议
  • 网络请求的延迟
  • 序列化和反序列化的工具,序列化的效率影响速度和网络传输数据量的大小
  • 怎么做到平台无关和语言无关,可以方便的跨语言跨平台去远程调用方法
  • 当服务发生变更时怎么处理,一般用 ZooKeeper

未完待续介绍 RPC 框架。

  • 9
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
RPC 的实现方式有很多种,其中一种常见的方式是使用表单(form)请求来传递参数和结果。在这种实现方式中,客户端将请求参数编码为表单参数,然后将请求发送到服务器。服务器接收到表单请求后,解析表单参数,调用对应的方法,并将结果编码为表单参数,返回给客户端。 下面是一个使用动态代理实现表单请求的例子。我们将使用Java动态代理功能来代理第三方接口的调用,实现远程过程调用。假设我们要远程调用的接口是一个计算器接口,可以进行加、减、乘、除等运算: ```java public interface Calculator { public int add(int a, int b); public int subtract(int a, int b); public int multiply(int a, int b); public int divide(int a, int b) throws Exception; } ``` 我们需要定义一个远程服务,这个服务可以接受表单请求,并将请求参数和调用结果以表单参数的形式返回。假设我们的远程服务是一个简单的HTTP服务,可以接受POST请求,并将请求参数和调用结果以表单参数的形式返回: ```java public class RemoteService { public Map<String, String> invoke(String url, Map<String, String> params) throws Exception { // 发送POST请求 HttpClient client = new DefaultHttpClient(); HttpPost post = new HttpPost(url); List<NameValuePair> formParams = new ArrayList<NameValuePair>(); for (Map.Entry<String, String> entry : params.entrySet()) { formParams.add(new BasicNameValuePair(entry.getKey(), entry.getValue())); } post.setEntity(new UrlEncodedFormEntity(formParams)); HttpResponse response = client.execute(post); // 解析响应结果 Map<String, String> result = new HashMap<String, String>(); String body = EntityUtils.toString(response.getEntity()); String[] parts = body.split("&"); for (String part : parts) { String[] pair = part.split("="); result.put(pair[0], pair[1]); } return result; } } ``` 接下来,我们需要实现一个动态代理类,这个代理类可以代理任意一个实现了Calculator接口的类,将接口方法调用转换为表单请求,并将调用结果返回给客户端。我们可以使用Java动态代理功能来实现这个代理类: ```java public class CalculatorProxy implements InvocationHandler { private String url; private RemoteService remoteService; public CalculatorProxy(String url) { this.url = url; this.remoteService = new RemoteService(); } public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // 将方法调用转换为表单请求 String methodName = method.getName(); Map<String, String> params = new HashMap<String, String>(); params.put("method", methodName); for (int i = 0; i < args.length; i++) { params.put("arg" + i, String.valueOf(args[i])); } // 发送表单请求 Map<String, String> result = remoteService.invoke(url, params); // 解析表单请求结果 if ("success".equals(result.get("status"))) { return Integer.parseInt(result.get("result")); } else { throw new Exception(result.get("error")); } } public static Calculator createProxy(String url) { return (Calculator) Proxy.newProxyInstance(Calculator.class.getClassLoader(), new Class[] { Calculator.class }, new CalculatorProxy(url)); } } ``` 在上面的代理类中,我们实现了InvocationHandler接口的invoke方法,这个方法会在代理对象上调用任意一个接口方法时被调用。在这个方法中,我们将接口方法调用转换为表单请求,并将请求结果解析为Java对象。最后,我们使用Proxy.newProxyInstance方法创建一个代理对象,并返回给客户端使用。 最后,我们可以在客户端代码中使用这个代理对象来调用远程服务,就像调用本地方法一样: ```java Calculator calculator = CalculatorProxy.createProxy("http://remote-service.com/calculator"); int result = calculator.add(1, 2); ``` 这个例子中,我们演示了如何使用Java动态代理功能实现一个简单的RPC框架。这个框架可以方便地代理任意一个实现了特定接口的类,并将接口方法调用转换为表单请求。这个框架的代码比较简单,易于理解和扩展,可以作为学习动态代理RPC的一个好例子。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值