文章目录
- Netty实现RPC框架
- 一、RPC简介
- 二、代码实现
- 1. 需求介绍
- 2. 实现步骤
- 3. 公共代码
- 4. 服务端代码
- 5. 客户端代码
一、RPC简介
RPC全称为remote procedure call,即远程过程调用。借助RPC可以做到像本地调用一样调用远程服务,是一种进程间的通信方式。
比如两台服务器A和B,A服务器上部署一个应用,B服务器上部署一个应用,A服务器上的应用想调用B服务器上的应用提供的方法。由于两个应用不在一个内存空间,不能直接调用,所以需要通过网络来表达调用的语义和传达调用的数据。
注意:RPC并不是一个具体的技术,而是指整个网络远程调用过程。
二、代码实现
1. 需求介绍
要求用Netty实现一个简单的RPC框架,消费者和提供者约定接口和协议,消费者远程调用提供者的服务。
2. 实现步骤
- 创建一个接口,定义抽象方法。用于消费者和提供者之间的约定。
- 创建一个提供者,该类需要监听消费者的请求,并按照约定返回数据。
- 创建一个消费者,该类需要透明的调用自己不存在的提供者的方法,内部需要使用Netty进行数据通信。
- 提供者与消费者数据传输使用 json 字符串数据格式。
- 使用 Netty 集成 SpringBoot 环境实现。
注意:服务端注解要使用在实现类上,用于标记服务是否对外暴露。
3. 公共代码
- 公共接口,消费者与提供者的约定
public interface IUserService {
/**
* 根据ID查询用户
*/
User getById(int id);
}
- 封装的请求对象
@Data
public class RpcRequest {
/**
* 请求对象的ID
*/
private String requestId;
/**
* 要调用方法所在类名
*/
private String className;
/**
* 要调用的方法名
*/
private String methodName;
/**
* 要调用方法的参数类型
*/
private Class<?>[] parameterTypes;
/**
* 要调用方法的入参
*/
private Object[] parameters;
}
- 封装的响应对象
@Data
public class RpcResponse {
/**
* 响应ID
*/
private String requestId;
/**
* 错误信息
*/
private String error;
/**
* 返回的结果
*/
private Object result;
}
- pojo类
@Data
public class User {
private int id;
private String name;
}
4. 服务端代码
- 定义注解,用于暴露服务接口,暴露服务端的getUserById方法
@Target(ElementType.TYPE) //用于类上
@Retention(RetentionPolicy.RUNTIME) //在运行时可以获取到
public @interface RpcService {
}
- 服务端实现公共接口,客户端要调用的方法
@RpcService //对外暴露,加自定义注解
@Service
public class UserServiceImpl implements IUserService {
Map<Object, User> userMap = new HashMap();
@Override
public User getById(int id) {
if (userMap.size() == 0) {
User user1 = new User();
user1.setId(1);
user1.setName("张三");
User user2 = new User();
user2.setId(2);
user2.setName("李四");
userMap.put(user1.getId(), user1);
userMap.put(user2.getId(), user2);
}
return userMap.get(id);
}
}
- 服务端代码
/**
* Netty的服务端
* 启动服务端监听端口
* 实现Spring容器生命周期接口DisposableBean,在Spring容器关闭的时候本类中的资源也要关闭
*/
@Component
public class NettyRpcServer implements DisposableBean {
/**
* 注入自定义的处理器
*/
@Autowired
NettyServerHandler nettyServerHandler;
// 定义boss和worker线程
EventLoopGroup bossGroup = null;
EventLoopGroup workerGroup = null;
/**
* 启动服务端,参数表示ip和端口号
*/
public void start(String host, int port) {
try {
//1.创建bossGroup和workerGroup
bossGroup = new NioEventLoopGroup(1);
workerGroup = new NioEventLoopGroup();
//2.设置启动助手
ServerBootstrap bootstrap = new ServerBootstrap();
//3.设置启动参数
bootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
//添加String的编解码器,使得客户端和服务器可以直接使用String类型进行交流
ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast(new StringEncoder());
//添加自定义处理器
ch.pipeline().addLast(nettyServerHandler);
}
});
//绑定ip和端口号
ChannelFuture channelFuture = bootstrap.bind(host, port).sync();
System.out.println("======Netty服务端启动成功======");
//监听通道的关闭状态(此处并没有真正的关闭),阻塞直到close事件发生
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
//关闭资源
if (bossGroup != null) {
bossGroup.shutdownGracefully();
}
if (workerGroup != null) {
workerGroup.shutdownGracefully();
}
}
}
/**
* 实现DisposableBean接口要重写的方法,Spring关闭的时候执行
*/
@Override
public void destroy() throws Exception {
//关闭资源
if (bossGroup != null) {
bossGroup.shutdownGracefully();
}
if (workerGroup != null) {
workerGroup.shutdownGracefully();
}
}
}
- 服务端自定义处理器
/**
* 服务端自定义处理器
* 1.将标有@RpcService注解的bean进行缓存,方便后续查找调用
* 2.接收客户端的请求
* 3.根据传递过来的beanName从缓存中查找bean
* 4.通过反射调用bean的方法
* 5.给客户端响应
*/
@Component
@ChannelHandler.Sharable //设置通道共享,NettyServerHandler默认是单例的,不能由多个客户端所共享
public class NettyServerHandler extends SimpleChannelInboundHandler<String> implements ApplicationContextAware {
/**
* 定义缓存,将将标有@RpcService注解的bean缓存至此map
*/
static Map<String, Object> SERVICE_INSTANCE_MAP = new HashMap<>();
/**
* 1.将标有@RpcService的注解的bean进行缓存,实现ApplicationContextAware接口后重写的方法
* @param applicationContext ioc容器
*/
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
//1.1 通过注解获取bean的集合(可能有多个类都使用了这个注解)
Map<String, Object> serviceMap = applicationContext.getBeansWithAnnotation(RpcService.class);
//1.2 循环遍历
Set<Map.Entry<String, Object>> entries = serviceMap.entrySet();
for (Map.Entry<String, Object> entry : entries) {
//serviceBean就是自定义的UserServiceImpl的对象
Object serviceBean = entry.getValue();
if (serviceBean.getClass().getInterfaces().length == 0) {
throw new RuntimeException("对外暴露的服务必须实现接口");
}
//默认处理第一个接口作为缓存bean的名字(一个类可以实现多个接口)
String serviceName = serviceBean.getClass().getInterfaces()[0].getName();
SERVICE_INSTANCE_MAP.put(serviceName, serviceBean);
System.out.println(SERVICE_INSTANCE_MAP);
//{包名.IUserService=包名.UserServiceImpl@44c73c26}
}
}
/**
* 2.接收客户端的请求,通道读取就绪事件,读取客户端的消息
* @param ctx 每一个Handler实例与Pipeline之间的桥梁就是ChannelHandlerContext实例
* @param msg 客户端发送过来的消息,是一个JSON格式的RpcRequest的对象
*/
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
// 接收客户端的请求
RpcRequest rpcRequest = JSON.parseObject(msg, RpcRequest.class);
//创建响应对象,请求id是请求中传递过来的
RpcResponse rpcResponse = new RpcResponse();
rpcResponse.setRequestId(rpcRequest.getRequestId());
//调用自定义的业务处理方法(上述的3、4步),并将处理结果保存到响应中
try {
rpcResponse.setResult(handler(rpcRequest));
} catch (Exception e) {
e.printStackTrace();
//业务处理过程中出现的异常也要返回给客户端
rpcResponse.setError(e.getMessage());
}
//5. 将响应对象给客户端响应
ctx.writeAndFlush(JSON.toJSONString(rpcResponse));
}
/**
* 定义业务处理方法(上述的3、4步)
*/
private Object handler(RpcRequest rpcRequest) throws InvocationTargetException {
//3.根据传递过来的beanName从缓存中查找,className是请求对象中的属性
Object serviceBean = SERVICE_INSTANCE_MAP.get(rpcRequest.getClassName());
if (serviceBean == null) {
throw new RuntimeException("服务端没有找到服务");
}
// 4.通过反射调用bean的方法
// 调用cglid提供的FastClass中的方法,创建指定类的代理对象
FastClass proxyClass = FastClass.create(serviceBean.getClass());
//参数分别表示客户端调用的服务端的方法和参数类型,通过返回的method完成最终的方法调用
FastMethod method = proxyClass.getMethod(rpcRequest.getMethodName(), rpcRequest.getParameterTypes());
return method.invoke(serviceBean, rpcRequest.getParameters()); //反射
}
}
通过上述代码可知,必须加了自定义注解 @RpcService
的 bean 才会被缓存(服务才可以被调用),即自定义注解用来暴露服务,且服务端必须实现接口,约定协议,否则抛出异常。
- 服务端启动类
/**
* 项目启动的时候需要将服务端启动并监听端口
* 启动类实现CommandLineRunner接口
* CommandLineRunner接口是在容器启动成功后的最后一步回调(类似开机自启动)
*/
@SpringBootApplication
public class ServerBootstrapApplication implements CommandLineRunner {
/**
* 注入Netty服务端
*/
@Autowired
NettyRpcServer rpcServer;
public static void main(String[] args) {
SpringApplication.run(ServerBootstrapApplication.class, args);
}
/**
* 实现CommandLineRunner接口后要重写的方法,创建线程调用服务端的方法启动服务端
*/
@Override
public void run(String... args) throws Exception {
new Thread(new Runnable() {
@Override
public void run() {
rpcServer.start("127.0.0.1", 8899);
}
}).start();
}
}
流程总结:
5. 客户端代码
- 客户端业务处理类,向服务端发送数据并接收返回结果
@Component
public class NettyRpcClientHandler extends SimpleChannelInboundHandler<String> implements Callable {
ChannelHandlerContext context; //全局的ChannelHandlerContext对象
private String reqMsg; //发送的消息
private String respMsg; //接收的消息
//给发送的消息赋值
public void setReqMsg(String reqMsg) {
this.reqMsg = reqMsg;
}
/**
* 通道读取就绪事件,读取服务端消息
* @param msg 接收到的服务端数据
*/
@Override
protected synchronized void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
respMsg = msg;
//唤醒call方法的等待线程
notify();
}
/***
* 通道连接就绪事件,给全局的ChannelHandlerContext对象赋值
* 不能连接建立之后就给服务端发送消息,因为不知道要发送什么消息,客户端调用controller方法才可发送消息,所以实现Callable接口,调用call方法时才发送消息
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
context = ctx;
}
/***
* 使用线程的等待与唤醒完成消息的同步处理,因为netty仅支持异步,不能发送完消息然后等待服务端的消息返回。
*/
/**
* 实现Callable接口重写的方法
* 通过全局的ChannelHandlerContext对象给服务端发送消息,调用call方法才会发送
*/
@Override
public synchronized Object call() throws Exception {
context.writeAndFlush(reqMsg);
//客户端向服务端发送消息之后,要等待服务端的响应,调用本类的channelRead0方法读取服务端的数据
//将线程处于等待状态
wait();
return respMsg; //被唤醒之后,返回从服务端接收到的消息
}
}
- 客户端代码
/**
* Netty客户端
* 1.连接服务端,工程一启动就去连接服务端,实现InitializingBean接口
* 2.提供发送消息的方法
* 3.工程停止后关闭资源,实现DisposableBean接口
*/
@Component
public class NettyRpcClient implements InitializingBean, DisposableBean {
EventLoopGroup group = null;
Channel channel = null;
//注入自定义的客户端处理器
@Autowired
NettyRpcClientHandler nettyRpcClientHandler;
//创建线程池,用于执行自定义处理器中重写的call方法,向服务端发送消息
ExecutorService service = Executors.newCachedThreadPool();
/**
* 实现InitializingBean接口要重写的方法
* 1.连接服务端
*/
@Override
public void afterPropertiesSet() throws Exception {
try {
//1.1 创建线程组
group = new NioEventLoopGroup();
//1.2 创建客户端启动助手
Bootstrap bootstrap = new Bootstrap();
//1.3 设置参数
bootstrap.group(group)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
//添加编解码器
ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast(new StringEncoder());
//添加自定处理类
ch.pipeline().addLast(nettyRpcClientHandler);
}
});
//1.4 连接服务端
channel = bootstrap.connect("127.0.0.1", 8899).sync().channel();
} catch (Exception e) {
e.printStackTrace();
if (channel != null) {
channel.close();
}
if (group != null) {
group.shutdownGracefully();
}
}
}
/**
* 3.工程停止后关闭资源
* 实现DisposableBean接口要重写的方法
*/
@Override
public void destroy() throws Exception {
if (channel != null) {
channel.close();
}
if (group != null) {
group.shutdownGracefully();
}
}
/**
* 2. 消息发送的方法,调用自定义处理器中的方法定义发送的消息
* @param msg 要发送的消息
* @return 服务端返回的消息
*/
public Object send(String msg) throws ExecutionException, InterruptedException {
//调用自定义处理器的方法给要发送的消息赋值
nettyRpcClientHandler.setReqMsg(msg);
//执行自定义处理器中的call方法,向服务端发送消息
Future submit = service.submit(nettyRpcClientHandler);
//得到call方法的返回值,也就是服务端返回的消息
return submit.get();
}
}
- 公共接口IUserService的代理
调用IUserService
接口的getById()
方法时,自动的封装RpcRequest,不然每调用一次方法就手动封装一个RpcRequest对象,不合理。
而且不论其他的controller访问公共接口时,不用再去创建公共接口,直接访问缓存中的代理对象即可。
/**
* IUserService的代理
* 1. 发送消息之前将消息封装成RpcRequest对象,并发送消息
* 2. 解析服务器返回的数据
*/
@Component
public class RpcClientProxy {
@Autowired
NettyRpcClient rpcClient;
//缓存创建好的代理对象,方便复用
Map<Class, Object> SERVICE_PROXY = new HashMap<>();
/**
* 获取代理对象,参数表示获取谁的代理对象
*/
public Object getProxy(Class serviceClass) {
//从缓存中查找
Object proxy = SERVICE_PROXY.get(serviceClass);
if (proxy == null) {
//创建代理对象
//第二参数表示IUserService
proxy = Proxy.newProxyInstance(this.getClass().getClassLoader(),
new Class[]{serviceClass}, new InvocationHandler() {
//invoke方法不会被立即调用,只有访问接口的方法时才会调用
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//1.封装请求对象
RpcRequest rpcRequest = new RpcRequest();
rpcRequest.setRequestId(UUID.randomUUID().toString());
//method是接口的方法封装后的对象,getclass得到的就是接口,而服务端的缓存bean的名字就是接口的名字
rpcRequest.setClassName(method.getDeclaringClass().getName());
rpcRequest.setMethodName(method.getName());
rpcRequest.setParameterTypes(method.getParameterTypes());
rpcRequest.setParameters(args);
try {
//2.发送消息,返回值表示服务端返回的数据
Object msg = rpcClient.send(JSON.toJSONString(rpcRequest));
//3.将服务端的消息转化
RpcResponse rpcResponse = JSON.parseObject(msg.toString(), RpcResponse.class);
if (rpcResponse.getError() != null) {
throw new RuntimeException(rpcResponse.getError());
}
if (rpcResponse.getResult() != null) {
return JSON.parseObject(rpcResponse.getResult().toString(),
method.getReturnType());
}
return null;
} catch (Exception e) {
e.printStackTrace();
throw e;
}
}
});
//将代理对象放入缓存
SERVICE_PROXY.put(serviceClass, proxy);
return proxy;
} else {
return proxy;
}
}
}
- 引用代理类
/**
* 引用代理类,使用在controller的IUserService上
*/
@Target(ElementType.FIELD) //作用于字段
@Retention(RetentionPolicy.RUNTIME) //在运行时可以获取得到
public @interface RpcReference {
}
- 用户控制类
/**
* 用户控制类,远程调用服务端的getUserById方法
*/
@RestController
@RequestMapping("/user")
public class UserController {
//使用了注解之后,目前还不能使用代理对象,必须经过后续的处理才能将代理对象注入进来
@RpcReference
IUserService userService;
@RequestMapping("/getUserById")
public User getUserById(int id) {
//调用服务端方法
return userService.getById(id);
}
}
- bean的后置增强,将代理对象注入到使用了自定义的
@RpcReference
注解的字段上
类似于@Autowired
注解,使用这个注解之后将某个对象注入到这个字段上
/**
* bean的后置增强
*/
@Component
public class MyBeanPostProcessor implements BeanPostProcessor {
@Autowired
RpcClientProxy rpcClientProxy;
/**
* 自定义注解的注入
*/
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
//1.得到bean(u)中的所有字段
Field[] declaredFields = bean.getClass().getDeclaredFields();
//遍历字段
for (Field field : declaredFields) {
//2.查找字段中是否包含RpcReference这个注解
RpcReference annotation = field.getAnnotation(RpcReference.class);
if (annotation != null) {
//3.获取代理对象
Object proxy = rpcClientProxy.getProxy(field.getType());
try {
//4.属性注入,field表示加了RpcReference注解的IUserService对象
field.setAccessible(true);
field.set(bean, proxy); //userService由代理对象创建
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
return bean;
}
}
流程总结:
运行结果:
将服务端先启动,然后启动客户端,进行访问: