IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 网络协议 -> 五子棋对战——重点实现 -> 正文阅读

[网络协议]五子棋对战——重点实现

项目功能

项目实现了用户的注册、登录、匹配、匹配时的双方信息、对战功能、结算时的分数加成。

注册
在这里插入图片描述
注册的时候通过 BCrypt 加密,防止用户的信息被黑客盗取。

大厅页面
在这里插入图片描述

对战页面
在这里插入图片描述
对战的时候,可以看到双方的信息,看到比赛场次和胜场,还有对方的积分情况。

对战结束的页面
在这里插入图片描述
对战结束回到大厅的时候,会刷新玩家的游戏信息。

具体功能如下视频

五子棋功能演示

WebSocket

是一种建立在客户端和服务器当中的链接。

  1. HTTP 协议是无状态、无连接、单向的应用层协议。采用了请求-响应模式,由客户端发送一个请求,由服务端返回一个响应。
  2. HTTP 协议的弊端是服务器无法主动向客户端发起消息。所以就导致客户端想要获取服务端连续的状态变化很困难。
  3. 大多是 web 程序是通过频繁的异步JavaScript和XML(AJAX)请求实现长轮询。轮询的效率低,非常浪费资源。所以就有了 WebSocket。
  4. 举例:就像我们在餐馆买了饭,如果不用 WebSocket 的话,就需要我们几分钟就跑过去问问老板熟没熟。如果用了 WebSocket 的话,就相当于饭熟了之后,老板直接给你端过来了。

请求头
在这里插入图片描述
返回头
在这里插入图片描述

Spring 内置的 WebSocket

通过 Spring,我们就可以直接使用 WebSocket 了。创建一个 Test 类,要继承自 TextWebSocketHandler:

public class Test extends TextWebSocketHandler {

    @Override
    //用户建立连接后触发的方法
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        super.afterConnectionEstablished(session);
    }

    @Override
    //收到文本消息后触发的方法
    public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
        super.handleMessage(session, message);
    }

    @Override
    //触发异常后触发的方法
    public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
        super.handleTransportError(session, exception);
    }

    @Override
    //关闭连接后触发的方法
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        super.afterConnectionClosed(session, status);
    }
}

再创建一个类,原来让 Spring 知道这是一个 WebSocket 配置类:

@Configuration
@EnableWebSocket//这个注释可以让Spring知道这是一个WebSocket配置类
public class WebSocketConfig implements WebSocketConfigurer {

    @Autowired
    private Test test;

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        //这个方法可以将一个消息处理器和一个路由关联上,访问这个路由后将使用testAPI的方法进行消息处理
        registry.addHandler(test,"/test");
    }
}

客户端代码

<body>
    <input type="text" id = "message">
    <input type="button" id = "submit" value="提交">

    <script>

        /* 创建一个websocket实例 */
        let url = "ws://127.0.0.1:8080/test"
        let websocket = new WebSocket(url)

        /* 给实例挂载一些回调函数 */
        websocket.onopen = function() {
            console.log("建立连接");
        }

        websocket.onmessage = function(e) {
            console.log("收到消息" + e.date);
        }

        websocket.onerror = function() {
            console.log("连接异常");
        }

        websocket.onclose = function() {
            console.log("连接关闭");
        }

        let input = document.querySelector('#message');
        let button = document.querySelector('#submit')
        button.onclick = function() {
            console.log("发送消息" + input.value);
            websocket.send(input.value);
        }
    </script>
</body>

运行结果如下:
在这里插入图片描述

实现匹配模块

实现匹配的时候,约定一个前后端的交互接口,然后通过匹配来把玩家放进一个房间。匹配的时候,也是通过消息推送机制的。就是通过 WebSocket 传输 JSON 格式的文本数据。

请求和响应

设置的请求和响应如下:
在这里插入图片描述

服务器逻辑

服务器当中要处理的事情有:对玩家进行匹配、对用户进行管理、处理连接对象、处理匹配请求、实现匹配器、实现对战的房间以及房间管理、对异常的处理。

在线玩家类处理

通过哈希表来进行记录用户是否在线,如果不在线,那么就需要对玩家的相关功能进行关闭。并且通过哈希表来判断房间是否在线,不在线就处理房间:

@Component
public class OnlineUserManager {
    //记录用户是否是在线状态
    private ConcurrentHashMap<Integer, WebSocketSession> gameHall = new ConcurrentHashMap<>();

    //用来记录用户在游戏房间是否在线
    private ConcurrentHashMap<Integer, WebSocketSession> gameRoom = 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);
    }

    public void enterGameRoom(int userId, WebSocketSession session) {
        gameRoom.put(userId, session);
    }

    public void exitGameRoom(int userId) {
        gameRoom.remove(userId);
    }

    public WebSocketSession getFromGameRoom(int userId) {
        return gameRoom.get(userId);
    }
}

匹配类

在匹配类当中,通过对玩家分数进行区分,来放入不同的匹配队列。如果匹配队列当中够了两个玩家,那么就把玩家拿出来,然后放到同一个房间。

@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() < 1500) {
            synchronized (normalQueue) {
                normalQueue.offer(user);
                normalQueue.notify();
            }
            System.out.println("玩家:" + user.getUsername() + " 加入到了 normalQueue 中!");
        } else if (user.getScore() >= 1500 && user.getScore() < 2000) {
            synchronized (highQueue) {
                highQueue.offer(user);
                highQueue.notify();
            }
            System.out.println("玩家:" + user.getUsername() + " 加入到了 highQueue 中!");
        } else {
            synchronized (veryHighQueue) {
                veryHighQueue.offer(user);
                veryHighQueue.notify();
            }
            System.out.println("玩家:" + user.getUsername() + " 加入到了 veryHighQueue 中!");
        }
    }

    //点击停止匹配的时候,就从匹配队列删除
    public void remove(User user) {
        if (user.getScore() < 1500) {
            synchronized (normalQueue) {
                normalQueue.remove(user);
            }
            System.out.println("玩家:" + user.getUsername() + " 退出了 normalQueue 中!");
        } else if (user.getScore() >= 1500 && user.getScore() < 2000) {
            synchronized (highQueue) {
                highQueue.remove(user);
            }
            System.out.println("玩家:" + user.getUsername() + " 退出了 highQueue 中!");
        } else {
            synchronized (veryHighQueue) {
                veryHighQueue.remove(user);
            }
            System.out.println("玩家:" + 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() {
                //扫描 highQueue
                while (true) {
                    handlerMatch(highQueue);
                }
            }
        };
        t2.start();

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

    private void handlerMatch(Queue<User> matchQueue) {
        synchronized (matchQueue) {
            try {
                //检测对了中的元素个数是否达到2,如果增加一个元素,任然还不能匹配,所以就使用 while
                while (matchQueue.size() < 2) {
                    matchQueue.wait();
                }
                //取出两个玩家
                User player1 = matchQueue.poll();
                User player2 = matchQueue.poll();
                System.out.println("匹配到的两个玩家:" + player1.getUsername() + " " + player2.getUsername());
                //获取到玩家的 websocket 会话,然后告诉玩家匹配到了
                WebSocketSession session1 = onlineUserManager.getFromGameHall(player1.getUserid());
                WebSocketSession session2 = onlineUserManager.getFromGameHall(player2.getUserid());
                //正常是在线的,因为之前判断过,这里再次判定
                if (session1 == null) {
                    //如果一个玩家不在线,就把另外一个玩家再放回去
                    matchQueue.offer(player2);
                    return;
                }
                if (session2 == null) {
                    //如果一个玩家不在线,就把另外一个玩家再放回去
                    matchQueue.offer(player1);
                    return;
                }
                //之前判断过,但还是再次判断是否多开
                if (session1 == session2) {
                    matchQueue.offer(player1);
                    return;
                }

                //把玩家放到一个匹配房间里面
                Room room = new Room();
                roomManager.add(room, player1.getUserid(), player2.getUserid());

                //告诉玩家匹配到对手了
                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 e) {
                e.printStackTrace();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

创建一个类来进行匹配

我们通过一个 MatchAPI 类来进行对玩家的匹配,然后重写里面的 afterConnectionEstablishedhandleTextMessagehandleTransportErrorafterConnectionClosed 方法。用来处理 WebSocket 请求。当然也需要在 WebSocketConfig 当中把这个类注册进来。

通过 afterConnectionEstablished 来设置玩家上线

@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
    //玩家上线,加入到 websocket 当中
    //获取当前用户身份,之前在拦截器当中加入了 httpsession
    // 这里直接拿就行了

    //登录中,已经有了 user 的数据,这里直接拿对象就行了
    //但是用户可能为空
    try {
        User user = (User) session.getAttributes().get("user");

        //判断用户是否已登录,如果已登录,其他地方就不能登了
        if (onlineUserManager.getFromGameHall(user.getUserid()) != null ||
            onlineUserManager.getFromGameRoom(user.getUserid()) != null) {
            //已登录,通知客户端,不能登录
            MatchResponse response = new MatchResponse();
            response.setOk(true);
            response.setReason("当前账号已登录,不能再次登录!");
            //说明当前多开了
            response.setMessage("repeatConnection");
            session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));

            return;
        }

        //设置玩家为在线状态
        onlineUserManager.enterGameHall(user.getUserid(), session);
        System.out.println("玩家:" + user.getUsername() + " 进入游戏大厅!");
    } catch (NullPointerException e) {
        System.out.println("MatchAPI afterConnectionEstablished 当前用户未登录!");
    }
}

通过 handleTextMessage 来处理匹配请求

@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
    //实现匹配请求和停止匹配请求
    User user = (User) session.getAttributes().get("user");
    //获取到客户端给服务器发送的数据
    String payload = message.getPayload();
    //把 JSON 字符串转化成 Java 对象
    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));
}

通过 handleTransportError 来处理玩家下线

@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
    try {
        //玩家下线,从 websocket 当中删除
        User user = (User) session.getAttributes().get("user");
        WebSocketSession tempSession = onlineUserManager.getFromGameHall(user.getUserid());
        //当前玩家和之前成功上线的玩家一样,才能下线
        if (tempSession == session) {
            onlineUserManager.exitGameHall(user.getUserid());
        }
        //玩家匹配的时候,websocket 断开了,也移除匹配队列
        matcher.remove(user);
    } catch (NullPointerException e) {
        System.out.println("MatchAPI handleTransportError 当前用户未登录!");
    }
}

通过 afterConnectionClosed 来删除玩家的 WebSocket 通信

@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
    try {
        //玩家下线,从 websocket 当中删除
        User user = (User) session.getAttributes().get("user");
        WebSocketSession tmpSession = onlineUserManager.getFromGameHall(user.getUserid());
        if (tmpSession == session) {
            onlineUserManager.exitGameHall(user.getUserid());
        }
        //玩家匹配的时候,websocket 断开了,也移除匹配队列
        matcher.remove(user);
    } catch (NullPointerException e) {
        System.out.println("MatchAPI afterConnectionClosed 当前用户未登录!");
    }
}

游戏类目设置

在这个类里面,要判断玩家登录的时候,是否已经匹配到房间,是否是账号多开:

@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
    GameReadyResponse response = new GameReadyResponse();

    //先获取到用户的身份信息,(从 HttpSession 拿到用户对象)
    User user = (User) session.getAttributes().get("user");
    if (user == null) {
        response.setOk(false);
        response.setReason("用户未登录");
        session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));
        return;
    }

    //判断是否已经在房间
    Room room = roomManager.getRoomByUserId(user.getUserid());
    if (room == null) {
        //说明玩家还没有匹配到房间
        response.setOk(false);
        response.setReason("玩家没有匹配到房间");
        session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));
        return;
    }

    //判断是否是多开
    if (onlineUserManager.getFromGameHall(user.getUserid()) != null
        || onlineUserManager.getFromGameRoom(user.getUserid()) != null) {
        //如果一个账号,一边在大厅,一边在房间,也当作多开
        response.setOk(true);
        response.setReason("不能多开账号!");
        response.setMessage("repeatConnection");
        session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));
        return;
    }

    //没多开,设置为上线状态
    onlineUserManager.enterGameRoom(user.getUserid(), session);

    //把玩家加入到房间中。到这里的时候,说明已经跳转页面了
    synchronized (room) {
        if (room.getUser1() == null) {
            //第一个玩家还没进入房间,就把当前进入房间的玩家作为 user1
            room.setUser1(user);
            //先进入房间的玩家作为先手
            room.setWhiteUser(user.getUserid());
            System.out.println("玩家 " + user.getUsername() + " 已经准备就绪! 作为玩家1");
            return;
        }

        if (room.getUser2() == null) {
            //第二个玩家还没进入房间,就把当前进入房间的玩家作为 user2
            room.setUser2(user);
            System.out.println("玩家 " + user.getUsername() + " 已经准备就绪! 作为玩家2");
            //两个玩家都加入了,就给玩家都返回数据,说双方都准备好了
            // 通知玩家1
            noticeGameReady(room, room.getUser1(), room.getUser2());
            // 通知玩家2
            noticeGameReady(room, room.getUser2(), room.getUser1());
            return;
        }
    }

    //如果还有玩家尝试连接同一个房间,就提示出错
    response.setOk(false);
    response.setReason("当前房间已满,您不能加入!");
    session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));
}

如果玩家中途退出之后,就视为放弃比赛,然后另外一方获胜。

  网络协议 最新文章
使用Easyswoole 搭建简单的Websoket服务
常见的数据通信方式有哪些?
Openssl 1024bit RSA算法---公私钥获取和处
HTTPS协议的密钥交换流程
《小白WEB安全入门》03. 漏洞篇
HttpRunner4.x 安装与使用
2021-07-04
手写RPC学习笔记
K8S高可用版本部署
mySQL计算IP地址范围
上一篇文章      下一篇文章      查看所有文章
加:2022-10-31 12:34:57  更:2022-10-31 12:39:21 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/19 12:20:20-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码