spring-websocket 简单使用

发布时间 2023-09-13 19:41:47作者: QiaoZhi

  之前自己基于netty 实现了websocket 协议,实现单聊以及群聊。这里记录下spring 封装的 spring-websocket 使用方式。

1. 后端

1. pom

<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 http://maven.apache.org/maven-v4_0_0.xsd">
    <parent>
        <artifactId>cloud</artifactId>
        <groupId>cn.qz.cloud</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>cloud-websocket</artifactId>
    <name>Archetype - cloud-websocket</name>
    <url>http://maven.apache.org</url>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <!--基础配置-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-websocket</artifactId>
            <version>5.3.9</version>
        </dependency>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.3</version>
        </dependency>
    </dependencies>
</project>

2. yml

server:
  port: 8802

3. 相关类

vo:
package cn.qz.vo;

import lombok.Data;

@Data
public class MessageRequestVO {

    /**
     * 业务消息类型
     */
    private int msgType;

    /**
     * 发送者userId
     */
    private Long sendUserId;

    /**
     * 业务类型
     */
    private int bizType;

    /**
     * 业务模块
     */
    private int bizOptModule;

    /**
     * 接收者userId
     */
    private Long receivedUserId;

    /**
     * 消息
     */
    private String msg;
}


WebSocketOneToOneController:
package cn.qz.web;

import cn.hutool.json.JSONUtil;
import cn.qz.vo.MessageRequestVO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RestController;

import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @description 聊天
 * <p>
 * 描述: 该对象是多例
 * 一对一聊天
 * @ServerEndpoint 注解是一个类层次的注解,它的功能主要是将目前的类定义成一个websocket服务器端,
 * 注解的值将被用于监听用户连接的终端访问URL地址,客户端可以通过这个URL来连接到WebSocket服务器端
 */
@RestController
@ServerEndpoint(value = "/webSocketOneToOne/{sendId}/{roomId}")
@Slf4j
public class WebSocketOneToOneController {

    // 静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
    private static int onlineCount;
    //实现服务端与单一客户端通信的话,可以使用Map来存放,其中Key为用户标识
    private static final Map<Long, WebSocketOneToOneController> connections = new ConcurrentHashMap<>();
    // 与某个客户端的连接会话,需要通过它来给客户端发送数据
    private Session session;
    private Long sendId;
    private String roomId;



    /**
     * 连接建立成功调用的方法
     *
     * @param session 可选的参数。session为与某个客户端的连接会话,需要通过它来给客户端发送数据
     */
    @OnOpen
    public void onOpen(@PathParam("sendId") Long sendId, @PathParam("roomId") String roomId, Session session) {
        this.session = session;
        this.sendId = sendId;             //用户标识
        this.roomId = roomId;         //会话标识
        connections.put(sendId, this);     //添加到map中
        addOnlineCount();               // 在线数加
        log.info("sendId:" + sendId + "roomId:" + roomId);
        System.out.println(this.session);
        System.out.println("有新连接加入!新用户:" + sendId + ",当前在线人数为" + getOnlineCount());
    }

    /**
     * 连接关闭调用的方法
     */
    @OnClose
    public void onClose() {
        connections.remove(sendId);  // 从map中移除
        subOnlineCount();          // 在线数减
        System.out.println("有一连接关闭!当前在线人数为" + getOnlineCount());
    }

    /**
     * 收到客户端消息后调用的方法
     *
     * @param message 客户端发送过来的消息
     * @param session 可选的参数
     */
    @OnMessage
    public void onMessage(String message, Session session) {
        // 打印消息
        System.out.println("来自客户端的消息:" + message);
        if ("ping".equals(message)) {
            return;
        }

        // 将消息落库等操作

        // 发送消息
        try {
            MessageRequestVO messageRequestVO = JSONUtil.toBean(message, MessageRequestVO.class);
            //如果消息接收者在线,发给消息接受者
            if (messageRequestVO != null && messageRequestVO.getReceivedUserId() != null) {
                WebSocketOneToOneController con = connections.get(messageRequestVO.getReceivedUserId());
                if (con != null) {
                    if (roomId.equals(con.roomId)) {
                        con.session.getBasicRemote().sendText(message);
                    }
                }
            }
            //通知发送消息的,消息已经发送成功
            WebSocketOneToOneController confrom = connections.get(sendId);
            if (confrom != null) {
                if (roomId.equals(confrom.roomId)) {
                    confrom.session.getBasicRemote().sendText("ok");
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 发生错误时调用
     *
     * @param session
     * @param error
     */
    @OnError
    public void onError(Session session, Throwable error) {
        System.out.println("发生错误");
        error.printStackTrace();
    }


    /**
     * @param msg             消息内容
     * @param sendId          发送人
     * @param receiveId       接收人
     * @param roomId          房间ID
     * @param msgType         消息类型
     * @param requestId       消息请求ID
     * @param lastMessageTime 最后一次的消息时间
     * @param giftId          礼物ID
     */
    public void send(String msg, Long sendId, Long receiveId, String roomId, int msgType, String requestId, String lastMessageTime, Long giftId) {
        try {
            //通知发送消息的狗逼,消息已经发送成功
            WebSocketOneToOneController confrom = connections.get(sendId);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static synchronized int getOnlineCount() {
        return onlineCount;
    }

    public static synchronized void addOnlineCount() {
        WebSocketOneToOneController.onlineCount++;
    }

    public static synchronized void subOnlineCount() {
        WebSocketOneToOneController.onlineCount--;
    }

}



WebSocketStompConfig 配置类:

package cn.qz.config;

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

/**
 * @description websocket 配置
 **/
@Configuration
public class WebSocketStompConfig {

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



Springboot 主启动类:
package cn.qz;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@SpringBootApplication
@RequestMapping("/")
public class MainApplication {

    public static void main(String[] args) {
        SpringApplication.run(MainApplication.class, args);
    }

    @GetMapping()
    @ResponseBody
    public String index() {
        return "index";
    }
}
  

2. 前端测试HTML

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/html">
  <head>
    <title>WebSocket Example</title>
  </head>
  <body>
    登录用户ID:<input type="text" id="sendUserId" /></br>
    接受用户ID:<input type="text" id="receivedUserId" /></br>
    发送消息内容:<input type="text" id="messageInput" /></br>
    接受消息内容:<input type="text" id="messageReceive" /></br>
    <button onclick="sendMessage()">Send</button>

    <script>
	  // 随机发送者
	  var sendUserId = Math.floor(Math.random() * 1000000);
	  // 房间号码。 真实的房间号码可以是 消息发送方和接收方组成的房间。 动态生成的。
	  var roomId = 123
      var socket = new WebSocket(`ws://localhost:8802/webSocketOneToOne/${sendUserId}/${roomId}`);

      document.getElementById("sendUserId").value = sendUserId;
      socket.onopen = function (event) {
        console.log("WebSocket is open now.");
        let loginInfo = {
          msgType: 2, //登录消息
          sendUserId: sendUserId,
          bizType: 1, //业务类型
          bizOptModule: 1, //业务模块
          roomId: roomId,
          msg: 'login'
        };
        socket.send(JSON.stringify(loginInfo));
      };

	  var messageReceive = document.getElementById("messageReceive");
      socket.onmessage = function (event) {
        var message = event.data;
        console.log("Received message: " + message);
        messageReceive.value = message;
      };

      socket.onclose = function (event) {
        console.log("WebSocket is closed now.");
      };

      function sendMessage() {
        var message = document.getElementById("messageInput").value;
        var receivedUserId = document.getElementById("receivedUserId").value;
        let operateInfo = {
          msgType: 100, //业务消息
          sendUserId: sendUserId,
          bizType: 1, //业务类型
          bizOptModule: 1, //业务模块
          roomId: roomId,
          receivedUserId: receivedUserId,
          msg: message		
        };
        socket.send(JSON.stringify(operateInfo));
      }

      // 发送ping 消息
      setInterval(() => {
        socket.send("ping");
      }, 30000);
    </script>
  </body>
</html>

3. 测试

  1. 开启两个浏览器, 会生成两个userId

  2. 一个浏览器 发送消息给另一个userId