分布式调用 - 服务间的远程调用RPC
文章目录
- 导图
- Pre
- RPC 概述
- RPC 调用过程
- RPC 动态代理
- 1. 接口定义 (`SeverProvider`)
- 2. 实现类 (`ServerProviderImpl`)
- 3. 动态代理类 (`DynamicProxy`)
- 4. 客户端 (`Client`)
- 5. 代码工作流程
- 6. 总结和注意点
- 7. 结果输出
- 8. 小结
- RPC 序列化
- 协议编码
- 网络传输
导图
服务和应用的调用基于场景的不同会分为几种情况
-
系统外的客户端调用系统内的服务时需要通过反向代理和负载均衡的方式; ------负载均衡
-
系统架构内部服务之间的调用需要通过 API 网关; ------API 网关
-
服务之间的互相感知需要用到服务注册与发现; ------服务注册与发现
-
服务之间的通信会使用 RPC 架构,RPC 的核心原理以及 Netty 的最佳实践 ------服务间的远程调用
Pre
分布式调用 - 那些关于负载均衡的一二事儿
分布式调用 - API网关和服务注册发现
RPC 概述
无论 API 网关,还是服务注册和发现,都在探讨服务与服务如何发现对方、如何选择正确路径进行调用,描述的是服务之间的关系。厘清关系后,我们再来谈谈服务之间的调用是如何完成的。
在分布式系统中,应用或者服务会被部署到不同的服务器和网络环境中,特别是在有微服务的情况下,应用被拆分为很多个服务,每个服务都有可能依赖其他服务。
假设客户端调用下单服务时,还会调用商品查询服务、扣减库存服务、订单更新服务,如果这三个服务分别对应三个数据库,那么一次客户端请求就会引发 6 次调用,要是这些服务或者数据库都部署在不同的服务器或者网络节点,这 6 次调用就会引发 6 次网络请求。因此,分布式部署方式在提高系统性能和可用性的前提下,对网络调用效率也发起了挑战。
为了面对这种挑战,需要选择合适的网络模型,对传输的数据包进行有效的序列化,调整网络参数优化网络传输性能。为了做到以上几点我们需要引入 RPC,下面就来介绍 RPC 是如何解决服务之间网络传输问题
RPC 调用过程
RPC 是 Remote Procedure Call(远程过程调用)的缩写,该技术可以让一台服务器上的服务通过网络调用另一台服务器上的服务,简单来说就是让不同网络节点上的服务相互调用。因此 RPC 框架会封装网络调用的细节,让调用远程服务看起来像调用本地服务一样简单。
由于微服务架构的兴起,RPC 的概念得到广泛应用,在消息队列、分布式缓存、分布式数据库等多个领域都有用到。可以将 RPC 理解为连接两个城市的高速公路,让车辆能够在城市之间自由通行。由于 RPC 屏蔽了远程调用和本地调用的区别,因此程序开发者无须过多关注网络通信,可以把更多精力放到业务逻辑的开发上.
上图描述了服务调用的过程,这里涉及左侧的服务调用方和右侧的服务提供方。既然是服务的调用过程,就存在请求过程和响应过程,这两部分用虚线圈出来了。
- 从图左侧的服务调用方开始,利用“动态代理”方式向服务提供方发起调用,这里会制定服务、接口、方法以及输入的参数;
- 将这些信息打包好之后进行“序列化”操作,由于 RPC 是基于 TCP 进行传输的,因此在网络传输中使用的数据必须是二进制形式,序列化操作就是将请求数据转换为二进制,以便网络传输;
- 打好二进制包后,需要对信息进行说明,比如协议标识、数据大小、请求类型等,这个过程叫作“协议编码”,说白了就是对数据包进行描述,并告诉数据接收方数据包有多大、要发送到什么地方去。
至此,数据发送的准备工作就完成了,数据包会通过“网络传输”到达服务提供方
- 服务提供方接收到数据包以后,先进行“协议解码”,并对解码后的数据“反序列化”,然后通过“反射执行”获取由动态代理封装好的请求
- 此时随着箭头到了图的最右边,顺着向下的箭头,服务提供方开始“处理请求”,处理完后就要发送响应信息给服务调用方了,之后的发送过程和服务调用方发送请求的过程是一致的,只是方向相反,依次为序列化→协议编码→网络传输→协议解码→反序列化→接收响应”。
以上便是整个 RPC 调用的请求、响应流程。
分析上述的 RPC 调用流程后,发现无论是服务调用方发送请求,还是服务提供方发送响应,有几个步骤都是必不可少的,分别为动态代理、序列化、协议编码和网络传输
RPC 动态代理
服务调用方访问服务提供方的过程是一个 RPC 调用。作为服务调用方的客户端通过一个接口访问作为服务提供方的服务端,这个接口决定了访问方法和传入参数,可以告诉客户端如何调用服务端,实际的程序运行也就是接口实现是在客户端进行的。
RPC 会通过动态代理机制,为客户端请求生成一个代理类,在项目中调用接口时绑定对应的代理类,之后当调用接口时,会被代理类拦截,在代理类里加入远程调用逻辑即可调用远程服务端。
来看个例子:
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;// 1. 接口命名规范
public interface ServerProvider {public String sayHello(String name);
}// 2. 实现类命名规范
public class ServerProviderImp implements ServerProvider {@Overridepublic String sayHello(String name) {System.out.println(name);return name;}
}// 3. 动态代理类,遵循标准命名和格式import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;public class DynamicProxy implements InvocationHandler {private final Object realObject;/*** 创建DynamicProxy实例,用于代理真实对象** @param realObject 被代理的真实对象*/public DynamicProxy(Object realObject) {this.realObject = realObject;}/*** 当代理对象被调用时,此方法将被调用.* 它的主要作用是将方法调用委托给实际对象.** @param proxy 代理对象,通常我们不会直接使用它.* @param method 被调用的方法对象,它代表了实际要执行的方法.* @param args 方法参数,传递给实际方法的参数.* @return 返回的是实际方法执行后的结果.* @throws Throwable 如果实际方法调用过程中抛出了异常,此方法也会抛出.*/@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {// 这里可以在方法调用前后添加日志或其他操作System.out.println("Before invoking method: " + method.getName());// 委托实际对象的方法调用,传递相同的参数,返回实际方法调用的结果.Object result = method.invoke(realObject, args);System.out.println("After invoking method: " + method.getName());return result;}
}// 4. 客户端测试代码public class Client {/*** 使用Java动态代理来代理真实的服务提供者* 动态代理允许我们在运行时创建遵守现有接口行为的新类*/public static void main(String[] args) {// 创建真实的服务提供者实例ServerProvider realSeverProvider = new ServerProviderImp();// 创建动态代理实例,它将代理真实的服务提供者InvocationHandler handler = new DynamicProxy(realSeverProvider);// 通过Java反射机制创建代理对象// 这个代理对象将拦截并处理所有通过代理发送的方法调用ServerProvider severProvider = (ServerProvider) Proxy.newProxyInstance(handler.getClass().getClassLoader(),realSeverProvider.getClass().getInterfaces(),handler);// 通过代理对象调用服务提供者的方法System.out.println(severProvider.sayHello("artisan"));}
}
1. 接口定义 (SeverProvider
)
public interface ServerProvider {public String sayHello(String name);
}
ServerProvider
是一个服务提供者接口,定义了一个sayHello
方法,接收一个String
类型的参数str
。这个接口是服务端暴露的远程调用接口。
2. 实现类 (ServerProviderImpl
)
public class ServerProviderImp implements ServerProvider {@Overridepublic String sayHello(String name) {System.out.println(name);return name;}
}
ServerProviderImpl
是SeverProvider
接口的实现类,实现了sayHello
方法,并打印出传入的str
参数。
3. 动态代理类 (DynamicProxy
)
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;public class DynamicProxy implements InvocationHandler {private final Object realObject;/*** 创建DynamicProxy实例,用于代理真实对象** @param realObject 被代理的真实对象*/public DynamicProxy(Object realObject) {this.realObject = realObject;}/*** 当代理对象被调用时,此方法将被调用.* 它的主要作用是将方法调用委托给实际对象.** @param proxy 代理对象,通常我们不会直接使用它.* @param method 被调用的方法对象,它代表了实际要执行的方法.* @param args 方法参数,传递给实际方法的参数.* @return 返回的是实际方法执行后的结果.* @throws Throwable 如果实际方法调用过程中抛出了异常,此方法也会抛出.*/@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {// 这里可以在方法调用前后添加日志或其他操作System.out.println("Before invoking method: " + method.getName());// 委托实际对象的方法调用,传递相同的参数,返回实际方法调用的结果.Object result = method.invoke(realObject, args);System.out.println("After invoking method: " + method.getName());return result;}
}
DynamicProxy
类实现了InvocationHandler
接口,后者是 Java 动态代理的核心接口。DynamicProxy
的构造方法接收一个Object
类型的realObject
参数,表示需要代理的真实对象(在这里是ServerProviderImpl
)。invoke
方法负责处理所有通过代理对象调用的方法。当代理对象的方法被调用时,invoke
方法会被触发,它通过反射(method.invoke(realObject, args)
)调用真实对象的方法。
4. 客户端 (Client
)
/*** 使用Java动态代理来代理真实的服务提供者* 动态代理允许我们在运行时创建遵守现有接口行为的新类*/public static void main(String[] args) {// 创建真实的服务提供者实例ServerProvider realSeverProvider = new ServerProviderImp();// 创建动态代理实例,它将代理真实的服务提供者InvocationHandler handler = new DynamicProxy(realSeverProvider);// 通过Java反射机制创建代理对象// 这个代理对象将拦截并处理所有通过代理发送的方法调用ServerProvider severProvider = (ServerProvider) Proxy.newProxyInstance(handler.getClass().getClassLoader(),realSeverProvider.getClass().getInterfaces(),handler);// 通过代理对象调用服务提供者的方法System.out.println(severProvider.sayHello("artisan"));}
- 步骤解析:
realSeverProvider
创建了真实的服务端对象ServerProviderImpl
,这是一个实现了ServerProvider
接口的具体实例。handler
是DynamicProxy
的实例,它被用来处理代理对象的所有方法调用。- 通过
Proxy.newProxyInstance
创建动态代理对象。这个方法需要三个参数:- ClassLoader:
handler.getClass().getClassLoader()
用来加载代理类。 - Interfaces:
realSeverProvider.getClass().getInterfaces()
获取真实对象实现的接口,这里是SeverProvider
。 - InvocationHandler:
handler
,指定动态代理的处理逻辑。
- ClassLoader:
- 最后,通过代理对象
severProvider
调用sayHello("world")
,实际上调用的是DynamicProxy
中的invoke
方法,间接执行ServerProviderImpl
的sayHello
方法。
5. 代码工作流程
- 客户端 通过
Proxy.newProxyInstance
创建一个代理对象,并指定代理类的InvocationHandler
。 - 当客户端调用代理对象的方法时,代理对象的
invoke
方法被触发。 - 在
invoke
方法中,调用method.invoke(realObject, args)
来执行真实对象的方法。
6. 总结和注意点
- 动态代理:动态代理的核心在于
InvocationHandler
接口,代理类不会直接实现接口中的方法,而是通过反射机制调用真实对象的方法,这样就实现了方法调用的“拦截”。 - 代理机制:这段代码是一个简单的 Java 动态代理实现,通过代理对象使得客户端可以间接调用服务端实现的远程方法。
7. 结果输出
Before invoking method: sayHello
artisan
After invoking method: sayHello
artisan
这段代码展示了 Java 动态代理的基本使用,它使得客户端能够通过代理对象调用服务端的实际方法,同时提供了灵活的拦截和增强功能。
8. 小结
在客户端和服务端之间加入了一层动态代理,这个代理用来代理服务端接口。客户端只需要知道调用服务端接口的方法名字和输入参数就可以了,并不需要知道具体的实现过程。在实际的 RPC 调用过程中,在客户端生成需要调用的服务端接口实例,将它丢给代理类,当代理类将这些信息传到服务端以后再执行。因此,RPC 动态代理是对客户端的调用和服务端的执行进行了解耦,目的是让客户端像调用本地方法一样调用远程服务。