设计模式学习笔记 - 项目实战一:设计实现一个支持各种算法的限流框架(实现)

概述

上篇文章,我们介绍了如何通过合理的设计,来实现框架的功能性需求的同时,满足易用、易扩展、灵活、低延迟、高容错等非功能性需求。在设计的过程中,我们也借鉴了之前讲过的一些开源项目的设计思想。比如 Spring 的低侵入松耦合、约定优于配置等设计思想,还借鉴了 Mybatis 通过 Mybatis-Spring 类库将框架的易用性做到极致等设计思路。

本章,我们讲解这样一个问题,针对限流框架的开发,如何做高质量的代码。说具体点就是,如何利用之前讲过的设计思想、原则、模式、编码规范、重构技巧等,写出易用、易扩展、易维护、灵活、简洁、可复用、易测试的代码。


V1 版本功能需求

前面提到,优秀的代码是重构出来的,复杂的代码也是慢慢堆砌出来的。小不快跑、逐步迭代是我比较推崇的开发模式。所以,针对限流框架,我们也不用已下载就做的大而全。况且,受章节篇幅所限,也不可能将一个大而全的代码阐述清楚。所以,我们可以先实现一个包含核心功能、基本功能的 V1 版本。

针对上两篇文章给出的需求和设计,我们重新梳理一下。看看有哪些功能要放到 V1 版本中实现。

在 V1 版本中:

  • 对于接口类型,我们只支持 HTTP 接口(也就是 URL)的限流,暂时不支持 RPC 等其他类型的接口限流。
  • 对于限流规则,我们只支持本地文件配置,配置文件格式只支持 YAML。
  • 对于限流算法,我们只支持固定时间窗口算法。
  • 对于限流模式,我们只支持单机限流。

尽管功能 “裁剪” 之后,V1 版本实现起来简单多了,但在编程开发的同时,我们还要考虑代码的扩展性,预留好扩展点。这样,在接下来的新版本开发中,我们才能够轻松地扩展新的限流算法、限流模式、限流规则和数据源。

最小原型代码

上篇文章讲到,项目实战中的实现等于面向对象设计加实现。而面向对象设计与实现一般可以分为四个步骤:划分职责识别类、定义属性和方法、定义类之间的交互关系、组装类并提供执行入口。在《实践:如何进行面向对象分析、设计与编码》中,我们还带你使用这个方法,设计和实现了一个接口鉴权框架。

不过,前面也讲过,在平时的工作中,大部分程序员都是边写代码边做设计,边思考重构,并不会严格的按照步骤,先做完类的设计再去写代码。而且,如果一下子把类设计得很好、和合理,也是比较难的。所以,我的习惯是,先完全不考虑设计和代码质量,先把功能写完,先把基本的流程走通,哪怕所有的代码都写在一个类中也无所谓。然后,我们在对这个 **MVP 代码(最小原型代码)**做优化重构,比如,将代码中比较独立的代码抽离出来,定义成独立的类或函数。

我们先按照 MVP 代码的思路,把代码实现出来。它的目录结构如下所示。代码非常简单,只包含 5 个类,接下来,我们针对每个类一一讲解下。

com.ratelimiter
  --RateLimiter
com.ratelimiter.rule
  --ApiLimit
  --RuleConfig
  --RateLimitRule
com.ratelimiter.alg
  --RateLimitAlg

先看下 RateLimiter 类

public class RateLimiter {
    private static final Logger log = LoggerFactory.getLogger(RateLimiter.class);
    // 为每个api在内存中存储限流计数器
    private Map<String, RateLimitAlg> counters = new ConcurrentHashMap<>();
    private RateLimitRule rule;

    public RateLimiter() {
        // 根据限流规则配置文件ratelimiter-rule.yaml中的内容读取到RuleConfig中
        InputStream in = null;
        RuleConfig ruleConfig = null;
        try {
            in = this.getClass().getResourceAsStream("/ratelimiter-rule.yaml");
            if (in != null) {
                Yaml yaml = new Yaml();
                ruleConfig = yaml.loadAs(in, RuleConfig.class);
            }
        } finally {
            if (in != null) {
                try {
                    in.close();
                } catch (IOException e) {
                    log.error("close file error:", e);
                }
            }
        }

        // 将限流规则构建成支持快速查找的数据结构RateLimitRule
        this.rule = new RateLimitRule(ruleConfig);
    }

    public boolean limit(String appId, String url) {
        ApiLimit apiLimit = rule.getLimit(appId, url);
        if (apiLimit == null) {
            return true;
        }

        // 获取api对应在内存中的限流计数器(rateLimitCounter)
        String counterKey = appId + ":" + apiLimit.getApi();
        RateLimitAlg rateLimitCounter = counters.get(counterKey);
        if (rateLimitCounter == null) {
            RateLimitAlg newRateLimitAlg = new RateLimitAlg(apiLimit.getLimit());
            rateLimitCounter = counters.putIfAbsent(counterKey, newRateLimitAlg);
            if (rateLimitCounter == null) {
                rateLimitCounter = newRateLimitAlg;
            }
        }

        // 判断是否限流
        return rateLimitCounter.tryAcquire();
    }
}

RateLimiter 类用来串联整个限流流程。它先读取限流规则读取我呢间,映射为内存中的 Java 对象(RuleConfig),然后再将这个中间结构构建成一个支持快速查询的数据结构(RateLimitRule)。此外,这个类还提供用户直接使用的最顶层接口(limit() 接口)。

再来看下 RuleConfig 和 ApiLimit 两个类

public class RuleConfig {
    private List<AppRuleConfig> configs;

    public List<AppRuleConfig> getConfigs() {
        return configs;
    }

    public void setConfigs(List<AppRuleConfig> configs) {
        this.configs = configs;
    }

    public static class AppRuleConfig {
        private String appId;
        private List<ApiLimit> limits;

        public AppRuleConfig() {
        }

        public AppRuleConfig(String appId, List<ApiLimit> limits) {
            this.appId = appId;
            this.limits = limits;
        }

        public String getAppId() {
            return appId;
        }

        public void setAppId(String appId) {
            this.appId = appId;
        }

        public List<ApiLimit> getLimits() {
            return limits;
        }

        public void setLimits(List<ApiLimit> limits) {
            this.limits = limits;
        }
    }
}

public class ApiLimit {
    private static final int DEFAULT_TIME_UNIT = 1; // 1 second
    private String api;
    private int limit;
    private int unit = DEFAULT_TIME_UNIT;

    public ApiLimit() {
    }

    public ApiLimit(String api, int limit, int unit) {
        this.api = api;
        this.limit = limit;
        this.unit = unit;
    }

    public String getApi() {
        return api;
    }

    public void setApi(String api) {
        this.api = api;
    }

    public int getLimit() {
        return limit;
    }

    public void setLimit(int limit) {
        this.limit = limit;
    }

    public int getUnit() {
        return unit;
    }

    public void setUnit(int unit) {
        this.unit = unit;
    }
}

从代码中,可以看出,RuleConfig 类嵌套了另外两个类 AppRuleConfigApiLimit。这三个类跟配置文件的三层嵌套结构完全对应。我把对应关系标注在了下面的示例中。

configs:                 <!--对应RuleConfig-->
- appId: app-1           <!--对应AppRuleConfig-->
  limits:
    - api: /v1/user      <!--对应ApiLimit-->
      limit: 100
      unit: 60
    - api: /v1/order
      limit: 50
- appId: app-2
  limits:
    - api: /v1/user
      limit: 50
    - api: /v1/order
      limit: 50

再看下 RateLimitRule 这个类

你可能会好奇,有了 RuleConfig 来存储限流规则,为什么还要 RateLimitRule 类呢?这是因为,限流过程中会频繁的查询接口对应地限流规则,为了尽可能提提高查询速度,我们需要将限流规则组织成一种支持按照 URL 快速查询的数据结构。考虑到 URL 的复杂度比较高,且需要按照前缀来匹配,我们这里选择使用 Trie 树这种数据结构。下面我举了个例子来解释下,如下图所示。左边的限流规则对应到 Trie 树,就是图片中的样子。

在这里插入图片描述
RateLimitRule 的实现代码比较多,这里只给出了定义。如果你感兴趣的话,可以自己实现下。

public class RateLimitRule {
    public RateLimitRule(RuleConfig ruleConfig) {
        // ...
    }

    public ApiLimit getLimit(String appId, String url) {
        ApiLimit apiLimit = null;
        // ...
        return apiLimit;
    }
}

最后,看下 RateLimitAlg 这个类

这个类是限流算法实现类。它实现了最简单的固定时间窗口限流算法。每个接口都要在内存中对应一个 RateLimitAlg 类的代码如下所示。对于代码的逻辑,你可以看上篇文章对固定时间窗口限流算法的讲解。

public class RateLimitAlg {
    /* timeout for {@code Lock.tryLock()} */
    private static final long TRY_LOCK_TIMEOUT = 200L; // 200 ms
    private Stopwatch stopwatch;
    private AtomicInteger currentCount = new AtomicInteger(0);
    private final int limit;
    private Lock lock = new ReentrantLock();

    public RateLimitAlg(int limit) {
        this(limit, Stopwatch.createStarted());
    }

    @VisibleForTesting
    public RateLimitAlg(int limit, Stopwatch stopwatch) {
        this.limit = limit;
        this.stopwatch = stopwatch;
    }


    public boolean tryAcquire() {
        int updatedCount = currentCount.incrementAndGet();
        if (updatedCount <= limit) {
            return true;
        }
        try {
            if (lock.tryLock(TRY_LOCK_TIMEOUT, TimeUnit.MICROSECONDS)) {
                if (stopwatch.elapsed(TimeUnit.MICROSECONDS) > TimeUnit.SECONDS.toMillis(1)) {
                    // 过了固定时间窗口,要重置计数器和时间统计
                    currentCount.set(0);
                    stopwatch.reset();
                }
                updatedCount = currentCount.incrementAndGet();
                return updatedCount <= limit;
            } else {
                throw new InternalErrorException("tryAcquire() wati lock too long: " + TRY_LOCK_TIMEOUT + " ms");
            }
        } catch (InterruptedException e) {
            throw new InternalErrorException("tryAcquire() is interrupted by timeout.", e);
        }
    }
}

Review 最小原型代码

刚刚给出的 MVP 代码,虽然代码量不多,但已经实现了 V1 版本中规划的功能。不过,从代码质量角度来看你,它还有很多值得优化的地方。现在,我们站在一个 Code Reviewer 的角度,来分析下这段代码的设计和实现。

接个 SOLID、DRY、KISS、LOD、基于接口而非实现编程、高内聚松耦合等经典的设计思想和原则,以及编码规范,我们从代码质量评判标准的角度重点剖析一下,这段代码在可读性扩展性等方面的表现,比如复用性、可测试性等。

首先,我们来看下代码的可读性

影响代码可读性的因素有很多。我们重点关注:

  • 目录设计(package 包)是否合理
  • 模块划分是否清晰
  • 代码结构是否符合高内聚低耦合
  • 以及是否符合统一的编码规范这几点。

因为涉及的代码不多,目录结构前面也给出了,总体来说比较简单,所以目录设计、包的划分没有问题

按照上篇文章的模块划分:

  • RuleConfigApiLimitRateLimitRule 属于 “限流规则模块”,负责限流规则的构建和查询。
  • RateLimitAlg 属于 “限流算法” 模块,提供了基于内存的单机固定时间窗口限流算法。
  • RateLimiter 属于 “集成使用” 模块,作为最顶层类,组装其他类,提供执行入口(也就是调用入口)。

    不过,RateLimiter 作为执行入口,我们希望它只是负责组装工作,而不应该包含具体的业务逻辑,所以,RateLimiter 类中,从配置文件中读取限流规则这块逻辑,应该拆分出来,设计成独立的类

如果,我们把类与类之间的依赖关系图画出来,你会发现,它们之间的依赖关系很简单,每个类职责也比较单一,所以类的设计满足单一职责原则、LOD 迪米特法则、高内聚松耦合的要求

从编码规范上来讲,没有超级大的类、函数、代码块类、函数、变量的命名基本达意,也符合最小惊奇原则。虽然,有些命名不能一眼就看出是干啥的,有些命名命名采用了缩写,比如 RateLimitAlg,但我们起码能猜个八九不离十,很容易理解和记忆。

总结一下,在最小原型代码中,目录设计、代码结构、模块划分、类的设计还算合理清理,基本符合编码规范,代码的可读性不错!

其次,再来看下代码的扩展性

实际上,这段代码最大的问题就是它的扩展性,也是我们最关注的,比较后续还有更多版本的迭代开发。编写可扩展代码,关键是建立扩展意识。这就像下象棋,我们要往前想几步,为以后做准备。在写代码时,我们要时刻思考,这段代码如果要扩展新的功能,那是否可以在尽量少改动代码的情况下完成,还是需要大动干戈,推到重写。

具体到 MVP 代码,不易扩展的最大原因是,没有遵循基于接口而非实现的编程思想,没有接口抽象意识。比如,RateLimitAlg 类只是实现了固定时间窗口限流算法,没有提炼出更加抽象的算法接口。如果我们要替换其他限流算法,就要改动比较多的代码。其他类的设计也同样有问题,比如 RateLimitRule

此外,RateLimiter 类中,配置文件的名称、路径,是硬编码在代码中的。尽管我们说约定优于配置,但也要兼顾灵活性,能够让用户在需要的时候,自定义配置文件名称、路径。而且,配置我呢间的格式目前也只支持 Yaml,之后扩展其他格式,需要对这部分代码做很大的改动。

重构最小原型代码

根据刚刚对 MVP 代码的剖析,我们发现它的可读性没有太大问题,问题主要在于可扩展性。主要的修改有两个,一个是将 RateLimiter 中的规则配置文件的读取解析逻辑拆出来,设计成独立的类,另一个是参照基于接口而非实现编程思想,对于 RateLimitAlgRateLimitRule 类提炼抽象接口。

按照这个修改思路,我们对代码进行重构。重构之后的目录结构如下所示。我们对每个类稍微都做了说明,你可以对比着重构前的目录来看。

// 重构前
com.ratelimiter
  --RateLimiter
com.ratelimiter.rule
  --ApiLimit
  --RuleConfig
  --RateLimitRule
com.ratelimiter.alg
  --RateLimitAlg

// 重构后
com.ratelimiter
  --RateLimiter(有所修改)
com.ratelimiter.rule
  --ApiLimit(不变)
  --RuleConfig(不变)
  --RateLimitRule(抽象接口)
  --TrieRateLimitRule(实现类,就是重构前的RateLimitRule)
com.ratelimiter.rule.parse
  --RuleConfigParser(抽象接口)
  --YamlRuleConfigParser(Yaml格式配置文件解析类)
  --JsonRuleConfigParser(Json格式配置文件解析类)
com.ratelimiter.rule.datasource
  --RuleConfigSource(抽象接口)
  --FileRuleConfigSource(基于本地文件的配置类)
com.ratelimiter.alg
  --RateLimitAlg(抽象接口)
  --FixedTimeWinRateLimitAlg(实现类,就是重构前的RateLimitAlg)

其中,RateLimiter 类重构之后的代码如下所示。代码的改动集中在构造函数中,通过调用 RuleConfigSource 来实现了限流规则配置文件的加载。

public class RateLimiter {
    private static final Logger log = LoggerFactory.getLogger(RateLimiter.class);
    // 为每个api在内存中存储限流计数器
    private Map<String, RateLimitAlg> counters = new ConcurrentHashMap<>();
    private RateLimitRule rule;

    public RateLimiter() {
        // 主要改动在这里:调用RuleConfigSource类来实现配置加载
        RuleConfigSource configSource = new FileRuleConfigSource();
        RuleConfig ruleConfig = configSource.load();
        this.rule = new TrieRateLimitRule(ruleConfig);
    }

    public boolean limit(String appId, String url) {
        ApiLimit apiLimit = rule.getLimit(appId, url);
        if (apiLimit == null) {
            return true;
        }
        String counterKey = appId + ":" + apiLimit.getApi();
        RateLimitAlg rateLimitCounter = counters.get(counterKey);
        if (rateLimitCounter == null) {
            // 这里也有一点点改动
            RateLimitAlg newRateLimitAlg = new FixedTimeWinRateLimitAlg(apiLimit.getLimit());
            rateLimitCounter = counters.putIfAbsent(counterKey, newRateLimitAlg);
            if (rateLimitCounter == null) {
                rateLimitCounter = newRateLimitAlg;
            }
        }
        return rateLimitCounter.tryAcquire();
    }
}

再来看下,从 RateLimiter 中拆分出来的限流规则加载的逻辑,现在是如何设计的。这部分涉及的类主要是下面几个。其中,各个 ParserRuleConfigSource 类的设计都有点类似策略模式,如果要添加新的格式的解析,只需要实现对应的 Parser 类,并添加到 FileRuleConfigSource 类的 PARSER_MAP 中就可以了。

com.ratelimiter.rule.parse
  --RuleConfigParser(抽象接口)
  --YamlRuleConfigParserYaml格式配置文件解析类)
  --JsonRuleConfigParserJson格式配置文件解析类)
com.ratelimiter.rule.datasource
  --RuleConfigSource(抽象接口)
  --FileRuleConfigSource(基于本地文件的配置类)




public interface RuleConfigParser {
    RuleConfig parse(String configText);
    RuleConfig parse(InputStream in);
}

public class YamlRuleConfigParser implements RuleConfigParser {
    @Override
    public RuleConfig parse(String configText) {
        try {
            return new Yaml().loadAs(configText, RuleConfig.class);
        } catch (Exception e) {
            throw new InternalErrorException("Parse yaml text[" + configText + "] config error", e);
        }
    }

    @Override
    public RuleConfig parse(InputStream in) {
        try {
            return new Yaml().loadAs(in, RuleConfig.class);
        } catch (Exception e) {
            throw new InternalErrorException("Parse yaml file config error", e);
        }
    }
}

public class JsonRuleConfigParser implements RuleConfigParser {
    @Override
    public RuleConfig parse(String configText) {
        try {
            return JSON.parseObject(configText, RuleConfig.class);
        } catch (Exception e) {
            throw new InternalErrorException("Parse json text[" + configText + "] config error", e);
        }
    }

    @Override
    public RuleConfig parse(InputStream in) {
        try {
            return JSON.parseObject(in, RuleConfig.class);
        } catch (IOException e) {
            throw new InternalErrorException("Parse json file config error", e);
        }
    }
}

public interface RuleConfigSource {
    RuleConfig load();
}

public class FileRuleConfigSource implements RuleConfigSource {
    private static final Logger logger = LoggerFactory.getLogger(FileRuleConfigSource.class);

    private static final String API_LIMIT_CONFIG_NAME = "ratelimiter-rule";
    private static final String YAML_EXTENSION = "yaml";
    private static final String YML_EXTENSION = "yml";
    private static final String JSON_EXTENSION = "json";

    private static final Map<String, RuleConfigParser> PARSER_MAP = new HashMap<>();

    static {
        PARSER_MAP.put(YAML_EXTENSION, new YamlRuleConfigParser());
        PARSER_MAP.put(YML_EXTENSION, new YamlRuleConfigParser());
        PARSER_MAP.put(JSON_EXTENSION, new JsonRuleConfigParser());
    }

    @Override
    public RuleConfig load() {
        for (String extension : PARSER_MAP.keySet()) {
            InputStream in = null;
            try {
                in = this.getClass().getResourceAsStream("/" + getFileNameByExt(extension));
                if (in != null) {
                    RuleConfigParser configParser = PARSER_MAP.get(extension);
                    return configParser.parse(in);
                }
            } finally {
                if (in != null) {
                    try {
                        in.close();
                    } catch (IOException e) {
                        logger.error("close file error:", e);
                    }
                }
            }
        }
        return null;
    }

    private String getFileNameByExt(String extension) {
        return API_LIMIT_CONFIG_NAME + "." + extension;
    }
}

此外,抽象的接口相关代码如下所示。

public interface RateLimitRule {
    ApiLimit getLimit(String appId, String url);
}

public class TrieRateLimitRule implements RateLimitRule {
    public TrieRateLimitRule(RuleConfig ruleConfig) {
        // 由你来实现...
    }

    @Override
    public ApiLimit getLimit(String appId, String url) {
        ApiLimit apiLimit = null;
        // 由你来实现...
        return apiLimit;
    }
}

public interface RateLimitAlg {
    boolean tryAcquire();
}

public class FixedTimeWinRateLimitAlg implements RateLimitAlg {
    /* timeout for {@code Lock.tryLock()} */
    private static final long TRY_LOCK_TIMEOUT = 200L; // 200 ms
    private Stopwatch stopwatch;
    private AtomicInteger currentCount = new AtomicInteger(0);
    private final int limit;
    private Lock lock = new ReentrantLock();

    public FixedTimeWinRateLimitAlg(int limit) {
        this(limit, Stopwatch.createStarted());
    }

    @VisibleForTesting
    public FixedTimeWinRateLimitAlg(int limit, Stopwatch stopwatch) {
        this.limit = limit;
        this.stopwatch = stopwatch;
    }

    @Override
    public boolean tryAcquire() {
        int updatedCount = currentCount.incrementAndGet();
        if (updatedCount <= limit) {
            return true;
        }
        try {
            if (lock.tryLock(TRY_LOCK_TIMEOUT, TimeUnit.MICROSECONDS)) {
                if (stopwatch.elapsed(TimeUnit.MICROSECONDS) > TimeUnit.SECONDS.toMillis(1)) {
                    // 过了固定时间窗口,要重置计数器和时间统计
                    currentCount.set(0);
                    stopwatch.reset();
                }
                updatedCount = currentCount.incrementAndGet();
                return updatedCount <= limit;
            } else {
                throw new InternalErrorException("tryAcquire() wait lock too long: " + TRY_LOCK_TIMEOUT + " ms");
            }
        } catch (InterruptedException e) {
            throw new InternalErrorException("tryAcquire() is interrupted by timeout.", e);
        }
    }
}

总结

优秀的代码都是重构出来的,复杂的代码是慢慢堆砌出来的。小步快跑、逐步迭代是我个人比较推崇的开发模式。追求完美主义会让我们迟迟无法下手。所以,为了克服这个问题,一方面,我们可以规划多个小版本来开发;另一方面,在编程实现的过程中,我们可以先实现 MVP 代码,并以此来重构优化

如果对 MVP 代码优化重构?

我们站在 Code Reviewer 的角度结合 SOLID、DRY、KISS、LOD、基于接口而非实现编程、高内聚松耦合等经典的设计思想和原则,以及编码规范,从代码质量评判标准的角度,来剖析代码在可读性、扩展性、可维护性、灵活、简洁、复用性、可测试性等方面的表现,并且针对性地去优化不足

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

细致讲解——不同类型LSA是作用以及相互之间的联系

目录 一.常见的LSA类型 二.OSPF特殊区域 1.区域类型 2.stub区域和totally stub区域 &#xff08;1&#xff09;stub区域 &#xff08;2&#xff09;totally stub区域 3.nssa区域和totally nssa区域 &#xff08;1&#xff09;nssa区域 &#xff08;2&#xff09;totall…

【Android】SharedPreferences阻塞问题深度分析

前言 Android中SharedPreferences已经广为诟病&#xff0c;它虽然是Android SDK中自带的数据存储API&#xff0c;但是因为存在设计上的缺陷&#xff0c;在处理大量数据时很容易导致UI线程阻塞或者ANR&#xff0c;Android官方最终在Jetpack库中提供了DataStore解决方案&#xf…

微信小程序使用echarts实现条形统计图功能

微信小程序使用echarts组件实现条形统计图功能 使用echarts实现在微信小程序中统计图的功能&#xff0c;其实很简单&#xff0c;只需要简单的两步就可以实现啦&#xff0c;具体思路如下&#xff1a; 引入echarts组件调用相应的函数方法 由于需要引入echarts组件&#xff0c;代…

.net报错异常及常用功能处理总结(持续更新)

.net报错异常及常用功能处理总结---持续更新 1. WebApi dynamic传参解析结果中ValueKind Object处理方法问题描述方案1&#xff1a;(推荐&#xff0c;改动很小)方案2&#xff1a; 2.C# .net多层循环嵌套结构数据对象如何写对象动态属性赋值问题描述JavaScript动态属性赋值.net…

WebSocket通信协议

WebSocket是一种网络通信协议.RFC6455定义了它的通信标准 WebSocket是HTML5开始提供的一种在单个TCP连接上进行全双向通信的协议 HTTP协议是一种无状态的,无连接的,单向的应用层协议.它采用了请求,响应的模式.通信请求只能由客户端发起,服务端对请求做出应答处理. 这种模型有…

PO框架【自动化测试】

对象&#xff1a;Tpshop商城 需求&#xff1a;更换头像 操作步骤&#xff1a; 个人信息–头像–上传图片–图片确认–确认保存 核心代码&#xff1a; # 进入frame框架[不熟] driver.switch_to.frame(driver.find_element_by_xpath(//*[id"layui-layer-iframe1"]))…

物联网实战--平台篇之(一)架构设计

本项目的交流QQ群:701889554 物联网实战--入门篇https://blog.csdn.net/ypp240124016/category_12609773.html 物联网实战--驱动篇https://blog.csdn.net/ypp240124016/category_12631333.html 一、平台简介 物联网平台这个概念比较宽&#xff0c;大致可以分为两大类&#x…

为什么要学音视频?

一直都在说“科技改变生活”&#xff0c;现实告诉我们这是真的。 随着通信技术和 5G 技术的不断发展和普及&#xff0c;不仅拉近了人与人之间的距离&#xff0c;还拉近了人与物&#xff0c;物与物之间的距离&#xff0c;万物互联也变得触手可及。 基于此背景下&#xff0c;音…

C++面经(简洁版)

1. 谈谈C和C的认识 C在C的基础上添加类&#xff0c;C是一种结构化语言&#xff0c;它的重点在于数据结构和算法。C语言的设计首要考虑的是如何通过一个过程&#xff0c;对输入进行运算处理得到输出&#xff0c;而对C&#xff0c;首先要考虑的是如何构造一个对象&#xff0c;通…

Node.js -- 包管理工具

文章目录 1. 概念介绍2. npm2.1 npm 下载2.2 npm 初始化包2.3 npm 包(1) npm 搜索包(2) npm 下载安装包(3) require 导入npm 包的基本流程 2.4 开发依赖和生产依赖2.5 npm 全局安装(1) 修改windows 执行策略(2) 环境变量Path 2.6 安装包依赖2.7 安装指定版本的包2.8 删除依赖2.…

jenkins教程

jenkins 一、简介二、下载安装三、配置jdk、maven和SSH四、部署微服务 一、简介 Jenkins是一个流行的开源自动化服务器&#xff0c;用于自动化软件开发过程中的构建、测试和部署任务。它提供了一个可扩展的插件生态系统&#xff0c;支持各种编程语言和工具。 Jenkins是一款开…

PotatoPie 4.0 实验教程(27) —— FPGA实现摄像头图像拉普拉斯边缘提取

拉普拉斯边缘提取有什么作用&#xff1f; 拉普拉斯边缘检测是一种常用的图像处理技术&#xff0c;用于检测图像中的边缘和边界。它的主要作用包括&#xff1a; 边缘检测&#xff1a;拉普拉斯算子可以帮助检测图像中的边缘&#xff0c;即图像中亮度快速变化的位置。这些边缘通常…

前端HTML5学习2(新增多媒体标签,H5的兼容性处理)

前端HTML5学习2新增多媒体标签&#xff0c;H5的兼容性处理&#xff09; 分清标签和属性新增多媒体标签新增视频标签新增音频标签新增全局属性 H5的兼容性处理 分清标签和属性 标签&#xff08;HTML元素&#xff09;和属性&#xff0c;标签定义了内容的类型或结构&#xff0c;而…

RocketMQ 消息重复消费

现象 触发消息后&#xff0c;在1s内收到了两次消息消费的日志。 消息消费日志重复&#xff0c;reconsumeTimes0&#xff0c;主机实例也不同&#xff0c;说明是同一条消息被消费了两次 分析 生产者发送消息的时候使用了重试机制&#xff0c;发送消息后由于网络原因没有收到MQ…

永磁同步电机PMSM负载状态估计simulink模型

永磁同步电机PMSM负载状态估计simulink模型&#xff0c;龙伯格观测器&#xff0c;各种卡尔曼滤波器&#xff0c;矢量控制&#xff0c;坐标变换&#xff0c;永磁同步电机负载转矩估计&#xff0c;pmsm负载转矩测量&#xff0c;负载预测&#xff0c;转矩预测的matlab/simulink仿真…

【C++】---STL容器适配器之queue

【C】---STL容器适配器之queue 一、队列1、队列的性质 二、队列类1、队列的构造2、empty()3、push()4、pop()5、size()6、front()7、back() 三、队列的模拟实现1、头文件&#xff08;底层&#xff1a;deque&#xff09;2、测试文件3、底层&#xff1a;list 一、队列 1、队列的…

【NR RedCap】Release 18标准中对5G RedCap的增强

博主未授权任何人或组织机构转载博主任何原创文章&#xff0c;感谢各位对原创的支持&#xff01; 博主链接 本人就职于国际知名终端厂商&#xff0c;负责modem芯片研发。 在5G早期负责终端数据业务层、核心网相关的开发工作&#xff0c;目前牵头6G技术研究。 博客内容主要围绕…

R语言贝叶斯方法在生态环境领域中的应用

贝叶斯统计已经被广泛应用到物理学、生态学、心理学、计算机、哲学等各个学术领域&#xff0c;其火爆程度已经跨越了学术圈&#xff0c;如促使其自成统计江湖一派的贝叶斯定理在热播美剧《The Big Bang Theory》中都要秀一把。贝叶斯统计学即贝叶斯学派是一门基本思想与传统基于…

使用微信开发者工具模拟微信小程序定位

哈喽&#xff0c;各位同僚们&#xff0c;我们平时在测试微信小程序的时候&#xff0c;如果小程序中有获取定位或者地图的功能&#xff0c;测试场景中常常需要去模拟不同的位置&#xff0c;例如我们模拟在电子围栏的外面、里面和边界区域等。那么&#xff0c;我们如何在模拟微信…

[笔试训练](八)

目录 022&#xff1a;求最小公倍数 023&#xff1a;数组中的最长连续子序列 024&#xff1a;字母收集 022&#xff1a;求最小公倍数 求最小公倍数_牛客题霸_牛客网 (nowcoder.com) 题目&#xff1a; 题解&#xff1a; 求最小公倍数公式&#xff1a;lcm(a,b)a*b/gcd(a,b)&am…