Netty实现自定义RPC框架
前言
Netty框架作为“java进阶之梯”,是程序员绕不开的一个框架。目前市面上很多开源框架, 基本上都是使用Netty作为网络通信的底层框架,包括Dubbo、RocketMQ、Canal、otter、HBase等等。 本文根据一个简单的业务需求,使用Netty框架搭建一个简单的RPC框架,包括服务端,客户端,注册中心等,暂时不考虑监控,主要目的是带大家熟悉一下Netty的一些常用API,以及熟悉下服务间的远程调用过程。Netty其实就是对网络通信的封装框架,说到底底层还是IO那一套。建议大家先熟悉一下NIO的三件套,即buffer,selector,channel。
什么是RPC?
RPC是一种远程调用过程,是一种通过网络远程调用其他服务的协议。通俗的说就是,A通过打电话的方式让B帮忙办一件事,B办完事后将结果告知A。
RPC的远程调用流程:
- client客户端发起请求到client stub(客户端存根:存放服务端地址信息,将客户端的请求参数编组成网络 消息,再通过网络发送给服务方),client stub接收到调用后负责将方法、参数等编组成能够进行网络传输的 消息体;在java中就是序列化的过程。
- client stub找到服务地址,并将消息通过网络发送到服务端(server),server stub(服务端存根:接受客户 端发送过来的消息并解组,再调用本地服务,再将本地服务执行结果发送给客户端)收到消息后进行解组。
- server stub根据解组结果调用本地的服务;
- 本地服务执行处理逻辑,将结果返回给server stub;
- server stub将返回结果编组成网络消息,通过网络并发送客户端;
- client stub接收到消息,并进行解组;
- 服务调用方client得到最终结果。
需求
现在我们模拟一个需求,订单服务(客户端)通过id调用商品服务(服务端),商品服务经过一系列业务逻辑,最终返回客户端商品的信息。
导入依赖
首先我们需要建一个maven项目,然后在pom文件中导入依赖。因为我们的实现只用到了Netty原生API,所以我们只需要导入两个依赖,分别是lombok的依赖和netty相关的依赖。
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12</version>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.18.Final</version>
</dependency>
服务提供端
服务端首先需要有5个类:
实体类:商品信息类
商品服务接口类:提供商品服务的相关功能接口
商品服务实现类: 具体实现商品服务的相关功能
服务业务处理类: 接收到客户端的请求后,会先经过处理类处理相关业务,如果请求很多的情况下,可以在 这个类里面做请求分发处理。
启动类: 启动商品,监听一个端口。
代码如下:
实体类
package cn.itsource.domain;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author : pankun
* @Version : 1.0
**/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ShopInfo {
/**
* 商品名称
*/
private String name;
/**
* 商品价格
*/
private String price;
/**
* 商品产地
*/
private String address;
}
服务接口
package cn.itsource.provider;
/**
* @author : pankun
* @Version : 1.0
**/
public interface ShopService {
String getShopInfoById(int a);
}
服务实现类
package cn.itsource.provider;
import cn.itsource.domain.ShopInfo;
import java.util.HashMap;
/**
* @author : pankun
* @Version : 1.0
*
**/
public class ShopServiceImpl implements ShopService {
private static HashMap<Integer, ShopInfo> map = new HashMap<Integer, ShopInfo>();
static {
map.put(1, new ShopInfo("苹果", "5","四川"));
map.put(2, new ShopInfo("香蕉", "6","海南"));
map.put(3, new ShopInfo("芒果", "8","云南"));
}
@Override
public String getShopInfoById(int a) {
return map.get(a).toString();
}
}
服务业务处理类
package cn.itsource.provider;
import cn.itsource.protocol.MyInvokerProtocol;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import java.io.File;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author : pankun
* @Version : 1.0
**/
public class RpcServerHandler extends ChannelInboundHandlerAdapter {
// 注册中心 (容器)
public static ConcurrentHashMap<String,Object> context = new ConcurrentHashMap<String,Object>();
// 类信息集合
private List<String> classNames = new ArrayList<String>();
// 构造器初始化
public RpcServerHandler(){
// 包扫描信息
scannerClass("cn.itsource.provider");
// 注册容器
doRegistry();
}
// 递归包扫描
private void scannerClass(String packageName) {
// 获取类加载器
URL url = this.getClass().getClassLoader().getResource(packageName.replaceAll("\\.", "/"));
File dir = new File(url.getFile());
for (File file : dir.listFiles()) {
//如果是一个文件夹,继续递归
if(file.isDirectory()){
scannerClass(packageName + "." + file.getName());
}else{
classNames.add(packageName + "." + file.getName().replace(".class", "").trim());
}
}
}
// 注册
private void doRegistry() {
if(classNames.isEmpty()){return;}
for (String className : classNames) {
try {
Class<?> clazz = Class.forName(className);
Class<?> anInterface = clazz.getInterfaces()[0]; // 因为此demo默认实现一个接口 所以此处写死获取当前第一个接口信息
context.put(anInterface.getName(),clazz.newInstance());
} catch (Exception e) {
e.printStackTrace();
}
}
}
// 请求到达 执行执行的业务逻辑
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
Object result = new Object();
MyInvokerProtocol request = (MyInvokerProtocol) msg; // netty 会按照我们自定义的策略进行转换
// 判断当前调用服务在容器中是否真正存在
if(context.containsKey(request.getClassName())){
// 确实存在执行对应的业务逻辑
Object clazz = context.get(request.getClassName());
// 获取真正执行的
Method method = clazz.getClass().getMethod(request.getMethodName(), request.getParames());
result = method.invoke(clazz, request.getValues());
}
if(result != null){
ctx.write(result);
}
ctx.flush();
ctx.close();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
启动类
package cn.itsource.provider;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
import io.netty.handler.codec.LengthFieldPrepender;
import io.netty.handler.codec.serialization.ClassResolvers;
import io.netty.handler.codec.serialization.ObjectDecoder;
import io.netty.handler.codec.serialization.ObjectEncoder;
/**
* @author : pankun
* @Version : 1.0
**/
public class RpcServer {
// 暴露端口
private int port;
public RpcServer(int port){
this.port = port;
}
/**
* 开启服务接口监听
* 调用 netty 相关API实现接口监听
*/
public void listen(){
// Boss线程 (Selector核心)
NioEventLoopGroup boss = new NioEventLoopGroup();
// Work线程 (工作线程)
NioEventLoopGroup work = new NioEventLoopGroup();
// 1. 建立服务
ServerBootstrap server = new ServerBootstrap();
// 2. 注入 Boos/Worker
server.group(boss,work)
.channel(NioServerSocketChannel.class) // 3. 管道执行 keys 轮询的核心
.childHandler(new ChannelInitializer<SocketChannel>() {
protected void initChannel(SocketChannel channel) throws Exception {
// 5. 对流数据进行解析
ChannelPipeline pipeline = channel.pipeline();
// 6. 自定义协议解码器 (取决于自己定义的规则对象)
/** 入参有5个,分别解释如下
* maxFrameLength:框架的最大长度。如果帧的长度大于此值,则将抛出TooLongFrameException。
* lengthFieldOffset:长度字段的偏移量:即对应的长度字段在整个消息数据中得位置
* lengthFieldLength:长度字段的长度。如:长度字段是int型表示,那么这个值就是4(long型就是8)
* lengthAdjustment:要添加到长度字段值的补偿值
* initialBytesToStrip:从解码帧中去除的第一个字节数
*/
pipeline.addLast(new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE, 0, 4, 0, 4));
//自定义协议编码器
pipeline.addLast(new LengthFieldPrepender(4));
// 7. 参数解析
//对象参数类型编码器
pipeline.addLast("encoder",new ObjectEncoder());
//对象参数类型解码器
pipeline.addLast("decoder",new ObjectDecoder(Integer.MAX_VALUE, ClassResolvers.cacheDisabled(null)));
// 8. 执行业务逻辑
pipeline.addLast(new RpcServerHandler());
}
}) // 4. 子线程 执行对应的业务逻辑
.option(ChannelOption.SO_BACKLOG,128) // 主线程最大连接数
.childOption(ChannelOption.SO_KEEPALIVE,true); // 子线程持续
try {
// 服务绑定端口
ChannelFuture future = server.bind(port).sync();
System.out.println("RPC start success, listen port is :" + port + " !!");
future.channel().closeFuture().sync(); // 回调
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
boss.shutdownGracefully();
work.shutdownGracefully();
}
}
public static void main(String[] args) {
// 监听启动
new RpcServer(8082).listen();
}
}
服务消费端
客户端我们需要4个类:
客户端启动类:客户端发起请求,开启远程调用,显示调用结果
RPC动态代理类:创建代理对象,封装request请求对象,创建RpcClient对象,
客户端业务处理类:主要是发送消息到服务端,接收服务端返回的消息
Netty启动类:连接netty服务端,提供消息发送等方法,并且可以自定义传输协议。
代码如下:
Netty启动类
package cn.itsource.consumer;
import cn.itsource.protocol.MyInvokerProtocol;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
import io.netty.handler.codec.LengthFieldPrepender;
import io.netty.handler.codec.serialization.ClassResolvers;
import io.netty.handler.codec.serialization.ObjectDecoder;
import io.netty.handler.codec.serialization.ObjectEncoder;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
/**
* @author : pankun
* @Version : 1.0
**/
public class RpcClient implements InvocationHandler {
private Class<?> clazz;
public RpcClient(Class<?> clazz){
this.clazz = clazz;
}
// 动态代理执行相应业务逻辑
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if(Object.class.equals(method.getDeclaringClass())){
// 如果当前就是一个实现类
return method.invoke(proxy,args);
}else{
return rpcInvoke(method,args);
}
}
private Object rpcInvoke(Method method, Object[] args) {
MyInvokerProtocol request = new MyInvokerProtocol();
request.setClassName(this.clazz.getName()); // 类名称
request.setMethodName(method.getName()); // 方法名称
request.setParames(method.getParameterTypes()); // 入参列表
request.setValues(args); // 实参列表
// TCP 远程调用
final RpcClientHandler consumerHandler = new RpcClientHandler();
NioEventLoopGroup work = new NioEventLoopGroup();
Bootstrap server = new Bootstrap();
server.group(work)
.channel(NioSocketChannel.class)// 客户端管道
.option(ChannelOption.TCP_NODELAY, true) // 开启
.handler(new ChannelInitializer<SocketChannel>() {
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast("frameDecoder", new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE, 0, 4, 0, 4));
//自定义协议编码器
pipeline.addLast("frameEncoder", new LengthFieldPrepender(4));
//对象参数类型编码器
pipeline.addLast("encoder", new ObjectEncoder());
//对象参数类型解码器
pipeline.addLast("decoder", new ObjectDecoder(Integer.MAX_VALUE, ClassResolvers.cacheDisabled(null)));
pipeline.addLast("handler",consumerHandler);
}
});
ChannelFuture future = null;
try {
future = server.connect("localhost", 8082).sync();
future.channel().writeAndFlush(request).sync();
future.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
work.shutdownGracefully();
}
return consumerHandler.getResponse();
}
}
客户端业务处理类
package cn.itsource.consumer;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
/**
* @author : pankun
* @Version : 1.0
**/
public class RpcClientHandler extends ChannelInboundHandlerAdapter {
private Object response;
public Object getResponse() {
return response;
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
response = msg;
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
super.exceptionCaught(ctx, cause);
}
}
RPC动态代理类
package cn.itsource.consumer;
import java.lang.reflect.Proxy;
/**
* @author : pankun
* @Version : 1.0
**/
public class RpcProxy {
public static <T> T invoke(Class<T> clazz){
Class<?> [] interfaces = clazz.isInterface() ?
new Class[]{clazz} :
clazz.getInterfaces();
T result = (T) Proxy.newProxyInstance(clazz.getClassLoader(),interfaces,new RpcClient(clazz));
return result;
}
}
客户端启动类
package cn.itsource.consumer;
import cn.itsource.provider.ShopService;
/**
* @author : pankun
* @Version : 1.0
**/
public class IRpcConsumer {
public static void main(String[] args) {
ShopService invoke1 = RpcProxy.invoke(ShopService.class);
System.out.println(invoke1.getShopInfoById(1));
System.out.println(invoke1.getShopInfoById(2));
System.out.println(invoke1.getShopInfoById(3));
}
}
自定义传输协议
使用Netty框架可以自定义传输协议,如果定义了自定义传输协议,Netty会按照我们自定义的策略进行转换,所以我们这里有一个类来自定义传输协议。代码如下:
package cn.itsource.protocol;
import lombok.Data;
import java.io.Serializable;
/**
* 自定义传输协议
*/
@Data
public class MyInvokerProtocol implements Serializable {
private String className;//类名
private String methodName;//函数名称
private Class<?>[] parames;//形参列表
private Object[] values;//实参列表
}
测试
测试远程调用,我们首先启动服务端启动类RpcServer。控制台出现如下信息表示启动成功。
然后我们启动客户端启动类IRpcConsumer,开启远程调用,如果客户端的控制台能够获取到服务端返回的相关商品信息,那么表示远程调用成功。如下图: