Netty RPC的实现

概述 

        什么是RPC? RPC(Remote Procedure Call)即远程过程调用,简单的理解是一个节点请求另一个节点提供的服务,本地过程调用通常是指直接的使用当前程序下的一个方法,而RPC指的是调用远程的不在本机的程序的方法,使用这些方法就好像是在使用本机方法一样,如通常在网络通信时我们有调用远程服务器的方法的需求。

比较正式的描述是:一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议

RPC的优点:

  • 提升系统可扩展性,随着业务的增大,可以在新增的服务器上调用原来的接口
  • 提升系统可维护性和持续交付能力
  • 实现系统高可用

RPC的缺点:

  • RPC框架调用成功率受限于网络状况
  • 复杂,开发难度大

file

简单代码理解

其中的代码是根据我们前面所设计的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<RpcRequestMessage> {
    @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<RpcResponseMessage> {
    @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设计

刚刚我们只是验证了客户端和服务器的通信部分,且方法也是写死了的,以后我们要想调用各种各样的方法当然是不行的,所以现在才真正的开始写RPC了。

1.同步接口

我们的服务器和客户端日后肯定不在一个地方,但是我们的客户端需要方便的使用到服务器的方法,所以我们最起码也要让服务器和客户端的接口相同(也可以使用命令的方式调用,不需要同步接口),如下,我的客户端扣了服务器的接口。

 2.创建代理

客户端有了接口以后,就相当于有了一个模板,但是还是缺少实现类,为了能够像运行本机方法一样的运行服务器方法,我们现在还需要实现类,但是实现类当然不能扣服务器的,这样也就没有意义了,我们这时候就需要用到动态代理了,把这个复杂的方法调用过程给屏蔽起来。代码如下:        

public class RpcProxyService {
    /**
     * 根据接口返回一个实现类,该实现类所需要做的就是发送对应的方法的Rpc请求给服务器
     * @param serviceClass 接口名
     * @return 代理实现类
     */
    public static <T> T getProxyService(Class<T> 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<Object> promise = new DefaultPromise<>(AppInfo.clientChannel.eventLoop());
                PromiseContainer.PROMISES_MAP.put(rpcMessage.getSequenceId(), promise);
                //4.同步等待promise的结果……等待RpcResponseMessageHandler唤醒,最多等待5s
                promise.await(5000);
                //5.得到结果
                if (promise.isSuccess()) {
                    //调用正常
                    return promise.getNow();
                } else {
                    //调用出现异常
                    throw new RuntimeException(promise.cause());
                }
            }
        });
        //返回代理对象
        return (T) proxy;
    }
}

取顺序号代码如下,如果需要考虑分布式可以使用雪花算法,这里只是单机的序列号生成

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<Integer, Promise<Object>> PROMISES_MAP =new ConcurrentHashMap<>();
}

3. 服务器接收和返回结果

服务器这边对应的handler如下,它的职责是接收到客户端的rpc请求,调用我们服务器的对应的方法,并返回结果

@ChannelHandler.Sharable
public class RpcRequestMessageHandler extends SimpleChannelInboundHandler<RpcRequestMessage> {
    @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("没有此类型");
        }
    }
}

4.测试

现在我们就可以在客户端里发送rpc请求了。 

如上是一个客户端请求rpc的代码片段,可以看出客户端已经完全对远程调用透明了,所有的事情都是代理类来帮我们做的(动态代理真是个好东西)。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值