五子棋双人对战项目(3)——匹配模块

news2024/11/20 1:22:42

一、分析需求

二、约定前后端接口

三、实现游戏大厅页面(前端代码)

四、实现后端代码

五、线程安全问题

六、忙等问题

一、分析需求

        需求多个玩家,在游戏大厅进行匹配,系统会把实力相近的玩家匹配到一起

        要想实现上述效果,就需要利用到消息推送机制,即需要使用到 WebSocket 协议。如图:


二、约定前后端交互接口

        通过需求分析,确认了要使用 WebSocket 协议,来实现消息推送的效果,因此,约定前后端交互接口也是根据 WebSocket 展开的。

        WebSocket 协议,可以传输文本数据,也可以传输二进制数据,这里就采用传输 JSON 格式的文本数据

匹配请求:

        这里并不需要传送用户信息,因为在前面登录的时候,就已经把当前用户信息保存到HttpSession中了,在进行WebSocket连接时,只需要把HttpSession中的Session拿过来就行了,并保存在WebSocket连接中。

匹配响应:

        这里会有两个不同的匹配响应。

匹配响应1是指玩家点击开始匹配,玩家的这个操作的请求发送成功,后端返回回来的响应(立即返回的响应)

匹配响应2指有两个玩家成功匹配到一起了,服务器主动推送回来的响应(多久返回这个响应?服务器并不知道)。(匹配到的对手信息保存在服务器中)


三、实现游戏大厅页面(前端代码)

game_hall.html:

        JSON字符串 和 JS对象的转换:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>游戏大厅</title>
    <link rel="stylesheet" href="css/common.css">
    <link rel="stylesheet" href="css/game_hall.css">
</head>

<body>
    <div class="nav">五子棋对战</div>
    <!-- 整个页面的容器元素‘ -->
    <div class="container">
        <!-- 这个div在container中是处于垂直水平居中这样的位置的 -->
        <div>
            <!-- 展示用户信息 -->
            <div id="screen"></div>
            <!-- 匹配按钮 -->
            <div id="match-button">开始匹配</div>
        </div>
    </div>

    <script src="./js/jquery.min.js"></script>

    <script>
        $.ajax({
            type: 'get',
            url: '/userInfo',
            success: function (body) {
                let screenDiv = document.querySelector("#screen");
                screenDiv.innerHTML = "玩家: " + body.username + "分数: " + body.score
                    + "<br> 比赛场次: " + body.totalCount + "获胜场数: " + body.winCount
            },
            error: function () {
                alert("获取用户信息失败");
            }
        });
        // 此处进行初始化 websocket,并且实现前端的匹配逻辑
        // 此处的路径必须写作 /findMatch
        let websocket = new WebSocket("ws://127.0.0.1:8080/findMatch");
        websocket.onopen = function () {
            console.log("onopen");
        }
        websocket.onclose = function () {
            console.log("onclose");
            // alert("游戏大厅中接收到了失败响应! 请重新登录");
            // location.assign("/login.html");
        }
        websocket.onerror = function () {
            console.log("onerror");
        }
        // 监听页面关闭事件,在页面关闭之前,手动调动这里的 websocket 的 close 方法
        window.onbeforeunload = function () {
            websocket.close();
        }

        //一会重点来实现,要处理服务器返回的响应
        websocket.onmessage = function (e) {
            // 处理服务器返回的响应数据,这个响应就是针对 "开始匹配" / "结束匹配" 来应对的
            //解析得到的响应对象,返回的数据是一个 JSON 字符串,解析成 js 对象
            let resp = JSON.parse(e.data);
            let matchButton = document.querySelector("#match-button");
            if (!resp.ok) {
                console.log("游戏大厅中接收到了失败响应! " + resp.reason);
                alert("游戏大厅中接收到了失败响应! " + resp.reason);
                location.assign("/login.html");
                return;
            }
            if (resp.message == 'startMatch') {
                //开始匹配请求发起成功
                console.log("进入匹配队列成功");
                matchButton.innerHTML = '匹配中...(点击停止)';
            } else if (resp.message == 'stopMatch') {
                //结束匹配请求发起成功
                console.log("离开匹配队列成功");
                matchButton.innerHTML = '开始匹配';
            } else if (resp.message == 'matchSuccess') {
                //已经匹配到对手了
                console.log("匹配到对手! 进入游戏房间");
                location.assign("/game_room.html");
            } else {
                console.log("收到了非法的响应! message=" + resp.message);
            }

        }

        // 给匹配按钮添加一个点击事件
        let matchButton = document.querySelector('#match-button');
        matchButton.onclick = function () {
            //在触发 websocket 请求之前,先确认下 websocket 连接是否好着
            if (websocket.readyState == websocket.OPEN) {
                //如果当前 readyState 处在 OPPEN状态,说明连接是好着的
                //这里发送的数据有两种可能,开始匹配/停止匹配
                if (matchButton.innerHTML == '开始匹配') {
                    console.log("开始匹配");
                    websocket.send(JSON.stringify({
                        message: 'startMatch',
                    }))
                } else if (matchButton.innerHTML == '匹配中...(点击停止)') {
                    console.log("停止匹配");
                    websocket.send(JSON.stringify({
                        message: 'stopMatch',
                    }));
                }
            } else {
                //这是说明当前连接是异常的状态
                alert("当前您的连接已经断开! 请重新登录!");
                location.assign('/login.html');
            }
        }
    </script>

</body>

</html>

common.css:

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

html, body {
    height: 100%;
    background-image: url(../image/blackboard.jpg);
    background-repeat: no-repeat;
    background-position: center;
    background-size: cover;
}

.nav {
    height: 50px;

    background: rgb(50, 50, 50);
    color: white;

    line-height: 50px;
    padding-left: 20px;
}

.container {
    width: 100%;
    height: calc(100% - 50px);

    display: flex;
    align-items: center;
    justify-content: center;
}

game_hall.css:

.container {
    width: 100%;
    height: calc(100% - 50px);

    display: flex;
    align-items: center;
    justify-content: center;
}

#screen {
    width: 400px;
    height: 200px;
    font-size: 20px;
    background-color: gray;
    color: white;
    border-radius: 10px;

    text-align: center;
    line-height: 100px;
}

#match-button {
    width: 400px;
    height: 50px;
    font-size: 20px;
    background-color: orange;
    color: white;
    border: none;
    outline: none;
    border-radius: 10px;

    text-align: center;
    line-height: 50px;
    margin-top: 20px;
}

#match-button:active{
    background-color: gray;
}   

四、实现后端代码

        后端要想建立WebSocket连接,需要创建一个专门的类(MatchAPI),来处理 WebSocket 的请求;同时,还要新建一个类(WebSocketConfig),进行WebSocket连接、配置 WebSocket 连接的路径,以及拿到之前 HTTP 连接时的 Session

WebSocketConfig

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
    @Autowired
    private MatchAPI matchAPI;

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(matchAPI, "/findMatch")
                .addInterceptors(new HttpSessionHandshakeInterceptor());
    }
}

MatchAPI(处理请求)

        JSON字符串 和 Java对象的转换:

//通过这个类来处理匹配功能中的 websocket 请求
@Slf4j
@Component
public class MatchAPI extends TextWebSocketHandler {
    @Autowired
    private OnlineUserManager onlineUserManager;

    @Autowired
    private Matcher matcher;

    private ObjectMapper objectMapper = new ObjectMapper();
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        // 玩家上线,加入到 OnlineUserManager 中

        //1、先获取到当前用户的身份信息(谁在游戏大厅中,建立的连接)
        // 此处的代码,之所以能够getAttributes,全靠了在注册 websocket 的时候,
        // 加上了 .addInterceptors(new HttpsessionHandshakeInterceptor())
        // 这个逻辑就把 HttpSession 中的 Attribute 都给拿到 WebSocketSession 中了
        // 在 Http 登录逻辑中,往 HttpSession 中存了 User 数据:httpSession.setAttribute("user", user)
        // 此时就可以在 WebSocketSession 中把之前 HttpSession 里存的 User 对象给拿到了
        // 注意,此处拿到的 user,可能是为空的
        // 如果之前用户压根就没有通过 HTTP 来进行登录,直接就通过 /game_hall.html 这个URL来进行访问游戏大厅了
        // 此时就会出现 user 为 null 的情况
        try {
            User user = (User) session.getAttributes().get("user");
            //2、拿到了身份信息之后,进行判断当前用户是否已经登录过(在线状态),如果已经是在线,就不该继续进行后续逻辑
            WebSocketSession tmpSession = onlineUserManager.getFromGameHall(user.getUserId());
            if(tmpSession != null) {
                //  说明该用户已经登录了
                //  针对这个情况,要告知客户端,你这里重复登录了
                MatchResponse response = new MatchResponse();
                response.setOk(false);
                response.setReason("当前用户已经登录, 静止多开!");
                session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));
                session.close();
                return;
            }
            onlineUserManager.enterGameHall(user.getUserId(), session);
//            System.out.println("玩家" + user.getUsername() + " 进入游戏大厅");
            log.info("玩家 {}",user.getUsername() + " 进入游戏大厅");
        } catch (NullPointerException e) {
            e.printStackTrace();
            // 出现空指针异常,说明当前用户的身份信息为空,也就是用户未登录
            // 就把当前用户尚未登录,给返回回去
            MatchResponse response = new MatchResponse();
            response.setOk(false);
            response.setReason("您尚未登录,不能进行后续匹配");
            session.sendMessage(new TextMessage(objectMapper.writeValueAsBytes(response)));
        }
    }

    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        //  实现处理开始匹配请求和停止匹配请求
        User user = (User) session.getAttributes().get("user");
        //  拿到客户端发给服务器的数据
        String payload = message.getPayload();
        //  当前传过来的数据是JSON格式的字符串,就需要把它转成 Java 对象:MatchRequest
        MatchRequest request = objectMapper.readValue(payload, MatchRequest.class);

        MatchResponse response = new MatchResponse();
        if(request.getMessage().equals("startMatch")) {
            //  进入匹配队列
            //  把当前用户加入到匹配队列中
            matcher.add(user);
            //  把玩家信息放入匹配队列后,就可以返回一个响应给客户端了
            response.setOk(true);
            response.setMessage("startMatch");
        } else if (request.getMessage().equals("stopMatch")) {
            //  退出匹配队列
            //  在匹配队列中把当前用户给删除了
            matcher.remove(user);
            // 在匹配队列中把当前用户给删除后,就可以返回一个响应给客户端了
            response.setOk(true);
            response.setMessage("stopMatch");
        } else {
            //  非法情况
            response.setOk(false);
            response.setMessage("非法的匹配请求");
        }

        String jsonString = objectMapper.writeValueAsString(response);
        session.sendMessage(new TextMessage(jsonString));
    }

    @Override
    public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
        // 玩家下线,删除 OnlineUserManager 中的该用户的Session
        try {
            User user = (User) session.getAttributes().get("user");
            WebSocketSession tmpSession = onlineUserManager.getFromGameHall(user.getUserId());
            if(tmpSession == session) {
                onlineUserManager.exitGameHall(user.getUserId());
            }
            // 如果玩家正在匹配中,但WebSocket断开了,就应该把该玩家移除匹配队列
            log.info("Error玩家: {}", user.getUsername() + " 下线");
            matcher.remove(user);
        } catch (NullPointerException e) {
            e.printStackTrace();
            MatchResponse response = new MatchResponse();
            response.setOk(false);
            response.setReason("您尚未登录,不能进行后续匹配");
            session.sendMessage(new TextMessage(objectMapper.writeValueAsBytes(response)));
        }
    }

    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        // 玩家下线,删除 OnlineUserManager 中的该用户的Session
        try {
            User user = (User) session.getAttributes().get("user");
            WebSocketSession tmpSession = onlineUserManager.getFromGameHall(user.getUserId());
            if(tmpSession == session) {
                onlineUserManager.exitGameHall(user.getUserId());
            }
            // 如果玩家正在匹配中,但WebSocket断开了,就应该把该玩家移除匹配队列
            log.info("Closed玩家: {}", user.getUsername() + " 下线");
            matcher.remove(user);
        } catch (NullPointerException e) {
            e.printStackTrace();
            MatchResponse response = new MatchResponse();
            response.setOk(false);
            response.setReason("您尚未登录,不能进行后续匹配");
            session.sendMessage(new TextMessage(objectMapper.writeValueAsBytes(response)));
        }
    }
}

        对于 WebSocket 请求、返回的响应,把传送的数据封装成这两个类:

MatchResponse(响应)

// 这是表示一个 WebSocket响应
@Data
public class MatchResponse {
    private boolean ok;
    private String reason;
    private String message;
}

MatchRequest (请求)

// 这是表示一个 WebSocket 请求
@Data
public class MatchRequest {
    private String message;
}

OnlineUserManager(用户在线状态)

        之所以要维护用户的在线状态,目的是为了能够在代码中比较方便的获取到某个用户当前的WebSocket 会话,从而通过这个会话来对客户端发送消息。

        同时,也能感知到用户的 在线/离线 状态~。

        此处使用 哈希表 来维护 userId 和 WebSocketSession 的映射关系。

@Component
public class OnlineUserManager {
    //这个hash表就是用来表示当前用户在游戏大厅的在线状态
    private ConcurrentHashMap<Integer, WebSocketSession> gameHall = new ConcurrentHashMap<>();

    public void enterGameHall(int userId, WebSocketSession webSocketSession) {
        gameHall.put(userId, webSocketSession);
    }

    public void exitGameHall(int userId) {
        gameHall.remove(userId);
    }

    public WebSocketSession getFromGameHall(int userId) {
        return gameHall.get(userId);
    }
}

Matcher(匹配器)

        通过这个匹配器,来处理玩家的匹配功能。

//  这个类表示匹配器,通过这个类来负责整个的匹配功能
@Slf4j
@Component
public class Matcher {
    //  创建三个匹配队列
    private Queue<User> normalQueue = new LinkedList<>();
    private  Queue<User> highQueue = new LinkedList<>();
    private Queue<User> veryHighQueue = new LinkedList<>();

    @Autowired
    private OnlineUserManager onlineUserManager;

    @Autowired
    private RoomManager roomManager;

    private ObjectMapper objectMapper = new ObjectMapper();

    //  操作匹配队列的方法
    //  把玩家放到匹配队列中
    public void add(User user) {
        if(user.getScore() < 2000) {
            synchronized (normalQueue) {
                normalQueue.offer(user);
                normalQueue.notify();
            }
            log.info("把玩家 " + user.getUsername() + " 加入到了 normalQueue 中");
        } else if(user.getScore() >= 2000 && user.getScore() < 3000) {
            synchronized (highQueue) {
                highQueue.offer(user);
                highQueue.notify();
            }
            log.info("把玩家 " + user.getUsername() + " 加入到了 highQueue 中");
        } else {
            synchronized (veryHighQueue) {
                veryHighQueue.offer(user);
                veryHighQueue.notify();
            }
            log.info("把玩家 " + user.getUsername() + " 加入到了 veryHighQueue 中");
        }
    }

    //  当玩家点击停止匹配,就需要把该玩家从匹配队列删除
    public void remove(User user) {
        if(user.getScore() < 2000) {
            synchronized (normalQueue) {
                normalQueue.remove(user);
            }
            log.info("玩家: " + user.getUsername() + " 在 normalQueue 队列被删除");
        } else if(user.getScore() >= 2000 && user.getScore() < 3000) {
            synchronized (highQueue) {
                highQueue.remove(user);
            }
            log.info("把玩家: " + user.getUsername() + " 在 highQueue 队列被删除");
        } else {
            synchronized (veryHighQueue) {
                veryHighQueue.remove(user);
            }
            log.info("把玩家: " + user.getUsername() + " 在 veryHighQueue 队列被删除");
        }
    }

    public Matcher() {
        //  创建三个线程,分别针对这三个匹配队列,进行操作
        Thread t1 = new Thread() {
            @Override
            public void run() {
                //  扫描normalQueue
                while(true) {
                    handlerMatch(normalQueue);
                }
            }
        };
        t1.start();

        Thread t2 = new Thread() {
            @Override
            public void run() {
                while (true) {
                    handlerMatch(highQueue);
                }
            }
        };
        t2.start();

        Thread t3 = new Thread() {
            @Override
            public void run() {
                while (true) {
                    handlerMatch(veryHighQueue);
                }
            }
        };
        t3.start();
    }

    public void handlerMatch(Queue<User> matchQueue) {
        synchronized (matchQueue) {
            try {
                //  1、检测队列中元素个数是否达到 2
                //  队列的初始情况可能是 空
                //  如果往队列中添加一个元素,这个时候,仍然是不能进行后续匹配操作的
                //  因此在这里使用 while 循环检查更合理
                while (matchQueue.size() < 2) {
                    matchQueue.wait();
                }
                //  2、尝试从队列中取出两个玩家
                User player1 = matchQueue.poll();
                User player2 = matchQueue.poll();
                log.info("匹配出两个玩家: " + player1.getUsername() + ", " + player2.getUsername());

                //  3、获取到玩家的 WebSocket 会话
                //     获取到会话的目的是为了告诉玩家,你排到了~
                WebSocketSession session1 = onlineUserManager.getFromGameHall(player1.getUserId());
                WebSocketSession session2 = onlineUserManager.getFromGameHall(player2.getUserId());
                //  理论上来说,匹配队列中的玩家一定是在线的状态
                //  因为前面的逻辑进行了处理,当玩家断开连接的时候,就把玩家从匹配队列移除了
                //  但是这里还是进行一次判定,进行双重判定会更稳妥一点
                if(session1 == null) {
                    //  如果玩家1不在线了,就把玩家2放回匹配队列
                    matchQueue.offer(player2);
                    return;
                }
                if(session2 == null) {
                    //  如果玩家1不在线了,就把玩家2放回匹配队列
                    matchQueue.offer(player1);
                    return;
                }

                //  当前能否排到两个玩家是同一个用户的情况吗?一个玩家入队列两次
                //  理论上也不会存在~
                //  1) 如果玩家下线,就会对玩家移除匹配队列
                //  2) 又禁止了玩家多开
                //  但是仍然在这里多进行一次判定,以免前面的逻辑出现 bug 时,带来严重的后果
                if(session1 == session2) {
                    //  把其中的一个玩家放回匹配队列
                    matchQueue.offer(player1);
                    return;
                }

                //  4、把这两个玩家放到同一个游戏房间中
                Room room = new Room();
                roomManager.add(room, player1.getUserId(), player2.getUserId());

                //  5、给玩家反馈信息
                //    通过 WebSocket 返回一个 message 为 “matchSuccess” 这样的响应
                MatchResponse response1 = new MatchResponse();
                response1.setOk(true);
                response1.setMessage("matchSuccess");
                String json1 = objectMapper.writeValueAsString(response1);
                session1.sendMessage(new TextMessage(json1));

                MatchResponse response2 = new MatchResponse();
                response2.setOk(true);
                response2.setMessage("matchSuccess");
                String json2 = objectMapper.writeValueAsString(response2);
                session2.sendMessage(new TextMessage(json2));

            } catch (IOException | InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

Room(游戏房间)

//  这个类表示一个游戏房间
@Data
public class Room {
    // 使用字符串类型来表示,方便生成唯一值
    private String roomId;

    private User user1;
    private User user2;

    public Room() {
        // 构造 Room 的时候生成一个唯一的字符串表示房间 id
        // 使用 UUID 来作为房间 id
        roomId = UUID.randomUUID().toString();
    }
}

RoomManager(房间管理器)

// 房间管理器类
// 这个类也希望有唯一实例
@Component
public class RoomManager {
    private ConcurrentHashMap<String, Room> rooms = new ConcurrentHashMap<>();
    private ConcurrentHashMap<Integer, String> userIdToRoomId = new ConcurrentHashMap<>();

    public void add(Room room, int userId1, int userId2) {
        rooms.put(room.getRoomId(), room);
        userIdToRoomId.put(userId1, room.getRoomId());
        userIdToRoomId.put(userId2, room.getRoomId());
    }

    public void remove(String roomId, int userId1, int userId2) {
        rooms.remove(roomId);
        userIdToRoomId.remove(userId1);
        userIdToRoomId.remove(userId2);
    }

    public Room getRoomByRoomId(String roomId) {
        return rooms.get(roomId);
    }

    public Room getRoomByUserId(int userId) {
       String roomId = userIdToRoomId.get(userId);
       if(roomId == null) {
           // userId -> roomId 映射关系不存在,直接返回 null
           return null;
       }
       return rooms.get(roomId);
    }
}

五、线程安全问题

1、HashMap 和 多开

        如果多个线程访问同一个HashMap,就容易出现线程安全问题。

        如果同时多个用户和服务器 建立/断开 连接,此时服务器就是并发的在针对 HashMap 进行修改。

        所以要避免这种情况,解决这个线程安全问题,可以直接使用ConcurrentHashMap。

        当多个浏览器,通时对一个用户进行登录,进入游戏大厅,会引发下面这种问题:

       所以,我们不仅要解决线程安全问题,也要考虑用户多开的情况,那么用户能进行多开操作吗?显然是不能的,所以上面的代码逻辑也是会处理这种多开的情况,如果当前用户已经登录,禁止其他地方再登录

2、三个队列

        在匹配模块(Matcher类),为了划分玩家水平实力,使用了三个队列表示不同的实力分段;

        同时,创建三个线程,当用户进行匹配时,就会不停的扫描这三个队列,看是否能匹配对局成功。

        但因为匹配时,就会把玩家加入到对应段位的队列,而停止匹配,也会把玩家从对应的队列删除,又有多个线程并发的去执行,所以,存在线程安全问题。

        怎么办?

        针对这三个队列对象,分别进行加锁,如图:

        这三个线程都是调用同一个方法。如图:

        因此,我们针对这一个方法加锁就好了:


六、忙等问题

        我们创建了三个线程:

        会不停的去扫描这三个队列,元素个数是否达到2,如果达到2,就要吧这两个用户取出来,放在同一个房间中进行对局。

        但这里要一直扫描码?显然是不用的,所以可以在这里 wait 一下。

        既然 wait了,那就要有 notify,来唤醒它,继续锁竞争。那什么时候唤醒呢?当然是这个队列有新的用户加进来了,那再进行唤醒,再重新判断用户个数是否达到2.

        这样,我们就能解决忙等的问题。

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

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

相关文章

使用cmake配置pcl环境

项目文件在https://pan.quark.cn/s/d347f72c7432 文件中包含CMakeLists.txt&#xff0c;一个pcd文件&#xff0c;一个cpp源文件。 这里的话&#xff0c;首先你需要下载好cmake软件&#xff0c;并将其添加到环境变量。 CMakeLists.txt文件内容如下 cmake_minimum_required(VER…

「漏洞复现」EDU 某智慧平台 PersonalDayInOutSchoolData SQL注入漏洞

0x01 免责声明 请勿利用文章内的相关技术从事非法测试&#xff0c;由于传播、利用此文所提供的信息而造成的任何直接或者间接的后果及损失&#xff0c;均由使用者本人负责&#xff0c;作者不为此承担任何责任。工具来自网络&#xff0c;安全性自测&#xff0c;如有侵权请联系删…

鸿蒙媒体开发系列16——图像变换与位图操作

如果你也对鸿蒙开发感兴趣&#xff0c;加入“Harmony自习室”吧&#xff01;扫描下方名片&#xff0c;关注公众号&#xff0c;公众号更新更快&#xff0c;同时也有更多学习资料和技术讨论群。 1、概述 图片处理指对PixelMap进行相关的操作&#xff0c;如获取图片信息、裁剪、缩…

鸿蒙媒体开发系列17——图片编码与EXIF处理

如果你也对鸿蒙开发感兴趣&#xff0c;加入“Harmony自习室”吧&#xff01;扫描下方名片&#xff0c;关注公众号&#xff0c;公众号更新更快&#xff0c;同时也有更多学习资料和技术讨论群。 1、图片编码 图片编码指将PixelMap编码成不同格式的存档图片&#xff08;当前仅支持…

完整网络模型训练(一)

文章目录 一、网络模型的搭建二、网络模型正确性检验三、创建网络函数 一、网络模型的搭建 以CIFAR10数据集作为训练例子 准备数据集&#xff1a; #因为CIFAR10是属于PRL的数据集&#xff0c;所以需要转化成tensor数据集 train_data torchvision.datasets.CIFAR10(root&quo…

《OpenCV》—— 指纹验证

用两张指纹图片中的其中一张对其验证 完整代码 import cv2def cv_show(name, img):cv2.imshow(name, img)cv2.waitKey(0)def verification(src, model):sift cv2.SIFT_create()kp1, des1 sift.detectAndCompute(src, None)kp2, des2 sift.detectAndCompute(model, None)fl…

消费电子制造企业如何使用SAP系统提升运营效率与竞争力

在当今这个日新月异的消费电子市场中&#xff0c;企业面临着快速变化的需求、激烈的竞争以及不断攀升的成本压力。为了在这场竞赛中脱颖而出&#xff0c;消费电子制造企业纷纷寻求数字化转型的突破点&#xff0c;其中&#xff0c;SAP系统作为业界领先的企业资源规划(ERP)解决方…

Python批量下载PPT模块并实现自动解压

日常工作中&#xff0c;我们总是找不到合适的PPT模板而烦恼。即使有免费的网站可以下载&#xff0c;但是一个一个地去下载&#xff0c;然后再批量解压进行查看也非常的麻烦&#xff0c;有没有更好方法呢&#xff1f; 今天&#xff0c;我们利用Python来爬取一个网站上的PPT&…

SSM整合:图书管理系统

图书管理系统 一.环境 1.数据库环境 CREATE DATABASE ssmbuild;USE ssmbuild;DROP TABLE IF EXISTS books;CREATE TABLE books (bookID INT(10) NOT NULL AUTO_INCREMENT COMMENT 书id,bookName VARCHAR(100) NOT NULL COMMENT 书名,bookCounts INT(11) NOT NULL COMMENT 数量…

Leecode热题100-48.旋转图像

给定一个 n n 的二维矩阵 matrix 表示一个图像。请你将图像顺时针旋转 90 度。 你必须在 原地 旋转图像,这意味着你需要直接修改输入的二维矩阵。请不要 使用另一个矩阵来旋转图像。 示例 1: 输入:matrix = [[1,2,3],[4,5,6],[7,8,9]] 输出:[[7,4,1],[8,5,2],[9,6,3]]示…

QML使用Qt自带软键盘例子

//注意&#xff1a;一定要保证Qt有安装VirtualKeyboard插件 import QtQuick 2.10 import QtQuick.Window 2.3 import QtQuick.Controls 2.3 import QtQuick.VirtualKeyboard 2.1 import QtQuick.VirtualKeyboard.Settings 2.1 Window { id: root visible: true w…

109.WEB渗透测试-信息收集-FOFA语法(9)

免责声明&#xff1a;内容仅供学习参考&#xff0c;请合法利用知识&#xff0c;禁止进行违法犯罪活动&#xff01; 内容参考于&#xff1a; 易锦网校会员专享课 上一个内容&#xff1a;108.WEB渗透测试-信息收集-FOFA语法&#xff08;8&#xff09; 未授权burp&#xff1a; …

净利润暴跌,撤了,募投资金大比例购置不动产,突击申请专利

开科唯识终止原因如下&#xff1a;首先&#xff0c;报告期内&#xff0c;开科唯识收入规模较小&#xff0c;2023年上半年净利润更是出现暴跌的情况&#xff0c;其2023年可能难以满足创业板上市新规。此外&#xff0c;开科唯识研发费用率始终低于同行业可比公司&#xff0c;仅有…

线性代数书中求解齐次线性方程组、非齐次线性方程组方法的特点和缺陷(附实例讲解)

目录 一、克拉默法则 1. 方法概述 2. 例16(1) P45 3. 特点 (1) 只适用于系数矩阵是方阵 (2) 只适用于行列式非零 (3) 只适用于唯一解的情况 (4) 只适用于非齐次线性方程组 二、逆矩阵 1. 方法概述 2. 例16(2) P45 3. 特点 (1) 只适用于系数矩阵必须是方阵且可逆 …

每日读则推(二)

n.免疫疗法 n.策略,行动计划,战略 n.一代 v.设计(engineer n.工程师&#xff0c;设计师 v.设计,建造) A novel immunotherapy strategy using in vivo generation of engineered CAR T cells can n.(长篇)小说 a.新颖的,珍奇的 …

WebGIS包括哪些技术栈?怎么学习?

WebGIS&#xff0c;其实是利用Web开发技术结合地理信息系统&#xff08;GIS&#xff09;的产物&#xff0c;它是一种通过Internet实现GIS交互操作和服务的最佳途径。 WebGIS通过图形化界面直观地呈现地理信息和特定数据&#xff0c;具有可扩展性和跨平台性。 它提供交互性&am…

CSP-J二轮模拟赛----张浩轩补题报告

1.题目报告 1.交替出场2.翻翻转转3.方格取数4.圆圆中的方方AC0分--文件读写0分20分--骗分 2.赛中概况 第一题比较顺利&#xff0c;五六分钟就开始敲代码&#xff0c;暴力AC。 第二题耗了30分钟左右才有思路&#xff0c;写的时候也不大顺利&#xff0c;用得递归。文件读写错了…

HTML+CSS 基础第三季课堂笔记

一、CSS基础概念 CSS有两个重要的概念&#xff0c;分别是样式和布局 CSS的样式分为两种&#xff0c;一种是文字的样式&#xff0c;一种是盒模型的样式 CSS的另一个重要的特性就是辅助页面布局&#xff0c;完成HTML不能完成的功能&#xff0c;比如并排显示&#xff0c;比如精…

Flowable之任务撤回(支持主流程、子流程相互撤回)

撤回任务&#xff1a;主流程 > 主流程 处室主管【送科长审核】 处室主管【撤回科长审核】 流程日志 撤回任务&#xff1a;子流程 > 子流程 会办接收岗【送处室主管】 会办接收岗【撤回处室主管】 会办接收岗【同意】 撤回任务&#xff1a;子流程 > 主流程 处室主管…

秋招内推--招联金融2025

【投递方式】 直接扫下方二维码&#xff0c;或点击内推官网https://wecruit.hotjob.cn/SU61025e262f9d247b98e0a2c2/mc/position/campus&#xff0c;使用内推码 igcefb 投递&#xff09; 【招聘岗位】 后台开发 前端开发 数据开发 数据运营 算法开发 技术运维 软件测试 产品策…