这是我参与8月更文挑战的第24天,活动详情查看: 8月更文挑战
本文使用源码地址:simple-rpc
在分布式服务框架-底层通信(3)中我们解决了Netty客户端调用的三个问题:
- 选择合适的序列化协议,解决Netty传输过程中出现的半包/粘包问题。
- 发挥长连接的优势,对Netty的Channel通道进行复用。
- Netty是异步框架,客户端发起服务调用后同步等待获取调用结果。
接下来我们要真正发起调用了。我们在服务引入时已经创建了服务调用方的动态代理类,通过实现InvocationHandler
接口,将复杂的远程调用通信逻辑封装在invoke(Object proxy, Method method, Object[] args)
方法中。
动态代理
@Override
public Object invoke(Object proxy, Method method, Object[] args) {
// 服务接口名称
String serviceItfName = targetItf.getName();
IRegisterCenter registerCenter = RegisterCenterImpl.getInstance();
// 获取服务列表
List<Provider> providerList = registerCenter.getServiceMetadata().get(serviceItfName);
if (CollectionUtils.isEmpty(providerList)) {
log.debug("can't find provider service={}, maybe need to reconnect zk server", serviceItfName);
} else {
// 根据负载均衡策略获取对应的服务提供者
LoadBalanceStrategy loadBalance = LoadBalanceStrategyEngine.getLoadBalanceStrategy(this.loadBalanceStrategy);
Provider provider = loadBalance.select(providerList);
Provider providerCopy = Provider.copy(provider);
providerCopy.setServiceMethod(method);
providerCopy.setServiceItf(targetItf);
Request request = Request.builder().args(args)
.invokeMethodName(method.getName())
.invokeTimeout(timeout)
.provider(providerCopy)
.uniqueKey(UUID.randomUUID().toString() + "-" + Thread.currentThread().getId())
.build();
try {
InetSocketAddress socketAddress = new InetSocketAddress(providerCopy.getServerIp(), providerCopy.getServerPort());
// 发起异步调用请求
Future<Response> responseFuture = executorService.submit(RevokerServiceCallable.of(socketAddress, request));
Response response = responseFuture.get(timeout, TimeUnit.MILLISECONDS);
if (response != null) {
return response.getResult();
}
} catch (Exception e) {
log.error("RevokerProxyBeanFactory invoke error, request={}", request, e);
throw new SRpcException("RevokerProxyBeanFactory invoke error", e);
}
}
return null;
}
public Object getProxy() {
return Proxy.newProxyInstance(
Thread.currentThread().getContextClassLoader(),
new Class[]{targetItf}, this);
}
先说一下getProxy()
方法,通过Proxy.newProxyInstance()
获取代理对象。接着我们看一下invoke()
到底做了什么:
- 从服务注册中心获取服务提供者列表;
- 实现软负载(后面会说),根据服务调用端配置的负载策略参数
loadBalanceStrategy
,获取具体的负载类,选择合适的服务提供者; - 组装服务调用请求
Request
; - 异步提交请求
RevokerServiceCallable
; - 通过阻塞队列机制同步等待请求结果。
可以看出来,除了第4步,其他的都是为了服务调用做准备的。那就很明白了,客户端发起服务调用的核心逻辑一定在RevokerServiceCallable
类中。
客户端异步服务调用
RevokerServiceCallable
类实现Callable<Response>
,实现了call()
方法。如果有不熟悉的可以自己复习一下。
@Override
public Response call() {
RevokerResponseHolder.initResponseData(request.getUniqueKey());
ArrayBlockingQueue<Channel> blockingQueue = NettyChannelPoolFactory.getInstance().acquire(inetSocketAddress);
try {
if (channel == null) {
channel = blockingQueue.poll(request.getInvokeTimeout(), TimeUnit.MILLISECONDS);
}
if (channel == null) {
log.error("can't find channel to resolve this request");
throw new SRpcException("can't find channel to resolve this request");
} else {
while (!channel.isOpen() || !channel.isActive() || !channel.isWritable()) {
log.warn("retry get new channel");
channel = blockingQueue.poll(request.getInvokeTimeout(), TimeUnit.MILLISECONDS);
if (channel == null) {
channel = NettyChannelPoolFactory.getInstance().registerChannel(inetSocketAddress);
}
}
ChannelFuture channelFuture = channel.writeAndFlush(request);
channelFuture.syncUninterruptibly();
long invokeTimeout = request.getInvokeTimeout();
return RevokerResponseHolder.getValue(request.getUniqueKey(), invokeTimeout);
}
} catch (Exception e) {
log.error("service invoke error", e);
throw new SRpcException("service invoke error", e);
} finally {
NettyChannelPoolFactory.getInstance().release(blockingQueue, channel, inetSocketAddress);
}
}
整个过程步骤如下:
- 初始化返回结果容器,将本次调用的唯一标识作为
Key
存入返回结果的Map
; - 根据本地调用服务提供者地址获取对应的
Netty
通道channel
队列; - 从队列中获取本次调用的
Netty
通道channel
,若队列中没有可用的Channel
,则重新注册一个Channel
。 - 将服务请求数据对象通过某种序列化协议编码成字节数组,通过
Channel
发送到服务端; - 同步等待服务端返回调用结果;
- 最后在本次调用完毕后,将
Netty
的通道Channel
重新释放到队列中,以便下次调用复用。
总结
至此,分布式服务框架底层通信篇也就结束了,现在客户端与服务端已经能够完成通信了,让我们来回顾一下:
-
第一篇我们介绍了Netty的基本使用方法和TCP半包粘包问题;
-
第二篇介绍了服务端启动的代码实现和服务端限流方案实现思路;
-
第三篇介绍了客户端如何完成Netty Channel复用和如何完成客户端发起服务调用后同步等待获取调用结果;
-
最终篇我们完成了客户端动态代理和异步请求。