百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

由浅入深让你搞透RPC,不要让框架遮住你的眼

ccwgpt 2024-09-17 12:49 36 浏览 0 评论

HTTP经常接触,大家也不陌生,这是一个超文本传输协议,能够在网络直接传输数据。目前微服务项目很火,微服务之间基本都是使用HTTP传输,例如Feign,OkHttp,RestTemlpate等等。但是分布式这个话题目前还不过时,今天在这里说下分布式的基石:RPC(Remote Procedure Call:远程过程调用)技术,在Java中称为RMI( Remote Method Invocation ,远程方法调用)。白话解释下:该技术就是让使用者在调用一个服务时的时候无感知地调用一个远程服务。这种无感知让大部分开发者不需要了解技术本身就可以很容易的使用 ,但是框架封装了底层(各种抽象、分层),使得无法知道内部是如何运行, 下面来介绍下什么是RPC

写文章不能千篇一律,专业名字一大堆,各种技术天上飞。我们一步步来,由浅入深。

存在这么一个方法

public class UserService {

    public String findNameById(Long id) {
        return "my name " + id;
    }
}

正常调用

public class Test {

    public static void main(String[] args) {
        UserService userService = new UserService();
        String name = userService.findNameById(1L);
        System.out.println(name);
    }

}

控制台

my name 1

Process finished with exit code 0

这里不涉及框架,只是基本功能。到这里方法也调用了,功能也实现了。

方法执行核心

但是,某一天领导告诉你,你写的UserService没有用,具体的处理流程是另一个人管的,而且另一个人不和你一个项目,他(提供者)写好了一个实现

public class OtherUserService {

    public String findNameById(Long id) {
        return "other name " + id ;
    }
}

此时你一想,他在大西洋,我怎么去调用他写的方法?于是RPC便可以出手了,RPC能干嘛?RPC能顺着网线去找到他。且看RPC如何做

先说方法调用: 正常情况下是通过实例对象+点操作符+方法名称调用

userService.findNameById(1L);

但是一个远程服务,是拿不到实例对象的,也就不能去写一个点操作符去调用,这时候方法的调用还可以通过反射

public Object invoke(Object obj, Object... args){...}
// 第一个参数为 实例对象,第二个对象为 参数列表

但是Method这个也只能通过反射来获取,

 public Method getMethod(String name, Class<?>... parameterTypes)
// 第一个参数为 方法名称,第二个参数为参数类型列表

这样以来,方法的调用便可以分解为

  • 获取类的Method实例(需要类标识、方法名称(标识)、方法参数类型列表)
  • 通过Method实例调用方法(需要类实例对象、参数列表)

其中通过实例是可以获取其类型的,instance->class,这里的参数列表便可以获取对应参数类型列表,类实例对象可以获取类Class,但是这是在同一个JVM中才能完成的,一旦分离开来,这些都不能获取,而且分离时类实例对象只有提供者具有,使用者是不知道的(如果知道就没必要去远程调用了)。

所以:远程执行一个方法最少需要4个条件:类标识、方法标识、方法参数类型列表、方法参数列表。拿到这些条件,提供方就可以找到实例对象、调用对应的方法了,而且为了不随意构造实例对象,提供者会主动控制实例对象的构建,之后需要放到一个地方以方便交给Method的来调用

所以基本功能示意图如下,描述为:顺着网线,带着四大金刚,直接找到提供方,围殴一顿后,把结果带回来给使用者

RPC基本示意图

RPC就是对以上功能的完善与封装,好吧,上面描述太粗鲁了,代码写的要优雅!!!,要符合设计原则

接口分离原则

使用方和提供方是两个不同的模块,模块与模块之间应该是通过接口解耦的。所以一般在使用RPC服务时都会把接口层给解耦出来,作为第三方jar给使用方和提供方使用,也便于锁定实例对象。

单一职责原则

一个功能就应该职责单一,不仅容易看懂,也便于后续重构。RPC其实是多个功能模块组合起来的。例如:使用方参数封装模块、数据传输模块职责、数据序列化模块、提供方管理类实例模块等。一个完善的RPC功能组件,其职责也会分的越细,每个模块功能越单一。

开放封闭原则

作为一个RPC,其设计应该是满足开放封闭原则的,每个模块都应该是可扩展的,但是RPC流程应该是对内封闭的。

里氏替换原则

构建实例对象的时候,其类型定义应该是接口类型,而不是具体的实现类型。

依赖倒置原则

高层模块不应该依赖低层模块,二者都应该依赖其抽象对象-->抽象不应该依赖细节,细节应该依赖抽象-->应该针对接口编程,不应该针对实现编程。

RPC简单设计

先定义接口


接口应该单独一个包,作为jar对外提供。使用方和提供方都依赖该jar

public interface IUserService {
    /**
     * 通过ID获取用户名
     *
     * @param id 用户ID
     * @return java.lang.String
     * @author Tinyice
     */
    String findNameById(Long id);
}

提供方实现


简单实现

public class OtherUserService implements IUserService {
    @Override
    public String findNameById(Long id) {
        return "other name " + id ;
    }
}

使用方请求参数封装


主要封装四大调用条件对象

@Getter
@Setter
public class RpcRequest implements java.io.Serializable {

    private static final long serialVersionUID = -7223166969378743326L;

    /**
     * 类名称
     */
    private String className;

    /**
     * 方法名称
     */
    private String methodName;

    /**
     * 参数类型列表
     */
    private Class<?>[] parameterTypes;

    /**
     * 参数列表
     */
    private Object[] parameters;
}

网络传输工具封装


网络传输方式有很多种,这里使用socket

@Slf4j
public class TcpTransport {

    private String host;

    private int port;

    public TcpTransport(String host, int port) {
        this.host = host;
        this.port = port;
    }

    public Socket newSocketInstance() {
        Socket socket;
        try {
            socket = new Socket(host, port);
            log.info("[{}] 客户端新建连接:服务端地址=【{}】",LocalDateTime.now(), socket.getRemoteSocketAddress());
            return socket;
        } catch (IOException e) {
            e.printStackTrace();
            throw new RuntimeException("客户端连接失败");
        }

    }

    public Object send(RpcRequest rpcRequest) {
        Socket socket = null;
        try {
            socket = newSocketInstance();
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());
            objectOutputStream.writeObject(rpcRequest);
            objectOutputStream.flush();
            log.info("[{}] 请求参数序列化完毕",LocalDateTime.now());

            ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());
            Object obj = objectInputStream.readObject();
            objectOutputStream.close();
            objectInputStream.close();
            log.info("[{}] 请求结果接收完毕",LocalDateTime.now());
            return obj;
        } catch (Exception e) {
            throw new RuntimeException("RPC 调用异常");
        } finally {
            try {
                if (socket != null) {
                    socket.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }

        }
    }
}

其中send方法就是数据传输方法,包含请求参数的传输和请求结果的接收。

网络请求发起


为了让使用方感知不到具体实现的位置(本地还是远程),一般都会使用代理技术给接口构建一个本地代理对象,通过代理对象来进行远程调用,使得使用者产生使用的是本地方法的错觉。。

动态代理,主要分为JDK动态代理和CGLIB动态代理,不懂原理的可以去看我写的文章。为了简单,这里使用JDK动态代理

public class RpcClientProxy {

    public <T> T clientProxy(final Class<T> interfaces, final String host, final int port) {

        return (T) Proxy.newProxyInstance(interfaces.getClassLoader(), new Class[]{interfaces}, new RpcInvocationHandler(host, port));
    }
}

RpcInvocationHandler为处理代理流程的类,也是网络请求发起类,发起位置在invoke方法,这是JDK动态代理的核心

@Slf4j
public class RpcInvocationHandler implements InvocationHandler {

    private String host;

    private int port;

    public RpcInvocationHandler(String host, int port) {
        this.host = host;
        this.port = port;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) {

        log.info("客户端开始封装参数【RpcRequest】");
        RpcRequest rpcRequest = new RpcRequest();
        rpcRequest.setClassName(method.getDeclaringClass().getName());
        rpcRequest.setMethodName(method.getName());
        rpcRequest.setParameterTypes(method.getParameterTypes());
        rpcRequest.setParameters(args);

        log.info("客户端开始获取传输工具【tcpTransport】");
        TcpTransport tcpTransport = new TcpTransport(host, port);

        log.info("客户端开始发送请求参数【RpcRequest】");
        return tcpTransport.send(rpcRequest);
    }
}

知道JDK动态代理,也就指定invoke对java方法调用的意义:方法的真正执行流程。这里封装了请求参数、然后发起网络请求,获取到网络请求结果。这个过程替代了方法的执行过程。

服务端接口监听


客户端和服务端是需要网络通信的,这边也要建立网络监听,这里为了简单,将类实例对象存储功能放在了一起

@Slf4j
public class RpcServer {

    private ExecutorService executorService=Executors.newCachedThreadPool();

    public void  publishServer(final Map<String,Object> serviceRegistCenter, int port){
        ServerSocket serverSocket;
        try {
            serverSocket=new ServerSocket(port);
            log.info("服务端启动监听,端口=[{}]",port);
            while (true) {
                Socket socket=serverSocket.accept();
                log.info("服务端监听到新链接,客户端地址=【{}】",socket.getRemoteSocketAddress());
                executorService.execute(new RpcServerProcessor(socket,serviceRegistCenter));
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

服务端方法调用


上面的RpcServerProcessor就是服务端对客户端请求的处理方法

@Slf4j
public class RpcServerProcessor implements Runnable {

    private Socket socket;

    private Map<String, Object> serviceRegistCenter;

    public RpcServerProcessor(Socket socket, Map<String, Object> serviceRegistCenter) {
        this.socket = socket;
        this.serviceRegistCenter = serviceRegistCenter;
    }

    @Override
    public void run() {
        //  获取客户端传输对象,并反序列化
        try (ObjectInputStream inputStream = new ObjectInputStream(socket.getInputStream());
             ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream())) {
            RpcRequest rpcRequest = (RpcRequest) inputStream.readObject();
            log.info("[{}] 服务端反序列化完毕", LocalDateTime.now());
            Object obj = invoke(rpcRequest);
            log.info("[{}] 服务端调用完毕", LocalDateTime.now());
            objectOutputStream.writeObject(obj);
            objectOutputStream.flush();
            log.info("[{}] 服务端序列化完毕——将结果发送给客户端", LocalDateTime.now());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private Object loadBalance(String className) {
        return serviceRegistCenter.get(className);
    }

    private Object invoke(RpcRequest rpcRequest) throws Exception {
        Object[] args = rpcRequest.getParameters();
        Class<?>[] types = rpcRequest.getParameterTypes();
        String className = rpcRequest.getClassName();
        // 服务发现,负载均衡
        Object service = loadBalance(className);
        // 服务调用
        Method method = service.getClass().getMethod(rpcRequest.getMethodName(), types);
        return method.invoke(service, args);

    }
}

这里也有个invoke方法,就是刚开始分析的反射方法调用,了解这块核心也就指定了RPC如何执行远程方法的。这里也涉及到了服务发现与负载均衡。

功能测试

先启动服务端,服务端发布服务

@Slf4j
public class ServerBootStrap {

    public static void main(String[] args) {
        // 注册中心
        Map<String, Object> registCenter = new HashMap<>(16, 1);
        // 实例构建
        IUserService userService = new OtherUserService();
        // 实例注册
        registCenter.put(IUserService.class.getName(), userService);
        RpcServer rpcServer = new RpcServer();
        log.info("[{}] 服务端发布对外服务【IUserService】", LocalDateTime.now());
        rpcServer.publishServer(registCenter, 8888);
    }
}

控制台

[2020-09-25T20:29:40.699] 服务端发布对外服务【IUserService】
服务端启动监听,端口=[8888]

客户端调用

@Slf4j
public class ClientBootStrap {

    public static void main(String[] args) {
        RpcClientProxy proxy = new RpcClientProxy();
        IUserService userService = proxy.clientProxy(IUserService.class, "localhost", 8888);
        log.info("[{}] 客户端开始发起RPC调用", LocalDateTime.now());
        String name = userService.findNameById(101L);
        System.out.println(name);

    }
}

客户端控制台

[2020-09-25T20:29:55.737] 客户端开始发起RPC调用
客户端开始封装参数【RpcRequest】
客户端开始获取传输工具【tcpTransport】
客户端开始发送请求参数【RpcRequest】
[2020-09-25T20:29:55.748] 客户端新建连接:服务端地址=【localhost/127.0.0.1:8888】
[2020-09-25T20:29:55.763] 请求参数序列化完毕
[2020-09-25T20:29:55.769] 请求结果接收完毕
other name 101

Process finished with exit code 0

服务端控制台

[2020-09-25T20:29:40.699] 服务端发布对外服务【IUserService】
服务端启动监听,端口=[8888]
服务端监听到新链接,客户端地址=【/127.0.0.1:29456】
[2020-09-25T20:29:55.766] 服务端反序列化完毕
[2020-09-25T20:29:55.767] 服务端调用完毕
[2020-09-25T20:29:55.767] 服务端序列化完毕——将结果发送给客户端

可以通过时间顺序观察下调用流程。

总结

以上示例说明了RPC调用流程:包括服务注册、服务发现、负载均衡、请求参数封装、数据传输、序列化与反序列化等等,这里为了简单示例,只是做了演示。一个完善的RPC也是对上面功能的完善。例如:

服务注册可以选择JVM、Redis、Zookeeper、Nacos等

负载均衡:随机负载、轮询负载、权重负载等

数据传输:可以定制传输协议,例如dubbo、http、tcp/ip等

序列化:开源的序列化工具也很多,选择适宜的序列化能提高RPC能力,常见的有:JDK序列化、Hessian、Hessian2、Kryo、protostuff等

其他优化:服务配置、重试策略、失败拒绝策略、版本控制、权限控制等等。

在与Spring Boot集成中,服务客户端调用对象的代理可以和Sping的代理配合,实现无缝连接,直接注入使用。

原文链接:https://my.oschina.net/u/4090547/blog/4650965?_from=gitee_search

如果觉得本文对你有帮助,可以转发关注支持一下

相关推荐

机器学习框架TensorFlow入门(tensorflow框架详解)

ensorFlow是一个广泛使用的开源机器学习框架,由GoogleBrain团队开发。它支持广泛的机器学习和深度学习任务,并且可以在CPU和GPU上运行。下面是一个使用TensorF...

合肥高新区企业本源发布量子机器学习框架VQNet 开辟量子机器学习的新领域

近日,高新区企业合肥本源量子计算科技有限责任公司通过研究混合实现变分量子算法和经典机器学习框架的可能性,全新开发了量子机器学习框架VQNet,可满足构建所有类型的量子机器学习算法,实现量子-经典混合任...

如何使用 TensorFlow 构建机器学习模型

在这篇文章中,我将逐步讲解如何使用TensorFlow创建一个简单的机器学习模型。TensorFlow是一个由谷歌开发的库,并在2015年开源,它能使构建和训练机器学习模型变得简单。我们接下...

机器学习框架底层揭秘:PyTorch、TensorFlow 如何高效“跑模型”

在使用PyTorch或TensorFlow时,你是否想过:这些深度学习框架底层到底是怎么运行的?为什么我们一行.backward()就能自动计算梯度?本篇将用最简单的语言,拆解几个关键概念...

2 个月的面试亲身经历告诉大家,如何进入 BAT 等大厂?

这篇文章主要是从项目来讲的,所以,从以下几个方面展开。怎么介绍项目?怎么介绍项目难点与亮点?你负责的模块?怎么让面试官满意?怎么介绍项目?我在刚刚开始面试的时候,也遇到了这个问题,也是我第一个思考的问...

基于SpringBoot 的CMS系统,拿去开发企业官网真香(附源码)

前言推荐这个项目是因为使用手册部署手册非常完善,项目也有开发教程视频对小白非常贴心,接私活可以直接拿去二开非常舒服开源说明系统100%开源模块化开发模式,铭飞所开发的模块都发布到了maven中央库。可...

【网络安全】关于Apache Shiro权限绕过高危漏洞的 预警通报

近日,国家信息安全漏洞共享平台(CNVD)公布了深信服终端检测平台(EDR)远程命令执行高危漏洞,攻击者利用该漏洞可远程执行系统命令,获得目标服务器的权限。一、漏洞情况ApacheShiro是一个强...

开发企业官网就用这个基于SpringBoot的CMS系统,真香

前言推荐这个项目是因为使用手册部署手册非常完善,项目也有开发教程视频对小白非常贴心,接私活可以直接拿去二开非常舒服。开源说明系统100%开源模块化开发模式,铭飞所开发的模块都发布到了maven中央库。...

这款基于SpringBoot 的CMS系统,开发企业官网确实香(附源码)

前言推荐这个项目是因为使用手册部署手册非常完善,项目也有开发教程视频对小白非常贴心,接私活可以直接拿去二开非常舒服开源说明系统100%开源模块化开发模式,铭飞所开发的模块都发布到了maven中央库。可...

【推荐】一款基于BPM和代码生成器的 AI 低代码开源平台

如果您对源码&技术感兴趣,请点赞+收藏+转发+关注,大家的支持是我分享最大的动力!!!项目介绍JeecgBoot是一款基于BPM和代码生成器的AI低代码平台,专为Java企业级Web应用而生。它采...

云安全日报200819:Apache发现重要漏洞 可窃取信息 控制系统 需要尽快升级

ApacheHTTPServer(简称Apache)是Apache软件基金会的一个开放源码的网页服务器,可以在大多数计算机操作系统中运行,由于其多平台和安全性被广泛使用,是最流行的Web服务器端软...

基于jeecgboot框架的cloud商城源码分享,兼容单体和微服务模式

3年时间里,随着关注java单商户商城系统的朋友越来越多,对cloud版本的商城呼声也越来越高。因此今年立项了cloud版本的开发,目前已发gitee开源,目前也基本测试完毕,欢迎大家体验以及提出宝贵...

SpringBoot + Mybatis + Shiro + mysql + redis智能平台源码分享

后端技术栈基于SpringBoot+Mybatis+Shiro+mysql+redis构建的智慧云智能教育平台基于数据驱动视图的理念封装element-ui,即使没有vue的使...

我敢保证,全网没有再比这更详细的Java知识点总结了,送你啊

接下来你看到的将是全网最详细的Java知识点总结,全文分为三大部分:Java基础、Java框架、Java+云数据小编将为大家仔细讲解每大部分里面的详细知识点,别眨眼,从小白到大佬、零基础到精通,你绝...

基于Spring+SpringMVC+Mybatis分布式敏捷开发系统架构(附源码)

前言zheng项目不仅仅是一个开发架构,而是努力打造一套从前端模板-基础框架-分布式架构-开源项目-持续集成-自动化部署-系统监测-无缝升级的全方位J2EE企业级开发解...

取消回复欢迎 发表评论: