前言
学习了netty和zookeeper后一直想写一个简单的RPC框架,网上的代码大多不符合我的要求,又或者技术太复杂。在此记录一下从头手写RPC框架的全过程
一、项目实现功能
实现多个服务之间的远程过程调用,使用起来非常简单。
二、使用步骤
1.引入库
Maven依赖如下(示例,最新版本请到Maven仓库获取):
<dependency>
<groupId>io.github.baijianruoli</groupId>
<artifactId>lidou</artifactId>
<version>1.3.0</version>
</dependency>
2.配置文件
spring boot配置文件(示例):
lidou.port=9997 //netty端口
lidou.servicePackage=com.zut.lpf.service //扫描@LidouService的包路径
lidou.zookeeper.url=192.168.247.129:2181 //zookeeper的url
3.代码如何使用
package com.zut.lpf.service.impl;
import com.zut.lpf.service.HelloService;
import io.github.baijianruoli.lidou.annotation.LidouService;
//使用@LidouService 标记要加入远程过程调用的Service
@LidouService
public class HelloServiceImpl implements HelloService {
@Override
public int hello(int sum) {
return sum*3;
}
}
@RestController
public class HelloController {
//使用@Reference注入Service接口的动态代理
@Reference(loadBalance = "random")
private HelloService helloService;
@RequestMapping("/hello")
public int hello(int sum)
{
long time1 = System.currentTimeMillis();
int hello = helloService.hello(sum);
long time2 = System.currentTimeMillis();
System.out.println(time2-time1);
return hello;
}
}
三.框架实现
1.服务的信息保存
首先为了保存多个服务的信息,这里采用zookeeper。服务启动后会扫描lidou.servicePackage下所有@LidouService的类,加入到zookeeper中。
zookeeper目录结构如下:
如何扫描自定义的@LidouService注解标记的类呢?
这里主要实现的方法是实现spring的后置工厂(BeanFactoryPostProcessor)以及继承类扫描器(ClassPathBeanDefinitionScanner)
在BeanFactoryPostProcessor中
postProcessBeanFactory可以拿到未初始化的bean工厂,这里我们重新扫描自定义包路径下的类,加入到ConfigurableListableBeanFactory 里
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {
AnnotationScanner scanner = AnnotationScanner.getScanner((BeanDefinitionRegistry) configurableListableBeanFactory, LidouService.class);
String property = applicationContext.getEnvironment().getProperty("lidou.servicePackage");
if(property==null)
property="io.github.baijianruoli";
scanner.setResourceLoader(applicationContext);
int count=scanner.scan(property);
}
在ClassPathBeanDefinitionScanner中
doScan方法为我们自定义的扫描路径,registerDefaultFilters方法为过滤含有特定注解的类,由此可以把所有@LidouService标记的类加入到spring工厂里。(很多框架都是用这种方式扫描路径的,例如mybatis)
@Override
public void registerDefaultFilters() {
// 添加需扫描的Annotation Class
this.addIncludeFilter(new AnnotationTypeFilter(staticTempAnnotationClazz));
}
// 以下为初始化后调用的方法
@Override
public Set<BeanDefinitionHolder> doScan(String... basePackages) {
return super.doScan(basePackages);
}
@Override
public boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
return super.isCandidateComponent(beanDefinition)
&& beanDefinition.getMetadata().hasAnnotation(this.selfAnnotationClazz.getName());
}
2.服务之间的通信
zookeeper有了各个服务的service信息,怎么相互之间通信呢?
这里在spring boot启动时使用高性能的网络框架netty并且采用protobuf传输数据,提高了传输速度。
NioEventLoopGroup bossGroup=new NioEventLoopGroup(1);
NioEventLoopGroup groupGroup=new NioEventLoopGroup(4);
ServerBootstrap serverBootstrap=new ServerBootstrap();
serverBootstrap.group(bossGroup,groupGroup).channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
pipeline.addLast(new ServerDecode());
pipeline.addLast(new ServerEncode());
pipeline.addLast(new IdleStateHandler(100,100,100, TimeUnit.SECONDS));
pipeline.addLast(new ServerHandler());
}
});
InetAddress address = InetAddress.getLocalHost();
String hostAddress = address.getHostAddress();
try{
ChannelFuture future= serverBootstrap.bind(hostAddress, port).sync();
log.info("服务端启动");
future.channel().closeFuture();
客户端请求服务端的参数
@Data
@NoArgsConstructor
public class BaseRequest {
private String className; //类全路径
private String methodName; //执行的方法名
private Object[] parameters; //参数
private Class<?>[] parameTypes; //参数类型
public BaseRequest(String className) {
this.className = className;
}
public BaseRequest(String className, String methodName, Object[] parameters, Class<?>[] parameTypes) {
this.className = className;
this.methodName = methodName;
this.parameters = parameters;
this.parameTypes = parameTypes;
}
}
netty服务端接收执行,返回结果到客户端
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
BaseRequest request = (BaseRequest) msg;
if("heart".equals(request.getClassName()))
return ;
String className = request.getClassName();
String methodName = request.getMethodName();
Class<?>[] parameTypes = request.getParameTypes();
Object[] parameters = request.getParameters();
Object o = InitRpcConfig.rpcServiceMap.get(className);
Object message=null;
try{
Method declaredMethod = o.getClass().getDeclaredMethod(methodName, parameTypes);
Object invoke = declaredMethod.invoke(o, parameters);//执行实现类
message=invoke;
}catch (NoSuchMethodException e)
{
log.info("bean实例化未找到");
ctx.writeAndFlush(new BaseResponse<String>(StatusCode.Fail,e.getMessage()));
}
ctx.writeAndFlush(new BaseResponse(StatusCode.Success,message));//把结果写回客户端
}
3.动态代理
服务信息已经保存,服务通信也可以使用,最后还缺少如何在客户端调用接口执行的是服务端的实现类。对每个被@Reference标记的属性,使用jdk动态代理注入代理对象,完成最后一步。
public Object getBean(final Class<?> serviceClass, final Object o,String mode) {
return Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class[]{serviceClass}, (proxy, method, args) -> {
BaseRequest baseRequest = new BaseRequest((String)o, method.getName(), args, method.getParameterTypes());
//负载均衡
//获得zookeeper路径
String url;
String port;
String path= PathUtils.addZkPath(serviceClass.getName());
List<String> children = zkClient.getChildren(path);
//负载均衡
String tmp = loadBalanceService.loadBalance(path, children, mode);
String[] split = tmp.split(":");
url=split[0];
port=split[1];
NioEventLoopGroup bossGroup = new NioEventLoopGroup(1);
ClientHandler clientHandler = new ClientHandler();
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(bossGroup).channel(NioSocketChannel.class).option(ChannelOption.TCP_NODELAY, true).handler(new ChannelInitializer<SocketChannel>() {
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
pipeline.addLast(new ClientEncode());
pipeline.addLast(new ClientDecode());
pipeline.addLast(new IdleStateHandler(80L, 80L, 80L, TimeUnit.SECONDS));
pipeline.addLast(clientHandler);
}
});
ChannelFuture future1 = bootstrap.connect(url, Integer.valueOf(port)).sync();
clientHandler.setPars(baseRequest);
Object result = executor.submit(clientHandler).get();
future1.channel().closeFuture();
bossGroup.shutdownGracefully();
return result;
});
}
最后返回服务端的结果,就像调用本地方法一样,因为又采用了负载均衡,大大提高了并发量。
4.最后
Github地址(如果还没有理解可以去github查看源码,也欢迎提交pr)
联系QQ:523892377