什么是RPC
RPC全称为remote procedure call,即远程过程调用。
借助RPC可以做到像本地调用一样调用远程服务,是一种进程间的通信方式
比如两台服务器A和B,A服务器上部署一个应用,B服务器上部署一个应用,A服务器上的应用想调用B服务器上的应用提供的方法,由于两个应用不在一个内存空间,不能直接调用,所以需要通过网络来表达调用的语义和传达调用的数据。
需要注意的是RPC并不是一个具体的技术,而是指整个网络远程调用过程
RPC架构
一个完整的RPC架构里面包含了四个核心的组件,分别是Client,Client Stub,Server以及Server Stub,这个Stub可以理解为存根。
- 客户端(Client),服务的调用方。
- 客户端存根(Client
Stub),存放服务端的地址消息,再将客户端的请求参数打包成网络消息,然后通过网络远 程发送给服务方。 - 服务端(Server),真正的服务提供者。
- 服务端存根(Server Stub),接收客户端发送过来的消息,将消息解包,并调用本地的方法。
RPC调用过程
(1) 客户端(client)以本地调用方式(即以接口的方式)调用服务;
(2) 客户端存根(client stub)接收到调用后,负责将方法、参数等组装成能够进行网络传输的消息体(将消息体对
象序列化为二进制);
(3) 客户端通过sockets将消息发送到服务端;
(4) 服务端存根( server stub)收到消息后进行解码(将消息对象反序列化);
(5) 服务端存根( server stub)根据解码结果调用本地的服务;
(6) 本地服务执行并将结果返回给服务端存根( server stub);
(7) 服务端存根( server stub)将返回结果打包成消息(将结果消息对象序列化);
(8) 服务端(server)通过sockets将消息发送到客户端;
(9) 客户端存根(client stub)接收到结果消息,并进行解码(将结果消息发序列化);
(10) 客户端(client)得到最终结果。
RPC的目标是要把2、3、4、7、8、9这些步骤都封装起来。
注意:无论是何种类型的数据,最终都需要转换成二进制流在网络上进行传输,数据的发送方需要将对象转换为二
进制流,而数据的接收方则需要把二进制流再恢复为对象。
在java中RPC框架比较多,常见的有Hessian、gRPC、Thri、HSF (High Speed Service Framework)、Dubbo 等,其实对于RPC框架而言,核心模块 就是通讯和序列化。
基于Netty自定义RPC
RPC又称远程过程调用,我们所知的远程调用分为两种,现在在服务间通信的方式也基本以这两种为主
1.是基于HTTP的restful形式的广义远程调用,以spring could的feign和restTemplate为代表,采用的协议是HTTP的7层调用协议,并且协议的参数和响应序列化基本以JSON格式和XML格式为主。
2.是基于TCP的狭义的RPC远程调用,以阿里的Dubbo为代表,主要通过netty来实现4层网络协议,NIO来异步传输,序列化也可以是JSON或者hessian2以及java自带的序列化等,可以配置。
接下来我们主要以第二种的RPC远程调用来自己实现
需求
案例版本:
server端与client端定义共用的接口,server端需要实现该接口。客户端通过jdk的proxy动态代理生成对象,在方法调用时执行invoke中的代码。invoke中使用netty与server端通信,我们在这里传输(类名#方法名)的字符串。server端收到数据后,根据类名#方法名去调用实现类并通过netty返回数据。
要求完成改造版本:
序列化协议修改为JSON,使用fastjson作为JSON框架,并将RpcRequest实体作为通信载体,服务端需根据客户端传递过来的RpcRequest对象通过反射,动态代理等技术,最终能够执行目标方法,返回字符串"success"。
要点提示:
(1)客户端代理的invoke方法中需封装RpcRequest对象,将其当做参数进行传递。
(2)服务端的UserServiceImpl类上添加@Service注解,在启动项目时,添加到容器中。
(3)服务端要添加@SpringBootApplication注解,main方法中添加。SpringApplication.run(ServerBootstrap.class, args);,进行启动扫描(注意项目启动类位置:扫描路径)。
(4)服务端在收到参数,可以借助反射及动态代理(如需用到ApplicationContext对象,可以借助实现ApplicationContextAware接口获取),来调用UserServiceImpl方法,最终向客户端返回”success“即可。
(5)既然传递的是RpcRequest对象了,那么客户端的编码器与服务端的解码器需重新设置。
实现
1.分析需求
- 原始版本使用serviceName#methodName#组合作为待调用的类名#方法名,我们需要使用RpcRequest类来封装请求,封装待调用的类、方法、参数。
- 使用fastjson作序列化
2.修改客户端,封装RpcRequest
//2)封装RpcRequest,解析providerParam
RpcRequest rpcRequest= new RpcRequest();
rpcRequest.setClassName(providerParam.split("#")[0]);
rpcRequest.setMethodName(providerParam.split("#")[1]);
rpcRequest.setParameters(objects);
rpcRequest.setParameterTypes(method.getParameterTypes());
3.修改客户端,修改UserClientHandler的 param参数 由String类型改为RpcRequest,并修改客户端的编码规则
//4)配置启动引导对象
bootstrap.group(group)
//设置通道为NIO
.channel(NioSocketChannel.class)
//设置请求协议为TCP
.option(ChannelOption.TCP_NODELAY,true)
//监听channel 并初始化
.handler(new ChannelInitializer<SocketChannel>() {
protected void initChannel(SocketChannel socketChannel) throws Exception {
//获取ChannelPipeline
ChannelPipeline pipeline = socketChannel.pipeline();
//设置编码
//pipeline.addLast(new StringEncoder());
pipeline.addLast( new RpcEncoder(RpcRequest.class, new JSONSerializer()));
pipeline.addLast(new StringDecoder());
//添加自定义事件处理器
pipeline.addLast(userClientHandler);
}
});
3.修改服务端,我们需要添加一个解码器
package com.lagou.entity;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import io.netty.handler.codec.MessageToByteEncoder;
import io.netty.handler.codec.MessageToMessageDecoder;
import java.nio.charset.Charset;
import java.util.List;
public class RpcDecoder extends ByteToMessageDecoder {
private Class<?> clazz;
private Serializer serializer;
public RpcDecoder(Class<?> clazz, Serializer serializer) {
this.clazz = clazz;
this.serializer = serializer;
}
@Override
protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) throws Exception {
if (clazz != null ) {
int lenght=byteBuf.readableBytes();
if (lenght < 1) return;
//读取长度
int len = byteBuf.readInt();
//读取请求bytes
byte[] bytes = new byte[len];
byteBuf.getBytes(4, bytes);
System.out.println(new String(bytes, Charset.forName("UTF-8")));
//使用fastjson转换
Object o=serializer.deserialize(clazz,bytes);
list.add(o);
byteBuf.clear();
}
}
}
4.修改服务端,添加springboot容器,将 UserServiceHandler、UserServiceImpl都加入到容器中进行管理,UserServiceHandler如果为单例的话会报错,所有为其设置@Scope(“prototype”)。其次使用ApplicationContext获取Request中的待调用的对象,进行调用
@Scope("prototype")
@Component
public class UserServiceHandler extends ChannelInboundHandlerAdapter implements ApplicationContextAware {
private ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
//当客户端读取数据时,该方法会被调用
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//注意: 客户端将来发送请求的时候会传递一个参数: UserService#sayHello#are you ok
//1.判断当前的请求是否符合规则
RpcRequest rpcRequest = (com.lagou.entity.RpcRequest) msg;
//获取对象;
Object e=applicationContext.getBean(rpcRequest.getClassName());
//requestClass.
Class requestClass=e.getClass();
//requestMethod.
Method method = requestClass.getMethod(rpcRequest.getMethodName(),rpcRequest.getParameterTypes());
//调用方法
Object result= method.invoke(e,rpcRequest.getParameters());
System.out.println("直接反射:"+result);
//返回success
ctx.writeAndFlush("success");
}
}
5.启动Consumer与provider即可看到结果,其次Netty有必要仔细学习一下,参考文章ByteBuf详解