什么是RPC? RPC(Remote Procedure Call)即远程过程调用,简单的理解是一个节点请求另一个节点提供的服务,本地过程调用通常是指直接的使用当前程序下的一个方法,而RPC指的是调用远程的不在本机的程序的方法,使用这些方法就好像是在使用本机方法一样,如通常在网络通信时我们有调用远程服务器的方法的需求。
比较正式的描述是:一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议
RPC的优点:
RPC的缺点:
其中的代码是根据我们前面所设计的MyMessage着手,在这里只做简单的介绍。
MyMessage是所有消息类型的父类
AbstractResponseMessage是所有响应消息的父类,同样继承了MyMessage
1.首先是Rpc调用的请求消息体,就是我的客户端需要远程调用服务器的方法,所以请求消息体里面需要有对应方法的类名、方法名、参数等……,且在服务器里面要根据这些请求消息里的信息用反射进行调用。(这两个消息体在客户端和服务器都需要有)
public class RpcRequestMessage extends MyMessage{
/**
* 调用的接口全限定名
*/
private String interfaceName;
/**
* 需要调用的方法名
*/
private String methodName;
/**
* 方法返回值类型
*/
private Class> returnType;
/**
* 方法各参数的类型数组
*/
private Class>[] parameterTypes;
/**
* 方法的参数值数组
*/
private Object[] parameterValues;
public RpcRequestMessage(String interfaceName, String methodName, Class> returnType, Class>[] parameterTypes, Object[] parameterValues) {
this.messageType=getMessageType();
this.interfaceName = interfaceName;
this.methodName = methodName;
this.returnType = returnType;
this.parameterTypes = parameterTypes;
this.parameterValues = parameterValues;
}
@Override
public int getMessageType() {
return MessageType.RpcRequestMessage;
}
}
Gson不支持Class类型的解析,所以使用Gson解析Class类型为字符串时要自定义编解码器!强烈建议使用Jackson,Gson坑太多
2.其次是Rpc响应信息体,服务器调用了对应的方法,需要给客户端返回返回值和异常信息。
public class RpcResponseMessage extends AbstractResponseMessage{
/**
* Rpc调用返回值
*/
private Object returnValue;
/**
* Rpc调用异常信息
*/
private Exception exceptionValue;
public RpcResponseMessage(Object returnValue, Exception exceptionValue) {
this.messageType=getMessageType();
this.returnValue = returnValue;
this.exceptionValue = exceptionValue;
}
@Override
public int getMessageType() {
return MessageType.RpcResponseMessage;
}
}
3.在Netty服务器这边添加监听Rpc请求的Handler
这个Handler的代码部分就是关键了,这里我先写死!
@ChannelHandler.Sharable
public class RpcRequestMessageHandler extends SimpleChannelInboundHandler {
@Override
protected void channelRead0(ChannelHandlerContext ctx, RpcRequestMessage msg) {
RpcResponseMessage res = new RpcResponseMessage(null, null);
try {
//这里先写死
if ("com.lxc.chatsystem.service.FriendsService".equals(msg.getInterfaceName())) {
//获得该接口的实现类对象
FriendsService service = FriendsServiceFactory.getFriendsService();
//反射调用
Method method = service.getClass().getMethod(msg.getMethodName(), msg.getParameterTypes());
//得到返回结果
Object result = method.invoke(service,msg.getParameterValues());
System.out.println(result);
res.setReturnValue(result);
} else {
System.out.println("错误");
}
} catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
e.printStackTrace();
//出异常的时候,要返回异常
res.setExceptionValue(e);
}
//返回响应对象
ctx.writeAndFlush(res);
}
}
4.同样的在Netty客户端这边加入Rpc调用的响应消息处理器,接收返回值和异常信息
打印出结果和异常即可
public class RpcResponseMessageHandler extends SimpleChannelInboundHandler {
@Override
protected void channelRead0(ChannelHandlerContext ctx, RpcResponseMessage msg) throws Exception {
Object returnValue = msg.getReturnValue();
Exception exceptionValue = msg.getExceptionValue();
System.out.println(returnValue);
System.out.println(exceptionValue);
}
}
5.测试
使用我的服务器中的一个方法进行测试,service接口代码如下(接口的实现类忽略):
现在客户端想要使用该方法,我们就需要给服务器发送Rpc请求了,请求信息包含了我要调用服务器方法的一切信息:
结果正确,客户端正常执行到了服务端的方法,并得到了返回值。
刚刚我们只是验证了客户端和服务器的通信部分,且方法也是写死了的,以后我们要想调用各种各样的方法当然是不行的,所以现在才真正的开始写RPC了。
我们的服务器和客户端日后肯定不在一个地方,但是我们的客户端需要方便的使用到服务器的方法,所以我们最起码也要让服务器和客户端的接口相同(也可以使用命令的方式调用,不需要同步接口),如下,我的客户端扣了服务器的接口。
客户端有了接口以后,就相当于有了一个模板,但是还是缺少实现类,为了能够像运行本机方法一样的运行服务器方法,我们现在还需要实现类,但是实现类当然不能扣服务器的,这样也就没有意义了,我们这时候就需要用到动态代理了,把这个复杂的方法调用过程给屏蔽起来。代码如下:
public class RpcProxyService {
/**
* 根据接口返回一个实现类,该实现类所需要做的就是发送对应的方法的Rpc请求给服务器
* @param serviceClass 接口名
* @return 代理实现类
*/
public static T getProxyService(Class serviceClass) {
ClassLoader classLoader = serviceClass.getClassLoader();
Class>[] interfaces = {serviceClass};
//创建代理对象
Proxy proxy = (Proxy) Proxy.newProxyInstance(classLoader, interfaces, new InvocationHandler() {
public Object invoke(Object o, Method method, Object[] args) throws Throwable {
//1.将方法调用的逻辑转为 发送rpc消息,这也是代理的重点
RpcRequestMessage rpcMessage = new RpcRequestMessage(serviceClass.getName(),
method.getName(),
method.getReturnType(),
method.getParameterTypes(),
args
);
//设置消息顺序号
rpcMessage.setSequenceId(SequenceIdGenerator.nextId());
//2.发送给服务器,我奥调用你的某个方法了
AppInfo.clientChannel.writeAndFlush(rpcMessage);
//3.准备一个promise用于接收结果,并放进存放结果的容器里等待 指定promise对象异步接收结果的线程
DefaultPromise
取顺序号代码如下,如果需要考虑分布式可以使用雪花算法,这里只是单机的序列号生成
public abstract class SequenceIdGenerator {
private static final AtomicInteger id=new AtomicInteger();
public static int nextId(){
return id.getAndIncrement();
}
}
Promise结果容器代码如下
public class PromiseContainer {
/**
* 用于存放rpc的结果
* Integer的键表示对应的序列号,
* Promise就是多线程之间传递结果的对象
*/
public static final Map> PROMISES_MAP =new ConcurrentHashMap<>();
}
服务器这边对应的handler如下,它的职责是接收到客户端的rpc请求,调用我们服务器的对应的方法,并返回结果
@ChannelHandler.Sharable
public class RpcRequestMessageHandler extends SimpleChannelInboundHandler {
@Override
protected void channelRead0(ChannelHandlerContext ctx, RpcRequestMessage msg) {
//1.准备好返回值
RpcResponseMessage res = new RpcResponseMessage(null, null);
//2.设置相同的序列号
res.setSequenceId(msg.getSequenceId());
try {
//3.根据客户端传过来的接口名称字符串得到服务器的对应接口的Class类型,可以使用spring管理更方便
Class> serviceClass = ServiceFactory.getServiceClass(msg.getInterfaceName());
//4.反射调用对应的方法
Method method = serviceClass.getMethod(msg.getMethodName(), msg.getParameterTypes());
Object returnValue = method.invoke(serviceClass.newInstance(), msg.getParameterValues());
//5.设置返回值
System.out.println(returnValue);
res.setReturnValue(returnValue);
} catch (Exception e) {
e.printStackTrace();
//出异常的时候,要返回异常
res.setExceptionValue(new RuntimeException("远程调用出错:" + e.getCause().getMessage()));
}
//返回响应对象给客户端
ctx.writeAndFlush(res);
}
}
其中的ServiceFactory如下:这里我们因为没有使用spring所有就根据业务自己写了个方法。用这种方法的话,客户端的接口和服务器的接口就不需要相同的路径,只需要最后一个接口名相同就可以识别出来。
public class ServiceFactory {
public static Class> getServiceClass(String serviceName){
int index = serviceName.lastIndexOf('.');
String simpleName = serviceName.substring(index + 1);
switch (simpleName) {
case "PersonService":
return PersonServiceImpl.class;
case "FriendsService":
return FriendsServiceImpl.class;
default:
throw new RuntimeException("没有此类型");
}
}
}
现在我们就可以在客户端里发送rpc请求了。
如上是一个客户端请求rpc的代码片段,可以看出客户端已经完全对远程调用透明了,所有的事情都是代理类来帮我们做的(动态代理真是个好东西)。