手把手教你如何使用SpringBoot和WebSocket打造猜数字游戏

目录


前言

在现代化的Web应用中,实时性是越来越重要的一种需求。当涉及到多个用户同时进行操作,例如在线游戏,WebSockets变得越来越流行。WebSockets提供了一个双向通信通道,使得服务器和客户端能够进行实时通信,而无需HTTP请求/响应周期。在本篇教程中,我们将使用SpringBoot和WebSocket技术,构建一个简单的猜数字游戏,以演示如何在Web应用程序中使用WebSockets进行实时通信。通过本教程,您将了解如何使用SpringBoot快速构建Web应用程序,并使用WebSocket技术实现实时通信。

一、涉及环境

SpringBoot/MAVEN/MYSQL

二、使用步骤

1.pom配置

代码如下(示例):


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.1.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>guess</groupId>
    <artifactId>guess-game</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>guess-game</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.2</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>tk.mybatis</groupId>
            <artifactId>mapper</artifactId>
            <version>4.1.5</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.76</version>
        </dependency>
    </dependencies>
</project>

2.四个实类层

1.参赛者的名称和猜的数字类

public class JoinGameRequest implements Serializable {

    private String memberName; 
    private Integer factor; 


    public String getMemberName() {
        return memberName;
    }

​

 2.响应消息类

​

public class JoinGameResponse {

    
    private String message; 

    
    public String getMessage() {
        return message;
    }

    
    public void setMessage(String message) {
        this.message = message;
    }
}

​

​

3. ListData



public class ListData {

    // 声明私有成员变量
    private Integer messageType; 
    private String memberName; 
    private String guessTime; 
    private Integer guessPosition; 
    private Integer guessNumber; 
    private String gameCode; 
    private Integer guessResult; 
{

​

4.猜的信息类

public class StatisticsMessage {

    private Integer messageType;

    private Integer alreadyGuessCount;

    private Integer alreadyGuessRightPosition;

    private Long durationMin;

}

3.webSocket层

这是一个用于在Spring Boot应用程序中配置WebSocket的配置类。
该类实现了WebSocketConfigurer接口,重写了registerWebSocketHandlers方法,
用于注册WebSocket处理程序和设置WebSocket服务端的路径。
在这个例子中,我们使用WebSocketServer作为WebSocket处理程序,并设置了路径为"/socket",并允许来自任何来源的请求。
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(new WebSocketServer(), "/socket").setAllowedOrigins("*");
    }
}

4.Controller层

GameController类是一个RESTful Web服务的控制器类
@RestController
@RequestMapping("gameinfo") 
@CrossOrigin  
public class GameController {
    @Autowired
    private GameHandler gameHandler;
    @PostMapping("/joinGame")  
    public JoinGameResponse joinGame(@RequestBody JoinGameRequest request){
        return gameHandler.join(request); 
    }
}

 5.DAO层

1.游戏表实体类


@Table(name = "t_game")
public class Game {
    @Id
    @GeneratedValue(
            generator = "JDBC",
            strategy = GenerationType.IDENTITY
    )
    private Long id;
    private Integer status;
    private Date createTime;

}

2.游戏用户表实体类

@Table(name = "t_game_user")
public class GameUser {
    @Id
    @GeneratedValue(
            generator = "JDBC", 
            strategy = GenerationType.IDENTITY 
    )
    private Long id;
    private Long gameId;
    private String hash; 
    private String username;
    private Integer numberLocation; 
    private Integer number; 
    private Boolean isRight; 
    private Date createTime; 
}

3. 逻辑层

@Component
@Transactional(rollbackFor = Exception.class)
public class GameHandler {
    @Autowired
    private WebSocketServer webSocketServer;
    @Autowired
    private GameMapper gameMapper;
    @Autowired
    private GameUserMapper userMapper;

    private static String hash = null;

    private static Integer maxLocation = null;

    public JoinGameResponse join(JoinGameRequest request) {
        synchronized (this) {
            Game gameing = gameMapper.selectGameing();
            if (gameing == null) {
                gameing = new Game();
                gameing.setCreateTime(new Date());
                gameing.setStatus(1);
                gameMapper.insertSelective(gameing);
            }

            //查询当前用户
            GameUser user = userMapper.selectByGameId(gameing.getId(), request.getMemberName());
            Integer location = user == null ? 1 : user.getNumberLocation() + 1;

            //开始游戏
            if (hash == null) {
                this.generateRandom();
            }

            //最大位置为空 则去数据库查询最大数据,没有则赋值0,最大数据相等则时间最早为第一名
            if (maxLocation == null) {
                GameUser fristUser = userMapper.selectMaxLaction(gameing.getId());
                if (fristUser != null) {
                    //本身存在第一名则广播汇总数据
                    this.broadcastCount(fristUser);
                }
                maxLocation = fristUser == null ? 0 : fristUser.getNumberLocation();
            }

            //用户数据入库
            GameUser userNewData = new GameUser();
            //这里charAt位置是从0开始所以要减1
            userNewData.setRight(Integer.parseInt(String.valueOf(hash.charAt(location - 1))) == request.getFactor());
            userNewData.setGameId(gameing.getId());
            userNewData.setHash(hash);
            userNewData.setNumberLocation(location);
            userNewData.setNumber(request.getFactor());
            userNewData.setUsername(request.getMemberName());
            userNewData.setCreateTime(new Date());
            userMapper.insertSelective(userNewData);

            if (userNewData.getRight()) {
                //答对 刷新hash
                this.generateRandom();
                //判断该答对位置是否为最大 则广播数据
                if (userNewData.getNumberLocation() > maxLocation) {
                    maxLocation = userNewData.getNumberLocation();
                    this.broadcastCount(userNewData);
                }
            }

            //等于32 该轮次结束
            if (maxLocation == 32) {
                gameing.setStatus(0);
                gameMapper.updateByPrimaryKey(gameing);
                hash = null;
                maxLocation = null;
            }
            this.broadcastListCount(userNewData);
            JoinGameResponse response = new JoinGameResponse();
            response.setMessage("当前位数:" + userNewData.getNumberLocation() + (userNewData.getRight() ? "答对了" : "答错了"));
            return response;
        }
    }

    //生成32位随机数
    private void generateRandom() {
        SecureRandom random = new SecureRandom();
        hash = String.format("%01d", random.nextInt(10)) +
                String.format("%09d", random.nextInt(1000000000)) +
                String.format("%010d", random.nextInt(1000000000)) +
                String.format("%010d", random.nextInt(1000000000)) +
                String.format("%02d", random.nextInt(100));
    }


    //广播汇总数据
    private void broadcastCount(GameUser user) {
        StatisticsMessage statisticsMessage = userMapper.selectCountData(user.getGameId(), user.getUsername());
        statisticsMessage.setMessageType(6);
        webSocketServer.sendMessageToAll(JSONObject.toJSONString(statisticsMessage));
    }

    //广播列表数据
    private void broadcastListCount(GameUser user) {
        ListData data = new ListData();
        data.setMessageType(1);
        data.setGameCode(user.getHash());
        data.setGuessNumber(user.getNumber());
        data.setGuessPosition(user.getNumberLocation());
        Date date = new Date(user.getCreateTime().getTime());
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String timeString = sdf.format(date);
        data.setGuessTime(timeString);
        data.setMemberName(user.getUsername());
        data.setGuessResult(user.getRight() ? 1 : 2);
        webSocketServer.sendMessageToAll(JSONObject.toJSONString(data));
    }


}

4.mapper层

1.查询是否有进行中的游戏

public interface GameMapper extends Mapper<Game> {
    /**
     * 查询是否有进行中的游戏
     */
    @Select("select * from t_game where status=1")
    Game selectGameing();
}

2.查询当前最高猜对位数


public interface GameUserMapper extends Mapper<GameUser> {
    /**
     * 查询用户 进行中的游戏当前最高猜对位数
     * @param gameId
     * @param name
     * @return
     */
    @Select("select * from t_game_user where is_right=1 and game_id=#{gameId} and username=#{name} order by number_location desc limit 1")
    GameUser selectByGameId(Long gameId,String name);

    @Select("select a.* from t_game_user as a where `is_right`=1 and game_id=#{gameId} and number_location = (select max(number_location) from t_game_user) order by create_time asc limit 1")
    GameUser selectMaxLaction(Long gameId);

    //查询统计数据
    @Select("select count(*) as already_guess_count,sum(`is_right`) as already_guess_right_position,max(create_time)-min(create_time) as duration_min from t_game_user where `username`=#{name} and game_id=#{gameId}")
    StatisticsMessage selectCountData(Long gameId,String name);

}

5.webSocket serve层

@Component
public class WebSocketServer extends TextWebSocketHandler {
    private static final List<WebSocketSession> sessions = new ArrayList<>();

    @Override
    public void afterConnectionEstablished(WebSocketSession session) {
        // WebSocket连接建立时将该session加入集合
        sessions.add(session);
    }

    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
        sessions.remove(session);
    }

    public void sendMessageToAll(String message) {

        for (WebSocketSession session : sessions) {
            try {
                session.sendMessage(new TextMessage(message));
            } catch (IOException e) {
                System.out.println("Error sending message to session " + session.getId());
            }
        }
    }
}

总结

如果你喜欢本篇教程并想深入了解SpringBoot和WebSocket的应用,欢迎联系我们千锋教育我们将为您提供更深入的学习内容和实践机会。同时,如果您对本篇教程有任何疑问或建议,也欢迎随时联系我进行交流。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
为什么需要websocket? 传统的实时交互的游戏,或服务器主动发送消息的行为(如推送服务),如果想做在微信上,可能你会使用轮询的方式进行,不过这太消耗资源,大量的请求也加重了服务器的负担,而且延迟问题比较严重。如果是自己开发的app,为了解决这些问题,很多团队会自建socket,使用tcp长链接、自定协议的方式与服务器进行相对实时的数据交互。有能力的团队,采用这种方式自然没什么大问题。不过小团队可能就要花费很多时间去调试,要解决很多难题,这个在成本上就划不来。 H5引入了webSocket来解决网页端的长链接问题,而微信小程序也支持websocket。这是一个非常重要的特性,所以本系列的文章会专门拿出一篇来讨论websocketwebSocket本质上也是TCP连接,它提供全双工的数据传输。一方面可以避免轮询带来的连接频繁建立与断开的性能损耗,另一方面数据可以是比较实时的进行双向传输(因为是长链接),而且WebSocket允许跨域通信(这里有个潜在的跨域安全的问题,得靠服务端来解决)。目前除IE外的浏览器已经对webSocket支持得很好了,微信小程序再推一把之后,它会变得更加流行。 我们来设计一个新的demo,一个比较有趣的小游戏,多人版扫雷,准确地讲,多人版挖黄金。 游戏规则是这样的:把雷换成金子,挖到金子加一分,每人轮流一次(A挖完轮到B,B挖完A才能再点击),点中金子就算你的,也不会炸,游戏继续,直到把场上所有的金子都挖完游戏才结束。跟扫雷一样,数字也是表示周边有几个金子,然后用户根据场上已经翻出来的数字哪一格可能有金子。 这种交互的游戏难点在于,用户的点击操作都要传到服务器上,而且服务器要实时的推送到其它玩家的应用上。另外用户自己也要接收对方操作时实时传过来的数据,这样才不至于重复点中同一个格子。简单讲,就是你要上报操作给服务器,而服务器也要实时给你推消息。为了简化整个模型,我们规定玩家必须轮流来点击,玩家A点完后,才能轮到玩家B,玩家B操作完,玩家A才能点。 我们分几步来实现这个功能。 一、实现思路 1、第一步,我们要先生成扫雷的地图场景 这个算法比较简单,简述一下。随机取某行某列就可以定位一个格子,标记成金子(-1表示金子)。mimeCnt表示要生成的金子的数量,用同样的方式循环标记mimeCnt个随机格子。生成完后,再用一个循环去扫描这些-1的格子,把它周边的格子都加1,当然必须是非金子的格子才加1。代码放在这里。 其中increaseArround用来把这格金子周边的格子都加1,实现也比较简单: 执行genMimeArr(),随机生成结果如下: -1表示金子。看了下貌似没什么问题。接下去,我们就要接入webSocket了。 (这个是js版本的,其实生成地图场景的工作是在后台生成,这个js版本只是一个演示,不过算法是一样的。) 2、我们需要一个支持webSocket的服务端 本例子中,我们使用python的tornado框架来实现(tornado提供了tornado.websocket模块)。当然读者也可以使用socket.io,专为webSocket设计的js语言的服务端,用起来非常简单,它也对不支持webSocket的浏览器提供了兼容(flash或comet实现)。 笔者本人比较喜欢使用tornado,做了几年后台开发,使用最多的框架之一的就是它,NIO模型,而且非常轻量级,同样的rps,java可能需要700-800M的内存,tornado只要30-40M,所以在一台4G内存的机子上可以跑上百个tornado服务,而java,对不起,只能跑3个虚拟机。微服务的时代,这一点对小公司很重要。当然如果读者本人对java比较熟悉的话,也可以选择netty框架尝试一下。 webSocket用tornado的另一个好处是,它可以在同一个服务(端口)上同时支持webSocket及http两种协议。tornado的官方demo代码中展示了怎么实现同时使用两种协议。在本游戏中,可以这么用:用户进入首页,用http协议去拉取当前的房间号及数据。因为首页是打开最多的,进了首页的用户不一定会玩游戏。所以首页还没必要建立webSocket链接,webSocket链接主要用来解决频繁请求及推送的操作。首页只有一个请求操作。选了房间号后,进去下一个游戏页面再开始建立webSocket链接。 3、客户端 使用微信小程序开发工具,直接连接是会报域名安全错误的,因为工具内部做了限制,对安全域名才会允许连接。所以同样的,这里我们也继续改下工具的源码,把相关的行改掉就行修改方式如下: 找到asdebug.js的这一行,把它改成: if(false)即可。
### 回答1: 使用 Spring Boot 实现 WebSocket 服务端和网页客户端需要以下步骤: 1. 在项目的 pom.xml 文件中添加 spring-boot-starter-websocket 依赖。 2. 创建一个 WebSocketConfig 类,并使用 @EnableWebSocketMessageBroker 注解开启 WebSocket 消息代理。 3. 在 WebSocketConfig 类中配置消息代理,可以使用 @Autowired 注入 SimpMessagingTemplate 类。 4. 创建一个 WebSocketController 类,并使用 @Controller 注解标记为控制器。在该类中可以定义处理客户端请求的方法,使用 @MessageMapping 注解标记方法,并使用 SimpMessagingTemplate 向客户端发送消息。 5. 在网页客户端中,使用 JavaScript 和 WebSocket API 连接服务器并发送和接收消息。 6. 在 spring boot 启动类中添加 @EnableWebSocket 即可。 更多细节请参考Spring官网相关文档。 ### 回答2: 在使用Spring Boot实现WebSocket服务端和网页客户端时,需要进行以下步骤: 1. 首先,创建一个Spring Boot项目,并在pom.xml文件中添加相关的依赖项,包括Spring Web和Spring WebSocket依赖。 2. 创建一个WebSocket配置类,通过@EnableWebSocket注解启用WebSocket,并实现WebSocketConfigurer接口,重写registerWebSocketHandlers方法。 3. 在registerWebSocketHandlers方法中,创建一个WebSocketHandler对象,并使用registerHandler方法注册该Handler,并指定相关的WebSocket连接路径。 4. 在WebSocketHandler中,继承TextWebSocketHandler,重写handleTextMessage方法来处理接收到的文本消息。 5. 在handleTextMessage方法中,可以处理接收到的消息,并通过WebSocketSession对象的sendMessage方法发送消息给客户端。 6. 创建一个Web页面作为WebSocket的客户端,并使用JavaScript中的WebSocket对象进行连接,指定WebSocket连接路径。 7. 在客户端使用WebSocket对象的onopen、onmessage、onclose和onerror方法来处理连接建立、接收到消息、连接关闭和连接错误的情况。 使用以上步骤,可以实现一个简单的WebSocket服务端和网页客户端。当客户端连接到服务端时,服务端可以接收到客户端发送的消息,并进行相应的处理,然后将处理结果发送给客户端。而客户端可以通过WebSocket对象发送消息给服务端,并接收到服务端发送的消息,完成双向通信的功能。 ### 回答3: 使用Spring Boot实现WebSocket服务端和网页客户端可以通过以下几个步骤完成。 1. 首先,在pom.xml文件中添加Spring Boot的WebSocket依赖。 ```xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> ``` 2. 接下来,在Spring Boot的主类上添加@EnableWebSocket注解,启用WebSocket支持。 ```java @SpringBootApplication @EnableWebSocket public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } } ``` 3. 创建一个WebSocket处理类,实现WebSocketHandler接口,并重写相应的方法。 ```java @Component public class MyWebSocketHandler implements WebSocketHandler { @Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { // 当与客户端建立连接后触发 } @Override public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception { // 当接收到客户端消息时触发 } @Override public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception { // 当发生传输错误时触发 } @Override public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception { // 当与客户端断开连接后触发 } @Override public boolean supportsPartialMessages() { return false; } } ``` 4. 在WebSocket处理类中可以利用session对象与客户端进行交互,发送消息或者接收客户端发送的消息。例如,可以在`afterConnectionEstablished`方法中使用`session.sendMessage()`方法发送欢迎消息给客户端,在`handleMessage`方法中处理客户端发送的消息。 5. 创建一个配置类来注册WebSocketHandler,并指定WebSocket的访问路径。 ```java @Configuration public class WebSocketConfig implements WebSocketConfigurer { @Autowired private MyWebSocketHandler myWebSocketHandler; @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(myWebSocketHandler, "/websocket").setAllowedOrigins("*"); } } ``` 6. 在网页客户端中,可以利用JavaScript的WebSocket API来与服务端建立连接,并进行通信。 ```javascript var socket = new WebSocket('ws://localhost:8080/websocket'); socket.onopen = function() { // 当与服务端建立连接后触发 } socket.onmessage = function(event) { var message = event.data; // 接收服务端发送的消息 } socket.onclose = function(event) { // 当与服务端断开连接后触发 } function sendMessage(message) { socket.send(message); // 发送消息给服务端 } ``` 以上就是使用Spring Boot实现WebSocket服务端和网页客户端的基本步骤。可以根据实际需求,进一步细化和定制化相关功能。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值