之前自己基于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 one rror(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. 测试
-
开启两个浏览器, 会生成两个userId
-
一个浏览器 发送消息给另一个userId