前言

  Rpc( Remote procedure call):是一种请求 - 响应协议。RPC由客户端启动,客户端向已知的远程服务器发送请求消息,以使用提供的参数执行指定的过程。远程服务器向客户端发送响应,应用程序继续其进程。当服务器正在处理该调用时,客户端被阻塞(它等待服务器在恢复执行之前完成处理),除非客户端向服务器发送异步请求,例如XMLHttpRequest。在各种实现中存在许多变化和细微之处,导致各种不同(不兼容)的RPC协议。

  技术选型:

  1. Protostuff:它基于 Protobuf 序列化框架,面向 POJO,无需编写 .proto 文件。
  2. Netty:基于NIO的网络编程框架,封装了NIO细节,使用更加方便
  3. 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实现(附源码)的更多相关文章

  1. 干货——基于Nop的精简版开发框架(附源码)

    .NET的开发人员应该都知道这个大名鼎鼎的高质量b2c开源项目-nopCommerce,基于EntityFramework和MVC开发,拥有透明且结构良好的解决方案,同时结合了开源和商业软件的最佳特性 ...

  2. ASP.NET MVC+EF框架+EasyUI实现权限管理(附源码)

    前言:时间很快,已经快到春节的时间了,这段时间由于生病,博客基本没更新,所以今天写一下我们做的一个项目吧,是对权限的基本操作的操作,代码也就不怎么说了,直接上传源码和图片展示,下面我们直接进入主题介绍 ...

  3. 教你搭建SpringMVC框架( 更新中、附源码)

    一.项目目录结构 二.SpringMVC需要使用的jar包 commons-logging-1.2.jar junit-4.10.jar log4j-api-2.0.2.jar log4j-core- ...

  4. SSM框架整合 详细步骤(备注) 附源码

    整合思路 将工程的三层结构中的JavaBean分别使用Spring容器(通过XML方式)进行管理. 整合持久层mapper,包括数据源.会话工程及mapper代理对象的整合: 整合业务层Service ...

  5. 基于TP框架的ThinkCMF,控制器display方法源码分析

    昨天在写代码的时候,看见写了无数次的模版渲染方法:$this->display(),突然很想弄清楚它是如何实现的. 今天不忙,就分析了一下. class TestController exten ...

  6. 教你搭建SpringSecurity3框架( 更新中、附源码)

    源码下载地址:http://pan.baidu.com/s/1qWsgIg0 一.web.xml <?xml version="1.0" encoding="UTF ...

  7. Docker Compose部署项目到容器-基于Tomcat和mysql的商城项目(附源码和sql下载)

    场景 Docker-Compose简介与Ubuntu Server 上安装Compose: https://blog.csdn.net/BADAO_LIUMANG_QIZHI/article/deta ...

  8. 基于Struts2+Hibernate开发小区物业管理系统 附源码

    开发环境: Windows操作系统开发工具: MyEclipse+Jdk+Tomcat+MySql数据库 运行效果图: 源码及原文链接:https://javadao.xyz/forum.php?mo ...

  9. Java:基于AOP的动态数据源切换(附源码)

    1 动态数据源的必要性 我们知道,物理服务机的CPU.内存.存储空间.连接数等资源都是有限的,某个时段大量连接同时执行操作,会导致数据库在处理上遇到性能瓶颈.而在复杂的互联网业务场景下,系统流量日益膨 ...

随机推荐

  1. castle windsor学习----- Services and Components 两者的定义

  2. Linux- 运维

    Linux运维遇到需要使用命令 查看Linux系统有多少用户 cat /etc/passwd | wc -l 查看用户占用的uid,默认情况下,ldap新增的用户和系统本地的用户uid是混在一起.在新 ...

  3. redis实现session共享,哨兵

    一.Redis介绍 1.redis是key-value的存储系统,属于非关系型数据库 2.特点:支持数据持久化,可以让数据在内存中保存到磁盘里(memcached:数据存在内存里,如果服务重启,数据会 ...

  4. AngularJS学习笔记(一) 关于MVVM和双向绑定

    写在前面: 因为需要开始学习ng,之前在知乎上听大神们介绍ng的时候说这个坑如何的大,学了一阵(其实也就三天),感觉ng做的很大很全,在合适的情境你可以完全使用ng搞定一切.这一点从诸如jqLite之 ...

  5. 2013面试C++小结

    2013年我在厦门c++求职小结 1.一般公司出的面试题目中的找错误,都是出自平常公司内部使用过程中出现的真实错误. 比如stl 中erase的使用:详细请见 :http://blog.csdn.ne ...

  6. appium-环境搭建(三)

    appium步骤:基本环境1.由于操作手机端操作,需要模拟器或者真机 itools模拟器,真机2.appium操作app,需要知道操作的app是什么?需要知道这个app包名 1.问开发 2.利用adt ...

  7. BEC listen and translation exercise 37

    You're supposed to do that before 10.30 in the morning, but obviously, if it's an emergency, you can ...

  8. linux命令学习笔记(18):locate 命令

    locate 让使用者可以很快速的搜寻档案系统内是否有指定的档案.其方法是先建立一个包括系统内所有档案名称及 路径的数据库,之后当寻找时就只需查询这个数据库,而不必实际深入档案系统之中了.在一般的 d ...

  9. POJ2406Power Strings (最小循环节)(KMP||后缀数组)

    Given two strings a and b we define a*b to be their concatenation. For example, if a = "abc&quo ...

  10. 9th

    2017-2018-2 20179212<网络攻防实践>第9周作业 视频学习 KaliSecurity压力测试工具 压力测试通过确定一个系统的瓶颈或者不能接受的性能点,来获得系统能够提供的 ...