基于Netty和SpringBoot实现一个轻量级RPC框架-Client篇
前提
前置文章:
前一篇文章相对简略地介绍了RPC服务端的编写,而这篇博文最要介绍服务端(Client)的实现。RPC调用一般是面向契约编程的,而Client的核心功能就是:把契约接口方法的调用抽象为使用Netty向RPC服务端通过私有协议发送一个请求。这里最底层的实现依赖于动态代理,因此动态代理是动态实现接口的最简单方式(如果字节码研究得比较深入,可以通过字节码编程实现接口)。需要的依赖如下:
JDK1.8+Netty:4.1.44.FinalSpringBoot:2.2.2.RELEASE
动态代理的简单使用
一般可以通过JDK动态代理或者Cglib的字节码增强来实现此功能,为了简单起见,不引入额外的依赖,这里选用JDK动态代理。这里重新搬出前面提到的契约接口HelloService:
public interface HelloService {
String sayHello(String name);
}
接下来需要通过动态代理为此接口添加一个实现:
public class TestDynamicProxy {
public static void main(String[] args) throws Exception {
Class<HelloService> interfaceKlass = HelloService.class;
InvocationHandler handler = new HelloServiceImpl(interfaceKlass);
HelloService helloService = (HelloService)
Proxy.newProxyInstance(interfaceKlass.getClassLoader(), new Class[]{interfaceKlass}, handler);
System.out.println(helloService.sayHello("throwable"));
}
@RequiredArgsConstructor
private static class HelloServiceImpl implements InvocationHandler {
private final Class<?> interfaceKlass;
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 这里应该根据方法的返回值类型去决定返回结果
return String.format("[%s#%s]方法被调用,参数列表:%s", interfaceKlass.getName(), method.getName(),
JSON.toJSONString(args));
}
}
}
// 控制台输出结果
[club.throwable.contract.HelloService#sayHello]方法被调用,参数列表:["throwable"]
这里可以确认两点:
InvocationHandler实现后会对被代理接口生成一个动态实现类。- 动态实现类(接口)方法被调用的时候,实际上是调用
InvocationHandler对应实例的invoke()方法,传入的参数就是当前方法调用的元数据。
Client端代码实现
Client端需要通过动态代理为契约接口生成一个动态实现类,然后提取契约接口调用方法时候所能提供的元数据,通过这些元数据和Netty客户端的支持(例如Netty的Channel)基于私有RPC协议组装请求信息并且发送请求。这里先定义一个请求参数提取器接口RequestArgumentExtractor:
@Data
public class RequestArgumentExtractInput {
private Class<?> interfaceKlass;
private Method method;
}
@Data
public class RequestArgumentExtractOutput {
private String interfaceName;
private String methodName;
private List<String> methodArgumentSignatures;
}
// 接口
public interface RequestArgumentExtractor {
RequestArgumentExtractOutput extract(RequestArgumentExtractInput input);
}
简单实现一下,解析结果添加到缓存中,实现类DefaultRequestArgumentExtractor代码如下:
public class DefaultRequestArgumentExtractor implements RequestArgumentExtractor {
private final ConcurrentMap<CacheKey, RequestArgumentExtractOutput> cache = Maps.newConcurrentMap();
@Override
public RequestArgumentExtractOutput extract(RequestArgumentExtractInput input) {
Class<?> interfaceKlass = input.getInterfaceKlass();
Method method = input.getMethod();
String methodName = method.getName();
Class<?>[] parameterTypes = method.getParameterTypes();
return cache.computeIfAbsent(new CacheKey(interfaceKlass.getName(), methodName,
Lists.newArrayList(parameterTypes)), x -> {
RequestArgumentExtractOutput output = new RequestArgumentExtractOutput();
output.setInterfaceName(interfaceKlass.getName());
List<String> methodArgumentSignatures = Lists.newArrayList();
for (Class<?> klass : parameterTypes) {
methodArgumentSignatures.add(klass.getName());
}
output.setMethodArgumentSignatures(methodArgumentSignatures);
output.setMethodName(methodName);
return output;
});
}
@RequiredArgsConstructor
private static class CacheKey {
private final String interfaceName;
private final String methodName;
private final List<Class<?>> parameterTypes;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
CacheKey cacheKey = (CacheKey) o;
return Objects.equals(interfaceName, cacheKey.interfaceName) &&
Objects.equals(methodName, cacheKey.methodName) &&
Objects.equals(parameterTypes, cacheKey.parameterTypes);
}
@Override
public int hashCode() {
return Objects.hash(interfaceName, methodName, parameterTypes);
}
}
}
在不考虑重连、断连等情况下,新增一个类ClientChannelHolder用于保存Netty客户端的Channel实例:
public class ClientChannelHolder {
public static final AtomicReference<Channel> CHANNEL_REFERENCE = new AtomicReference<>();
}
接着新增一个契约动态代理工厂(工具类)ContractProxyFactory,用于为契约接口生成代理类实例:
public class ContractProxyFactory {
private static final RequestArgumentExtractor EXTRACTOR = new DefaultRequestArgumentExtractor();
private static final ConcurrentMap<Class<?>, Object> CACHE = Maps.newConcurrentMap();
@SuppressWarnings("unchecked")
public static <T> T ofProxy(Class<T> interfaceKlass) {
// 缓存契约接口的代理类实例
return (T) CACHE.computeIfAbsent(interfaceKlass, x ->
Proxy.newProxyInstance(interfaceKlass.getClassLoader(), new Class[]{interfaceKlass}, (target, method, args) -> {
RequestArgumentExtractInput input = new RequestArgumentExtractInput();
input.setInterfaceKlass(interfaceKlass);
input.setMethod(method);
RequestArgumentExtractOutput output = EXTRACTOR.extract(input);
// 封装请求参数
RequestMessagePacket packet = new RequestMessagePacket();
packet.setMagicNumber(ProtocolConstant.MAGIC_NUMBER);
packet.setVersion(ProtocolConstant.VERSION);
packet.setSerialNumber(SerialNumberUtils.X.generateSerialNumber());
packet.setMessageType(MessageType.REQUEST);
packet.setInterfaceName(output.getInterfaceName());
packet.setMethodName(output.getMethodName());
packet.setMethodArgumentSignatures(output.getMethodArgumentSignatures().toArray(new String[0]));
packet.setMethodArguments(args);
Channel channel = ClientChannelHolder.CHANNEL_REFERENCE.get();
// 发起请求
channel.writeAndFlush(packet);
// 这里方法返回值需要进行同步处理,相对复杂,后面专门开一篇文章讲解,暂时统一返回字符串
// 如果契约接口的返回值类型不是字符串,这里方法返回后会抛出异常
return String.format("[%s#%s]调用成功,发送了[%s]到NettyServer[%s]", output.getInterfaceName(),
output.getMethodName(), JSON.toJSONString(packet), channel.remoteAddress());
}));
}
}
最后编写客户端ClientApplication的代码:
@Slf4j
public class ClientApplication {
public static void main(String[] args) throws Exception {
int port = 9092;
EventLoopGroup workerGroup = new NioEventLoopGroup();
Bootstrap bootstrap = new Bootstrap();
try {
bootstrap.group(workerGroup);
bootstrap.channel(NioSocketChannel.class);
bootstrap.option(ChannelOption.SO_KEEPALIVE, Boolean.TRUE);
bootstrap.option(ChannelOption.TCP_NODELAY, Boolean.TRUE);
bootstrap.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(1024, 0, 4, 0, 4));
ch.pipeline().addLast(new LengthFieldPrepender(4));
ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
ch.pipeline().addLast(new RequestMessagePacketEncoder(FastJsonSerializer.X));
ch.pipeline().addLast(new ResponseMessagePacketDecoder());
ch.pipeline().addLast(new SimpleChannelInboundHandler<ResponseMessagePacket>() {
@Override
protected void channelRead0(ChannelHandlerContext ctx, ResponseMessagePacket packet) throws Exception {
Object targetPayload = packet.getPayload();
if (targetPayload instanceof ByteBuf) {
ByteBuf byteBuf = (ByteBuf) targetPayload;
int readableByteLength = byteBuf.readableBytes();
byte[] bytes = new byte[readableByteLength];
byteBuf.readBytes(bytes);
targetPayload = FastJsonSerializer.X.decode(bytes, String.class);
byteBuf.release();
}
packet.setPayload(targetPayload);
log.info("接收到来自服务端的响应消息,消息内容:{}", JSON.toJSONString(packet));
}
});
}
});
ChannelFuture future = bootstrap.connect("localhost", port).sync();
// 保存Channel实例,暂时不考虑断连重连
ClientChannelHolder.CHANNEL_REFERENCE.set(future.channel());
// 构造契约接口代理类实例
HelloService helloService = ContractProxyFactory.ofProxy(HelloService.class);
String result = helloService.sayHello("throwable");
log.info(result);
future.channel().closeFuture().sync();
} finally {
workerGroup.shutdownGracefully();
}
}
}
先启动《基于Netty和SpringBoot实现一个轻量级RPC框架-Server篇》一文中的ServerApplication,再启动ClientApplication,控制台输出如下:
// 服务端日志
2020-01-16 22:34:51 [main] INFO c.throwable.server.ServerApplication - 启动NettyServer[9092]成功...
2020-01-16 22:36:35 [nioEventLoopGroup-3-1] INFO club.throwable.server.ServerHandler - 服务端接收到:RequestMessagePacket(interfaceName=club.throwable.contract.HelloService, methodName=sayHello, methodArgumentSignatures=[java.lang.String], methodArguments=[PooledUnsafeDirectByteBuf(ridx: 0, widx: 11, cap: 11/144)])
2020-01-16 22:36:35 [nioEventLoopGroup-3-1] INFO club.throwable.server.ServerHandler - 查找目标实现方法成功,目标类:club.throwable.server.contract.DefaultHelloService,宿主类:club.throwable.server.contract.DefaultHelloService,宿主方法:sayHello
2020-01-16 22:36:35 [nioEventLoopGroup-3-1] INFO club.throwable.server.ServerHandler - 服务端输出:{"attachments":{},"errorCode":200,"magicNumber":10086,"message":"Success","messageType":"RESPONSE","payload":"\"throwable say hello!\"","serialNumber":"63d386214d30410c9e5f04de03d8b2da","version":1}
// 客户端日志
2020-01-16 22:36:35 [main] INFO c.throwable.client.ClientApplication - [club.throwable.contract.HelloService#sayHello]调用成功,发送了[{"attachments":{},"interfaceName":"club.throwable.contract.HelloService","magicNumber":10086,"messageType":"REQUEST","methodArgumentSignatures":["java.lang.String"],"methodArguments":["throwable"],"methodName":"sayHello","serialNumber":"63d386214d30410c9e5f04de03d8b2da","version":1}]到NettyServer[localhost/127.0.0.1:9092]
2020-01-16 22:36:35 [nioEventLoopGroup-2-1] INFO c.throwable.client.ClientApplication - 接收到来自服务端的响应消息,消息内容:{"attachments":{},"errorCode":200,"magicNumber":10086,"message":"Success","messageType":"RESPONSE","payload":"\"throwable say hello!\"","serialNumber":"63d386214d30410c9e5f04de03d8b2da","version":1}
小结
Client端主要负责契约接口调用转换为发送RPC协议请求这一步,核心技术就是动态代理,在不进行模块封装优化的前提下实现是相对简单的。这里其实Client端还有一个比较大的技术难题没有解决,上面例子中客户端日志输出如果眼尖的伙伴会发现,Client端发送RPC请求的线程(main线程)和Client端接收Server端RPC响应处理的线程(nioEventLoopGroup-2-1线程)并不相同,这一点是Netty处理网络请求之所以能够如此高效的根源(简单来说就是请求和响应是异步的,两个流程本来是互不感知的)。但是更多情况下,我们希望外部请求是同步的,希望发送RPC请求的线程得到响应结果再返回(这里请求和响应有可能依然是异步流程)。下一篇文章会详细分析一下如果对请求-响应做同步化处理。
Demo项目地址:
(c-2-d e-a-20200116)
基于Netty和SpringBoot实现一个轻量级RPC框架-Client篇的更多相关文章
- 基于Netty和SpringBoot实现一个轻量级RPC框架-Client端请求响应同步化处理
前提 前置文章: <基于Netty和SpringBoot实现一个轻量级RPC框架-协议篇> <基于Netty和SpringBoot实现一个轻量级RPC框架-Server篇> & ...
- 基于Netty和SpringBoot实现一个轻量级RPC框架-协议篇
基于Netty和SpringBoot实现一个轻量级RPC框架-协议篇 前提 最近对网络编程方面比较有兴趣,在微服务实践上也用到了相对主流的RPC框架如Spring Cloud Gateway底层也切换 ...
- 基于Netty和SpringBoot实现一个轻量级RPC框架-Server篇
前提 前置文章: Github Page:<基于Netty和SpringBoot实现一个轻量级RPC框架-协议篇> Coding Page:<基于Netty和SpringBoot实现 ...
- 基于netty轻量的高性能分布式RPC服务框架forest<下篇>
基于netty轻量的高性能分布式RPC服务框架forest<上篇> 文章已经简单介绍了forest的快速入门,本文旨在介绍forest用户指南. 基本介绍 Forest是一套基于java开 ...
- 基于netty轻量的高性能分布式RPC服务框架forest<上篇>
工作几年,用过不不少RPC框架,也算是读过一些RPC源码.之前也撸过几次RPC框架,但是不断的被自己否定,最近终于又撸了一个,希望能够不断迭代出自己喜欢的样子. 顺便也记录一下撸RPC的过程,一来作为 ...
- 微博轻量级RPC框架Motan
Motan 是微博技术团队研发的基于 Java 的轻量级 RPC 框架,已在微博内部大规模应用多年,每天稳定支撑微博上亿次的内部调用.Motan 基于微博的高并发和高负载场景优化,成为一套简单.易用. ...
- 一个入门rpc框架的学习
一个入门rpc框架的学习 参考 huangyong-rpc 轻量级分布式RPC框架 该程序是一个短连接的rpc实现 简介 RPC,即 Remote Procedure Call(远程过程调用),说得通 ...
- 微博轻量级RPC框架Motan正式开源:支撑千亿调用
支撑微博千亿调用的轻量级 RPC 框架 Motan 正式开源了,项目地址为https://github.com/weibocom/motan. 微博轻量级RPC框架Motan正式开源 Motan 是微 ...
- 轻量级RPC框架开发
nio和传统io之间工作机制的差别 自定义rpc框架的设计思路 rpc框架的代码运行流程 第2天 轻量级RPC框架开发 今天内容安排: 1.掌握RPC原理 2.掌握nio操作 3.掌握netty简单的 ...
随机推荐
- Codeforces Round #187 (Div. 1 + Div. 2)
A. Sereja and Bottles 模拟. B. Sereja and Array 维护全局增量\(Y\),对于操作1(即\(a_{v_i}=x\))操作,改为\(a_{v_i}=x-Y\). ...
- webpack学习(一)项目中安装webpack
如何在项目中安装webpack,webpack-cli? 前提:电脑安装了 node和npm包管理工具 1 创建项目文件夹或者在已有的项目中打开终端 输入相关命令: npm init 因为已经安装好 ...
- P1048 数组中的逆序对
题目描述 在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对.输入一个数组,求出这个数组中的逆序对的总数. 输入格式 第一行包含一个整数 \(n\) ,表示数组中的元素个数 ...
- java编程规范大全
JAVA编程规范大全 命名规范 定义这个规范的目的是让项目中所有的文档都看起来像一个人写的,增加可读性,减少项目组中因为换人而带来的损失.(这些规范并不是一定要绝对遵守,但是一定要让程序有良好的可读性 ...
- C# 标准性能测试高级用法
本文告诉大家如何在项目使用性能测试测试自己写的方法 在 C# 标准性能测试 已经告诉大家如何使用 BenchmarkDotNet 测试性能,本文会告诉大家高级的用法. 建议是创建一个控制台项目用来做性 ...
- 关于react打包之后静态资源加载错误的问题
之前在打包react项目时发现一些问题,打包出来后我的一部分png图标加载不出来,开发者模式发现他们的路径中莫名其妙混入了我在react-router路由中使用<Browserrouter> ...
- Destoon系统目录树SEO属性目录开发实例
如何在destoon里设置树形目录结构的SEO优化方式官方给的SEO伪静态实例是没有这个方式的 楼主后来想了一下,就干脆自己做一个吧,已经测试完全无误通过,特意分享给大家 目前比如sell模块下类别[ ...
- Android多媒体框架
Android系统的多媒体架构图 OpenMax做编解码作用(codec),从上到下依次是AL(应用层,在多媒体中间件和应用程序之间提供一个标准化接口).IL(集成层,解码编码器).DL(开发层,供应 ...
- Elasticsearch搜索调优
最近把搜索后端从AWS cloudsearch迁到了AWS ES和自建ES集群.测试发现search latency高于之前的benchmark,可见模拟数据远不如真实数据来的实在.这次在产线的bac ...
- ajax异步发送时遇到的问题
问题原因是:controller中方法名与url中的名字不一样造成的 解决办法:找到错误的方法名,将其与url中的方法名统一: