• Dubbo入门


    Dubbo

    分布式理论基础

    • 分布式是若干独立计算机的集合,这些计算机对于用户来说就像单个相关系统
    • 分布式系统是建立在网络之上的软件系统

    ​ 随着和互联网的发展,网站应用的规模不断扩大,常规的垂直应用架构已无法应对,急需一个治理系统确保架构有条不紊的演进。

    互联网架构发 展演变

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Uni11xGd-1661390620958)(C:\Users\everybody\AppData\Roaming\Typora\typora-user-images\image-20220818163223082.png)]

    • 单体应用架构
      • 单体架构。就是一种把系统中所有的功能、模块耦合在一个应用中的架构方式,一般只操作一个数据库。代表技术:Struct2、SpringMVC、Spring、Mybatis等。
      • 特点:打包成一个独立的包或者是war包。会以一个进程的方式来运行
      • 优点:项目易于管理、部署简单
      • 缺点:测试成本高、可伸缩性差、可靠性高差、迭代困难、跨语言成都差、团队协作难。
    • RPC架构
      • 远程过程调用,是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。代表技术Thrift、Hessian等等。
      • 特点:应用直接调用服务,服务之间是隔离的
      • 缺点:服务过多时,管理成本高昂。服务治理、服务注册、发现、服务容错、服务跟踪、服务网关、IP暴漏等都是此架构无法避免的问题。
    • SOA架构
      • SOA(Service oriented Architectyre):面向服务架构
      • ESB(Enterparise Service Bus):企业服务总线,服务中介。主要是提供了一个服务与服务之间的交互。
      • ESB包含的功能:负载均衡,流量控制,加密处理,服务监控,异常处理,监控告急等。
    • 微服务架构
      • 微服务是一种架构风格。一个大型的复杂软件应用,由一个或多个微服务组成。系统中的各个微服务可被独立部署,各个微服务之间时松耦合的。每隔微服务仅关注于完成一件任务并很好的完成该任务。微服务就是一个轻量级的服务治理方案。对比SOA架构,使用注册中心代替ESB服务总线,注册中心相比服务总线来说,更加轻量级,代表表技术:SpringCloud、 Dubbo等。
      • 特点:系统是由多个服务构成,每个服务可以单独独立部署,每个服务之间是松耦合的。服务内部是高内聚的,外部表是低耦合的。高内聚就是每个服务只关注完成一个功能。低耦合就是服务之间没有直接关联。
      • 优点:
        • 测试容易:服务高内聚低耦合,每个服务可以独立测试
        • 可伸缩性强:服务相对独立,可随时增删服务实现系统服务变化,可只针对某服务独立水平扩展
        • 可靠性强:服务出现问题,受影响的位置是当前服务,不会影响其他服务,应用健壮性有更好的保证。
        • 跨语言程度会更加灵活:可针对服务特性使用不同的语言开发,尽可能发挥出每种语言的特性。
        • 团队协作容易:段对专注自主研发的服务,对其他服务的了解可局限在服务的调用上。
        • 系统迭代容易:当服务发生变更时,只需针对单一服务进行系统省级迭代/
      • 缺点:
        • 运维成本过高,部署数量较多:服务过多导致运维成本成倍提升。
        • 接口兼容多版本:因服务可独立省级迭代,所以会导致接口版本过多。
        • 分布式系统的复杂性:系统分部,导致通讯成版本提升,系统复杂程度提升。
        • 分布式事务:分布式系统会引出分布式事务的出现,现在有很多的额分布式事务解决方案,分布式事务不是不可解决的。这不会影响微服务架构的应用

    RPC基本概念

    RPC协议(Remote Procedure Call Protocol)

    ​ 远程过程调用协议,它是通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。RPC协议假定某些传输协议的存在,如TCP或UDP,为通信程序之间携带信息数据。在OSI网络通信模型中,RPC跨越了传输层和应用层。RPC使得开发包括网络分布式多程序在内的应用程序更加容易。

    ​ RPC采用C/S模式,请求程序是一个客户机,而服务提供程序就是一个服务器。首先,客户机调用进程发送一个有进程参数的调用信息到服务进程,然后等待应答信息。在服务器端,进程保持睡眠状态知道调用信息到达位置。当一个调用信息到达,服务器获得进程参数,计算结果,发送答复信息,然后等待下一个调用信息,最后,客户端调用进程接收到答复信息,获得进程结果,然后调用执行继续进行。

    RPC框架

    ​ 在单机时代一台电脑运行多个进程,进程间无法通讯。显然这回浪费很多资源,因此后来出现IPC(Inter-process-communication 单机中运行得进程之间相互通信),这样就能允许进程之间进程通讯,比如在一台计算机中得A进程写了一个吃饭的方法,那再以前如果再B进程中也要有一个吃饭的方法,必须要再B进程中进行创建,但有了RPC后 B进程只需要调用A进程的程序即可完成。后来网络时代的出现,大家电脑都连起来,这是可以调用其他电脑上的进程,这样RPC框架就出现了。严格意义上来讲Unix的生态系统之中RPC可以正在同一台电脑上不同的进程进行,也可以在不同电脑上进行,而在Windows里面同一台电脑上不同进程间对额通信还可以采用LPC(本地访问)。综上:RPC或LPC是上层建筑,IPC是底层基础。

    RPC与HTTP、TCP/UDP、Socket的区别

    ​ TCP/UDP:都是传输协议,主要区别是TCP协议连接需要三次握手,断开需要四次挥手,是通过流来传输的,就是确定连接后,一直发送信息,传完后断开。UDP不需要进行连接,直接把信息封装成多个报文,直接发送。所以UDP的速度更快。但是不保证数据的完整性。

    ​ HTTP:超文本本传输协议是一种中应用层协议,建立在TCP协议之上。

    ​ Socket:是再应用程序层面上对TCP/IP协议的封装和应用。其实是一个调用接口,方便程序员使用TCP/IP协议栈而已。程序员通过Socket来使用TCP/IP协议。但是Socket并不是一定要使用TCP/IP协议,Socket编程接口在设计的时候,就希望也能适应其他的网络协议。

    ​ RPC是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网技术的协议。所以RPC的实现可以通过不同的协议去实现,比如:Http 、RMI等。

    RPC的运行流程

    ​ 首先,要解决通讯的问题,主要是通过在客户端和服务器之间建立TCP连接,远程过程调用的说有交换的数据都在这个连接里传输。连接可以是按需连接,调用结束后就断开,也可以是长连接,多个远程过程调用共享一个连接。

    ​ 第二,要解决寻址的问题,也就是说,A服务器上的应用怎么告诉底层的RPC框架,如何连接到B服务器(如主机或IP地址)以及特定的端口,方法名称是什么,这样才能完成调用。比如基于Web服务协议的RPC,就要提供一个 Endpoint URI,或者是从UDDI服务上查找。如果是RMI调用的话,还需要一个RMI Registry来注册服务的地址。

    ​ 第三,当A服务器上的应用发起远程过程调用时,方法的参数需要通过底层的网络协议如TCP传递到B服务器,由于网络协议是基于二进制的,内存中的参数的值需要序列化成二进制的形式,也就是序列化(Serialize) 或编组(Marshal),通过寻址和传输将序列化的二进制发送给B服务器。

    ​ 第四,B服务器收到请求后。需要对参数进行反序列化,恢复为内存中的表达方式,然后找到对应的方法进行把本地调用,然后得到返回值。

    ​ 第五,返回组织还要发送回服务器A上的调用,也要经过序列化的方式发送,服务器A接收到后,在反序列化,恢复为内存中的表达方式,交给A服务器上的应用。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kYb8qRhh-1661390620960)(C:\Users\everybody\AppData\Roaming\Typora\typora-user-images\image-20220824170625216.png)]

    为什么需要RPC

    ​ 论复杂度,RPC框架肯定高于简单的HTTP接口的,但毋庸置疑,HTTP接口由于受限于HTTP协议,需要到HTTP请求头,导致传输起来效率或者说安全性不如RPC。
    ​ HTTP接口实在接口不多、系统与系统交互较少的情况下,解决信息初期常用的一种通信手段:优点就是简单、直接、开发方便。利用线程的HTTP协议进行传输。但是如果是一个大型的网站,内部子系统较多,接口非常多的情况下,RPC框架的好处就显示出来了,首先是长连接,不必每次通信都要像HTTP一样3次握手,减少了网络开销,其次就是RPC框架一般都有注册中心,有丰富的监控管理,发布、下线接口、动态扩展等,对调用方来说是无感知、统一化的操作。第三个来说就是安全性。最后就是流行的服务化架构、服务化治理,RPC框架是一个强力的支撑。

    ​ RPC是一种概念,HTTP也是RPC实现的一种方式,用HTTP交互其实就已经属于RPC了。

    ​ RPC:远程过程调用。RPC的核心并不在于使用什么协议。RPC的目的是让你在本地调用远程的方法,而对你来说这个调用是透明的,你并不知道这个调用的方法是不只是在哪里。通过RPC能解耦服务,这才是使用RPC的真正目的。RPC的原理主要用到了动态代理模式,至于HTTP协议,只是传输协议而已。

    ​ RPC是一个软件结构概念,是构建分布式应用的理论基础。就好比为什么家里可以用到发电厂发出来的电,是因为电可以传输的。至于使用铜线还是其他导线,就是使用HTTP还是其他协议的问题。

    ​ 在Java中最基本的就是RMI技术,它是Java原生的应用层分布式技术。我们可以肯定的是在传输性能方面,RMI的性能是优于HTTP的。

    RPC基于RMI的简单实现

    新建一个rmi-demo父工程,分别在rmi-demo父工程中创建rmi-api模块、rmi-server模块、rmi-client模块

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-H4vC5Vff-1661390620960)(C:\Users\everybody\AppData\Roaming\Typora\typora-user-images\image-20220825083737614.png)]

    首先在rmi-api模块中定义一个Service服务:UserService

    public interface UserService {
    
        /**
         * @Describe  根据用户id查询用户记录
         */
        public User queryUserByUserId(Integer userId) throws RemoteException;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    pojo:由于要通过网络进行调用,所以实体类要实现序列化接口

    public class User implements Serializable {
    
        private Integer id;
        private String userName;
        @Override
        public String toString() {
            return "User{" +
                    "id=" + id +
                    ", userName='" + userName + '\'' +
                    '}';
        }
        public Integer getId() {
            return id;
        }
        public void setId(Integer id) {
            this.id = id;
        }
        public String getUserName() {
            return userName;
        }
        public void setUserName(String userName) {
            this.userName = userName;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    在rmi-server模块中首先继承rmi-api模块

    定义接口的实现类:UserServiceImpl: 实现类与平常的web项目一样写就可以

    @Service
    public class UserServiceImpl implements UserService {
        @Override
        public User queryUserByUserId(Integer userId) throws RemoteException {
            System.out.println("服务端收到请求,请求参数 --> " + userId);
            User user = new User();
            user.setId(userId);
            user.setUserName("admin");
            return user;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    对外暴露服务:RmiServer

    @Configuration
    public class RmoServer {
        //注入service
        @Resource
        private UserService userService;
        //注册成为Spring中的一个Bean对象
        @Bean
        public RmiServiceExporter rmiServiceExporter(){
            RmiServiceExporter exporter = new RmiServiceExporter();
            //配置服务端口
            exporter.setRegistryPort(2002);
            //服务名字
            exporter.setServiceName("userService");
            //指定服务实现
            exporter.setService(userService);
            //设置服务实现的接口
            exporter.setServiceInterface(UserService.class);
    
            //对外导出
            return exporter;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    在rmi-client中定义一个Controller方法来调用rmi-server中的UserServiceImpl:

    @RestController
    public class UserController {
        @Resource
        private UserService userService;
        @GetMapping("/user/{userId}")
        public User queryUserByUserId(@PathVariable Integer userId){
            User user = null;
            try {
                user = userService.queryUserByUserId(userId);
            } catch (RemoteException e) {
                throw new RuntimeException(e);
            }
            return user;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    订阅rmi

    @Configuration
    public class RmiClientConfig {
        //相当于订阅服务
        @Bean(value = "userService")
        public RmiProxyFactoryBean getUserService(){
            RmiProxyFactoryBean rmiProxyFactoryBean = new RmiProxyFactoryBean();
            //指定服务地址
            rmiProxyFactoryBean.setServiceUrl("rmi://127.0.0.1:2002/userService");
            //设置接口对象
            rmiProxyFactoryBean.setServiceInterface(UserService.class);
            return rmiProxyFactoryBean;
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    启动测试,通过

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ph1jQ64o-1661390620961)(C:\Users\everybody\AppData\Roaming\Typora\typora-user-images\image-20220825084539948.png)]

    Dubbo入门

    Dubbo介绍

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zBpYiqET-1661390620961)(C:\Users\everybody\AppData\Roaming\Typora\typora-user-images\image-20220825084750904.png)]

    节点角色:

    • Provider:暴露服务的服务提供方
    • Consumer:调用远程服务的服务消费方
    • Registry:服务注册与发现的注册中心
    • Monitor:统计服务的调用次数和调用时间的监控中心
    • Container:服务运行容器

    调用关系:

    0.服务器负责启动,加载,运行服务提供者

    1.服务提供者在启动时,向注册中心注册自己提供的服务。

    2.服务消费者在启动时,向注册中心订阅自己所需要的服务。

    3.注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。

    4.服务消费者,从提供者地址列表中,基于负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。

    5.服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。

    SpringBoot整合Dubbo (基于yml配置)

    新建一个Spring-boot-dubbo-demo父工程,正在父工程中导入相关依赖:

    <modules>
        <module>spring-boot-dubbo-apimodule>
        <module>spring-boot-dubbo-providermodule>
        <module>spring-boot-dubbo-consumermodule>
    modules>
    <parent>
        <artifactId>spring-boot-starter-parentartifactId>
        <groupId>org.springframework.bootgroupId>
        <version>2.2.1.RELEASEversion>
        <relativePath />
    parent>
    <properties>
        <maven.compiler.source>8maven.compiler.source>
        <maven.compiler.target>8maven.compiler.target>
        <dubbo.version>2.7.6dubbo.version>
    properties>
    
    <dependencies>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>
        <dependency>
            <groupId>org.apache.dubbogroupId>
            <artifactId>dubbo-spring-boot-starterartifactId>
        dependency>
    
        
        <dependency>
            <groupId>org.apache.dubbogroupId>
            <artifactId>dubbo-dependencies-zookeeperartifactId>
            <version>2.7.7version>
            <type>pomtype>
        dependency>
    
    
    dependencies>
    
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.apache.dubbogroupId>
                <artifactId>dubbo-spring-boot-starterartifactId>
                <version>${dubbo.version}version>
            dependency>
        dependencies>
    dependencyManagement>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47

    父工程创建完成之后依次创建spring-boot-dubbo-api、spring-boot-dubbo-provider、spring-boot-dubbo-consumer三个模块

    其中provider和consumer模块都依赖于api和父工程

    spring-boot-dubbo-api

    和基于rmi实现一样先定义一个接口和pojo

    public interface UserService {
    
        public User queryByUserId(Integer userId);
    }
    
    • 1
    • 2
    • 3
    • 4
    public class User  implements Serializable {
        private Integer userId;
    
        private String userName;
    
        @Override
        public String toString() {
            return "User{" +
                    "userId=" + userId +
                    ", userName='" + userName + '\'' +
                    '}';
        }
    
        public Integer getUserId() {
            return userId;
        }
    
        public void setUserId(Integer userId) {
            this.userId = userId;
        }
    
        public String getUserName() {
            return userName;
        }
    
        public void setUserName(String userName) {
            this.userName = userName;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    实体类要通过网络传输数据,所以实现序列化接口

    spring-boot-dubbo-provider

    接口的实现类,其中@Service注解要使用dubbo提供的

    import org.apache.dubbo.config.annotation.Service;
    
    
    @Service(version = "1.0")
    public class UserServiceImpl implements UserService {
        @Override
        public User queryByUserId(Integer userId) {
            System.out.println("服务提供方收到请求,查询参数 -- > " + userId);
            User user = new User();
            user.setUserId(userId);
            user.setUserName("admin");
            return user;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    对项目进行配置:

    server:
      port: 8001
    
    dubbo:
      application:
        name: dubbo-provider
      registry:
      	# 使用广播形式,官网有说明
        address: multicast://224.5.6.7:1234
      protocol:
        name: dubbo
        port: 20880
    
      scan:
        base-packages: top.ccl317.provider.service
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    spring-boot-dubbo-consumer

    写一个控制器调用UserService

    @RestController
    public class UserController {
    
        @Reference(version = "1.0" , parameters = {"unicast" , "false"})
        private UserService userService;
    
    
        @GetMapping("/user/{userId}")
        public User getUserByUserId(@PathVariable Integer userId){
            return userService.queryByUserId(userId);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    对项目进行配置:

    server:
      port: 8002
    dubbo:
      application:
        name: dubbo-consumer
      registry:
      	# 广播地址
        address:multicast://224.5.6.7:1234
      protocol:
        name: dubbo
        port: 20880
      metadata-report:
        timeout: 100000
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    注册中心

    ​ 注册中心作用:就是更高效的管理系统的服务:比如服务接口的发布、自动剔除无效的服务、自动恢复服务等。

    ​ Dubbo支持四种注册中心:multicast、zookeeper、redis(不常用)、simple(不常用)

    ​ Dubbo3.0支持Nacos注册中心

    Multicast注册中心

    ​ Multicast注册中心不需要启动任何中心节点,只要广播地址一样就可以实现相互发现。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7Wgqf9Xf-1661390620962)(C:\Users\everybody\AppData\Roaming\Typora\typora-user-images\image-20220825090949067.png)]

    1. 提供方启动时广播自己的地址
    2. 消费方启动时广播订阅请求
    3. 提供方收到订阅请求时,单播自己的地址给订阅者,如果设置了unicast-false,则广播给订阅者
    4. 消费方收到提供方地址时,连接该地址进行RPC调用
    5. 组播受网络结构限制,只适合小规模应用或开发阶段使用。组播地址段:224.0.0.0 - 239.255.255.255

    Zookeeper注册中心

    ​ Zookeeper是Apache Hadoop的子项目,是一个树形的目录服务支持变更推送,适合作为Dubbo服务的注册中心,工业强度较高,可用于生产环境。推荐使用。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-k86Zj3In-1661390620963)(C:\Users\everybody\AppData\Roaming\Typora\typora-user-images\image-20220825091453373.png)]

    zookeeper简介与安装

    ​ Zookeeper是Apache Hadoop的子项目,是一个树形的目录服务支持变更推送,适合作为Dubbo服务的注册中心,工业强度较高,可用于生产环境。推荐使用。

    Zookeeper 安装(Linux单机版):

    将下载好的zookeeper安装包上传到linux,解压,解压完成之后创建两个目录:/data /log

    修改配置文件

    将zoo_temp.cfg复制一份为zoo.cfg 将创建好的两个目录配置进去

    启动:

    cd到bin目录使用

    ./zkServer.sh satart   启动zookeeper
    ./zkServer.sh status   查看zookeeper状态
    ./zkServer.sh stop     停止zookeeper运行
    
    • 1
    • 2
    • 3

    将Dubbo注册中心配置为zookeeper,只需要修改之前的配置文件

    consumer

    server:
      port: 8002
    
    
    dubbo:
      application:
        name: dubbo-consumer
      registry:
        address: zookeeper://43.142.79.131:2181
        timeout: 999999
      protocol:
        name: dubbo
        port: 20880
      metadata-report:
        timeout: 100000
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    provider

    server:
      port: 8001
    
    dubbo:
      application:
        name: dubbo-provider
      registry:
        address: zookeeper://43.142.79.131:2181
        timeout: 999999
      protocol:
        name: dubbo
        port: 20880
    
      scan:
        base-packages: top.ccl317.provider.service
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
  • 相关阅读:
    HTML5+CSS3小实例:脉冲波纹催眠动画特效
    编程技术对网络社会的益作用?
    Kafka集成flume
    【21天学习经典算法】冒泡排序(附Python完整代码)
    R语言——条形图数据可视化的多种方式
    调整Windows键盘上只能看到拼音而无法看到实际的文本以及关闭输入法悬浮窗方法
    人工智能AI 全栈体系(六)
    小程序当前页面栈以及跳转
    根据经纬度查地址
    Day 92
  • 原文地址:https://blog.csdn.net/qq_17514043/article/details/126517745