P4. 微服务: 匹配系统(下)

P4. 微服务: 匹配系统 下

    • 0 概述
    • 1 游戏同步系统
      • 1.1 游戏同步的设计
      • 1.2 游戏同步的实现
    • 2 匹配系统微服务的实现
      • 2.1 微服务概述
      • 2.2 匹配系统接口url的实现
      • 2.3 微服务之间的通信
      • 2.4 匹配逻辑的实现
      • 2.5 匹配系统的权限控制
    • 3 bug的解决
      • 3.1 自己匹配自己
      • 3.2 断开连接问题

0 概述

  • 本章是匹配系统的后篇,前篇见P4. 微服务: 匹配系统(上),前篇主要介绍了前后端之间的 websocket 通信,后篇主要介绍了整个游戏系统的实现,以及如何实现匹配系统独立成微服务Spring 中微服务之间如何进行通信如何进行 url 的访问权限控制
  • 整个匹配系统相对来说还是比较复杂的,涉及到每个模块内部的逻辑,各个模块之间的通信(前后端的websocket,微服务之间的restTemplate),如何新开一个线程,新开的线程如何加锁保证不出现冲突,等等。
  • 检验是否完全理解整个模块,我认为需要自己能够自行编码实现匹配系统和游戏同步系统,知道每个工具怎么使用,包括 websocket, Thread, restTemplate, ReentrantLock 等等。

1 游戏同步系统

1.1 游戏同步的设计

在上一节P4. 微服务: 匹配系统(上)的末尾实现了蛇和棋盘的同步,也就是匹配在一起的两名玩家会收到相同的棋盘。现在还需要实现游戏具体逻辑的判断过程,接收用户输入过程

每一局游戏一共包含三个棋盘,每位用户各一个棋盘,后端维护一个棋盘,基本想法是: (1) 两名用户分别输入下一步操作给后端,(2) 后端执行游戏逻辑过程,(3) 把每一轮的执行结果同步广播给两个前端。

较为复杂的点在于第(2)步的实现,第(2)步的具体整个过程: (1) 匹配成功之后,new 一个 game 对象维护游戏信息,(2) 创建地图并广播给两位用户,(3) 读取两名玩家的输入,(4) 根据输入执行每一轮的游戏逻辑。

会发现有个问题,如果有多名玩家匹配成功,例如有4名玩家匹配成功,分别有两个游戏 game1, game2,然而一般的程序都是单线程的,也就是在 game1 执行完成后才会执行 game2,因此 game 不能用单线程来处理,于是就涉及到线程的通信和加锁问题。

在这里插入图片描述


1.2 游戏同步的实现

  • 首先将 Game 类继承 Thread 类以实现多线程,需要在 Game 中重写 run 方法,run 方法是开启新线程的入口函数,在两名玩家匹配成功后通过 game.start() 进入一个新线程(之后执行 Game 中的 run),每个 websocket 连接维护一个自己的 game 棋盘。

    Game game = new Game(13, 14, 20, a.getId(), b.getId());
    
  • run 中要实现的是具体游戏逻辑,由于游戏一定会在1000轮以内结束,因此循环1000次代替死循环。

    每一轮执行的内容和设计图中的一样,首先判断双方是否都有输入,如果一方没有输入则直接判负,记录败者,广播给前端结果;如果都有输入,则进入裁判逻辑进行局面判断,判断有结果了则结束游戏,记录败者,广播给前端结果,否则把双方的下一步操作同步广播给游戏双方的前端,再在前端渲染。

    @Override
    public void run() {
        for (int i = 0; i < 1000; i ++ ) {
            if (nextStep()) {
                // 双方都有输入
                judge();
                if ("playing".equals(status)) {
                    sendMove();
                } else {
                    sendResult();
                    break;
                }
            } else {
                // 有一方没有输入操作,超时判输
                status = "finished";
                lock.lock();
                try {
                    if (nextStepA == null && nextStepB == null) {
                        loser = "all";
                    } else if (nextStepA == null) {
                        loser = "A";
                    } else if (nextStepB == null) {
                        loser = "B";
                    }
                } finally {
                    lock.unlock();
                }
    
                sendResult(); // 向两名玩家广播结果
                break;
            }
        }
    }
    
  • nextStep() 是判断获取双方的下一步操作,那就要在 Game 中通过 nextStepA, nextStepB 来记录每名玩家的下一步操作,在外部线程中要修改这两个变量,内部线程中要读取这两个变量,因此就出现了两个线程同时读写同一个变量的问题,于是要加锁解决。

    前端获取输入 → 发送消息给后端 → 后端维护的 websocket 连接调用 setNextStepA()Game 中读取 nextStepA

    private ReentrantLock lock = new ReentrantLock();
    
    public void setNextStepA(Integer nextStepA) {
        lock.lock();
        try {
            this.nextStepA = nextStepA;
        } finally {
            lock.unlock();
        }
    }
    

    这边设定如果超过5s未获取用户输入,则判定该用户超时,直接判负。

    /* 判断双方是否都有输入,如果有则记录下输入并返回 true,如果有一方没有输入则返回 false */
    private boolean nextStep() {
        for (int i = 0; i < 5; i ++ ) {
            try {
                sleep(1000);
                lock.lock();
                try {
                    if (nextStepA != null && nextStepB != null) {
                        playerA.getSteps().add(nextStepA);
                        playerB.getSteps().add(nextStepB);
                        return true;
                    }
                } finally {
                    lock.unlock();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    
        return false;
    }
    
  • 在双方都有输入后,需要向前端广播两者的输入,再让前端进行渲染;如果有没有输入,也需要向前端返回结果。于是又涉及到前后端之间通过 websocket 进行通信,在P4. 微服务: 匹配系统(上)有详细说明。

    private void sendAllMessage(String message) {
        WebSocketServer.users.get(playerA.getId()).sendMessage(message);
        WebSocketServer.users.get(playerB.getId()).sendMessage(message);
    }
    
    private void sendMove() {
        lock.lock();
        try {
            JSONObject resp = new JSONObject();
            resp.put("event", "move");
            resp.put("a_direction", nextStepA);
            resp.put("b_direction", nextStepB);
            nextStepA = nextStepB = null;
            sendAllMessage(resp.toJSONString());
        } finally {
            lock.unlock();
        }
    }
    
    private void sendResult() {
        JSONObject resp = new JSONObject();
        resp.put("event", "result");
        resp.put("loser", loser);
        sendAllMessage(resp.toJSONString());
    }
    
  • 前端向后端通信,对应的是设置 game 线程中的 nextStepA, nextStepB

    // 前端通过 socket.send 发送 JSON 格式消息给后端
    add_listening_events() {
        this.ctx.canvas.focus();
    
        const [snake0, snake1] = this.snakes;
    
        this.ctx.canvas.addEventListener("keydown", e => {
            let d = -1;
            if (e.key === 'w') d = 2;
            else if (e.key === 'd') d = 1;
            else if (e.key === 's') d = 0;
            else if (e.key === 'a') d = 3;
    
            if (d >= 0) {
                this.store.state.pk.socket.send(JSON.stringify({
                    event: "move",
                    direction: d,
                }));
            }
        });
    }
    
    
    
    
    
    
  • 之后在前端进行调试,通过 onmessage 接收到后端传来的消息后,通过 event 进行判断,如果是 move 则渲染蛇移动的方向,如果是 result 则渲染蛇已经死亡的情况。

    为了方便调试,可以自行在前端写一下 a, b 的位置信息。

    socket.onmessage = msg => {
        const data = JSON.parse(msg.data);
        if (data.event === "match_success") {
            /* ... */
        } else if (data.event === "move") {
            console.log(data);
            const game = store.state.pk.gameObject;
            const [snake0, snake1] = game.snakes;
    
            snake0.set_direction(data.a_direction);
            snake1.set_direction(data.b_direction);
        } else if (data.event === "result") {
            console.log(data);
            const game = store.state.pk.gameObject;
            const [snake0, snake1] = game.snakes;
    
            if (data.loser === "all" || data.loser === "A") {
                snake0.status = "die";
            }
    
            if (data.loser === "all" || data.loser === "B") {
                snake1.status = "die";
            }
        }
    }
    
    private void move(int direction) {
        if (game.getPlayerA().getId().equals(user.getId())) {
            game.setNextStepA(direction);
        } else if (game.getPlayerB().getId().equals(user.getId())) {
            game.setNextStepB(direction);
        }
    }
    
    @OnMessage
    public void onMessage(String message, Session session) {
        System.out.println("received!");
        JSONObject data = JSONObject.parseObject(message);
        String event = data.getString("event");
        if ("start-matching".equals(event)) {
            startMatching();
        } else if ("stop-matching".equals(event)) {
            stopMatching();
        } else if ("move".equals(event)) {
            move(data.getInteger("direction"));
        }
    }
    
  • 最后在后端翻译一下P1.创建菜单与游戏界面中介绍的游戏裁判逻辑实现 judge 就完成了整个游戏同步系统。

    private boolean check_valid(List<Cell> cellsA, List<Cell> cellsB) {
        int n = cellsA.size();
        Cell cell = cellsA.get(n - 1);
        if (g[cell.x][cell.y] == 1) return false;
    
        for (int i = 0; i < n - 1; i ++ ) {
            if (cellsA.get(i).x == cell.x && cellsA.get(i).y == cell.y)
                return false;
        }
    
        for (int i = 0; i < n - 1; i ++ ) {
            if (cellsB.get(i).x == cell.x && cellsB.get(i).y == cell.y)
                return false;
        }
    
        return true;
    }
    
    private void judge() {  // 判断两名玩家下一步操作是否合法
        List<Cell> cellsA = playerA.getCells();
        List<Cell> cellsB = playerB.getCells();
    
        boolean validA = check_valid(cellsA, cellsB);
        boolean validB = check_valid(cellsB, cellsA);
        if (!validA || !validB) {
            status = "finished";
    
            if (!validA && !validB) {
                loser = "all";
            } else if (!validA) {
                loser = "A";
            } else {
                loser = "B";
            }
        }
    }
    
  • 前端中计分板,重新匹配按钮等小细节的实现就略过了,请自行实现。


2 匹配系统微服务的实现

2.1 微服务概述

微服务可以理解成为一个独立的程序,本质上是新开了一个 Springboot。和原来实现的后端服务 backend 呈并列关系,也就是两个独立的 SpringBoot,均可以接收和发送信息,在 Spring 中通过 url 进行 http 通信。

在 King of Bots 中选择把匹配系统单独拉出来做一个微服务,其实也可以开一个新线程直接实现,但是为了学习新技术就拉出来做一个微服务,学习一下怎么创建微服务,微服务如何和之前实现的后端 backend 进行通信。

微服务的创建就是新建一个父项目,包含 matchingsystem, backend 两个模块,父项目要添加 springcloud 依赖。

在这里插入图片描述


2.2 匹配系统接口url的实现

MatchingSystem 一共要实现两个接口 addPlayer, removePlayer,都是通过 controller, service, service.impl 的步骤实现。

service 中的接口:

public interface MatchingService {
    String addPlayer(Integer userId, Integer rating);
    String removePlayer(Integer userId);
}

service.impl 先简单的调试一下:

@Service
public class MatchingServiceImpl implements MatchingService {
    @Override
    public String addPlayer(Integer userId, Integer rating) {
        System.out.println("Player: " + userId + " rating: " + rating + "add!");
        return "add player success";
    }

    @Override
    public String removePlayer(Integer userId) {
        System.out.println("Player: " + userId + " remove!");
        return "remove player success";
    }
}

controller 定义一下 url:

@RestController
public class MatchingController {
    @Autowired
    private MatchingService matchingService;

    @PostMapping("/player/add/")
    public String addPlayer(@RequestParam MultiValueMap<String, String> data) {
        Integer userId = Integer.parseInt(Objects.requireNonNull(data.getFirst("user_id")));
        Integer rating = Integer.parseInt(Objects.requireNonNull(data.getFirst("rating")));
        return matchingService.addPlayer(userId, rating);
    }

    @PostMapping("player/remove/")
    public String removePlayer(@RequestParam MultiValueMap<String, String> data) {
        Integer userId = Integer.parseInt(Objects.requireNonNull(data.getFirst("user_id")));
        return matchingService.removePlayer(userId);
    }
}

2.3 微服务之间的通信

之前在P4. 微服务: 匹配系统(上)中 WebsocketServer 后端本地实现了一个傻瓜式匹配,现在开始匹配 startMatching 之后,应该向微服务发送一个请求,表示传一个玩家过去;在取消匹配 stopMatching 之后,应该发送一个请求,表示取消当前玩家的匹配。

向后端发请求会用到 SpringBoot 中的一个工具 RestTemplate,需要先进行配置。

RestTemplate 用于在两个 SpringBoot 之间进行通信。

@Configuration
public class RestTemplateConfig {
    @Bean
    public RestTemplate getRestTemplate() {
        return new RestTemplate();
    }
}

之后直接通过 restTemplate.postForObject 调用另一个微服务的 url,一共有3个参数 url, data, 返回值的class.

private static RestTemplate restTemplate;
private final String addPlayerUrl = "http://127.0.0.1:3001/player/add/";

@Autowired
private void setRestTemplate(RestTemplate restTemplate) {
    WebSocketServer.restTemplate = restTemplate;
}

private void startMatching() {
    System.out.println("Start Matching!");

    MultiValueMap<String, String> data = new LinkedMultiValueMap<>();
    data.put("user_id", Collections.singletonList(this.user.getId().toString()));
    data.put("rating", Collections.singletonList(this.user.getRating().toString()));

    restTemplate.postForObject(addPlayerUrl, data, String.class);
}

2.4 匹配逻辑的实现

在匹配中通过匹配池 MatchingPool 进行匹配,创建 service.impl.utils.MatchingPool 继承 Thread,和之前的一样,重写 run方法实现多线程,这边用多线程是因为每秒都要看有没有玩家可以匹配,是一个死循环,如果单线程就会卡死在这个循环中。

线程的启动设置成在 SpringBoot 服务开启的时候启动:

@SpringBootApplication
public class MatchingSystemApplication {
    public static void main(String[] args) {
        MatchingServiceImpl.matchingPool.start();
        SpringApplication.run(MatchingSystemApplication.class, args);
    }
}

在接收到 backendWebsocketServer 调用 /player/add/url 后,找到 serviceImpl 中对应的方法,该方法调用辅助类 MatchingPooladdPlayer 方法,向匹配池中添加一位玩家,removePlayer 同理。

@Service
public class MatchingServiceImpl implements MatchingService {

    public final static MatchingPool matchingPool = new MatchingPool();
    @Override
    public String addPlayer(Integer userId, Integer rating) {
        System.out.println("Player: " + userId + " rating: " + rating + " add!");
        matchingPool.addPlayer(userId, rating);
        return "add player success";
    }

    @Override
    public String removePlayer(Integer userId) {
        System.out.println("Player: " + userId + " remove!");
        matchingPool.removePlayer(userId);
        return "remove player success";
    }
}
// 辅助类 MatchingPool
public class MatchingPool extends Thread {
    private static List<Player> players = new ArrayList<>();
    private ReentrantLock lock = new ReentrantLock();

    public void addPlayer(Integer userId, Integer rating) {
        lock.lock();
        try {
            players.add(new Player(userId, rating, 0));
        } finally {
            lock.unlock();
        }
    }

    public void removePlayer(Integer userId) {
        lock.lock();
        try {
            List<Player> newPlayers = new ArrayList<>();
            for (Player player : players)
                if (!player.getUserId().equals(userId))
                    newPlayers.add(player);
            players = newPlayers;
        } finally {
            lock.unlock();
        }
    }

    @Override
    public void run() {
		/* 具体匹配逻辑,看个人喜好实现就行 */
    }
}

具体的匹配逻辑在 run 中实现,通常是通过 while(true)Thread.sleep(1000) 每秒对匹配池中所有玩家进行检查,符合条件的玩家对会进行匹配。

匹配完成之后 matchingsystem 需要 sendResultWebsocketServer,所以 backend 中需要写个 url 可以接收到这个消息,其中 service 的逻辑如下,startGame 为之前写的逻辑,具体是开始一场游戏,并传信息给前端。

要记得在 SecurityConfig 中对权限进行设置,只允许本地调用。

@Override
public String startGame(Integer aId, Integer bId) {
    System.out.println("start game: " + aId + " " + bId);
    WebSocketServer.startGame(aId, bId);
    return "game start success!";
}

最后在 sendResult 中进行调用,一样使用 RestTemplate 实现,

private final static String startGameUrl = "http://127.0.0.1:3000/pk/game/start/";

private void sendResult(Player a, Player b) {
    MultiValueMap<String, String> data = new LinkedMultiValueMap<>();
    data.put("a_id", Collections.singletonList(a.getUserId().toString()));
    data.put("b_id", Collections.singletonList(b.getUserId().toString()));

    restTemplate.postForObject(startGameUrl, data, String.class);
}

2.5 匹配系统的权限控制

匹配系统的 url 只能允许本地进行访问,不能让外部进行访问,避免恶意行为,因此使用 Spring Security 进行权限控制。

我们希望只能后端服务器 backend 访问 /player/add/, /player/remove/,和之前的一样,在 matchingsystem 中添加依赖并写一个网关 SecurityConfig 配置。

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                .antMatchers("/player/add/", "/player/remove/").hasIpAddress("127.0.0.1")
                .antMatchers(HttpMethod.OPTIONS).permitAll()
                .anyRequest().authenticated();
    }
}

3 bug的解决

3.1 自己匹配自己

先点击匹配,进入匹配池,再刷新页面(此时已经断开连接并建立新连接),再次点击匹配,会出现自己匹配自己的问题。

解决方法: 在断开连接的时候调用微服务的 removePlayer

@OnClose
public void onClose() {
    System.out.println("disconnected!");
    if (this.user != null) {
        users.remove(this.user.getId());
        MultiValueMap<String, String> data = new LinkedMultiValueMap<>();
        data.put("user_id", Collections.singletonList(user.getId().toString()));

        restTemplate.postForObject(removePlayerUrl, data, String.class);
    }
}

3.2 断开连接问题

会出现某位玩家突然断电导致断开连接,或者妈妈回来了,紧急按 alt + F4 结束进程,等等这种情况。

会导致该玩家仍然在匹配池里,能够匹配成功,但是后端和前端无法通过 websocket 进行通信,导致报错。

因此,在用到 users 的地方都要进行非空判断,解决这种情况,以下举个例子:

if (users.get(a.getId()) != null) users.get(a.getId()).game = game;
if (users.get(b.getId()) != null) users.get(b.getId()).game = game;

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

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

相关文章

3403两个图像分析引擎差异

1.设置环境变量差异 2.获取数据大小差异 3.ATC差异

【EXCEL技巧】Excel如何将数字前面的0去掉

Excel文件中经常会遇到数据是0001345这种&#xff0c;那么&#xff0c;如何将数字前面的0去掉呢&#xff1f;今天和大家分享方法。 首先&#xff0c;选中一列空的单元格&#xff0c;然后在单元格中输入公式TEXT(D3,0)&#xff0c;这里的D3指的是前面带有0的数据的位置 回车之后…

Elasticsearch:Painless scripting 语言(一)

Painless 是一种高性能、安全的脚本语言&#xff0c;专为 Elasticsearch 设计。你可以使用 Painless 在 Elasticsearch 支持脚本的任何地方安全地编写内联和存储脚本。 Painless 提供众多功能&#xff0c;这些功能围绕以下核心原则&#xff1a; 安全性&#xff1a;确保集群的…

【征服数据结构】:期末通关秘籍

【征服数据结构】&#xff1a;期末通关秘籍 &#x1f498; 数据结构的基本概念&#x1f608; 数据结构的基本概念&#x1f608; 逻辑结构和存储结构的区别和联系&#x1f608; 算法及其特性&#x1f608; 简答题 &#x1f498; 线性表&#xff08;链表、单链表&#xff09;&…

RPC架构基本结构和核心技术

当你在构建一个分布式系统时&#xff0c;势必需要考虑的一个问题是&#xff1a;如何实现服务与服务之间高效调用&#xff1f;当然&#xff0c;你可以使用Dubbo或Spring Cloud等分布式服务框架来完成这个目标&#xff0c;这些框架帮助我们封装了技术实现的复杂性。那么&#xff…

【论文阅读】-- 研究时间序列可视化,提升用户体验

Investigating Time Series Visualisations to Improve the User Experience 摘要1 引言2 相关工作互动技巧视觉编码坐标系 3 用户研究时间序列可视化互动技巧任务实验设计 4 结果交互技术的效果视觉编码的影响坐标系的影响 5 讨论交互技术的效果视觉编码的影响坐标系的影响 6 …

[JS]正则表达式

介绍 正则表达式是定义匹配字符串的规则, 在JS中, 正则表达式也是对象, 通常用于查找或替换符合规则的文本 许多语言都支持正则表达式, 在前端中常见的场景就是表单验证和敏感词替换 语法 正则字面量 / / const str 好好学习,天天向上 // 1.定义规则: const reg /好///…

17964 水桶打水

这是一个优先队列&#xff08;堆&#xff09;和贪心算法的问题。我们可以使用C来解决这个问题。 首先&#xff0c;我们需要创建一个优先队列来存储每个水龙头的结束时间。然后&#xff0c;我们将所有人的打水时间从小到大排序。接着&#xff0c;我们将每个人分配给最早结束的水…

深入解析Flowable:工作流与业务流程管理引擎

深入解析Flowable&#xff1a;工作流与业务流程管理引擎 引言 在数字化时代&#xff0c;企业对流程自动化的需求日益增长。有效的工作流和业务流程管理系统可以帮助组织提高生产力、优化资源分配以及增强决策支持。Flowable是一款开源的工作流和业务流程管理&#xff08;BPM&a…

MeterSphere v3.0全新启航,让软件测试工作更简单、更高效

2024年7月1日&#xff0c;MeterSphere v3.0版本正式发布。MeterSphere v3.0是新一代的测试管理和接口测试工具&#xff0c;致力于让软件测试工作更简单、更高效&#xff0c;不再成为持续交付的瓶颈。 在团队协作方面&#xff0c;针对目前企业软件测试团队所面临的测试工具不统…

springboot项目requestId设置、统一responsebody封装以及切面

利用filter设置requestId import cn.hutool.core.lang.UUID; import lombok.extern.slf4j.Slf4j; import org.slf4j.MDC; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.s…

绿联NAS进入SSH的方法

1. 进入【设备管理】&#xff0c;在调试功能中&#xff0c;开启远程调试功能&#xff0c;发送手机验证码&#xff0c;你将得到一个3天有效期的验证码&#xff0c;就是ssh登录密码。 2. 使用终端工具或ssh命令直接登录SSH。 端口是922&#xff0c;账号是&#xff1a;root&#…

界面组件DevExpress WPF v24.1 - 增强的可访问性 UI自动化

DevExpress WPF拥有120个控件和库&#xff0c;将帮助您交付满足甚至超出企业需求的高性能业务应用程序。通过DevExpress WPF能创建有着强大互动功能的XAML基础应用程序&#xff0c;这些应用程序专注于当代客户的需求和构建未来新一代支持触摸的解决方案。 DevExpress WPF控件日…

全网最详细的 gin框架请求数据绑定Bind 源码解析 -- 帮助你全面了解gin框架的请求数据绑定原理和方法

在gin框架中&#xff0c;我们可以将多种请求数据&#xff08;json, form,uri&#xff0c;header等&#xff09;直接绑定到我们定义的结构体&#xff0c;底层是通过反射方式获取我们定义在结构体上面的tag来实现请求数据到我们的结构体数据的绑定的。 在gin的底层有2大体系的数据…

华为HCIP Datacom H12-821 卷19

1.多选题 如图所示,RTA 的 GE0/0/0、GE0/0/1 接口分别连接部门 1 和 2,其网段分别为 10.1.2.0/24、 10.1.3.0/24 网段,为限制部门 1 和 2 之间的相互访问,在 RTA 上部署 traffic-filter,以下哪些部署方式是正 确? A、配置 ACL3000 拒绝源为 10.1.2.0/24 目的为 10.1.3.0…

matlab仿真 通信信号和系统分析(上)

&#xff08;内容源自详解MATLAB&#xff0f;SIMULINK 通信系统建模与仿真 刘学勇编著第三章内容&#xff0c;有兴趣的读者请阅读原书&#xff09; 一、求离散信号卷积和 主要还是使用卷积函数conv&#xff0c;值得注意的是&#xff0c;得到的卷积和长度结果为81&#xff0…

【正点原子K210连载】第十四章 按键输入实验 摘自【正点原子】DNK210使用指南-CanMV版指南

1&#xff09;实验平台&#xff1a;正点原子ATK-DNK210开发板 2&#xff09;平台购买地址https://detail.tmall.com/item.htm?id731866264428 3&#xff09;全套实验源码手册视频下载地址&#xff1a; http://www.openedv.com/docs/boards/xiaoxitongban 第十四章 按键输入实…

短信验证码API的防护策略?怎么优化更新?

短信验证码API的定制化服务怎么样&#xff1f;如何选择API服务&#xff1f; 短信验证码API成为保护用户账户和数据的重要工具&#xff0c;对短信验证码API的防护也显得尤为重要。AoKSend将探讨短信验证码API的防护策略&#xff0c;帮助企业和开发者确保系统的安全性和可靠性。…

FatFs(文件系统)

1官网 FatFs - 通用 FAT 文件系统模块 (elm-chan.org) FatFs 是用于小型嵌入式系统的通用 FAT/exFAT 文件系统模块。FatFs 模块是按照 ANSI C &#xff08;C89&#xff09; 编写的&#xff0c;并且与磁盘 I/O 层完全分离。因此&#xff0c;它独立于平台。它可以集成到资源有限…

2024 vue3入门教程:01vscode终端命令创建第一个vue项目

参考vue官网手册&#xff1a;https://cn.vuejs.org/guide/quick-start.html 一、找个盘符&#xff0c;新建文件夹存储以后得vue项目 我的是e盘下创建了vueproject 二、使用vscode打开存储vue项目的文件夹 因为我生成过项目&#xff0c;所以有文件&#xff0c;你们初次是没有…