前言:
在写RPC之前我们需要明白一点,什么是RPC?
为了回答好这个问题,我们换个思路,什么不是RPC?
这个应该比较好回答一些了,从java来看:同一个jvm进程中的方法调用就不属于RPC,所以换言之,跨jvm进程间的方法调用就属于RPC了,将这个概念突破一下语言的限制,那就是:跨进程间的方法调用就属于RPC。
本文带大家进一步熟悉一下mini-dubbo所提供的基础功能,让大家先有个直观的认识,下一篇 《从零开始写一个微型dubbo框架 – 实战篇》将侧重探讨具体的实现
一、mini-dubbo支持的功能列表
支持功能 | 默认实现 | 可选实现 |
---|---|---|
注册中心 | zookeeper(ZookeeperRegistryService) | redis(RedisRegistryService) |
负载均衡 | 随机(RandomLoadBalance) | 轮询(PollingLoadBalance) |
动态代理 | jdk | - |
序列化 | google gson | - |
编解码 | LengthFieldBasedFrameDecoder、LengthFieldPrepender | - |
通信 | netty4 | - |
通信协议 | 自定义(RpcProtocol) | - |
ioc容器 | google guice | - |
心跳检测 | HeartbeatHandler | - |
失败重试 | ReconnectHandler | - |
重写默认配置 | RpcContext | - |
二、服务端启动过程解析
2.1 “零配置”
mini-dubbo的“零配置”是建立在启用默认配置的基础上来实现的,所以在启动服务之前需要初始化默认配置
2.1.1 默认配置
如下配置意味着完全使用了mini-dubbo设置的默认值,例如:注册中心使用的是zookeeper,连接的是本地127.0.0.1:2181
RpcContext.initContext();
2.1.2 指定配置
多数情况下,我们都希望可以灵活的切换自己喜欢的组件,例如:我们不想使用zookeeper,而是要用redis,则使用如下配置:
RpcContext.setConfiguration(new Configuration(RedisRegistryService.class));
当然,这里依然存在一个问题:虽然指定了使用的组件,但是并没有指定Redis服务器所在的地址,所以它依然默认连接的是本地的reids服务,我们还可以进一步设置:
RpcContext.setConfiguration(new Configuration(new RedisRegistryService("redis://127.0.0.1:6379")));
如果你的redis是一个集群服务,那么还可以进一步配置如下:
RpcContext.setConfiguration(new Configuration(new RedisRegistryService("redis://127.0.0.1:6379","redis://127.0.0.1:16379")));
2.2 暴露服务
RPC服务端需要在服务正常启动后主动将自己的服务地址(ip
:port)注册到注册中心,客户端发起调用时会在注册中心找可用服务
2.2.1 服务绑定
熟悉spring ioc的朋友们都知道,spring会在服务启动的时候扫描用户项目的类路径加载被@Service、@Component、@Controller等注解修饰过的类,主要就是要完成对象初始化及依赖注入。
mini-dubbo为了追求自身的“轻量级”并没有选择去依赖spring,而是使用了google的guice来替代spring ioc
所以,在项目启动的时候我们要将需要暴露的服务显式的进行注册
RpcContext.setConfiguration(new ServerConfig()); //ServerConfig 可参考demo中的源码
所以,完整的配置就是以下两行代码:
Configuration config = new RedisRegistryService("redis://127.0.0.1:6379","redis://127.0.0.1:16379"));
RpcContext.setConfiguration(config,new ServerConfig());
最精简的配置如下:
RpcContext.setConfiguration(new RedisRegistryService("redis://127.0.0.1:6379","redis://127.0.0.1:16379")),new ServerConfig());
2.2.2 启动服务
由于mini-dubbo通信层只使用了netty,所以启动mini-dubbo实际上就是启动了netty的服务端,在服务启动完成后再把服务端的地址(ip:port)注册到注册中心,如下代码则是完成了这个启动与注册的过程:
RpcContext.getBean(Protocol.class).export();
至此,服务端就启动完成了
三、客户端启动的过程
相对于服务端而言,客户端启动就简单一些了,说简单其实就是少了一个服务绑定、注册的环节,实际上启动的过程也不是在这个阶段完成的,真正的客户端启动发生在发起调用时(继续往下看“RPC调用过程解析”)
3.1 连接注册中心
RpcContext.setConfiguration(new Configuration(new RedisRegistryService("redis://127.0.0.1:6379")));
3.2 获取代理对象
在java中直接使用类名调用的方法只能是静态方法,而我们客户端只有一个接口名称,并且接口下面的方法也不一定是静态方法,所以我们需要先获取一个对象才能发起调用。mini-dubbo使用jdk的动态代理来获取一个对象,代码如下:
GreetingService greetingService = ProxyFactory.getProxyObject(GreetingService.class);
发起rpc调用
greetingService.sayHello("mini-dubbo");
四、RPC调用过程解析
4.1 封装rpc协议
Invocation invocation = new Invocation();
invocation.setInterfaceName(clz.getName()); invocation.setMethodName(method.getName());
invocation.setParameterType(Util.getParameterTypes(method.getParameterTypes()));
invocation.setArguments(args);
4.2 发送netty消息
封装RpcProtocol协议对象发起调用消息,然后使用Object的wait(), 让当前线程处于阻塞状态,等待处理结果的返回
# 代码片段
RpcProtocol protocol = new RpcProtocol();
protocol.setType(Constants.getType(Invocation.class));
protocol.setRequestId(requestId);
protocol.setBody(invocation);
channel.writeAndFlush(protocol);
4.3 服务端执行方法
使用反射机制执行方法
# 代码片段
method.invoke(RpcContext.getBean(clz),arguments))
4.4 返回netty消息
将返回结果封装为RpcProtocol协议对象,并发送结果消息
# 代码片段
Result result = new Result();
result.setReturnType(method.getReturnType().getName());
result.setData(method.invoke(RpcContext.getBean(clz),arguments));
protocol.setBody(result);
protocol.setType(Constants.getType(Result.class));
ctx.channel().writeAndFlush(protocol);
4.5 获取RPC调用结果
唤醒处于等待状态的线程获取返回结果
# 代码片段
Result result = AbstractInvoker.getResult(protocol.getRequestId());
result.setReturnType(((Result)protocol.getBody()).getReturnType());
result.setData(((Result)protocol.getBody()).getData());
result.notifyResult();
五、相关资源
示例代码
点击下载:mini-dubbo-demo
源码地址
https://github.com/applesline/mini-dubbo.git
姐妹篇