📒 研究一款入门级别的 RPC 框架 -- XXL-RPC
XXL-RPC 框架是 XXL-JOB 作者出品, 官网地址 分布式服务框架XXL-RPC
XXL-RPC 是一款轻量、简单,但五脏俱全的 RPC 框架,本地环境搭建十分简单, 对初学 RPC 的人来说,非常的友好,建议阅读学习,对于理解 RPC 大有裨益。 本文也是围绕 XXL-RPC 框架行文的。
官网文章已经写得很好,而本文是基于自己的学习和理解而成文的;内容会涉及源码,篇幅偏长。为了更好理解 XXL-PRC,需要有 Spring 初始化过程和 Netty 知识背景。
📖一、RPC 基础
1.1 入门理解
PRC 是为调用远程的服务像调用本地一样服务简单。
举例:在 node1 节点上调用 node2 的 serviceB,就像 node1 上调用 node1 上的 serviceB 一样。
要实现这一目标,由上图可见,通信组件必不可少。如果不考虑集群,可以将 node2 的各种信息配置写入 node1,但是考虑集群的扩容和缩容,就必须要有一个组件能及时地更新各节点的信息,暂且称这个组件为服务注册与发现组件。
对 RPC 有了最基本的了解后,接着我们再进一步看 RPC 的理论和设计。
1.2 基础原理和设计
如果 node1 要调用 node2 服务就需要设计一个简单的通信;可以粗略概括如下:
为了更好的理解上图,补充一下术语:
术语 | 理解 |
---|---|
stub | Stub 是在客户端端点的代理程序,用于代表客户端应用程序与远程服务进行通信。它隐藏了底层的网络通信细节,当客户端应用程序调用 stub 中的函数时,stub 将负责将请求打包并通过网络发送给远程服务端。 |
skeleton | Skeleton 是在服务端端点的代理程序,用于接收来自客户端的请求,并将其解包后传递给实际的服务实现。Skeleton 隐藏了底层的网络通信细节,并负责将请求分派到相应的服务实现函数上。当服务实现函数执行完毕后,skeleton将负责将结果打包并发送给客户端 |
stub 和 skeleton 是 RPC 中用于代理客户端和服务端之间通信的组件。
RPC 并没有具体规范,但是大致原理是一样的, 一次简单 RPC 调用过程如下:
有了这些基础概念以后,接下来我们正式进入 XXL-RPC 这个框架的学习。
📜二、XXL-RPC 的设计和理论
2.1 XXL-RPC 的设计
XXL-RPC 中关于 RPC 的一次调用过程,原理剖析如下,作者的设计实现,本文的代码分析也是围绕下面这张图详细展开理解的。
其中 TCP 的模型, XXL-RPC 采用 NIO 进行底层通讯。
XXL-RPC 底层通信使用了 Netty。 如果要想学习了解 RPC 框架, Netty 是必须要掌握的。
客户端可以跟每一个服务提供者( ip + port
)建立一个 socket 链接。 如果服务提供者是集群,那么每个客户端可以跟这些服务提供者一一建立链接,形成连接池。客户端请求的时候,根据负载均衡算法从中取一个进行使用即可。
kotlin
// 地址由 ip 和 port 组成
this.registryAddress = IpUtil.getIpPort(this.ip, this.port);
2.2 XXL-RPC 工程
XXL-RPC 工程代码如下,有两个示例,接下来就从示例进行入手 (provider 和 consumer/invoker) 到这里了,不妨拉一下源码看看。
工程是以 spring 为基础的,从 @XxlRpcService
和 @XxlRpcReference
两个注解入手比较合适。而两个注解都是通过 spring 加载过程的后置处理器进行处理,如果把这两个注解捋顺了,整个过程就基本清晰了。
bash
/xxl-rpc-sample-springboot-client :服务消费方 consumer 调用示例
/xxl-rpc-sample-springboot-server :服务提供方 provider 示例
📓三、源码理解
@XxlRpcService
和 @XxlRpcReference
框架是依赖 spring 容器。在启动过程中,后置处理器会进行各自的初始化处理,源码理解就围绕这两个注解进行展开。
3.1 @XxlRpcService
XxlRpcService 处理的入口是 XxlRpcSpringProviderFactory, 也是服务提供者的核心类; XxlRpcSpringProviderFactory 实现 ApplicationContextAware 和 InitializingBean 两个接口, 这个类在创建的时候,会扫描所有 XxlRpcService 的注解类。在 afterPropertiesSet 中启动 Netty 服务端。

接下来详细查看一下源码:
- 扫描所有 XxlRpcService 类,Spring 后置处理,比较简单。
Java
// 将带有 XxlRpcService 注解的所有类都解析放入到 Map 中
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
Map<String, Object> serviceBeanMap = applicationContext.getBeansWithAnnotation(XxlRpcService.class);
if (serviceBeanMap!=null && serviceBeanMap.size()>0) {
for (Object serviceBean : serviceBeanMap.values()) {
// valid
if (serviceBean.getClass().getInterfaces().length ==0) {
throw new XxlRpcException("XXL-RPC, service(XxlRpcService) must inherit interface.");
}
// add service
XxlRpcService xxlRpcService = serviceBean.getClass().getAnnotation(XxlRpcService.class);
String iface = serviceBean.getClass().getInterfaces()[0].getName();
String version = xxlRpcService.version();
// iface + version 作为 key,将其放入到 map 中
super.addService(iface, version, serviceBean);
}
}
}
- 回调 afterPropertiesSet ,触发 Netty 服务端的创建。
scala
public class XxlRpcSpringProviderFactory extends XxlRpcProviderFactory implements ApplicationContextAware, InitializingBean,DisposableBean {
// 回调调用 start 创建服务
@Override
public void afterPropertiesSet() throws Exception {
super.start();
}
}
- 创建 Netty 服务端
Netty 服务端启动代码; 业务核心处理类是 NettyServerHandler 类;这个类会处理具体请求,不复杂,模板式的代码。
JAVA
public class NettyServer extends Server {
private Thread thread;
@Override
public void start(final XxlRpcProviderFactory xxlRpcProviderFactory) throws Exception {
thread = new Thread(new Runnable() {
@Override
public void run() {
// 设置线程数大小。默认 60、300
final ThreadPoolExecutor serverHandlerPool = ThreadPoolUtil.makeServerThreadPool(
NettyServer.class.getSimpleName(),
xxlRpcProviderFactory.getCorePoolSize(),
xxlRpcProviderFactory.getMaxPoolSize());
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
// start server
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel channel) throws Exception {
channel.pipeline()
// 心跳检活
.addLast(new IdleStateHandler(0,0, Beat.BEAT_INTERVAL*3, TimeUnit.SECONDS)) // beat 3N, close if idle
// 编解码
.addLast(new NettyDecoder(XxlRpcRequest.class, xxlRpcProviderFactory.getSerializerInstance()))
.addLast(new NettyEncoder(XxlRpcResponse.class, xxlRpcProviderFactory.getSerializerInstance()))
// 处理请求的具体类
.addLast(new NettyServerHandler(xxlRpcProviderFactory, serverHandlerPool));
}
})
.childOption(ChannelOption.TCP_NODELAY, true)
.childOption(ChannelOption.SO_KEEPALIVE, true);
// bind
ChannelFuture future = bootstrap.bind(xxlRpcProviderFactory.getPort()).sync();
// 激活服务注册
onStarted();
// wait util stop
future.channel().closeFuture().sync();
.......
}
上面的 Netty 服务端启动代码是非常标准的模板式代码。具体调用过程如下:

- NettyServerHandler 服务请求处理的 handler
在这个 handler 中,将任务提交到线程池;再通过反射处理请求,再将结果写回 Netty 通道。
Java
@Override
public void channelRead0(final ChannelHandlerContext ctx, final XxlRpcRequest xxlRpcRequest) throws Exception {
......
// do invoke
try {
serverHandlerPool.execute(new Runnable() {
@Override
public void run() {
// invoke + response
// 真正做调用的地方
XxlRpcResponse xxlRpcResponse = xxlRpcProviderFactory.invokeService(xxlRpcRequest);
// 写回数据
ctx.writeAndFlush(xxlRpcResponse);
}
});
} catch (Exception e) {
.......
ctx.writeAndFlush(xxlRpcResponse);
}
}
- 反射调用接口 xxlRpcProviderFactory.invokeService。 从 Map 中寻找 service,然后通过反射进行调用。
Java
// 通过反射进行调用
Class<?> serviceClass = serviceBean.getClass();
String methodName = xxlRpcRequest.getMethodName();
Class<?>[] parameterTypes = xxlRpcRequest.getParameterTypes();
Object[] parameters = xxlRpcRequest.getParameters();
Method method = serviceClass.getMethod(methodName, parameterTypes);
method.setAccessible(true);
Object result = method.invoke(serviceBean, parameters);
服务端的代码还是比较简单和清晰。大致有以下几个步骤:
- XxlRpcSpringProviderFactory 实现了 ApplicationContextAware 接口,所以 spring 启动会回调; 在回调的时候,将所有 XxlRpcService 注解的类都标记成为服务提供的类,用一个 Map 容器承载。
- XxlRpcSpringProviderFactory 实现了 InitializingBean 接口,在所有类都初始化完成后,回调 afterPropertiesSet() 方法,这个时候会通过这个方法启动 Netty 服务端。
- Netty 服务端是非常标准的模板代码,其中 NettyServerHandler 为核心的业务 handler,用来处理服务请求的核心。
- NettyServerHandler 中使用线程池来,这样能够处理大量请求;进行具体业务方法的调用
- xxlRpcProviderFactory.invokeService 完成具体方法调用。
上面的代码就是服务端(provider) 启动和调用的过程,比较简单清晰的。
服务注册启动是在 Netty 启动的时候,回调 start 事件启动的。
接下来讲解客户端(consumer/invoker) 的一个启动和调用过程
3.2 @XxlRpcReference
带有 XxlRpcReference 注解接口,客户端都没有显示的实现类, 因此需要依赖 XxlRpcReference 注解,代理生成一个具体的实现类,这个实现类在被调用具体方法的时候,能够映射成远程方法请求,并将结果再返回。

整个调用可以分为以下两个核心点:
- 代理对象生成
- 远程方法调用
梳理整个过程,大致如下:

接下来重点分析生成可远程调用对象的代码。
- XxlRpcSpringInvokerFactory 为入口类。在这个类,需要触发生成 @XxlRpcReference 的具体代理对象。
下图是启动的时候去填充 @XxlRpcReference 描述的字段代码

比如 DemoService 被 @XxlRpcReference 描述, 就需要生成具体的代理对象
JAVA
@Controller
public class IndexController {
@XxlRpcReference
private DemoService demoService;
......
}
代码如下:给所有 @XxlRpcReference 描述的属性字段生成具体的代理对象
JAVA
public class XxlRpcSpringInvokerFactory implements InitializingBean, DisposableBean, BeanFactoryAware, InstantiationAwareBeanPostProcessor {
.......
// spring 后置处理接口,填充属性字段
@Override
public boolean postProcessAfterInstantiation(final Object bean, final String beanName) throws BeansException {
// collection
final Set<String> serviceKeyList = new HashSet<>();
// parse XxlRpcReferenceBean
// 处理属性字段
ReflectionUtils.doWithFields(bean.getClass(), new ReflectionUtils.FieldCallback() {
@Override
public void doWith(Field field) throws IllegalArgumentException, IllegalAccessException {
if (field.isAnnotationPresent(XxlRpcReference.class)) {
// valid
Class iface = field.getType();
if (!iface.isInterface()) {
throw new XxlRpcException("XXL-RPC, reference(XxlRpcReference) must be interface.");
}
XxlRpcReference rpcReference = field.getAnnotation(XxlRpcReference.class);
// init reference bean
XxlRpcReferenceBean referenceBean = new XxlRpcReferenceBean();
......
referenceBean.setInvokerFactory(xxlRpcInvokerFactory);
// get proxyObj 生成代理对象
Object serviceProxy = null;
try {
// 核心,生成远程调用的代理对象
serviceProxy = referenceBean.getObject();
} catch (Exception e) {
throw new XxlRpcException(e);
}
// set bean 反射设置属性
field.setAccessible(true);
field.set(bean, serviceProxy);
......
}
}
});
}
- serviceProxy = referenceBean.getObject(); 包装成远程调用的代理对象。

整个客户端的核心代码,有必要认真阅读一下
阅读前需要先理解 callType 有四种方式,而 referenceBean.getObject() 实现了下面 4 种方式的调用:
callType | 描述 |
---|---|
SYNC | 同步等返回结果 |
FUTURE | future 获取结果 |
CALLBACK | 返回结果进行方法回调 |
ONEWAY | 不关注返回结果 |
由于 NIO 是异步通讯模型,调用线程并不会阻塞获取调用结果,因此,XXL-RPC 实现了在异步通讯模型上的同步调用,即 "sync-over-async" 。即如何恰当地返回异步结果,下图是作者设计的思路,即通过 wait 和 notify 来实现异步结果的获取。

有了上面的理解,我们再阅读核心的生成代理对象的代码就比较容易了。
下面生成代理的代码也是围绕如何进行远程调用(省略参数封装、泛化调用), 逻辑并不复杂
Java
public Object getObject() throws Exception {
......
// newProxyInstance 生成代理对象
return Proxy.newProxyInstance(Thread.currentThread()
.getContextClassLoader(), new Class[] { iface },
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
.......
// send 核心,发送远程调用,对应四种方式
if (CallType.SYNC == callType) {
// future-response set
XxlRpcFutureResponse futureResponse = new XxlRpcFutureResponse(invokerFactory, xxlRpcRequest, null);
try {
// do invoke
clientInstance.asyncSend(finalAddress, xxlRpcRequest);
// future get
XxlRpcResponse xxlRpcResponse = futureResponse.get(timeout, TimeUnit.MILLISECONDS);
if (xxlRpcResponse.getErrorMsg() != null) {
throw new XxlRpcException(xxlRpcResponse.getErrorMsg());
}
return xxlRpcResponse.getResult();
} catch (Exception e) {
.....
} finally{
// future-response remove
futureResponse.removeInvokerFuture();
}
} else if (CallType.FUTURE == callType) {
// future-response set
XxlRpcFutureResponse futureResponse = new XxlRpcFutureResponse(invokerFactory, xxlRpcRequest, null);
try {
// invoke future set
XxlRpcInvokeFuture invokeFuture = new XxlRpcInvokeFuture(futureResponse);
XxlRpcInvokeFuture.setFuture(invokeFuture);
// do invoke
clientInstance.asyncSend(finalAddress, xxlRpcRequest);
return null;
} catch (Exception e) {
logger.info(">>>>>>>>>>> XXL-RPC, invoke error, address:{}, XxlRpcRequest{}", finalAddress, xxlRpcRequest);
// future-response remove
futureResponse.removeInvokerFuture();
......
} else if (CallType.CALLBACK == callType) {
// get callback
XxlRpcInvokeCallback finalInvokeCallback = invokeCallback;
XxlRpcInvokeCallback threadInvokeCallback = XxlRpcInvokeCallback.getCallback();
if (threadInvokeCallback != null) {
finalInvokeCallback = threadInvokeCallback;
}
if (finalInvokeCallback == null) {
throw new XxlRpcException("XXL-RPC XxlRpcInvokeCallback(CallType="+ CallType.CALLBACK.name() +") cannot be null.");
}
// future-response set
XxlRpcFutureResponse futureResponse = new XxlRpcFutureResponse(invokerFactory, xxlRpcRequest, finalInvokeCallback);
try {
clientInstance.asyncSend(finalAddress, xxlRpcRequest);
} catch (Exception e) {
.......
}
return null;
} else if (CallType.ONEWAY == callType) {
clientInstance.asyncSend(finalAddress, xxlRpcRequest);
return null;
....
}
}
});
}
关于如何实现 "sync-over-async" 细节参考 3.3 sync 和 future 模式。
最终的远程通信还是使用 Netty。
- clientInstance.asyncSend(finalAddress, xxlRpcRequest);
jAVA
@Override
public void send(XxlRpcRequest xxlRpcRequest) throws Exception {
this.channel.writeAndFlush(xxlRpcRequest).sync();
}
做一个简单的总结:
- XxlRpcSpringInvokerFactory 在启动的时候,实现了 postProcessAfterInstantiation 接口,在对象填充属性前调用,这个时候对拥有 XxlRpcReference 属性字段的对象做该字段的填充,即生成具体的代理对象
- XxlRpcReferenceBean 代理核心类,最核心的作用就是包装,远程调用。
- 远程调用提供了四种类型。核心是实现 "sync-over-async"
基本上我们已经摸清楚 provider/服务端 和 consumer/Invoker/客户端的实现和调用过程。
下面再追加 sync 和 future 模式的理解。
3.3 sync 和 future 模式
通过 wait() 和 notifyAll() 来处理异步结果,作者的解释,可以结合图进行理解
- 1、consumer发起请求:consumer 会根据远程服务的 stub 实例化远程服务的代理服务,在发起请求时,代理服务会封装本次请求相关底层数据,如服务iface、methods、params等等,然后将数据经过 serialization 之后发送给provider;
- 2、provider 接收请求:provider 接收到请求数据,首先会deserialization获取原始请求数据,然后根据 stub 匹配目标服务并调用;
- 3、provider 响应请求:provider 在调用目标服务后,封装服务返回数据并进行serialization,然后把数据传输给 consumer;
- 4、consumer 接收响应:consumer 接收到相应数据后,首先会deserialization 获取原始数据,然后根据 stub 生成调用返回结果,返回给请求调用处。结束。

get() 等待获取结果对象
csharp
@Override
public XxlRpcResponse get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
if (!done) {
// lock.wait() 等待集合
synchronized (lock) {
try {
if (timeout < 0) {
lock.wait();
} else {
long timeoutMillis = (TimeUnit.MILLISECONDS==unit)?timeout:TimeUnit.MILLISECONDS.convert(timeout , unit);
lock.wait(timeoutMillis);
}
} catch (InterruptedException e) {
throw e;
}
}
}
......
return response;
}
调用成功后,在 notifyAll()
JAVA
// ---------------------- for invoke back ----------------------
public void setResponse(XxlRpcResponse response) {
this.response = response;
// 接触等待
synchronized (lock) {
done = true;
lock.notifyAll();
}
}
设计还是比较精彩。
下面是 future 方式获取结果对象的方式。
Java
// invoke future set
XxlRpcInvokeFuture invokeFuture = new XxlRpcInvokeFuture(futureResponse);
XxlRpcInvokeFuture.setFuture(invokeFuture);
// do invoke
clientInstance.asyncSend(finalAddress, xxlRpcRequest);
重写了 future 。 其中 get 也是在复用上面能力。
Java
public class XxlRpcInvokeFuture implements Future {
.......
@Override
public Object get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
try {
// 也是使用 await 和 notifyAll
XxlRpcResponse xxlRpcResponse = futureResponse.get(timeout, unit);
if (xxlRpcResponse.getErrorMsg() != null) {
throw new XxlRpcException(xxlRpcResponse.getErrorMsg());
}
return xxlRpcResponse.getResult();
} finally {
stop();
}
}
.......
}
通过上面两个注解过程的讲解,RPC 的调用过程就已经讲完了。
接下来再稍微分析一下服务注册与发现(上面的内容是核心重点)
📚四、服务中心系统设计(粗讲)
内部通过广播机制,集群节点实时同步服务注册信息,确保一致。客户端借助 long pollong 实时感知服务注册信息,简洁、高效;
4.1 服务注册
服务端的调用是在 Netty 启动后注册的回调事件进行服务注册的。
JAVA
serverInstance.setStartedCallback(new BaseCallback() { // serviceRegistry started
@Override
public void run() throws Exception {
// start registry 注册信息
if (serviceRegistry != null) {
registerInstance = serviceRegistry.newInstance();
registerInstance.start(serviceRegistryParam);
if (serviceData.size() > 0) {
// XxlRpcService 所有的接口。进行注册
registerInstance.registry(serviceData.keySet(), registryAddress);
}
}
}
});
serverInstance.setStopedCallback(new BaseCallback() { // serviceRegistry stoped
@Override
public void run() {
// stop registry
if (registerInstance != null) {
if (serviceData.size() > 0) {
registerInstance.remove(serviceData.keySet(), registryAddress);
}
registerInstance.stop();
registerInstance = null;
}
}
});
然后在内部启动一个独立的线程,每隔 10 秒进行,上报注册信息
JAVA
// registry thread
registryThread = new Thread(new Runnable() {
@Override
public void run() {
while (!registryThreadStop) {
try {
if (registryData.size() > 0) {
// 使用 http 上报元数据信息
boolean ret = registryBaseClient.registry(new ArrayList<XxlRpcAdminRegistryDataItem>(registryData));
logger.debug(">>>>>>>>>>> xxl-rpc, refresh registry data {}, registryData = {}", ret?"success":"fail",registryData);
}
} catch (Exception e) {
......
try {
// 休息 10 s
TimeUnit.SECONDS.sleep(10);
} catch (Exception e) {
......
}
}
}
});
服务提供者在启动 Netty 后,上报数据到服务注册中心。

使用的是 http 往注册中心进行上报(注册过程)。


4.2 服务发现
客户端调用,就是拉取注册中心的数据,选择一个合适的地址进行调用(负载均衡算法)
Java
if (finalAddress==null || finalAddress.trim().length()==0) {
if (invokerFactory!=null && invokerFactory.getRegister()!=null) {
// 拉取数据
String serviceKey = XxlRpcProviderFactory.makeServiceKey(className, varsion_);
TreeSet<String> addressSet = invokerFactory.getRegister().discovery(serviceKey);
// 负载均衡调用
if (addressSet==null || addressSet.size()==0) {
// pass
} else if (addressSet.size()==1) {
finalAddress = addressSet.first();
} else {
finalAddress = loadBalance.xxlRpcInvokerRouter.route(serviceKey, addressSet);
}
}
}
另外在程序中有一个独立的线程在不断地进行数据的定时刷新 com.xxl.rpc.core.registry.impl.xxlrpcadmin.XxlRpcAdminRegistryClient#XxlRpcAdminRegistryClient。 核心代码 refreshDiscoveryData
typescript
discoveryThread = new Thread(new Runnable() {
@Override
public void run() {
while (!registryThreadStop) {
......
} else {
try {
.......
// 定时更新数据。(10s)
refreshDiscoveryData(discoveryData.keySet());
} catch (Exception e) {
.......
}
}
});
在 xxl-rpc-admin 模块中 com.xxl.rpc.admin.service.impl.XxlRpcRegistryServiceImpl#afterPropertiesSet 有几个线程在不断刷新数据和广播消息,实现服务治理,限于篇幅就不再深入。
负载均衡、泛化调用、服务监控就不一一展开了,感兴趣的可以阅读代码进行研究。
到这里,对 RPC 的整体实现已经有了一个清晰的认识了。再来看看作者这张架构图就不难理解了。

✒️五、总结
XXL-RPC 是比较小而美的 RPC 框架,很轻量、简单; 对于 RPC 初学者非常的友好。 通过学习这个框架再去了解 Dubbo 就会比较轻松。
XXL-RPC 代码结构比较清晰, 底层 Netty 也是模板式的代码,XXL-RPC 作者对线程的理解也非常到位,值得推荐。
因为轻量,所以很多细节并没有考虑周全,比如线程池的优雅关闭等,这些问题,我们学习 Dubbo 的时候再细说,本文到此结束。