Skip to content

Latest commit

 

History

History
648 lines (480 loc) · 24.6 KB

File metadata and controls

648 lines (480 loc) · 24.6 KB

RPC

我们要实现一个RPC框架,使得远程调用就和本地调用一样,屏蔽底层的网络通信的复杂性,让我们更加专注于业务,同时传输性能要高。

1. 协议

A服务调用B服务,A服务需要传递B服务需求的参数,同时要接收B服务的返回

在上方的需求中,很明显需要发起一个网络调用,涉及到两个协议传输协议序列化协议

传输协议的选择有HTTP/1.1,HTTP/2,TCP

序列化协议的选择有很多,比如JSON,Protobuf,Thrift,Hessian等

1.1 HTTP/1.0和HTTP/1.1的区别

HTTP1.0最早在网页中使用是在1996年,那个时候只是使用一些较为简单的网页上和网络请求上,而HTTP1.1则在1999年才开始广泛应用于现在的各大浏览器网络请求中,同时HTTP1.1也是当前使用最为广泛的HTTP协议。 主要区别主要体现在:

  1. 缓存处理,在HTTP1.0中主要使用header里的If-Modified-Since,Expires来做为缓存判断的标准,HTTP1.1则引入了更多的缓存控制策略例如Entity tag,If-Unmodified-Since, If-Match, If-None-Match等更多可供选择的缓存头来控制缓存策略。
  2. 带宽优化及网络连接的使用,HTTP1.0中,存在一些浪费带宽的现象,例如客户端只是需要某个对象的一部分,而服务器却将整个对象送过来了,并且不支持断点续传功能,HTTP1.1则在请求头引入了range头域,它允许只请求资源的某个部分,即返回码是206(Partial Content),这样就方便了开发者自由的选择以便于充分利用带宽和连接。
  3. 错误通知的管理,在HTTP1.1中新增了24个错误状态响应码,如409(Conflict)表示请求的资源与资源的当前状态发生冲突;410(Gone)表示服务器上的某个资源被永久性的删除。
  4. Host头处理,在HTTP1.0中认为每台服务器都绑定一个唯一的IP地址,因此,请求消息中的URL并没有传递主机名(hostname)。但随着虚拟主机技术的发展,在一台物理服务器上可以存在多个虚拟主机(Multi-homed Web Servers),并且它们共享一个IP地址。HTTP1.1的请求消息和响应消息都应支持Host头域,且请求消息中如果没有Host头域会报告一个错误(400 Bad Request)。
  5. 长连接,HTTP 1.1支持长连接(PersistentConnection)和请求的流水线(Pipelining)处理,在一个TCP连接上可以传送多个HTTP请求和响应,减少了建立和关闭连接的消耗和延迟,在HTTP1.1中默认开启Connection: keep-alive,一定程度上弥补了HTTP1.0每次请求都要创建连接的缺点。

1.2 HTTP/1.1和HTTP/2的区别

  • 新的二进制格式(Binary Format),HTTP1.x的解析是基于文本。基于文本协议的格式解析存在天然缺陷,文本的表现形式有多样性,要做到健壮性考虑的场景必然很多,二进制则不同,只认0和1的组合。基于这种考虑HTTP2.0的协议解析决定采用二进制格式,实现方便且健壮。
  • 多路复用(MultiPlexing),即连接共享,即每一个request都是是用作连接共享机制的。一个request对应一个id,这样一个连接上可以有多个request,每个连接的request可以随机的混杂在一起,接收方可以根据request的 id将request再归属到各自不同的服务端请求里面。
  • header压缩,如上文中所言,对前面提到过HTTP1.x的header带有大量信息,而且每次都要重复发送,HTTP2.0使用encoder来减少需要传输的header大小,通讯双方各自cache一份header fields表,既避免了重复header的传输,又减小了需要传输的大小。
  • 服务端推送(server push)HTTP2.0也具有server push功能。

2. 协议选择

2.1 HTTP/1.1

比如使用HTTP+JSON的解决方案,这也是大多数REST 架构采取的方案

选择构建在 HTTP 之上,有两个最大的优势:

  • HTTP 的语义和可扩展性能很好的满足 RPC 调用需求。
  • 通用性,HTTP 协议几乎被网络上的所有设备所支持,具有很好的协议穿透性。

存在比较明显的问题:

  • 典型的 Request – Response 模型,一个链路上一次只能有一个等待的 Request 请求。会产生 HOL。
  • Human Readable Headers,使用更通用、更易于人类阅读的头部传输格式,但性能相当差
  • 无直接 Server Push 支持

队首阻塞(Head-of-line blocking, HOL)是一种出现在缓存式通信网络交换中的一种现象。交换结构通常由缓存式先进先出输入端口、一个交换结构以及缓存式先进先出输出端口组成。

发生原因

由于FIFO(先进先出)队列机制造成的,每个输入端的FIFO首先处理的是在队列中最靠前的数据,而这时队列后面的数据对应的出口缓存可能已经空闲,但因为得不到处理而只能等待,这样既浪费了带宽又降低了系统性能。

举个现实生活中的例子,这就如同你在只有一条行车路线的马路上右转,但你前面有直行车,虽然这时右行线已经空闲,但你也只能等待。

2.2 HTTP/2

基于 HTTP2 的协议足够简单,用户学习成本低,天然有 server push/ 多路复用 / 流量控制能力

2.3 TCP

使用TCP,需要自定义协议内容,协议的内容包含三部分

  • 数据交换格式: 定义 RPC 的请求和响应对象在网络传输中的字节流内容,也叫作序列化方式
  • 协议结构: 定义包含字段列表和各字段语义以及不同字段的排列方式
  • 协议通过定义规则、格式和语义来约定数据如何在网络间传输。一次成功的 RPC 需要通信的两端都能够按照协议约定进行网络字节流的读写和对象转换。如果两端对使用的协议不能达成一致,就会出现鸡同鸭讲,无法满足远程通信的需求。

基于TCP 传输层的RPC协议,需要考虑很多问题:

  1. 通用性: 统一的二进制格式,跨语言、跨平台、多传输层协议支持
  2. 扩展性: 协议增加字段、升级、支持用户扩展和附加业务元数据
  3. 穿透性:能够被各种终端设备识别和转发:网关、代理服务器等 通用性和高性能通常无法同时达到,需要协议设计者进行一定的取舍。
  4. 性能:看自己的实现

2.4 选择

从上面分析来看,传输协议选择很明显HTTP/2有优势,没什么太大的缺点,但实际上,由于我们实现的是一个RPC框架,如果可能,应该尽可能将之都实现,将选择权交给使用者,根据实际使用场景来选择。

序列化协议的选择也应该尽量都支持,但一般都会有一个默认的序列化协议,当然是选一个性能较高的,比如Protobuf

3. 架构

A服务调用B服务,我们把A服务称为服务消费者,服务B称为服务提供者

image-20211023103133944

这是最简单的模型,我们先从简单开始

4. HTTP方式

在当前的资料下,有初始代码,代码结构如下:

image-20211023152032955

说明:

  1. provider的service有一个服务

    public interface GoodsService {
    
        /**
         * 根据商品id 查询商品
         * @param id
         * @return
         */
        Goods findGoods(Long id);
    }
  2. consumer的ConsumerController需要调用provider的GoodsService的findGoods方法

4.1 provider提供api服务

package com.mszlu.rpc.provider.controller;

import com.mszlu.rpc.provider.service.GoodsService;
import com.mszlu.rpc.provider.service.modal.Goods;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("provider")
public class ProviderController {

    @Autowired
    private GoodsService goodsService;

    @GetMapping("/goods/{id}")
    public Goods findGood(@PathVariable Long id){

        return goodsService.findGoods(id);
    }
}

4.2 Consumer使用RestTemplate调用

package com.mszlu.rpc.consumer.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

@Configuration
public class RestConfig {

	//定义restTemplate,spring提供
    ////发起http请求,传递参数,解析返回值( Class<T> responseType)
    @Bean
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }
}

RestTemplate 需要使用一个实现了 ClientHttpRequestFactory 接口的类为其提供 ClientHttpRequest 实现:

  1. 基于 JDK HttpURLConnectionSimpleClientHttpRequestFactory
  2. 基于 Apache HttpComponents Client 的 HttpComponentsClientHttpRequestFactory
  3. 基于 OkHttp 3的 OkHttp3ClientHttpRequestFactory
  4. 基于 Netty4 的 Netty4ClientHttpRequestFactory
package com.mszlu.rpc.consumer.controller;

import com.mszlu.rpc.consumer.vo.Goods;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

@RestController
@RequestMapping("consumer")
public class ConsumerController {

    @Autowired
    private RestTemplate restTemplate;

    @GetMapping("/find/{id}")
    public Goods find(@PathVariable Long id){

        ResponseEntity<Goods> forEntity = restTemplate.getForEntity("http://localhost:7777/provider/goods/1", Goods.class);
        if (forEntity.getStatusCode().is2xxSuccessful()){
            return forEntity.getBody();
        }
        return null;
    }
}

4.3 测试

浏览器访问:http://localhost:5555/consumer/find/1

5. 改进HTTP方式

上述的HTTP方式,过于繁琐,我们想使用以下的方式:

   @Autowired
    private GoodsHttpRpc goodsHttpRpc;

    @GetMapping("/find/{id}")
    public Goods find(@PathVariable Long id){
        return goodsHttpRpc.findGoods(id);
    }
package com.mszlu.rpc.consumer.rpc;


import com.mszlu.rpc.annontation.MsHttpClient;
import com.mszlu.rpc.annontation.MsMapping;
import com.mszlu.rpc.provider.service.modal.Goods;
import org.springframework.web.bind.annotation.PathVariable;

@MsHttpClient(value = "goodsHttpRpc")
public interface GoodsHttpRpc {

    @MsMapping(url = "http://localhost:7777",api = "/provider/goods/{id}")
    public Goods findGoods(@PathVariable Long id);
}

这样,我们调用远程服务的时候,就和使用本地的Service一样了,非常方便

5.1 新建模块ms-rpc

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>rpc-parent</artifactId>
        <groupId>com.mszlu.rpc</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>ms-rpc</artifactId>
    <properties>
        <spring.version>5.3.7</spring.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.3.7</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-web</artifactId>
            <version>5.3.7</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>
</project>

5.2 自定义注解

package com.mszlu.rpc.annontation;


import java.lang.annotation.*;

//可用于类上
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface MsHttpClient {
    String value();
}
package com.mszlu.rpc.annontation;


import java.lang.annotation.*;

//可用于方法上
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface MsMapping {

    String api() default "";

    String url() default "";
}

在consumer端导入ms-rpc:

<dependency>
            <groupId>com.mszlu.rpc</groupId>
            <artifactId>ms-rpc</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>

5.3 实现注解功能

从代码中可以看出,我们定义的GoodsHttpRpc是一个接口,我们实现的第一个功能是需要将此接口生成一个代理类,并且交给spring管理,这样使用的时候就可以注入了

package com.mszlu.rpc.bean;

import com.mszlu.rpc.annontation.EnableHttpClient;
import com.mszlu.rpc.annontation.MsHttpClient;
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.*;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.env.Environment;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.springframework.util.Assert;

import java.util.Map;
import java.util.Set;

/**
 * 1. ImportBeanDefinitionRegistrar类只能通过其他类@Import的方式来加载,通常是启动类或配置类。
 * 2. 使用@Import,如果括号中的类是ImportBeanDefinitionRegistrar的实现类,则会调用接口方法,将其中要注册的类注册成bean
 * 3. 实现该接口的类拥有注册bean的能力
 */
public class MsBeanDefinitionRegistry implements ImportBeanDefinitionRegistrar,
        ResourceLoaderAware, EnvironmentAware {

    private Environment environment;

    private ResourceLoader resourceLoader;

    public MsBeanDefinitionRegistry(){}

    @Override
    public void setEnvironment(Environment environment) {
        this.environment = environment;
    }

    @Override
    public void setResourceLoader(ResourceLoader resourceLoader) {
        this.resourceLoader = resourceLoader;
    }

    @Override
    public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
        registerMsHttpClient(metadata,registry);
    }

    private void registerMsHttpClient(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
        Map<String, Object> annotationAttributes = metadata.getAnnotationAttributes(EnableHttpClient.class.getCanonicalName());
        //找到Enable注解,获取其中的basePackage属性,此属性标明了@MsHttpClient所在的包
        Object basePackage = annotationAttributes.get("basePackage");
        if (basePackage != null){
            String base = basePackage.toString();
            //ClassPathScanningCandidateComponentProvider是Spring提供的工具,可以按自定义的类型,查找classpath下符合要求的class文件
            ClassPathScanningCandidateComponentProvider scanner = getScanner();
            scanner.setResourceLoader(resourceLoader);
            AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(MsHttpClient.class);
            scanner.addIncludeFilter(annotationTypeFilter);
            //上方定义了要找@MsHttpClient注解标识的类,这里进行对应包的扫描,扫描后就找到了所有被@MsHttpClient注解标识的类
            Set<BeanDefinition> candidateComponents = scanner.findCandidateComponents(base);
            for (BeanDefinition candidateComponent : candidateComponents) {
                if (candidateComponent instanceof  AnnotatedBeanDefinition){
                    //这就是被@MsHttpClient注解标识的类
                    AnnotatedBeanDefinition annotatedBeanDefinition = (AnnotatedBeanDefinition) candidateComponent;
                    AnnotationMetadata beanDefinitionMetadata = annotatedBeanDefinition.getMetadata();
                    Assert.isTrue(beanDefinitionMetadata.isInterface(),"@MsHttpClient 必须定义在接口上");
                    //获取此注解的属性
                    Map<String, Object> clientAnnotationAttributes = beanDefinitionMetadata.getAnnotationAttributes(MsHttpClient.class.getCanonicalName());
                    //这里判断是否value设置了值,value为此Bean的名称,定义bean的时候要用
                    String beanName = getClientName(clientAnnotationAttributes);
                    //Bean的定义,通过建造者Builder模式来实现,需要一个参数,FactoryBean的实现类
                    //FactoryBean是一个工厂Bean,可以生成某一个类型Bean实例,它最大的一个作用是:可以让我们自定义Bean的创建过程。
                    BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(MsHttpClientFactoryBean.class);
                    //设置FactoryBean实现类中自定义的属性,这里我们设置@MsHttpClient标识的类,用于生成代理实现类
                    beanDefinitionBuilder.addPropertyValue("interfaceClass",beanDefinitionMetadata.getClassName());
                    assert beanName != null;
                    //定义Bean
                    registry.registerBeanDefinition(beanName,beanDefinitionBuilder.getBeanDefinition());
                }
            }
        }
    }

    private String getClientName(Map<String, Object> clientAnnotationAttributes) {
        if (clientAnnotationAttributes == null){
            throw new RuntimeException("value必须有值");
        }
        Object value = clientAnnotationAttributes.get("value");
        if (value != null && !value.toString().equals("")){
            return value.toString();
        }
        return null;
    }

    protected ClassPathScanningCandidateComponentProvider getScanner() {
        return new ClassPathScanningCandidateComponentProvider(false, this.environment) {
            @Override
            protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
                boolean isCandidate = false;
                if (beanDefinition.getMetadata().isIndependent()) {
                    if (!beanDefinition.getMetadata().isAnnotation()) {
                        isCandidate = true;
                    }
                }
                return isCandidate;
            }
        };
    }
}
package com.mszlu.rpc.annontation;

import com.mszlu.rpc.bean.MsBeanDefinitionRegistry;
import com.mszlu.rpc.spring.MsRpcSpringBeanPostProcessor;
import org.springframework.context.annotation.Import;

import java.lang.annotation.*;

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import({MsBeanDefinitionRegistry.class})
public @interface EnableHttpClient {

    String basePackage();
}

将接口代理注册到spring容器中,需要使用FactoryBean的实现类,所有的类都是通过bean工厂生产的

package com.mszlu.rpc.bean;

import com.mszlu.rpc.proxy.MsRpcClientProxy;
import org.springframework.beans.factory.FactoryBean;
//FactoryBean是一个工厂Bean,可以生成某一个类型Bean实例,它最大的一个作用是:可以让我们自定义Bean的创建过程。
public class MsHttpClientFactoryBean<T> implements FactoryBean<T> {

    private Class<T> interfaceClass;
    //返回的对象实例
    @Override
    public T getObject() throws Exception {
        return new MsHttpClientProxy().getProxy(interfaceClass);
    }
    //Bean的类型
    @Override
    public Class<?> getObjectType() {
        return interfaceClass;
    }

    //true是单例,false是非单例  在Spring5.0中此方法利用了JDK1.8的新特性变成了default方法,返回true
    @Override
    public boolean isSingleton() {
        return true;
    }

    public Class<?> getInterfaceClass() {
        return interfaceClass;
    }

    public void setInterfaceClass(Class<T> interfaceClass) {
        this.interfaceClass = interfaceClass;
    }
}

5.4 动态代理

使用jdk的动态代理

package com.mszlu.rpc.proxy;

import com.mszlu.rpc.annontation.MsMapping;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;

import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.lang.reflect.Proxy;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

//每一个动态代理类的调用处理程序都必须实现InvocationHandler接口,
// 并且每个代理类的实例都关联到了实现该接口的动态代理类调用处理程序中,
// 当我们通过动态代理对象调用一个方法时候,
// 这个方法的调用就会被转发到实现InvocationHandler接口类的invoke方法来调用
public class MsHttpClientProxy implements InvocationHandler {


    public MsHttpClientProxy(){

    }

    /**
     * proxy:代理类代理的真实代理对象com.sun.proxy.$Proxy0
     * method:我们所要调用某个对象真实的方法的Method对象
     * args:指代代理对象方法传递的参数
     */
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //在这里实现调用
        MsMapping msMapping = method.getAnnotation(MsMapping.class);
        if (msMapping != null){
            RestTemplate restTemplate = new RestTemplate();
            String api = msMapping.api();
            Pattern compile = Pattern.compile("(\\{\\w+})");
            Matcher matcher = compile.matcher(api);
            if (matcher.find()){
                //简单判断一下 代表有路径参数需要替换
                int x = matcher.groupCount();
                for (int i = 0; i< x; i++){
                    String group = matcher.group(i);
                    api = api.replace(group, args[i].toString());
                }
            }
            ResponseEntity forEntity = restTemplate.getForEntity(msMapping.url()+ api, method.getReturnType());
            return forEntity.getBody();
        }
        return null;
    }

    /**
     * get the proxy object
     */
    @SuppressWarnings("unchecked")
    public <T> T getProxy(Class<T> clazz) {
        return (T) Proxy.newProxyInstance(clazz.getClassLoader(), new Class<?>[]{clazz}, this);
    }

    public static void main(String[] args) {
        Pattern compile = Pattern.compile("(\\{\\w+})");
        Matcher matcher = compile.matcher("casd/asd/{id}");
        System.out.println(matcher.find());
    }
}

5.5 Consumer使用

做一个配置类,开启MsHttpClient的支持,并且设置扫包路径

package com.mszlu.rpc.consumer.config;

import com.mszlu.rpc.annontation.EnableHttpClient;
import com.mszlu.rpc.bean.MsBeanDefinitionRegistry;
import com.mszlu.rpc.spring.MsRpcSpringBeanPostProcessor;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

@Configuration
@EnableHttpClient(basePackage = "com.mszlu.rpc.consumer.rpc")
public class RpcConfig {
}
package com.mszlu.rpc.consumer.rpc;


import com.mszlu.rpc.annontation.MsHttpClient;
import com.mszlu.rpc.annontation.MsMapping;
import com.mszlu.rpc.provider.service.modal.Goods;
import org.springframework.web.bind.annotation.PathVariable;

@MsHttpClient(value = "goodsHttpRpc")
public interface GoodsHttpRpc {

    @MsMapping(url = "http://localhost:7777",api = "/provider/goods/{id}")
    public Goods findGoods(@PathVariable Long id);
}
  @Autowired
    private GoodsHttpRpc goodsHttpRpc;

    @GetMapping("/find/{id}")
    public Goods find(@PathVariable Long id){
        return goodsHttpRpc.findGoods(id);
    }

至此,我们改造完成,当然实现的很简陋,后期我们慢慢补全。

如果大家学过Spring Cloud Feign,会发现这个调用过程和Feign很像(因为我们就是模仿的Feign,其中的一些代码也是参考的Feign的源码)