并不简单的代理,Dubbo是如何做服务引用的

news2024/11/26 16:28:32

系列文章目录

【收藏向】从用法到源码,一篇文章让你精通Dubbo的SPI机制
面试Dubbo ,却问我和Springcloud有什么区别?
超简单,手把手教你搭建Dubbo工程(内附源码)
Dubbo最核心功能——服务暴露的配置、使用及原理


不满足于RPC,Dubbo是如何做服务引用的

  • 系列文章目录
  • 前言
  • 一、服务引用的介绍
  • 二、配置与模型
    • 1. 服务引用配置
    • 2. 整体实现模型
  • 三. 引用的具体实现
    • 1. 触发创建代理对象
    • 2. 创建代理的实现
    • 3. 代理的获取
  • 四、小结


前言

我们在Dubbo与SpringCloud对比的那一期就说过,Dubbo并不满足于作为RPC框架,除了RPC所需要的内容,还提供了如服务治理等额外的支持。但是无奈,很多人对Dubbo的印象依旧停留在RPC框架。所以,我们在学习的初期,我们先不急着学习那些分支内容,还是专注其核心,即围绕RPC循序渐进的学习。那么上一篇我们讲了服务暴露,今天就一起来学习RPC桥梁的另一端:消费者,即服务的引用。当然,在此之前,还是希望大家带着问题来学习,比如说:

  • 服务引用在底层替我们做了什么?
  • Spring怎么和服务引用融合的?

📕作者简介:战斧,多年开发及管理经验,爱好广泛,致力于创作更多高质量内容
📗本文收录于 Dubbo专栏,有需要者,可直接订阅专栏实时获取更新
📘高质量专栏 RabbitMQ、Spring全家桶 等仍在更新,欢迎指导
📙Zookeeper Redis kafka docker netty等诸多框架,以及架构与分布式专题即将上线,敬请期待

一、服务引用的介绍

在JAVA中,引用的概念是一直贯穿的。大部分情况,我们都是进行着对象引用。但是通过类比,我们不难得出服务引用的概念:服务引用是指在程序中使用另一个程序或服务提供的功能或数据的方式。通过服务引用,程序可以调用其他程序或服务提供的功能,获取返回值并将其用于自己的业务逻辑中

二、配置与模型

1. 服务引用配置

同服务暴露一样,实现服务引用的可以通过XML文件和注解两种途径来实现
XML 文件:

<dubbo:reference  interface="com.zhanfu.dubbo.demo.dubbo.api.DemoService" timeout="2000"/>

或者 @DubboReference 注解

@Component
public class Consumer implements CommandLineRunner {
    @DubboReference(timeout = 2000)
    private DemoService demoService;

    @Override
    public void run(String... args) throws Exception {

        String result = demoService.sayHello("战斧");
        System.out.println("收到消息:" + result);
    }
}

2. 整体实现模型

如果我们做了上面的配置,那么它是如何实现调用远方或本地的服务的呢?我们先简单概括一下:Dubbo 为我们的引用创建了一个代理对象,这个代理对象如同一个电话筒,当我们对电话筒说话的时候,它会将我们的话传到指定的人那里。同理,当我们调用这个代理对象时,它会把我们的调用请求传递到服务提供方那里,获得结果后,再反馈给我们当然,这里仅仅是将其简单化成一个代理对象,实际其结合了容错策略后,实现还是很复杂的
在这里插入图片描述

三. 引用的具体实现

如同前几期的一样,我们今天仍然以与Spring框架结合下的Dubbo进行分析

1. 触发创建代理对象

服务引用的操作其实和服务暴露一样,因此把上次服务暴露的图稍微改下各位就明白了
在这里插入图片描述
与服务暴露仅有后置处理器不同:

服务暴露的工厂后置处理器为ServiceAnnotationPostProcessor ,而现在服务引用的工厂后置处理器为ReferenceAnnotationBeanPostProcessor,当然,其扫描的注解也有不同,服务引用扫描的注解为

  1. org.apache.dubbo.config.annotation.DubboReference
  2. org.apache.dubbo.config.annotation.Reference
  3. com.alibaba.dubbo.config.annotation.Reference

至于容器刷新监听,其实都是由 DubboDeployApplicationListener 进行监听,最终也都会落在在同一个方法里
onContextRefreshedEvent -> deployer.start() -> startSync()

// DefaultModuleDeployer.class
private synchronized Future startSync() throws IllegalStateException {
    if (isStopping() || isStopped() || isFailed()) {
        throw new IllegalStateException(getIdentifier() + " is stopping or stopped, can not start again");
    }
    try {
        ......
        // 服务暴露
        exportServices();
        ......
        // 引用服务
        referServices();
        ......
    } catch (Throwable e) {
        onModuleFailed(getIdentifier() + " start failed: " + e, e);
        throw e;
    }
    return startFuture;
}

接下来,就让我们专注于真正的引用服务方法 DefaultModuleDeployer.referServices()

// DefaultModuleDeployer
private void referServices() {
    configManager.getReferences().forEach(rc -> {
        try {
            ReferenceConfig<?> referenceConfig = (ReferenceConfig<?>) rc;
            if (!referenceConfig.isRefreshed()) {
                referenceConfig.refresh();
            }

			// 每一个rc 其实就是一个引用配置,比如我们在DemoService上加了@DubboReference注解,就会被解析成一个引用配置,形如
			// <dubbo:reference sticky="false" timeout="2000" id="demoService" interface="com.zhanfu.dubbo.demo.dubbo.api.DemoService" />
            if (rc.shouldInit()) {
                if (referAsync || rc.shouldReferAsync()) {
                    ExecutorService executor = executorRepository.getServiceReferExecutor();
                    CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
                        try {
                            referenceCache.get(rc);
                        } catch (Throwable t) {
                            logger.error(CONFIG_FAILED_EXPORT_SERVICE, "", "", "Failed to async export service config: " + getIdentifier() + " , catch error : " + t.getMessage(), t);
                        }
                    }, executor);

                    asyncReferringFutures.add(future);
                } else {
                	// 从引用缓存中尝试获取该引用配置的代理对象,注意此处仅仅执行该方法,并没有获取其返回值
                    referenceCache.get(rc);
                }
            }
        } catch (Throwable t) {
            logger.error(CONFIG_FAILED_REFERENCE_MODEL, "", "", "Model reference failed: " + getIdentifier() + " , catch error : " + t.getMessage(), t);
            referenceCache.destroy(rc);
            throw t;
        }
    });
}

上文中的 referenceCache 其实是一个标注了 final 的 SimpleReferenceCache 对象,其储存着所有的引用代理的情况,需要注意的是,真正的代理对象并不存储在它这里。

public <T> T get(ReferenceConfigBase<T> rc) {
	// 为该配置生成key, 即接口的全限定名,此处就是com.zhanfu.dubbo.demo.dubbo.api.DemoService
    String key = generator.generateKey(rc);
    // 接口的类型 com.zhanfu.dubbo.demo.dubbo.api.DemoService.calss
    Class<?> type = rc.getInterfaceClass();

    boolean singleton = rc.getSingleton() == null || rc.getSingleton();
    T proxy = null;
    // 单例类才需要从缓存取,不然每次都得新取一个
    if (singleton) {
        proxy = get(key, (Class<T>) type);
    } else {
        logger.warn(CONFIG_API_WRONG_USE, "", "", "Using non-singleton ReferenceConfig and ReferenceCache at the same time may cause memory leak. " +
            "Call ReferenceConfig#get() directly for non-singleton ReferenceConfig instead of using ReferenceCache#get(ReferenceConfig)");
    }
    // 第一次或者不是单例,创建代理信息
    if (proxy == null) {
    	// 代理对象并不存储,存储的是引用与引用配置的映射
        List<ReferenceConfigBase<?>> referencesOfType = referenceTypeMap.computeIfAbsent(type, _t -> Collections.synchronizedList(new ArrayList<>()));
        referencesOfType.add(rc);
        List<ReferenceConfigBase<?>> referenceConfigList = referenceKeyMap.computeIfAbsent(key, _k -> Collections.synchronizedList(new ArrayList<>()));
        referenceConfigList.add(rc);
        // 真正创建代理的地方,还是由引用配置对象自己来实现的
        proxy = rc.get();
    }
    return proxy;
}

至此,结合Spring的项目,我们对服务引用的准备就完成了,接下来就是其核心内容——代理的创建

2. 创建代理的实现

紧接上文,我们看到了 rc.get() ,其实这里就是真正创建代理的地方

// ReferenceConfig.class
public T get() {
    if (destroyed) {
        throw new IllegalStateException("The invoker of ReferenceConfig(" + url + ") has already destroyed!");
    }
    if (ref == null) {
        // 确保模块已启动,兼容老版本
        getScopeModel().getDeployer().start();
        synchronized (this) {
            if (ref == null) {
                init();
            }
        }
    }
    return ref;
}

init() 方法内容较多,我们仅看关键的一行,此处的referenceParameters包含了引用所需要的各种信息

 ref = createProxy(referenceParameters);

在这里插入图片描述
那么我们再进入 createProxy 一探究竟

ProxyFactory proxyFactory = this.getExtensionLoader(ProxyFactory.class).getAdaptiveExtension();
private T createProxy(Map<String, String> referenceParameters) {
	// 创建本地调用器
    if (shouldJvmRefer(referenceParameters)) {
        createInvokerForLocal(referenceParameters);
    } else {
        urls.clear();

        meshModeHandleUrl(referenceParameters);

        if (StringUtils.isNotEmpty(url)) {
            // user specified URL, could be peer-to-peer address, or register center's address.
            parseUrl(referenceParameters);
        } else {
            // if protocols not in jvm checkRegistry
            if (!LOCAL_PROTOCOL.equalsIgnoreCase(getProtocol())) {
                aggregateUrlFromRegistry(referenceParameters);
            }
        }
    	// 创建远程调用器
        createInvokerForRemote();
    }

    URL consumerUrl = new ServiceConfigURL(CONSUMER_PROTOCOL, referenceParameters.get(REGISTER_IP_KEY), 0,
            referenceParameters.get(INTERFACE_KEY), referenceParameters);
    consumerUrl = consumerUrl.setScopeModel(getScopeModel());
    consumerUrl = consumerUrl.setServiceModel(consumerModel);
    // 发布订消费者元信息,注册到注册中心上
    MetadataUtils.publishServiceDefinition(consumerUrl, consumerModel.getServiceModel(), getApplicationModel());

    // 创建引用代理,此处的 proxyFactory 是Dubbo 的SPI接口,不是Spring提供的那个类
    return (T) proxyFactory.getProxy(invoker, ProtocolUtils.isGeneric(generic));
}

在这里插入图片描述
可以看到这又是一个自适应的代理工厂类,那么其具体采用那个实现类,就要看入参了,此刻我们采用的远程服务,使用的MigrationInvoker ,最终使用的是默认的 ”javassist“ ,即 org.apache.dubbo.rpc.proxy.javassist.JavassistProxyFactory 实现类,并最终拼接成操作一个代理类,并将其实例化
在这里插入图片描述
在这里插入图片描述
这中间通过javaassit生成的过程十分繁杂,我们直接将生成的代理进行反编译,其代码如下

class DemoServiceDubboProxy0
{
   public DemoServiceDubboProxy0();
   public DemoServiceDubboProxy0(java.lang.reflect.InvocationHandler);

    public void $destroy();
    public java.lang.String sayHello(java.lang.String);
    public java.lang.Object $echo(java.lang.Object);

   public static [Ljava.lang.reflect.Method; methods;
   private java.lang.reflect.InvocationHandler handler;
}

因此,我们知道了,代理内其实还是一个 MigrationInvoker 在执行操作,但是我们现在并不看其内部在干什么,留待讲调用的章节再来详谈,因此点到为止,完成了一个代理对象的创建。

3. 代理的获取

完成了代理对象的创建,工作并没有结束,我们在业务代码中,还需要获取代理。当然在Spring框架下,更贴切的叫法叫做注入。把代理对象注入我们业务代码中。如下图
在这里插入图片描述

我们首先要知道的是通过xml 或@DubboReference 注解标志的接口,最终会被解析成一个 ReferenceBean 对象。

public class ReferenceBean<T> implements FactoryBean<T>,
        ApplicationContextAware, BeanClassLoaderAware, BeanNameAware, InitializingBean, DisposableBean

因为其实现了FactoryBean 接口,所以我们应当能猜到重点并不是其本身,而是通过它的 getObject() 方法能获得什么东西。 (不了解FactoryBean 的看这里: BeanFactory 和 FactoryBean 的关联与区别)

public T getObject() {
        if (lazyProxy == null) {
            createLazyProxy();
        }
        return (T) lazyProxy;
}

果不其然,我们想要的东西很快就出现了,那就是代理对象的创建,我们关注到,这里其实提供的是一个惰性代理,这种设计是为了防止一些Bean过早产生,因为当时它的配置和环境未必已经准备完成,此时直接初始化该Bean可能会出现各种问题。(其实我们在开发中,也有很多时候会使用@Lazy 注解来达到类似的目的)那么这个代理是如何创建的呢?

private void createLazyProxy() {
	// 使用的是Spring提供的代理工厂
    ProxyFactory proxyFactory = new ProxyFactory();
    proxyFactory.setTargetSource(new DubboReferenceLazyInitTargetSource());
    proxyFactory.addInterface(interfaceClass);
    // 代理还需要实现一些额外的接口,此处是 EchoService.class, Destroyable.class
    Class<?>[] internalInterfaces = AbstractProxyFactory.getInternalInterfaces();
    for (Class<?> anInterface : internalInterfaces) {
        proxyFactory.addInterface(anInterface);
    }
    if (!StringUtils.isEquals(interfaceClass.getName(), interfaceName)) {
        //add service interface
        try {
            Class<?> serviceInterface = ClassUtils.forName(interfaceName, beanClassLoader);
            proxyFactory.addInterface(serviceInterface);
        } catch (ClassNotFoundException e) {
            // generic call maybe without service interface class locally
        }
    }
    // 创建代理
    this.lazyProxy = proxyFactory.getProxy(this.beanClassLoader);
}

我们这里要注意一个重点,我们为 proxyFactory 代理工厂设置了一个目标对象 DubboReferenceLazyInitTargetSource,熟悉 SpringAop 的朋友应该知道 proxyFactory 的目标对象就是真正执行方法的对象,代理只是在调用该对象的前后加入了一些操作。那么我们来看看 DubboReferenceLazyInitTargetSource 有什么特殊之处

// ReferenceBean 的内部类
private class DubboReferenceLazyInitTargetSource extends AbstractLazyCreationTargetSource {

    @Override
    protected Object createObject() throws Exception {
        return getCallProxy();
    }

    @Override
    public synchronized Class<?> getTargetClass() {
        return getInterfaceClass();
    }
}

我们可以看到该对象最大的特点是其实现了 AbstractLazyCreationTargetSource ,两个方法也是重写的父类的。限于主题与篇幅,我们此处不针对该父类详细展开,仅对其作用进行描述:

AbstractLazyCreationTargetSource是 Spring Framework 中的一个抽象类,它实现了
TargetSource 接口。它的主要作用是作为一个懒加载的代理对象,在第一次调用它时,通过回调方法创建并缓存目标对象。

换而言之,它就是一个临时工,正主有事它先占着位置,等到真正需要工作时它再去叫正主,换句话说,如果我们仅是获取代理,里面的目标对象就是这个,但是如果我们调用了代理的方法,它就会通过createObject方法来叫真正的对象,我们可以看到,它是通过getCallProxy来叫真正的对象

private Object getCallProxy() throws Exception {
	// 惰性代理的作用,只有当环境与配置都准备完毕,才会真正产生可用的代理目标
    if (referenceConfig == null) {
        throw new IllegalStateException("ReferenceBean is not ready yet, please make sure to call reference interface method after dubbo is started.");
    }
    // 对象的创建需要给容器上锁,放置死锁
    synchronized (((DefaultSingletonBeanRegistry)getBeanFactory()).getSingletonMutex()) {
        return referenceConfig.get();
    }
}
// ReferenceConfig ,即引用配置
public T get() {
    if (destroyed) {
        throw new IllegalStateException("The invoker of ReferenceConfig(" + url + ") has already destroyed!");
    }

    if (ref == null) {
        // ensure start module, compatible with old api usage
        getScopeModel().getDeployer().start();

        synchronized (this) {
            if (ref == null) {
                init();
            }
        }
    }

    return ref;
}

在这里插入图片描述

到这里,我们终于看到了返回的代理对象,即上文里的ref,而ref的诞生,我们在上一个小节,创建代理对象里已经讲过了。至此,业务代码如何获取的代理我们也看完了。

四、小结

通篇下来,我们看了大量的源码,并辅以文字解释,总算讲完了Dubbo是如何做服务引用的,一句话概括就是使用了代理,但是这个代理的设计与实现却很复杂,总结有三个原因:

  • 全面的SPI自适应设计,使得代理工厂有好几个可选择
  • 本地或者远程,会创建不同的invoker,即调用器
  • 为了和Spring配合,使用了惰性代理避免过早的初始化而出错

目前我们已经把服务引用和服务暴露都讲了,虽然还有大量的细节有待补充,但其核心流程已经说完,相当于一座大桥的两头我们已经搭好,下一次,我们将进行这座大桥的合拢————即看看服务调用

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/753566.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

2023/07/14 UML图/流程图/泳道图是什么

UML图 UML图中的几种图简介&#xff08;时序图&#xff0c;协作图&#xff0c;状态图&#xff0c;活动图&#xff0c;对象图&#xff09; 泳道图 适合做这种效果&#xff0c;体现角色关系 流程图 定义 绘制要素 开始/结束&#xff1a;用一个椭圆标识&#xff0c;代表流畅的开…

优维EasyOps产品使用最佳实践:Agent存活性监控

优维EasyOps平台内置Agent存活性监控啦&#xff01; Agent作为自动化/监控底层核心组件&#xff0c;它的可用性直接影响了上层功能的使用&#xff0c;故我们会非常关注它的状态。但如果有网络波动、Agent升级或机器故障等都可能导致Agent异常&#xff0c;这时用户希望这种异常…

初级 - 如何搭建一个Java Web项目 - 记录

目录 序言一、使用 Spring Initializr 创建创建一个Java 项目基本框架的方法1. 新建项目时&#xff0c;安装依赖理解Developer Tools 选项 &#xff01;Web 选项 &#xff01; 其他选项具体详情请最下面的参考链接&#xff0c;这里就不一一列举了&#xff0c;只筛选出笔者当前需…

科技政策 | 国家网信办等七部门联合公布《生成式人工智能服务管理暂行办法》

文 | BFT机器人 近日&#xff0c;国家网信办联合国家发展改革委、教育部、科技部、工业和信息化部、公安部、广电总局公布《生成式人工智能服务管理暂行办法》&#xff08;以下称《办法》&#xff09;&#xff0c;自2023年8月15日起施行。国家互联网信息办公室有关负责人表示&a…

零代码编程:用ChatGPT自动登陆微信公众号后台

要实现微信公众号后台自动登陆&#xff0c;可以使用ChatGPT来编写Python代码实现。 微信公众平台账号密码登陆&#xff0c;要先点击“使用账号登录”&#xff0c;源代码是&#xff1a;<a href"javascript:;" class"login__type__container__select-type"…

Python自动化之pytest常用插件

目录 1、失败重跑 pytest-rerunfailures 2、多重校验 pytest-assume 3、设定执行顺序 pytest-ordering 4、用例依赖&#xff08;pytest-dependency&#xff09; 5.分布式测试(pytest-xdist) 6.生成报告&#xff08;pytest-html&#xff09; 1、失败重跑 pytest-rerunfailu…

web 前端 Day 4

盒子模型 <style>div {width: 300px;height: 300px;background-color: pink;padding-left: 4px; 左侧内边距border: 3px solid red;margin: 50px;}</style> padding 内边距 </head> ​ <body> ​<div>cfdaffshydghjgdjdnjjjjjjjjjjjjjjj&l…

springboot网吧管理系统

着科学技术发展&#xff0c;电脑已成为人们生活中必不可少的生活办公工具&#xff0c;在这样的背景下&#xff0c;网络技术被应用到各个方面&#xff0c;为了提高办公生活效率&#xff0c;网络信息技术飞速发展。在这样的背景下人类社会进入了全新的信息化的时代。网吧管理一直…

Jenkins持续集成项目实践 —— 基于Python Selenium自动化测试(二)

上一篇讲了如何搭建jenkins&#xff0c;这篇主要讲&#xff0c;怎么将自动化代码与jenkins衔接起来 jenkins上运行的两种方式&#xff1a; 第一种&#xff0c;在jenkins上面运行本地代码&#xff0c;操作如下: 新建项目&#xff1a;项目名称根据自己项目情况填写并选择自由模…

【C语言初阶(16)】操作符2

文章目录 Ⅰ关系操作符Ⅱ 逻辑操作符⒈操作符介绍⒉短路求值 Ⅲ 条件操作符Ⅳ 逗号表达式Ⅴ 下标引用、函数调用和结构成员⒈[ ] 下标引用操作符⒉( ) 函数调用操作符⒊结构体成员访问操作符 Ⅵ 表达式求值⒈隐式类型转换&#xff08;整型提升&#xff09;⒉算术转换⒊操作符的…

精品项目源码第52期运动会管理系统(代号V052)

精品项目源码第52期运动会管理系统(代号V052) 大家好&#xff0c;小辰今天给大家介绍一个运动会管理系统&#xff0c;演示视频公众号&#xff08;小辰哥的Java&#xff09;对号查询观看即可 文章目录 精品项目源码第52期运动会管理系统(代号V052)难度指数&#xff08;中高等&…

uboot、kernel启动过程分析

00、uboot的宏观启动 第1种&#xff1a;bootROM读取SPL到片内RAM&#xff0c;SPL初始化DDR&#xff0c;SPL把uboot程序copy到DDR&#xff0c;uboot启动进行必要外设初始化、自我拷贝、重定位等。 第2种&#xff1a;bootROM直接读取uboot的头部信息&#xff08;IVT、DCD&#xf…

python详解(8)——进阶(2):初步算法

目录 &#x1f3c6;一、前言 &#x1f3c6;二、时间复杂度 &#x1f3c6;三、递推 &#x1f6a9;1.简介 &#x1f6a9;2.爬楼梯 &#x1f6a9;3、猴子吃桃 &#x1f3c6;四、递归 &#x1f6a9;1、简介 &#x1f6a9;2、递归求斐波那契数列 &#x1f6a9;3、递归求阶乘 &#x…

【Git】Git 拉取的快速方法(含项目示例)

文章目录 一、问题的提出二、问题的尝试解决 一、问题的提出 在我们之前的拉取中&#xff0c;速度可能比较慢&#xff0c;例如&#xff0c;我们要拉取CLIP的项目。 (ldm) rootI1385efcc2300601b29:/hy-tmp/latent-diffusion# pip install githttps://github.com/openai/CLIP.…

Redis 从入门到精通【进阶篇】之高可用集群(Redis Cluster)详解

文章目录 0. 前言设计目标核心概念 1. 架构设计和原理1.1. 数据分片2. 节点间通信6. 扩容和缩容 2. 总结3. Redis从入门到精通系列文章4. Redis Cluster面试题4.1. Redis Cluster如何进行扩容和缩容&#xff1f;4.2. Redis Cluster如何进行故障转移&#xff1f;4.3. Redis Clus…

【计算机视觉 | 图像分类】arxiv 计算机视觉关于图像分类的学术速递(7 月 14 日论文合集)

文章目录 一、分类|识别相关(10篇)1.1 Video-FocalNets: Spatio-Temporal Focal Modulation for Video Action Recognition1.2 Watch Your Pose: Unsupervised Domain Adaption with Pose based Triplet Selection for Gait Recognition1.3 YOLIC: An Efficient Method for Obj…

【JavaEE】HTTP请求的构造

目录 1、通过form表单构造HTTP请求 2、通过JS的ajax构造HTTP请求 3、Postman的安装和简单使用 常见的构造HTTP请求的方式有一下几种&#xff1a; 直接通过浏览器的地址栏&#xff0c;输入一个URL&#xff0c;就可以构造一个GET请求HTML中的一些特殊标签&#xff0c;也会触发…

【Linux】1、装机、装操作系统、部署

文章目录 一、装系统1.0 格式化 U 盘1.1 做启动盘1.1.2 rufus1.1.2 poweriso 1.2 安装步骤 二、恢复系统2.1 BootManager2.2 recovery mode 一、装系统 下载地址&#xff1a; http://old-releases.ubuntu.com/releases/16.04.5/ubuntu-16.04.5-server-amd64.isohttps://mirro…

基于STM32 ARM+FPGA伺服控制系统(二)软件及FPGA设计

完整的伺服系统所包含的模块比较多&#xff0c;因此无法逐一详细介绍&#xff0c;所以本章着重介绍 设计难度较高的 FPGA 部分并简单介绍 ARM 端的工作流程。 FPGA 部分主要有 FOC 算法、电流采样算法及编码器采样算法&#xff0c;是整个控制系统的基础&#xff0c;直接…

本地appserv外挂网址如何让外网访问?快解析端口映射

一、appserv是什么&#xff1f; AppServ 是 PHP 网页架站工具组合包&#xff0c;作者将一些网络上免费的架站资源重新包装成单一的安装程序&#xff0c;以方便初学者快速完成架站&#xff0c;AppServ 所包含的软件有&#xff1a;Apache[、Apache Monitor、PHP、MySQL、phpMyAdm…