基于netty框架的轻量级RPC实现(附源码)
前言
Rpc( Remote procedure call):是一种请求 - 响应协议。RPC由客户端启动,客户端向已知的远程服务器发送请求消息,以使用提供的参数执行指定的过程。远程服务器向客户端发送响应,应用程序继续其进程。当服务器正在处理该调用时,客户端被阻塞(它等待服务器在恢复执行之前完成处理),除非客户端向服务器发送异步请求,例如XMLHttpRequest。在各种实现中存在许多变化和细微之处,导致各种不同(不兼容)的RPC协议。
技术选型:
- Protostuff:它基于 Protobuf 序列化框架,面向 POJO,无需编写 .proto 文件。
- Netty:基于NIO的网络编程框架,封装了NIO细节,使用更加方便
- SpringBoot:Spring 的组件的集合,内部封装服务器,实现了自动加载
1.封装请求的pojo和响应的pojo
public class RpcRequest {
public RpcRequest() {
} private Long id;
/**
* rpc name
*/
private String className;
/**
* 方法名
*/
private String methodName;
/**
* 参数
*/
private HashMap<Class<?>, Object> arguments; //get and set ...
public class RpcResponse {
public RpcResponse() {
} private Long id;
private Integer code;
private Object result;
private String failMsg;
// get and set ...
2.server端对request进行解码,对response进行编码。反之client端对request进行编码,对response进行解码,因此需要编写两个编码和解码器,在不同端,对不同pojo进行编码解码
编码类只对属于某个 genericClass的类进行编码,SerializationUtil为使用Protobuffer工具封装的一个工具类
@ChannelHandler.Sharable
public class RpcEncode extends MessageToByteEncoder {
//client 端为 request, server 端为 response
private Class<?> genericClass; public RpcEncode(Class<?> clazz) {
this.genericClass = clazz;
} @Override
protected void encode(ChannelHandlerContext channelHandlerContext,
Object o, ByteBuf byteBuf) throws Exception {
if (genericClass.isInstance(o)) {
byte[] data = SerializationUtil.serialize(o);
byteBuf.writeInt(data.length);
byteBuf.writeBytes(data);
}
}
}
同样的,解码
public class RpcDecode extends ByteToMessageDecoder {
private Class<?> genericClass; public RpcDecode(Class<?> clazz) {
this.genericClass = clazz;
} @Override
protected void decode(ChannelHandlerContext ctx,
ByteBuf in, List<Object> out) throws Exception {
int dataLength = in.readInt();
//一个整数4个字节
if (dataLength < 4) {
return;
}
in.markReaderIndex();
if (in.readableBytes() < dataLength) {
in.resetReaderIndex();
return;
}
byte[] data = new byte[dataLength];
in.readBytes(data);
Object obj = SerializationUtil.deserialize(data, genericClass);
out.add(obj);
}
3. server端将数据解码后,开始使用handler处理client的请求,handler里包含一个map,里面value是使用@RpcService后的bean,key是注解的value,通过RpcRequest的className,从map的key进行匹配,找到bean之后,通过反射执行 methodName对应的方法 和arguments的参数
@RpcService用于标识在发布的服务类上,value为client 请求的classname,该注解继承了@Component注解,将会被spring注册为bean
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Component
public @interface RpcService {
String value(); String description() default "";
}
通过@RpcService 标识的类,将被注册为bean实例,这里将在 LrpcHandlerAutoConfiguration类中,将这些标识了该注解的bean实例找出来,传入handler中执行client的请求方法
@Configurable
@Component
public class LrpcHandlerAutoConfiguration implements ApplicationContextAware {
private ApplicationContext context;
@Value("${lrpc.server}")
public String port; @Bean
public RpcHandler rpcHandler() {
Map<String, Object> rpcBeans = context.getBeansWithAnnotation(RpcService.class);
Set<String> beanNameSet = rpcBeans.keySet();
for (String beanName : beanNameSet) {
Object obj = rpcBeans.get(beanName);
//rpcService注解会 把value的值传递给component
RpcService annotation = obj.getClass().getDeclaredAnnotation(RpcService.class);
//默认bean name
if (StringUtils.isBlank(annotation.value()) || annotation.value().equals(beanName)) {
continue;
}
rpcBeans.put(annotation.value(), rpcBeans.get(beanName));
//去掉重复
rpcBeans.remove(beanName);
}
return new RpcHandler(rpcBeans);
}
//..........................
RpcHandler的构造函数,注入了一份rpcBeans的引用,当client的RpcRequest请求时,将从该rpcBeans中获取对应的bean
@ChannelHandler.Sharable
public class RpcHandler extends SimpleChannelInboundHandler<RpcRequest> {
private static final Logger logger = Logger.getLogger(RpcHandler.class.getName());
private Map<String, Object> rpcBeans; public RpcHandler(Map<String, Object> rpcBeans) {
this.rpcBeans = rpcBeans;
} @Override
protected void channelRead0(ChannelHandlerContext ctx, RpcRequest msg) throws Exception {
RpcResponse rpcResponse = handle(msg);
ctx.channel().writeAndFlush(rpcResponse).addListener(ChannelFutureListener.CLOSE);
} private RpcResponse handle(RpcRequest msg) throws InvocationTargetException {
RpcResponse rpcResponse = new RpcResponse();
Object obj = rpcBeans.get(msg.getClassName());
//TODO 暂时这样吧
if (Objects.isNull(obj)) {
System.out.println("未找到service");
rpcResponse.setResult(null);
rpcResponse.setCode(404);
logger.warning("请求的service未找到,msg:" + msg.toString());
return rpcResponse;
}
rpcResponse.setId(msg.getId());
//解析请求,执行相应的rpc方法
Class<?> clazz = obj.getClass();
String methodName = msg.getMethodName();
HashMap<Class<?>, Object> arguments = msg.getArguments();
FastClass fastClass = FastClass.create(clazz);
FastMethod method = fastClass.getMethod(methodName,
arguments.keySet().toArray(new Class[arguments.size()]));
Object result = method.invoke(obj, arguments.values().toArray());
rpcResponse.setResult(result);
rpcResponse.setCode(200);
return rpcResponse;
} @Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
logger.warning(cause.toString());
}
}
4. 启动 LrpcServer,LrpcChannelInit 在 LrpcHandlerAutoConfiguration中进行初始化,同时注入 lrpc.server 环境变量给port参数
@Component
public class LrpcServerImpl implements LrpcServer, ApplicationListener<ApplicationReadyEvent> {
@Autowired
LrpcChannelInit lrpcChannelInit; @Override
public void connect() {
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup)
.handler(new LoggingHandler(LogLevel.INFO))
.channel(NioServerSocketChannel.class)
.childHandler(lrpcChannelInit)
.option(ChannelOption.SO_BACKLOG, 128)
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000)
.childOption(ChannelOption.SO_KEEPALIVE, true);
ChannelFuture future = bootstrap.bind(new InetSocketAddress(lrpcChannelInit.getPort())).sync();
future.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
} @Override
public void onApplicationEvent(ApplicationReadyEvent event) {
connect();
}
}
5. 客户端handler类 LrpClientHandler,里面持一把对象锁,因为netty返回数据总是异步的,这里将异步转成同步,利用 Object的wait()和notify()方法实现,LrpClientHandler这里是多例的,不存在竞争状态,因此是线程安全的
public class LrpClientHandler extends SimpleChannelInboundHandler<RpcResponse> {
private final Object lock = new Object();
private volatile RpcResponse rpcResponse = null; @Override
protected void channelRead0(ChannelHandlerContext ctx, RpcResponse msg) throws Exception {
rpcResponse = msg;
synchronized (lock) {
lock.notifyAll();
}
} @Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
} public Object getLock() {
return lock;
} public RpcResponse getRpcResponse() {
return rpcResponse;
} public void setRpcResponse(RpcResponse rpcResponse) {
this.rpcResponse = rpcResponse;
} }
6. LrpcClientChannelInit 中持有一个LrpcClientHandler的引用,在初始化该类时同时初始化LrpcClientHandler
public class LrpcClientChannelInit extends ChannelInitializer {
private LrpClientHandler lrpClientHandler; public LrpcClientChannelInit() {
lrpClientHandler = new LrpClientHandler();
} @Override
protected void initChannel(Channel ch) {
//请求加密
ch.pipeline().addLast(new RpcEncode(RpcRequest.class))
.addLast(new RpcDecode(RpcResponse.class))
.addLast(new LoggingHandler(LogLevel.INFO))
.addLast(lrpClientHandler);
}
public synchronized void initHandler(LrpClientHandler lrpClientHandler){
this.lrpClientHandler = lrpClientHandler;
}
public LrpClientHandler getLrpClientHandler() {
return lrpClientHandler;
} public void setLrpClientHandler(LrpClientHandler lrpClientHandler) {
this.lrpClientHandler = lrpClientHandler;
}
}
7. 持有执行远程方法的host和port,execute(RpcRequest r)中连接,传递参数
public class LrpcExecutorImpl implements LrpcExecutor {
private String host;
private Integer port; public LrpcExecutorImpl(String host, Integer port) {
this.host = host;
this.port = port;
} @Override
public RpcResponse execute(RpcRequest rpcRequest) {
LrpcClientChannelInit lrpcClientChannelInit = new LrpcClientChannelInit();
Bootstrap b = new Bootstrap();
EventLoopGroup group = null;
ChannelFuture future = null;
try {
group = new NioEventLoopGroup();
b.group(group)
.channel(NioSocketChannel.class)
.handler(lrpcClientChannelInit)
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000)
.option(ChannelOption.SO_KEEPALIVE, true);
future = b.connect(new InetSocketAddress(host, port)).sync();
//TODO 连接好了直接发送消息,同步则阻塞等待通知
future.channel().writeAndFlush(rpcRequest).sync();
Object lock = lrpcClientChannelInit.getLrpClientHandler().getLock();
synchronized (lock) {
lock.wait();
}
RpcResponse rpcResponse = lrpcClientChannelInit.getLrpClientHandler().getRpcResponse();
if (null != rpcResponse) {
future.channel().closeFuture().sync();
}
return rpcResponse;
} catch (Exception e) {
e.printStackTrace();
} finally {
lrpcClientChannelInit.getLrpClientHandler().setRpcResponse(null);
if (null != group) {
group.shutdownGracefully();
}
}
return null;
}
//get and set ...
}
8.使用实例
Server端发布服务
@RpcService("HelloService")
public class HelloServiceImpl implements HelloService {
@Override
public String say(String msg) {
return "Hello Word!" + msg;
}
}
在application.properties中,注入环境变量:
# lrpc.server=8888
Client 配置服务地址和LrpcExecutor
lrpc.hello.host=xxxxxxxxxxxxxxxxxxx
lrpc.hello.port=8888
lrpc.hello.desc=hello rpc调用
配置调用服务执行器 LrpcExecutor,保存在spring 容器bean里,可通过依赖注入进行调用
@Configuration
@Component
public class RpcConfiguration { @Bean("rpc.hello")
@ConfigurationProperties(prefix = "lrpc.hello")
public RpcServerProperties rpcClientCallProperties() {
return new RpcServerProperties();
} @Bean("helloRpcExecutor")
LrpcExecutor lrpcExecutor(@Qualifier(value = "rpc.hello") RpcServerProperties rpcServerProperties) {
return invoke(rpcServerProperties);
} private LrpcExecutor invoke(RpcServerProperties config) {
return new LrpcExecutorImpl(config.getHost(), config.getPort());
}
}
调用服务,methodName为ClassName对应类下的方法名:
@Autowired
@Qualifier(value = "helloRpcExecutor")
private LrpcExecutor helloRpcExecutor; @GetMapping("/say")
public String invoke(String msg) {
RpcRequest rpcRequest = new RpcRequest();
rpcRequest.setClassName("HelloService");
rpcRequest.setMethodName("say");
rpcRequest.setId(111L);
HashMap<Class<?>, Object> arguments = new HashMap<>(8);
arguments.put(String.class, "good");
rpcRequest.setArguments(arguments);
RpcResponse execute = helloRpcExecutor.execute(rpcRequest);
System.out.println(execute.toString());
return execute.toString();
}
最后,以上为个人练手demo,未来有时间会把未完善的地方慢慢完善,最终目标是做成像dobble那样的pj,如有好的意见或疑惑欢迎各位大佬指点(morty630@foxmail.com),附上 github完整代码:https://github.com/To-echo/lrpc-all (你的点赞是我的动力)
相关技术文档
objenesis反射工具:http://objenesis.org/details.html
Protobuf 协议:https://developers.google.com/protocol-buffers/
Protobuffer序列化工具:https://github.com/protostuff/protostuff
RPC介绍:https://en.wikipedia.org/wiki/Remote_procedure_call
Netty官网:https://netty.io/
基于netty框架的轻量级RPC实现(附源码)的更多相关文章
- 干货——基于Nop的精简版开发框架(附源码)
.NET的开发人员应该都知道这个大名鼎鼎的高质量b2c开源项目-nopCommerce,基于EntityFramework和MVC开发,拥有透明且结构良好的解决方案,同时结合了开源和商业软件的最佳特性 ...
- ASP.NET MVC+EF框架+EasyUI实现权限管理(附源码)
前言:时间很快,已经快到春节的时间了,这段时间由于生病,博客基本没更新,所以今天写一下我们做的一个项目吧,是对权限的基本操作的操作,代码也就不怎么说了,直接上传源码和图片展示,下面我们直接进入主题介绍 ...
- 教你搭建SpringMVC框架( 更新中、附源码)
一.项目目录结构 二.SpringMVC需要使用的jar包 commons-logging-1.2.jar junit-4.10.jar log4j-api-2.0.2.jar log4j-core- ...
- SSM框架整合 详细步骤(备注) 附源码
整合思路 将工程的三层结构中的JavaBean分别使用Spring容器(通过XML方式)进行管理. 整合持久层mapper,包括数据源.会话工程及mapper代理对象的整合: 整合业务层Service ...
- 基于TP框架的ThinkCMF,控制器display方法源码分析
昨天在写代码的时候,看见写了无数次的模版渲染方法:$this->display(),突然很想弄清楚它是如何实现的. 今天不忙,就分析了一下. class TestController exten ...
- 教你搭建SpringSecurity3框架( 更新中、附源码)
源码下载地址:http://pan.baidu.com/s/1qWsgIg0 一.web.xml <?xml version="1.0" encoding="UTF ...
- Docker Compose部署项目到容器-基于Tomcat和mysql的商城项目(附源码和sql下载)
场景 Docker-Compose简介与Ubuntu Server 上安装Compose: https://blog.csdn.net/BADAO_LIUMANG_QIZHI/article/deta ...
- 基于Struts2+Hibernate开发小区物业管理系统 附源码
开发环境: Windows操作系统开发工具: MyEclipse+Jdk+Tomcat+MySql数据库 运行效果图: 源码及原文链接:https://javadao.xyz/forum.php?mo ...
- Java:基于AOP的动态数据源切换(附源码)
1 动态数据源的必要性 我们知道,物理服务机的CPU.内存.存储空间.连接数等资源都是有限的,某个时段大量连接同时执行操作,会导致数据库在处理上遇到性能瓶颈.而在复杂的互联网业务场景下,系统流量日益膨 ...
随机推荐
- wampserver2 配置ssl 经过验证 可用
本文原地址 http://blog.csdn.net/taosst/article/details/2182966 http://forum.wampserver.com/read.php?2,329 ...
- CodeForces 292D Connected Components (并查集+YY)
很有意思的一道并查集 题意:给你n个点(<=500个),m条边(<=10000),q(<=20000)个询问.对每个询问的两个值xi yi,表示在从m条边内删除[xi,yi]的边后 ...
- JavaScript 的 async/await
随着 Node 7 的发布,越来越多的人开始研究据说是异步编程终级解决方案的 async/await. 异步编程的最高境界,就是根本不用关心它是不是异步. async 函数就是隧道尽头的亮光,很多人认 ...
- 英语发音规则---oo
英语发音规则---oo 一.总结 一句话总结: 1.重读音节词尾的字母组合oo发音素[u:]的音? too [tu:] adv.太;也 zoo [zu:] n.动物园 room [ru:m] n.房间 ...
- python ddt 重写
对此方法重写 def mk_test_name(name, value, index=0): 重写前 index = "{0:0{1}}".format(index + 1, in ...
- hdu3572线性欧拉筛
用线性筛来筛,复杂度O(n) #include<bits/stdc++.h> #include<ext/rope> #define fi first #define se se ...
- 利用Hibernate 框架,实现对数据库的增删改查
增: package com.maya.test; import org.hibernate.*; import org.hibernate.cfg.*; import com.maya.model. ...
- 更新github上代码
前面一篇已经实现首次上传代码到github了,本篇继续讲如何把本地更新的代码同步更新到github上 一.clone代码 1.把大神的代码clone到本地,或者clone自己github上的代码,使用 ...
- L106 Three things we learned from day one at the World Cup
Hosts Russia got the World Cup off to a flying start by hammering Saudi Arabia 5-0 in the opening ga ...
- BEC listen and translation exercise 36
你所持的护照可使你享有免费医疗.Your passport qualifies you to receive free medical treatment.公司指使其职员挖对手的客户.The comp ...