SpringBoot学习笔记-实现微服务:匹配系统(上)

news/2024/7/10 2:23:22 标签: spring boot, spring, vue, 微服务, 后端

笔记内容转载自 AcWing 的 SpringBoot 框架课讲义,课程链接:AcWing SpringBoot 框架课。

CONTENTS

  • 1. 配置WebSocket
  • 2. 前后端WebSocket通信
    • 2.1 WS通信的建立
    • 2.2 加入JWT验证
  • 3. 前后端匹配业务
    • 3.1 实现前端页面
    • 3.2 实现前后端交互逻辑
    • 3.3 同步游戏地图

我们的游戏之后是两名玩家对战,因此需要实现联机功能,在这之前还需要实现一个匹配系统,能够匹配分数相近的玩家进行对战。

想要进行匹配就至少要有两个客户端,当两个客户端都向服务器发送匹配请求后并不会马上得到返回结果,一般会等待一段时间,这个时间是未知的,因此这个匹配是一个异步的过程,对于这种异步的过程或者是计算量比较大的过程我们都会用一个额外的服务来操作。

那么这个额外的用于匹配的服务可以称为 Matching System,这是另外一个程序(进程),当后端服务器接收到前端的请求后就会将请求发送给 Matching System,这个匹配系统维护了一堆用户的集合,它会不断地去匹配分数最接近的用户,当匹配成功一组用户后就会将结果返回给后端服务器,再由后端将匹配结果立即返回给对应的前端。这种服务就被称为微服务,可以用 Spring Cloud 实现。

用以前的 HTTP 请求很难达到这种效果,之前我们是在客户端向后端发送请求,且后端在短时间内就会返回结果,HTTP 请求只能满足这种一问一答式的服务。而我们现在需要实现的效果是客户端发送请求后不知道经过多长时间后端才会返回结果,对于这种情况需要使用 WebSocket 协议(WS),该协议不仅支持客户端向服务器发送请求,也支持服务器向客户端发送请求。

在前端向服务器发送请求后,服务器会维护好一个 WS 链接,这个链接其实就是一个 WebSocketServer 类的实例,所有和这个链接相关的信息都会存到这个类中。

1. 配置WebSocket

我们之前每次刷新网页就会随机生成游戏地图,该过程是在浏览器本地执行的,当我们要实现匹配功能时,地图就不能由两名玩家各自的客户端生成,否则就基本不可能完全一样了。

当匹配成功后应该由服务器端创建一个 Game 任务,将游戏放到该任务下执行,统一生成地图,且判断移动或者输赢等逻辑之后也应该移到后端来执行。

生成好地图后服务器就将地图传给两名玩家的前端,然后等待玩家的键盘输入或者是 Bot 代码的输入,Bot 代码的输入也属于一个微服务

首先我们先在 pom.xml 文件中添加以下依赖:

  • spring-boot-starter-websocket
  • fastjson

接着在 config 包下创建 WebSocketConfig 配置类:

package com.kob.backend.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

@Configuration
public class WebSocketConfig {
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

然后我们创建一个 consumer 包,在其中创建 WebSocketServer 类:

package com.kob.backend.consumer;

import org.springframework.stereotype.Component;

import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;

@Component
@ServerEndpoint("/websocket/{token}")  // 注意不要以'/'结尾
public class WebSocketServer {
    @OnOpen
    public void onOpen(Session session, @PathParam("token") String token) {
        // 建立链接
    }

    @OnClose
    public void onClose() {
        // 关闭链接
    }

    @OnMessage
    public void onMessage(String message, Session session) {
        // 从Client接收消息
    }

    @OnError
    public void onError(Session session, Throwable error) {
        error.printStackTrace();
    }
}

之前我们配置的 Spring Security 设置了屏蔽除了授权之外的其他所有链接,因此我们需要在 SecurityConfig 类中放行一下 WebSocket 的链接:

package com.kob.backend.config;

import com.kob.backend.config.filter.JwtAuthenticationTokenFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {  // AuthenticationManager用于处理身份验证
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {  // 配置HttpSecurity
        http.csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                .antMatchers("/user/account/login/", "/user/account/register/").permitAll()  // 需要公开的链接在这边写即可
                .antMatchers(HttpMethod.OPTIONS).permitAll()
                .anyRequest().authenticated();

        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/websocket/**");
    }
}

如果是使用新版的配置而不是使用 WebSecurityConfigurerAdapter 可以按以下方式配置:

package com.kob.backend.config;

import com.kob.backend.config.filter.JwtAuthenticationTokenFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public AuthenticationManager authenticationManagerBean(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                .antMatchers("/user/account/login/", "/user/account/register/").permitAll()
                .antMatchers(HttpMethod.OPTIONS).permitAll()
                .anyRequest().authenticated();

        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }

    @Bean
    public WebSecurityCustomizer webSecurityCustomizer(){
        return (web) -> web.ignoring().antMatchers("/websocket/**");
    }
}

2. 前后端WebSocket通信

2.1 WS通信的建立

WebSocket 不属于单例模式(同一个时间每个类只能有一个实例,我们每建一个 WS 链接都会新创建一个实例),不是标准的 Spring 中的组件,因此在注入 Mapper 时不能用 @Autowired 直接注入,一般是将 @Autowired 写在一个 set() 方法上,Spring 会根据方法的参数类型从 IoC 容器中找到该类型的 Bean 对象注入到方法的行参中,并且自动反射调用该方法。

我们先假设前端传过来的是用户 ID 而不是 JWT 令牌:

package com.kob.backend.consumer;

import com.kob.backend.mapper.UserMapper;
import com.kob.backend.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;

import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;

@Component
@ServerEndpoint("/websocket/{token}")  // 注意不要以'/'结尾
public class WebSocketServer {
    // ConcurrentHashMap是一个线程安全的哈希表,用于将用户ID映射到WS实例
    private static final ConcurrentHashMap<Integer, WebSocketServer> users = new ConcurrentHashMap<>();
    private User user;
    private Session session = null;

    private static UserMapper userMapper;

    @Autowired
    public void setUserMapper(UserMapper userMapper) {
        WebSocketServer.userMapper = userMapper;
    }

    @OnOpen
    public void onOpen(Session session, @PathParam("token") String token) {
        this.session = session;
        System.out.println("Connected!");

        Integer userId = Integer.parseInt(token);
        this.user = userMapper.selectById(userId);
        users.put(userId, this);
    }

    @OnClose
    public void onClose() {
        System.out.println("Disconnected!");
        if (this.user != null) {
            users.remove(this.user.getId());
        }
    }

    @OnMessage
    public void onMessage(String message, Session session) {
        System.out.println("Receive message!");
    }

    @OnError
    public void onError(Session session, Throwable error) {
        error.printStackTrace();
    }

    public void sendMessage(String message) {  // 从后端向当前链接发送消息
        synchronized (this.session) {  // 由于是异步通信,需要加一个锁
            try {
                this.session.getBasicRemote().sendText(message);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

然后我们先在前端的 PKIndexView 组件中调试,当组件被挂载完成后发出请求建立 WS 链接,当被卸载后关闭 WS 链接:

<template>
  <PlayGround />
</template>

<script>
import PlayGround from "@/components/PlayGround.vue";
import { onMounted, onUnmounted } from "vue";
import { useStore } from "vuex";

export default {
  components: {
    PlayGround,
  },
  setup() {
    const store = useStore();

    let socket = null;
    let socket_url = `ws://localhost:3000/websocket/${store.state.user.id}/`;

    onMounted(() => {
      socket = new WebSocket(socket_url);

      store.commit("updateOpponent", {
        username: "我的对手",
        photo: "https://cdn.acwing.com/media/article/image/2022/08/09/1_1db2488f17-anonymous.png",
      });

      socket.onopen = () => {  // 链接成功建立后会执行
        console.log("Connected!");
        store.commit("updateSocket", socket);
      };

      socket.onmessage = (msg) => {  // 接收到后端消息时会执行
        const data = JSON.parse(msg.data);  // Spring传过来的数据是放在消息的data中
        console.log(data);
      };

      socket.onclose = () => {  // 关闭链接后会执行
        console.log("Disconnected!");
      };
    });

    onUnmounted(() => {
      socket.close();  // 如果不断开链接每次切换页面都会创建新链接,就会导致有很多冗余链接
    });
  },
};
</script>

<style scoped></style>

现在我们在对战页面每次刷新后都可以在浏览器控制台或后端控制台中看到 WS 的输出信息。

接下来我们要将 WebSocket 存到前端的 store 中,在 store 目录下创建 pk.js 用来存储和对战页面相关的全局变量:

export default {
  state: {
    status: "matching",  // 当前状态,matching表示正在匹配,playing表示正在对战
    socket: null,  // 前端和后端建立的链接
    opponent_username: "",  // 对手的用户名
    opponent_photo: "",  // 对手的头像
  },
  getters: {},
  mutations: {
    updateSocket(state, socket) {
        state.socket = socket;
    },
    updateOpponent(state, opponent) {
        state.opponent_username = opponent.username;
        state.opponent_photo = opponent.photo;
    },
    updateStatus(state, status) {
        state.status = status;
    },
  },
  actions: {},
  modules: {},
};

同时要在 store/index.js 中引入进来:

import { createStore } from "vuex";
import ModuleUser from "./user";
import ModulePk from "./pk";

export default createStore({
  state: {},
  getters: {},
  mutations: {},
  actions: {},
  modules: {
    user: ModuleUser,
    pk: ModulePk,
  },
});

2.2 加入JWT验证

现在我们直接使用用户的 ID 建立 WS 链接,这是不安全的,因为前端可以自行修改这个 ID,因此就需要加入 JWT 验证。

WebSocket 中没有 Session 的概念,因此我们在验证的时候前端就不用将信息放到表头里了,直接放到链接中就行:

...

<script>
...

export default {
  ...

  setup() {
    ...

    let socket_url = `ws://localhost:3000/websocket/${store.state.user.jwt_token}/`;

    ...
  },
};
</script>

...

验证的逻辑可以参考之前的 JwtAuthenticationTokenFilter,我们可以把这个验证的模块单独写到一个文件中,在 consumer 包下创建 utils 包,然后创建一个 JwtAuthentication 类:

package com.kob.backend.consumer.utils;

import com.kob.backend.utils.JwtUtil;
import io.jsonwebtoken.Claims;

public class JwtAuthentication {
    public static Integer getUserId(String token) {
        int userId = -1;
        try {
            Claims claims = JwtUtil.parseJWT(token);
            userId = Integer.parseInt(claims.getSubject());
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return userId;
    }
}

然后就可以在 WebSocketServer 中解析 JWT 令牌:

package com.kob.backend.consumer;

import com.kob.backend.consumer.utils.JwtAuthentication;
import com.kob.backend.mapper.UserMapper;
import com.kob.backend.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;

import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;

@Component
@ServerEndpoint("/websocket/{token}")  // 注意不要以'/'结尾
public class WebSocketServer {
    ...

    @OnOpen
    public void onOpen(Session session, @PathParam("token") String token) throws IOException {
        this.session = session;
        System.out.println("Connected!");

        Integer userId = JwtAuthentication.getUserId(token);
        this.user = userMapper.selectById(userId);

        if (user != null) {
            users.put(userId, this);
        } else {
            this.session.close();
        }
    }

    ...
}

3. 前后端匹配业务

3.1 实现前端页面

我们需要实现一个前端的匹配页面,并能够切换匹配和对战页面,可以根据之前在 store 中存储的 status 状态来动态展示页面。首先在 components 目录下创建 MatchGround.vue 组件,其中需要展示玩家自己的头像和用户名以及对手的头像和用户名,当点击开始匹配按钮时向 WS 链接发送开始匹配的消息,点击取消按钮时发送取消匹配的消息:

<template>
  <div class="matchground">
    <div class="row">
      <div class="col-md-6" style="text-align: center;">
        <div class="photo">
          <img class="img-fluid" :src="$store.state.user.photo">
        </div>
        <div class="username">
          {{ $store.state.user.username }}
        </div>
      </div>
      <div class="col-md-6" style="text-align: center;">
        <div class="photo">
          <img class="img-fluid" :src="$store.state.pk.opponent_photo">
        </div>
        <div class="username">
          {{ $store.state.pk.opponent_username }}
        </div>
      </div>
      <div class="col-md-12 text-center" style="margin-top: 14vh;">
        <button @click="click_match_btn" type="button" class="btn btn-info btn-lg">
          {{ match_btn_info }}
        </button>
      </div>
    </div>
  </div>
</template>

<script>
import { ref } from "vue";
import { useStore } from "vuex";

export default {
  setup() {
    const store = useStore();
    let match_btn_info = ref("开始匹配");

    const click_match_btn = () => {
      if (match_btn_info.value === "开始匹配") {
        match_btn_info.value = "取消";
        store.state.pk.socket.send(JSON.stringify({  // 将json封装成字符串发送给后端后端会在onMessage()中接到请求
          event: "start_match",  // 表示开始匹配
        }));
      } else {
        match_btn_info.value = "开始匹配";
        store.state.pk.socket.send(JSON.stringify({
          event: "stop_match",  // 表示停止匹配
        }));
      }
    };

    return {
      match_btn_info,
      click_match_btn,
    };
  },
};
</script>

<style scoped>
div.matchground {
  width: 60vw;
  height: 70vh;
  margin: 40px auto;
  border-radius: 10px;
  background-color: rgba(50, 50, 50, 0.5);
}

img {
  width: 35%;
  border-radius: 50%;
  margin: 14vh 0 1vh 0;
}

.username {
  font-size: 24px;
  font-weight: bold;
  color: white;
}
</style>

3.2 实现前后端交互逻辑

当用户点击开始匹配按钮后,前端要向服务器发出一个请求,后端接收到请求后应该将该用户放入匹配池中,由于目前还没有实现微服务,因此我们先在 WebSocketServer 后端用一个 Set 维护正在匹配的玩家,当匹配池中满两名玩家就将其匹配在一起,然后将匹配结果返回给两名玩家的前端:

package com.kob.backend.consumer;

import com.alibaba.fastjson2.JSONObject;
import com.kob.backend.consumer.utils.JwtAuthentication;
import com.kob.backend.mapper.UserMapper;
import com.kob.backend.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;

import java.io.IOException;
import java.util.Iterator;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;

@Component
@ServerEndpoint("/websocket/{token}")  // 注意不要以'/'结尾
public class WebSocketServer {
    // ConcurrentHashMap是一个线程安全的哈希表,用于将用户ID映射到WS实例
    private static final ConcurrentHashMap<Integer, WebSocketServer> users = new ConcurrentHashMap<>();
    // CopyOnWriteArraySet也是线程安全的
    private static final CopyOnWriteArraySet<User> matchPool = new CopyOnWriteArraySet<>();  // 匹配池
    private User user;
    private Session session = null;

    private static UserMapper userMapper;

    @Autowired
    public void setUserMapper(UserMapper userMapper) {
        WebSocketServer.userMapper = userMapper;
    }

    @OnOpen
    public void onOpen(Session session, @PathParam("token") String token) throws IOException {
        this.session = session;
        System.out.println("Connected!");

        Integer userId = JwtAuthentication.getUserId(token);
        this.user = userMapper.selectById(userId);

        if (user != null) {
            users.put(userId, this);
        } else {
            this.session.close();
        }
    }

    @OnClose
    public void onClose() {
        System.out.println("Disconnected!");
        if (this.user != null) {
            users.remove(this.user.getId());
            matchPool.remove(this.user);
        }
    }

    @OnMessage
    public void onMessage(String message, Session session) {  // 一般会把onMessage()当作路由
        System.out.println("Receive message!");
        JSONObject data = JSONObject.parseObject(message);
        String event = data.getString("event");  // 取出event的内容

        if ("start_match".equals(event)) {
            this.startMatching();
        } else if ("stop_match".equals(event)) {
            this.stopMatching();
        }
    }

    @OnError
    public void onError(Session session, Throwable error) {
        error.printStackTrace();
    }

    public void sendMessage(String message) {  // 从后端向当前链接发送消息
        synchronized (this.session) {  // 由于是异步通信,需要加一个锁
            try {
                this.session.getBasicRemote().sendText(message);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private void startMatching() {
        System.out.println("Start matching!");
        matchPool.add(this.user);

        while (matchPool.size() >= 2) {  // 临时调试用的,未来要替换成微服务
            Iterator<User> it = matchPool.iterator();
            User a = it.next(), b = it.next();
            matchPool.remove(a);
            matchPool.remove(b);

            JSONObject respA = new JSONObject();  // 发送给A的信息
            respA.put("event", "match_success");
            respA.put("opponent_username", b.getUsername());
            respA.put("opponent_photo", b.getPhoto());
            users.get(a.getId()).sendMessage(respA.toJSONString());  // A不一定是当前链接,因此要在users中获取

            JSONObject respB = new JSONObject();  // 发送给B的信息
            respB.put("event", "match_success");
            respB.put("opponent_username", a.getUsername());
            respB.put("opponent_photo", a.getPhoto());
            users.get(b.getId()).sendMessage(respB.toJSONString());
        }
    }

    private void stopMatching() {
        System.out.println("Stop matching!");
        matchPool.remove(this.user);
    }
}

接着修改一下 PKIndexView,当接收到 WS 链接从后端发送过来的匹配成功消息后需要更新对手的头像和用户名:

...

<script>
...

export default {
  ...

  setup() {
    ...

    onMounted(() => {
      ...

      socket.onmessage = (msg) => {  // 接收到后端消息时会执行
        const data = JSON.parse(msg.data);  // Spring传过来的数据是放在消息的data中
        console.log(data);

        if (data.event === "match_success") {  // 匹配成功
          store.commit("updateOpponent", {
            username: data.opponent_username,
            photo: data.opponent_photo,
          });
          setTimeout(() => {  // 3秒后再进入游戏地图界面
            store.commit("updateStatus", "playing");
          }, 3000);
        }
      };

      socket.onclose = () => {  // 关闭链接后会执行
        console.log("Disconnected!");
        store.commit("updateStatus", "matching");  // 进入游戏地图后玩家点击其他页面应该是默认退出游戏
      };

      ...
    });

    ...
  },
};
</script>

...

测试的时候需要用两个浏览器,如果没有两个浏览器可以在 Edge 浏览器的右上角设置菜单中新建 InPrivate 窗口,这样就可以自己登录两个不同的账号进行匹配测试。

3.3 同步游戏地图

现在匹配成功后两名玩家进入游戏时看到的地图是不一样的,因为目前地图还都是在每名玩家本地的浏览器生成的,那么我们就需要将生成地图的逻辑放到服务器端。

先在后端consumer.utils 包下创建 Game 类,用来管理整个游戏流程。


http://www.niftyadmin.cn/n/5204302.html

相关文章

ios打包,证书获取

HBuilderX 打包ios界面&#xff1a; Bundle ID(AppID)&#xff1a; 又称应用ID&#xff0c;是每一个ios应用的唯一标识&#xff0c;就像一个人的身份证号码&#xff1b; 每开发一个新应用&#xff0c;首先都需要先去创建一个Bundle ID Bundle ID 格式&#xff1a; 一般为&…

2023年09月 Scratch(二级)真题解析#中国电子学会#全国青少年软件编程等级考试

Scratch等级考试(1~4级)全部真题・点这里 一、单选题(共25题,每题2分,共50分) 第1题 点击绿旗,运行程序后,舞台上的图形是?( ) A:画笔粗细为4的三角形 B:画笔粗细为5的六边形 C:画笔粗细为4的六角形 D:画笔粗细为5的三角形 答案:D 第2题 如下图所示,从所给…

D. Secret Santa(构造)

首先n点n边&#xff0c;是一个基环树&#xff0c; 可以观察得到其实最大值是不变的 剩下的人自己随便找个人匹配即可 所以关键是构造一个方案解决匹配到自己的情况 找到所有没送出礼物的人&#xff0c;然后直接匹配&#xff0c;如果匹配到自己 因为没有送出礼物的人想送出…

计数排序+桶排序+基数排序 详讲(思路+图解+代码详解)

文章目录 计数排序桶排序基数排序一、计数排序概念&#xff1a;写法一&#xff1a;写法二&#xff1a; 二、桶排序概念代码 三、基数排序概念1.LSD排序法&#xff08;最低位优先法&#xff09;2.MSD排序法&#xff08;最高位优先法&#xff09; 基数排序VS基数排序VS桶排序 计数…

2022年06月 Scratch(二级)真题解析#中国电子学会#全国青少年软件编程等级考试

Scratch等级考试(1~4级)全部真题・点这里 一、单选题(共25题,每题2分,共50分) 第1题 角色初始位置如图所示,下面哪个选项能让角色移到舞台的左下角? A: B: C: D: </

如何使用YOLOv8代码框架中的RT-DETR

1. RT-DETR RT-DETR是由由此&#xff0c;百度推出了——RT-DETR (Real-Time DEtection TRansformer) &#xff0c;一种基于 DETR 架构的实时端到端检测器&#xff0c;其在速度和精度上取得了 SOTA 性能。 RT-DETR开源的代码在百度自己的飞桨paddlepaddle上&#xff0c;因此非…

最新PHP熊猫头图片表情斗图生成源码

这是一款能生成熊猫头表情斗图的自适应系统源码&#xff0c;无论是在电脑还是手机上都可以正常使用&#xff01;这个源码集成了搜狗搜索图片接口&#xff0c;可以轻松地一键搜索数百万张图片&#xff0c;并且还包含了表情制作等功能模块。对于一些新站来说&#xff0c;这是一个…

opencv- CLAHE 有限对比适应性直方图均衡化

CLAHE&#xff08;Contrast Limited Adaptive Histogram Equalization&#xff09;是一种对比度有限的自适应直方图均衡化技术&#xff0c;它能够提高图像的对比度而又避免过度增强噪声。 在OpenCV中&#xff0c;cv2.createCLAHE() 函数用于创建CLAHE对象&#xff0c;然后可以…