SpringBoot整合webSocket制作联机对战五子棋平台

概述

众所周知,http协议没发起一次请求都需要三次握手两次挥手,并且结束后自动断开连接.
如果我们想要制作一款联机游戏/即时通讯这种模式的网站程序的话,我们需要不停地发起请求,从而消耗了大量的性能. 因此当我们遇到这种场景的时候,一般采用webSocket作为通信协议. 在之前学习了Python socket实现局域网聊天 这样一个功能. 实际上流程都是差不多.

一般情况下我们使用后端语言编写服务端,然后前端使用Js进行接收. 代码中前端前端框架使用了Vue.js.

演示网站: wzq.ku-m.cn
前端代码: https://gitee.com/kumuuuu/websocket_online_gobang__vue
后端代码: https://gitee.com/kumuuuu/websocket_online_gobang_java

SpringBoot配置webSocket

引入Springboot内置好的启动器~(太贴心了...

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-websocket</artifactId>
    </dependency>

创建配置文件. 后面解释它的作用~

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

}

自此,Springboot配置完毕啦~ 哈哈哈哈,没错就是这么简单.

然后我们需要需要编写一下,当客户端连接成功到我们服务端执行什么方法呀,连接关闭执行什么呀,当收到客户端信息执行什么呀,这些都是需要我们自己进行注册的.

首先我们需要创建一个servce~ 然后加上注解 @ServerEndpoint(value = "/test")
通过这个注解,就是声明我们webSocket协议的相应地址为: ws://localhost:8080/test ,注意! 这里就不再是http开头啦,我们使用的是webSocket协议,端口由自己而决定.

当然,不要忘记再加 @Component 将它交给spring进行管理.

@Slf4j   (注册log4J日志)
@Component
@ServerEndpoint(value = "/test")
public class MySocket {}

接下来就是注册上面我们说到的,打开连接,关闭连接,对应的方法了.
值得注意的是,当发起请求的时候,客户端会携带一个Session,我们可以把它保存起来,然后通过它就可以实现服务端给客户端发送信息了!

@OnOpen
public void onOpen(Session session) {
    this.session = session;
}

其他的也是大同小异,注解分别为: @OnClose, @OnMessage(拥有一个string参数,即用户传来的文本).很显然,分别是声明关闭方法和收到信息的方法.

前面说了,我们可以通过session进行给客户端发送信息,那么只要我们把这个session用map记录下来,我们事后想给某个人发送信息,不就可以直接取出来发送了嘛.

所以我们一般使用ConcurrentHashMap进行记录,它与hashmap的区别是,它是属于线程安全的~

接下来我们就来看看如何使用session进行发送信息.

    public void sendMessage(Session session, String message) throws IOException {
        session.getBasicRemote().sendText(message);
   }

我们可以直接使用session下的getBasicRemote()方法进行sendText() ,即可发送. 实际上还有另外一种方法,为

        session.getAsyncRemote().sendText(message);

他们的区别是,前者是属于同步发送,后者属于异步发送. 百度得知一般我们都采用异步发送,但在编写Demo的途中发现使用异步发送会出错,具体原因没有深究.

写到这里,已经得知信息如何发送,可是这样是不是有一些单一? 我们不可能只是单一的发送一些消息什么的,我们需要的是传输数据. 而数据又理应分类! 比如五子棋项目,首先我们需要连接双方的信息/游戏状况的信息...等!

所以我们一般采用Json数据形式,由此来整理分类各种信息.

@Data
@AllArgsConstructor
@NoArgsConstructor
public class infoMsg {
    private int mode;
    private String msg;

    public String toJson(){
        return JSON.toJSONString(this);
    }

}

像五子棋项目,我定义了一共9个类型.
mode 0 ~ 2 == 以此为前端发送普通消息/成功消息/警告信息. 3是获取列表,4是通知各个用户新增的用户~ ..等

自此,后端项目的基本结构已经阐述完毕.

Js连接webSocket

前端很简单,直接连接我们创建的地址就ok.

ws = new WebSocket("ws://121.199.52.206:5003/test")

同后端一样,我们只需要创建对应的事件方法就好了~ 连接后(onopen)/连接错误(onerror)/收到信息(onmessage).

ws.onopen = () => {
  _this.$notify({
    title: '成功',
    message: '服务器连接成功!',
    type: 'success'
  })
}
ws.onerror = () => {
  _this.$notify({
    title: '警告',
    message: '服务器连接失败!',
    type: 'warning'
  });
}

二者大同小异.

主要业务代码

向所有在线用户发送信息

用意是提醒用户有新用户上线,其实就是遍历存储用户session的hashmap然后sendMessage.


public void sendUserAll(String name) throws IOException {
    for (Map.Entry<String, Session> entry : webSocketMap.entrySet()) {
        sendMessage(entry.getValue(), new infoMsg(4, name).toJson());
    }
}

判断游戏是否结束

就是判断用户当前的操作是否已达成五子连珠. 本来打算从网上复制一份的,但总感觉网上别人写的好智障23333...
思路很简单,只需要判断用户当前下的那个棋的上,下,左,右,上左,上右,下左,下右,八个方向与相连的方向一起是否构成五子连珠.

    public boolean GameCheck(int x, int y, int[][] arr) {
        int base = arr[x][y];
        int[][] pn = {{-1,-1},{-1,0},{-1,1},{1,-1},{1,0},{1,1},{0,-1},{0,1}};
        int[] result = new int[8];
        for (int i = 0; i < 8; i++) {
            int count = 0;
            int x1 = x;
            int y1 = y;
            while (true) {
                x1 += pn[i][0];
                y1 += pn[i][1];
                if (x1 < 0 || y1 < 0 || x1 >= arr.length || y1 >= arr.length
                        || arr[x1][y1] != base) {
                    result[i] = count;
                    break;
                } else {
                    count++;
                }
            }
        }
        if (result[0] + result[5] == 4 || result[1] + result[4] == 4 ||
            result[2] + result[3] == 4 || result[6] + result[7] == 4) {
            return true;
        }
    return false;

}