当前位置: 首页 > news >正文

WebSoket的简单使用

一、WebSocket简介

1.1、双向通信/全双工

客户端和服务器之间同时双向传输,全双工通信允许客户端和服务器随时互相发送消息,不需等一方发送请求后另一方才进行响应。
适用要低延迟/实时交互的场景,如在线游戏、即时通讯、股票行情等。

1.2、开发中的问题

1、处理客户端断网的策略。
心跳检测:定期发送心跳消息以确保连接的有效性。
超时处理:在特定时间内未收到客户端的消息或心跳回应时关闭连接。
异常处理:捕获并处理连接异常,如断网错误。
2、连接管理
连接断开和重连:客户端可能会由于网络波动、服务器重启等原因导致连接断开。需要实现自动重连机制。
  解决方案:客户端实现自动重连,服务器端实现连接状态的监控和重连处理。
连接数量限制:服务器可能会面临大量的并发连接,需要管理连接的生命周期。
  解决方案:使用连接池、限制单个用户的最大连接数、负载均衡。
3、数据传输
消息顺序:WebSocket 是全双工通信,消息可能会乱序到达。
  解决方案:在消息中添加序列号,客户端根据序列号重排消息。
消息大小:某些应用可能需要传输大数据,WebSocket 本身对消息大小有一定限制。
  解决方案:将大消息分割成小块发送,在客户端重新组装。
4、安全性
数据加密:WebSocket 传输的数据可以被中间人截获。
  解决方案:使用 wss:// 协议(基于 TLS 加密的 WebSocket)确保传输安全。
身份验证和授权:需要确保只有经过认证和授权的客户端才能建立 WebSocket 连接。
  解决方案:在握手阶段进行身份验证,使用 JWT 或其他令牌机制。
跨站脚本攻击 (XSS):WebSocket 可能成为 XSS 攻击的目标。
  解决方案:在服务器端验证和过滤输入数据,确保数据格式和内容安全。
5、性能优化
延迟:需要尽量减少消息传输的延迟。
  解决方案:优化网络路径、使用更快的服务器、减少数据量。
带宽消耗:频繁的消息传输会消耗大量带宽。
  解决方案:压缩消息、优化数据结构。
6、服务器架构
扩展性:需要确保 WebSocket 服务器能处理大量并发连接。
  解决方案:使用集群和负载均衡,将连接分配到多个服务器上。
高可用性:需要确保服务器在出现故障时能迅速恢复。
  解决方案:使用容错和故障转移机制,配置多个冗余服务器。

1.3、JavaScript 中 WebSocket 对象的属性和方法

WebSocket 对象:WebSocket 对象表示一个新的 WebSocket 连接。
WebSocket.onopen 事件处理程序:当 WebSocket 连接打开时触发。
WebSocket.onmessage 事件处理程序:当接收到来自 WebSocket 的消息时触发。
WebSocket.onerror 事件处理程序:当 WebSocket 发生错误时触发。
WebSocket.onclose 事件处理程序:当 WebSocket 连接关闭时触发。
WebSocket.send 方法:向 WebSocket 发送数据。
WebSocket.close 方法:关闭 WebSocket 连接。

1.4、WebSocket 的错误处理

WebSocket is not supported:当浏览器不支持 WebSocket 时,会出现此错误。解决方法是在浏览器兼容性列表中检查是否支持 WebSocket。
WebSocket connection closed:当 WebSocket 连接被关闭时,会出现此错误。解决方法是在 WebSocket.onclose 事件处理程序中进行错误处理。
WebSocket error:当 WebSocket 发生错误时,会出现此错误。解决方法是在 WebSocket.onerror 事件处理程序中进行错误处理。
WebSocket timeout:当 WebSocket 连接超时时,会出现此错误。解决方法是在 WebSocket.ontimeout 事件处理程序中进行错误处理。
WebSocket handshake error:当 WebSocket 握手失败时,会出现此错误。解决方法是在 WebSocket.onerror 事件处理程序中进行错误处理。
WebSocket closed by server:当 WebSocket 连接被服务器关闭时,会出现此错误。解决方法是在 WebSocket.onclose 事件处理程序中进行错误处理。
WebSocket closed by protocol:当 WebSocket 连接被协议错误关闭时,会出现此错误。解决方法是在 WebSocket.onclose 事件处理程序中进行错误处理。
WebSocket closed by network:当 WebSocket 连接被网络错误关闭时,会出现此错误。解决方法是在 WebSocket.onclose 事件处理程序中进行错误处理。
WebSocket closed by server:当 WebSocket 连接被服务器错误关闭时,会出现此错误。解决方法是在 WebSocket.onclose 事件处理程序中进行错误处理。

二、需求

使用人数500~1000人,时时使用不间断。

2.1、一对一单独聊天

2.1.1、聊天会话保存聊天记录(头像、图片、视频、表情包)。
2.1.2、支持发送视频、图片、表情包。
2.1.3、发出去的消息支持限时撤回(保存历史记录)。
2.1.4、会话支持删除,支持聊天信息清屏等。
2.1.5、支持聊天信息的摘取、转发。
2.1.6、支持艾特人、引用消息。

2.2、群聊

2.2.1、聊天会话保存聊天记录(头像、图片、视频、表情包)。
2.2.2、支持发送视频、图片、表情包。
2.2.3、发出去的消息支持限时撤回(保存历史记录)。
2.2.4、会话支持删除,支持聊天信息清屏等。
2.2.5、支持艾特人、引用消息。
2.2.6、有群组、管理员功能。
2.2.7、可剔除群员、支持禁言及全部禁言。

2.3、聊天会话管理

2.3.1、会话制顶、会话加特别关注、会话消息免打扰。
2.3.2、会话删除。

三、设计

3.1、系统架构(单WebSocket连接占10KB内存,单连1.2w)

3.1.1、三台服务器构建 WebSocket 服务。
3.1.2、用 Redis 存储全局会话、分布式锁

3.2、通信

3.2.1、SpringMVC: 创建/编辑/查询聊天会话,群成员的的增减等操作。
3.2.2、WebSocket: 聊天内容的传递、群公告的发布。

3.3、数据存储

3.3.1、Mysql: 用户表、用户—会话关联表、会话(单聊/群聊)表、群-成员关联表。
3.3.2、MongoDB: 聊天记录集、群公告、备份会话详情表。
3.3.3、Redis: 储存全局会话。

3.4、交互设计

3.4.1、查: 用户登录查询自己的参与的会话列表、点击会话查看聊天信息、查看群成员信息。
3.4.2、增/改:新增会话(单聊/群聊)、关闭单聊/退出群聊、修改群聊名称、屏蔽/关注群聊。
3.4.3、单聊/群聊时先创建会话,然后用SessionID去进行聊天交互。

3.5、问题点

3.5.1.MongoDB嵌套数组分页查询。
3.5.2.聊天信息的修改、撤回更新到 MongoDB中。
3.5.3.多人聊天会话很多。
3.5.4.会话是否有关闭。
3.5.5.一个会话聊天过长怎么办。

四、代码实现

4.1、引入依赖及配置

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-websocket</artifactId>
</dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId>
</dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId>
</dependency><!--mapStruct依赖 高性能对象映射-->
<!--mapstruct核心-->
<dependency><groupId>org.mapstruct</groupId><artifactId>mapstruct</artifactId><version>1.4.1.Final</version>
</dependency>
<!--mapstruct编译-->
<dependency><groupId>org.mapstruct</groupId><artifactId>mapstruct-processor</artifactId><version>1.4.1.Final</version>
</dependency><!-- fastjson2 -->
<dependency><groupId>com.alibaba.fastjson2</groupId><artifactId>fastjson2</artifactId><version>2.0.50</version>
</dependency>
spring.data.mongodb.host= 127.0.0.1
spring.data.mongodb.database= test
spring.data.mongodb.port= 27017

4.2、MongoDB端实体类

/*** 聊天信息*/
@Data
@Accessors(chain = true)
public class ChatMessageMg {/*** 会话 ID*/private String sessionId;/*** 消息体 ID*/@Idprivate String messageId;/*** 发送人ID*/private String userId;/*** 发送人工号*/private String userNo;/*** 发送时间*/private Date sendTime;/*** 聊天内容*/private String chatMessage;/*** 引用内容*/private String refMessage;/*** 序号*/private Integer sortNo;/*** 标识(0: 原信息, 1: 已撤回 , 2: 已修改)*/private Integer messageType;/*** 版本(默认0,累加)*/private Integer messageVersion;
}/*** 群聊框-会话*/
@Data
@Accessors(chain = true)
public class ChatSessionGroupMg {/*** 聊天-会话 ID*/@Idprivate String sessionId;/*** 会话框类型(0单聊,1群聊)*/private Integer chatType;/*** 群名称*/private String chatName;/*** 创建时间*/private Date chatCreateTime;/*** 最后活跃时间*/private Date lastActiveTime;/*** 会话创建人(或群主工号)*/private String chatCreateUserNo;/*** 历史聊天集合*/private List<ChatMessageMg> messageList;
}/*** 单聊框-会话*/
@Data
@Accessors(chain = true)
public class ChatSessionSingleMg {/*** 聊天-会话 ID*/@Idprivate String sessionId;/*** 会话框类型(0单聊,1群聊)*/private Integer chatType;/*** 创建时间*/private Date chatCreateTime;/*** 最后活跃时间*/private Date lastActiveTime;/*** 会话甲(创建人)名称*/private String userAName;/*** 会话甲(创建人)工号*/private String userANo;/*** 会话甲(创建人)头像*/private String userAUrl;/*** 会话乙 名称*/private String userBName;/*** 会话乙 工号*/private String userBNo;/*** 会话乙 头像*/private String userBUrl;/*** 历史聊天集合*/private List<ChatMessageMg> messageList;
}/*** 群公告*/
@Data
@Accessors(chain = true)
public class NoticeMg {}

4.3、Mysql端实体类

/*** 群聊框-会话*/
@Data
@Accessors(chain = true)
public class ChatSessionGroup {/*** 聊天-会话 ID*/@Idprivate String sessionId;/*** 会话框类型(0单聊,1群聊)*/private Integer chatType;/*** 群名称*/private String chatName;/*** 创建时间*/private Date chatCreateTime;/*** 最后活跃时间*/private Date lastActiveTime;/*** 会话创建人(或群主工号)*/private String chatCreateUserNo;
}/*** 单聊框-会话*/
@Data
@Accessors(chain = true)
public class ChatSessionSingle {/*** 聊天-会话 ID*/@Idprivate String sessionId;/*** 会话框类型(0单聊,1群聊)*/private Integer chatType;/*** 创建时间*/private Date chatCreateTime;/*** 最后活跃时间*/private Date lastActiveTime;/*** 会话甲(创建人)名称*/private String userAName;/*** 会话甲(创建人)工号*/private String userANo;/*** 会话甲(创建人)头像*/private String userAUrl;/*** 会话乙 名称*/private String userBName;/*** 会话乙 工号*/private String userBNo;/*** 会话乙 头像*/private String userBUrl;
}/*** 会话 - 成员*/
@Data
@Accessors(chain = true)
public class ChatSessionUser {/*** ID*/private String id;/*** 会话ID*/private String sessionId;/*** 会话框类型(0单聊,1群聊)*/private Integer chatType;/*** 用户工号*/private String userNo;/*** 用户名称*/private String userName;/*** 用户头像*/private String userUrl;/*** 入群时间*/private Date inGroupTime;/*** 最后活跃时间*/private Date lastActiveTime;/*** 是否群主(0否,1是)*/private Integer isGroupLeader;/*** 是否管理员(0否,1是)*/private Integer isAdmin;/*** 是否禁言(0否,1是)*/private Integer isSilence;/*** 禁言时长*/private Integer silenceDuration;/*** 禁言开启时间*/private Date openSpeakTime;
}/*** 用户*/
@Data
@Accessors(chain = true)
public class User {private String name;private Integer age;
}

4.4、封装参数类

@Data
public class ChatSessionParam {/*** 会话ID*/private String sessionId;/*** 群名称*/private String chatName;/*** 是否 废弃 (0否 , 1是)*/private Integer isDiscard;/*** 是否 屏蔽 (0否 , 1是)*/private Integer isShield;/*** 是否 关注 (0否 , 1是)*/private Integer isWatch;/*** 群员工号*/private List<String> userNos;}@Data
@Accessors(chain = true)
public class SocketMessageParam {/*** 用户ID*/private String userId;/*** 用户工号*/private String userNo;/*** 此聊天会话 ID*/private String chatSessionId;/*** 消息*/private String message;}

4.5、MapStruct 工具类(不建议用)

import com.example.demo.bean.mongo.ChatSessionGroupMg;
import com.example.demo.bean.mongo.ChatSessionSingleMg;
import com.example.demo.bean.mysql.ChatSessionGroup;
import com.example.demo.bean.mysql.ChatSessionSingle;import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;/*** MapStruct 实体类 转换工具*/
@Mapper
public interface SourceDestinationMapper {SourceDestinationMapper INSTANCE = Mappers.getMapper(SourceDestinationMapper.class);ChatSessionSingle ChatSessionSingleMg_ChatSessionSingle(ChatSessionSingleMg chatSessionSingleMg);ChatSessionGroup ChatSessionGroupMg_ChatSessionGroup(ChatSessionGroupMg chatSessionGroupMg);
}

4.6、WebSocket端代码(Demo)

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;/*** @Description: WebSocket配置类。开启WebSocket的支持*/
@Configuration
public class WebSocketConfig {@Beanpublic ServerEndpointExporter serverEndpointExporter() {return new ServerEndpointExporter();}
}import com.alibaba.fastjson2.JSON;
import com.example.demo.bean.mongo.ChatMessageMg;
import com.example.demo.bean.query.SocketMessageParam;
import com.example.demo.service.WebSocketService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.stereotype.Component;import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;/*** WebSocket 服务器类,用于处理WebSocket连接、消息、错误等事件。*/
@Component
@Slf4j
@ServerEndpoint("/websocket/{userId}")
public class WebSocketServer {@Autowiredprivate WebSocketService webSocketService;@Autowiredprivate MongoTemplate mongoTemplate;// 记录当前在线客户端的数量private static AtomicInteger onlineSessionClientCount = new AtomicInteger(0);// 存储在Redis中private static Map<String, Session> userIdSessionMap= new ConcurrentHashMap<>();/*** 当WebSocket连接打开时调用的方法* @param userId 用户唯一ID* @param session 当前WebSocket会话*/@OnOpenpublic void onOpen(@PathParam("userId") String userId, Session session) {log.info("连接建立中 ==> session_id = {}, sid = {}", session.getId(), userId);// 将当前会话添加到在线会话映射表中userIdSessionMap.put(userId, session);// 在线数量+1onlineSessionClientCount.incrementAndGet();// 发送消息给当前连接sendToOne(userId, "连接成功");// 输出当前在线数量以及连接信息log.info("连接建立成功,当前在线数为:{} ==> 开始监听新连接:session_id = {}, sid = {},。", onlineSessionClientCount, session.getId(), userId);}/*** 当 WebSocket 连接关闭时调用的方法* @param userId 客户端的唯一标识符* @param session 当前WebSocket会话*/@OnClosepublic void onClose(@PathParam("userId") String userId, Session session) {// 在线数量-1onlineSessionClientCount.decrementAndGet();// 输出当前在线数量以及关闭的连接信息log.info("连接关闭成功,当前在线数为:{} ==> 关闭该连接信息:userId = {}, sid = {},。", onlineSessionClientCount, session.getId(), userId);}/*** 当从客户端接收到消息时调用的方法。* @param socketMessage 接收到的消息* @param session 当前WebSocket会话*/@OnMessagepublic void onMessage(String socketMessage, Session session) {log.info("服务端收到客户端消息 ==> fromSid = {},  socketMessage = {}", session.getId(), socketMessage);SocketMessageParam messageParam = JSON.parseObject(socketMessage,SocketMessageParam.class);System.out.println("----messageParam: "+messageParam);if(messageParam!=null){String chatSessionId = messageParam.getChatSessionId();String userNo = messageParam.getUserNo();String message= messageParam.getMessage();List<String> userIdList = webSocketService.getChatUserByUserId(userNo);if(userIdList==null||userIdList.isEmpty()){return;}if(userIdList.size()==1){String userId = userIdList.get(0);Session otherSession = userIdSessionMap.get(userId);otherSession.getAsyncRemote().sendText(message);ChatMessageMg chatMessageMg = new ChatMessageMg().setSessionId(chatSessionId).setUserId(userId).setUserNo(userNo).setMessageVersion(0).setMessageType(0).setSendTime(new Date()).setChatMessage(message);webSocketService.saveSingleChatMessage(chatMessageMg);}else {for(String userId : userIdList){Session otherSession = userIdSessionMap.get(userId);if(otherSession!=null) {otherSession.getAsyncRemote().sendText(message);ChatMessageMg chatMessageMg = new ChatMessageMg().setSessionId(chatSessionId).setUserId(userId).setUserNo(userNo).setMessageVersion(0).setMessageType(0).setSendTime(new Date()).setChatMessage(message);webSocketService.saveGroupChatMessage(chatMessageMg);}}}}}/*** 当WebSocket发生错误时调用的方法。* @param session 当前WebSocket会话* @param error 错误信息*/@OnErrorpublic void onError(Session session, Throwable error) {log.error("WebSocket发生错误,错误信息为:" + error.getMessage());error.printStackTrace();}/*** 向所有在线客户端发送消息。* @param message 要发送的消息*/public void sendToAll(String message) {// 遍历在线会话映射表userIdSessionMap.forEach((onlineSid, toSession) -> {try {log.info("服务端给客户端群发消息 ==> toSid = {}, message = {}", onlineSid, message);toSession.getAsyncRemote().sendText(message);} catch (Exception e) {log.error("发送消息给客户端时发生错误:{}", onlineSid, e);userIdSessionMap.remove(onlineSid);}});}/*** 向单个客户端发送消息。* @param toSid 目标客户端的唯一标识符* @param message 要发送的消息*/public void sendToOne(String toSid, String message) {Session toSession = userIdSessionMap.get(toSid);if (toSession == null) {log.error("服务端给客户端发送消息 ==> toSid = {} 不存在, message = {}", toSid, message);return;}// 使用异步方式发送消息log.info("服务端给客户端发送消息 ==> toSid = {}, message = {}", toSid, message);toSession.getAsyncRemote().sendText(message);}
}

4.7、MVC端的代码(Demo)

import com.example.demo.bean.mongo.ChatSessionGroupMg;
import com.example.demo.bean.mongo.ChatSessionSingleMg;
import com.example.demo.bean.mysql.ChatSessionGroup;
import com.example.demo.bean.mysql.ChatSessionSingle;
import com.example.demo.bean.mysql.User;
import com.example.demo.bean.mysql.ChatSessionUser;
import com.example.demo.bean.query.ChatSessionParam;
import com.example.demo.mapper.SourceDestinationMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;@Slf4j
@RestController
@RequestMapping("/sessionController/")
public class WebSessionController {@Autowiredprivate MongoTemplate mongoTemplate;/*** 创建会话*/@PostMapping("createSession")public String createSession(@RequestBody ChatSessionParam sessionParam) {if(sessionParam==null||sessionParam.getUserNos()==null||sessionParam.getUserNos().isEmpty()){return "参数、聊天人员不能为空";}List<String> userNos = sessionParam.getUserNos();// 获取当前登录用户信息String userANo ="NO123654";// 去重处理Set<String> userNoSets = new HashSet<>(userNos);// 当前时间Date nowTime = new Date();// 会话 与 用户关联 集合List<ChatSessionUser> sessionUserList = new ArrayList<>();if(userNoSets.size()==1){/* 单聊情况 */// 1.判断是否有之前的 会话记录,有则获取之前的会话聊天记录。// 获取单聊人员工号。String userBNo =userNoSets.iterator().next();if(userANo.equals(userBNo)){return "不能和自己聊天";}// 创建单聊会话ChatSessionSingleMg chatSessionMg = new ChatSessionSingleMg().setChatType(0).setChatCreateTime(nowTime).setLastActiveTime(nowTime).setUserANo(userANo).setUserBNo(userBNo).setMessageList(new ArrayList<>());ChatSessionSingle chatSessionSingle = SourceDestinationMapper.INSTANCE.ChatSessionSingleMg_ChatSessionSingle(chatSessionMg);// 持久化到 MonGoDB 并返回 SessionIdmongoTemplate.insert(chatSessionMg);String sessionId = chatSessionMg.getSessionId();chatSessionSingle.setSessionId(sessionId);// chatSession 持久化到 Mysql ...// 会话 与 用户关联ChatSessionUser chatSessionUserA = new ChatSessionUser().setSessionId(sessionId).setChatType(0).setUserNo(userANo).setOpenSpeakTime(nowTime).setLastActiveTime(nowTime).setIsGroupLeader(0).setIsAdmin(0).setIsGroupLeader(0);ChatSessionUser chatSessionUserB = new ChatSessionUser().setSessionId(sessionId).setChatType(0).setUserNo(userBNo).setIsGroupLeader(0).setIsAdmin(0).setIsGroupLeader(0);sessionUserList.add(chatSessionUserA);sessionUserList.add(chatSessionUserB);// sessionUserList 持久化到 Mysql、Redis ...return sessionId;}else {/* 群聊情况 */// 创建群聊对象ChatSessionGroupMg chatSessionGroupMg =new ChatSessionGroupMg().setChatType(1).setChatCreateTime(nowTime).setLastActiveTime(nowTime).setChatName(sessionParam.getChatName()).setChatCreateUserNo(userANo).setMessageList(new ArrayList<>());mongoTemplate.insert(chatSessionGroupMg);String sessionId = chatSessionGroupMg.getSessionId();ChatSessionGroup chatSessionGroup = SourceDestinationMapper.INSTANCE.ChatSessionGroupMg_ChatSessionGroup(chatSessionGroupMg);chatSessionGroup.setSessionId(sessionId);// 群主ChatSessionUser leadSenssionUser = new ChatSessionUser().setChatType(1).setSessionId(sessionId).setUserNo(userANo).setSessionId(sessionId).setIsGroupLeader(1).setIsAdmin(1).setIsSilence(0).setInGroupTime(nowTime).setLastActiveTime(nowTime);sessionUserList.add(leadSenssionUser);for(String usNo : userNoSets){if(userANo.equals(usNo)){continue;}// 创建群成员ChatSessionUser senssionUser = new ChatSessionUser().setChatType(1).setSessionId(sessionId).setUserNo(usNo).setSessionId(sessionId).setIsGroupLeader(1).setIsAdmin(1).setIsSilence(0).setInGroupTime(nowTime).setLastActiveTime(nowTime);sessionUserList.add(senssionUser);}// chatSessionGroup、sessionUserList 持久化到 Mysql、Redis ...return sessionId;}}/*** 会话的(置顶、屏蔽、特别关注)*/@PostMapping("sessionEdit")public String sessionEdit(@RequestBody ChatSessionParam chatSessionParam) {// 获取当前登录用户信息,查询此用户下有效的会话信息。String userNo ="NO123654";return "Success";}/*** 获取当前用户会话列表*/@RequestMapping("getSessionList")public List<ChatSessionUser> getSessionList() {// 获取当前登录用户信息,查询此用户下有效的会话信息。return new ArrayList<>();}// http://127.0.0.1:8080/html@RequestMapping("/html")public String html() {return "index.html";}@ModelAttributepublic void parseUser(@RequestParam(name = "name", defaultValue = "unknown user") String name, @RequestParam(name = "age", defaultValue = "12") Integer age, User user) {user.setName("zhangsan");user.setAge(18);}
}import com.example.demo.bean.mongo.ChatMessageMg;
import com.example.demo.bean.mongo.ChatSessionGroupMg;
import com.example.demo.bean.mongo.ChatSessionSingleMg;
import com.example.demo.service.WebSocketService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;import org.springframework.stereotype.Service;import java.util.ArrayList;
import java.util.List;@Slf4j
@Service
public class WebSocketServiceImpl implements WebSocketService {@Autowiredprivate MongoTemplate mongoTemplate;/*** 通过聊天会话Id 获取会话成员集合* @param chatSessionId*/public List<String> getChatUserByUserId(String chatSessionId){System.out.println("----chatSessionId: "+chatSessionId);// 查询 会话成员关系 获取此会话下全部成员信息List<String> userIdList = new ArrayList<>();userIdList.add("111");userIdList.add("222");userIdList.add("333");return userIdList;}/*** 记录聊天内容* @param chatMessageMg*/public boolean saveSingleChatMessage(ChatMessageMg chatMessageMg){if(chatMessageMg==null||chatMessageMg.getChatMessage()==null){return false;}String chatSessionId = chatMessageMg.getSessionId();Query query = Query.query(Criteria.where("_id").is(chatSessionId));Update update = new Update();update.push("messageList", chatMessageMg);mongoTemplate.updateFirst(query, update, ChatSessionSingleMg.class);return true;}/*** 记录聊天内容* @param chatMessageMg*/public boolean saveGroupChatMessage(ChatMessageMg chatMessageMg){if(chatMessageMg==null||chatMessageMg.getChatMessage()==null){return false;}String chatSessionId = chatMessageMg.getSessionId();Query query = Query.query(Criteria.where("_id").is(chatSessionId));Update update = new Update();update.push("messageList", chatMessageMg);mongoTemplate.updateFirst(query, update, ChatSessionGroupMg.class);return true;}
}

4.8、页面

<!DOCTYPE HTML>
<html>
<head><meta charset="UTF-8"><title>WebSocket Demo</title>
</head>
<body>
<input id="text" type="text" /><button onclick="send()">发送消息</button><button onclick="closeWebSocket()">关闭连接</button>
<div id="message">
</div>
</body>
<script type="text/javascript">var websocket = null;var userId = Math.random().toString(36).substr(2);if('WebSocket' in window){// 连接 WebSocket 节点websocket = new WebSocket("ws://localhost:8080/websocket/"+userId);} else{alert('Not support websocket')}//连接发生错误的回调方法websocket.onerror = function(){setMessageInnerHTML("error");};//连接成功建立的回调方法websocket.onopen = function(){setMessageInnerHTML("连接成功");}//接收到消息的回调方法websocket.onmessage = function(event){setMessageInnerHTML(event.data);}//连接关闭的回调方法websocket.onclose = function(){setMessageInnerHTML("close");}//监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。window.onbeforeunload = function(){websocket.close();}//将消息显示在网页上function setMessageInnerHTML(innerHTML){document.getElementById('message').innerHTML += innerHTML + '<br/>';}//发送消息function send(){var message = document.getElementById('text').value;websocket.send(message);}//关闭连接function closeWebSocket() {websocket.close();}
</script>
</html>

相关文章:

WebSoket的简单使用

一、WebSocket简介 1.1、双向通信/全双工 客户端和服务器之间同时双向传输&#xff0c;全双工通信允许客户端和服务器随时互相发送消息&#xff0c;不需等一方发送请求后另一方才进行响应。 适用要低延迟/实时交互的场景&#xff0c;如在线游戏、即时通讯、股票行情等。 1.2…...

01_线性表

一、线性表的顺序存储 逻辑上相邻的数据元素&#xff0c;物理次序也相邻。占用连续存储空间&#xff0c;用“数组”实现&#xff0c;知道初始位置就可推出其他位置。 00_宏定义 // 函数结果状态代码 #define TRUE 1 #define FALSE 0 #define OK 1 #define ERROR 0 #defin…...

STL详解 - map和set

目录 一、关联式容器概述 二、键值对 三、树形结构的关联式容器 &#xff08;一&#xff09;set 1. set的介绍 2. set的使用 &#xff08;1&#xff09;模板参数列表 &#xff08;2&#xff09;构造函数 &#xff08;3&#xff09;迭代器函数 &#xff08;4&#xff…...

SpringBoot 集成滑块验证码AJ-Captcha行为验证码 Redis分布式 接口限流 防爬虫

介绍 滑块验证码比传统的字符验证码更加直观和用户友好&#xff0c;能够很好防止爬虫获取数据。 AJ-Captcha行为验证码&#xff0c;包含滑动拼图、文字点选两种方式&#xff0c;UI支持弹出和嵌入两种方式。后端提供Java实现&#xff0c;前端提供了php、angular、html、vue、u…...

高并发PHP部署演进:从虚拟机到K8S的DevOps实践优化

一、虚拟机环境下的部署演进 1. 低并发场景&#xff08;QPS<10&#xff09;的简单模式 # 典型部署脚本示例 ssh userproduction "cd /var/www && git pull origin master" 技术痛点&#xff1a; 文件替换期间导致Nginx返回502错误&#xff08;统计显示…...

vue引入物理引擎matter.js

vue引入物理引擎matter.js 在 Vue 项目中集成 Matter.js 物理引擎的步骤如下: 1. 安装 Matter.js npm install matter-js # 或 yarn add matter-js2. 创建 Vue 组件 <template><div ref="physicsContainer" class="physics-container"><…...

【实战项目】简易版的 QQ 音乐:一

> 作者&#xff1a;დ旧言~ > 座右铭&#xff1a;松树千年终是朽&#xff0c;槿花一日自为荣。 > 目标&#xff1a;能自我实现简易版的 QQ 音乐。 > 毒鸡汤&#xff1a;有些事情&#xff0c;总是不明白&#xff0c;所以我不会坚持。早安! > 专栏选自&#xff1a…...

部署Superset BI(三)连接HANA数据库

metabase和redash都不支持HANA数据库&#xff0c;选择superset就是看重这一点&#xff0c;开始尝试连接HANA数据库。 按Superset的技术文档&#xff1a;pip install hdbcli sqlalchemy-hana or pip install apache-superset[hana] --进入容器 rootNocobase:/usr/superset/supe…...

快速学会Linux的WEB服务

一.用户常用关于WEB的信息 什么是WWW www是world wide web的缩写&#xff0c;及万维网&#xff0c;也就是全球信息广播的意思 通常说的上网就是使用www来查询用户所需要的信息。 www可以结合文字、图形、影像以及声音等多媒体&#xff0c;超链接的方式将信息以Internet传递到世…...

如何搭建spark yarn模式集群的集群

以下是搭建 Spark YARN 模式集群的一般步骤&#xff1a; 准备工作 确保集群中各节点已安装并配置好 Java 环境&#xff0c;且版本符合 Spark 要求。规划好集群中节点的角色&#xff0c;如 Master 节点、Worker 节点等&#xff0c;并确保各节点之间网络畅通&#xff0c;能相互…...

端口安全基本配置

1.top图 2.交换机配置 交换机swa <SWA> system-view [SWA] vlan batch 10 20[SWA] interface GigabitEthernet0/0/1 [SWA-GigabitEthernet0/0/1] port link-type trunk [SWA-GigabitEthernet0/0/1] port trunk allow-pass vlan 10[SWA] interface GigabitEthernet0/0/2 …...

【JsonCpp、Muduo、C++11】JsonCpp库、Muduo库、C++11异步操作

JsonCpp库、Muduo库、C11异步操作 1. JsonCpp库1.1 Json数据格式1.2 JsonCpp介绍1.3 JsonCpp使用 2. Muduo库2.1 Muduo库常见接口介绍2.1.1 TcpServer类基础介绍2.1.2 EventLoop类基础介绍2.1.3 TcpConnection类基础介绍2.1.4 Buffer类基础介绍2.1.5 TcpClient类基础介绍 2.2 M…...

【自然语言处理与大模型】LlamaIndex的词嵌入模型和向量数据库

&#xff08;1&#xff09;嵌入模型的定义和作用 嵌入模型&#xff08;Embedding Model&#xff09;的本质在于将高维的、稀疏的数据转换为低维的、密集的向量表示&#xff0c;使得这些向量能够在数学空间中表达原始数据的语义信息。作用如下&#xff1a; 降维表示&#xff1a;…...

SLAM算法工程师面经大全:2025年面试真题解析与实战指南

SLAM算法工程师面经大全&#xff1a;2025年面试真题解析与实战指南 一、SLAM技术概述与核心原理 1&#xff0e;SLAM基础理论框架 SLAM算法的核心在于同步定位与建图&#xff0c;这一过程涉及传感器数据融合、运动建模与优化方法。在传感器数据融合方面&#xff0c;不同类型的…...

虚拟现实视频播放器 2.6.1 | 支持多种VR格式,提供沉浸式观看体验的媒体播放器

虚拟现实媒体播放器是一款专为在智能手机上播放VR&#xff08;虚拟现实&#xff09;照片和视频而设计的应用程序。它支持多种格式的影像内容&#xff0c;包括360和180等距矩形柱面、标准镜头和鱼眼镜头拍摄的照片和视频&#xff0c;并且兼容3D立体并排、上/下以及收缩媒体格式。…...

从黔西游船侧翻事件看极端天气预警的科技防线——疾风气象大模型如何实现精准防御?

近日,贵州省黔西市一起载人游船侧翻事故令人痛心。调查显示,事发时当地突遇强风暴雨,水面突发巨浪导致船只失控。这一事件再次凸显:在极端天气频发的时代,传统“经验式防灾”已不足够,唯有依靠智能化的气象预警技术,才能筑牢安全底线。 极端天气预警的痛点:为什么传统方…...

颠覆监测体验!WM102无线温湿度记录仪开启智能物联新时代

当温湿度失控成为企业隐形成本杀手&#xff0c;您是否还在用传统设备疲于应对&#xff1f;一款集智能、精准、全能于一身的神器横空出世——WM102无线温湿度记录仪&#xff0c;用硬核科技重新定义环境监测&#xff01; ▌5大场景痛点 一招破解 无论是医药冷库里的疫苗守护战&am…...

Linux云服务器配置git开发环境

文章目录 1. 安装 git2. git clone3. git add .4. git commit -m 提交记录5. git push&#x1f351; 异常原因&#x1f351; 解决办法 6. git pull7. git log8. git rm9. git mv10. git status 1. 安装 git sudo yum install git -y2. git clone 此命令的作用是从远程仓库把代…...

山东大学软件学院项目实训-基于大模型的模拟面试系统-个人主页头像上传

采用图床的方案&#xff0c;存储用户头像。 实现步骤 引入OSS依赖 在我们的SpringBoot项目中引入OSS依赖 <dependency><groupId>com.aliyun.oss</groupId><artifactId>aliyun-sdk-oss</artifactId><version>3.10.2</version> &l…...

AI智能体|扣子(Coze)实战【天气查询插件开发教程】

大家好&#xff0c;我是偶然&#xff0c;AI洞察&#xff0c;AI智能体&#xff0c;AI实战案例分享 今天教大家用 Coze 开发一个天气插件&#xff0c;本来我是想教大家怎么用 AI 写代码节点实现节点功能的&#xff0c;但是我感觉太复杂了。 起码来说&#xff0c;我还没做到用特别…...

VBA ListBox/ComboBox 响应鼠标滚轮操作

一般情况下&#xff0c;在Excel的vba窗口中&#xff0c;我们是无法使用鼠标滚轮控制例如 ListBox 和 ComboBox 控件的。我们只能通过鼠标点击的方式来逐个选择选项。 我们都知道&#xff0c;通过鼠标滚轮快速的上下滚动候选项&#xff0c;以快速的定位选择选项&#xff0c;这是…...

Linux 更改内存交换 swap 为 zram 压缩,减小磁盘写入

1、查看当前 swap 的方式 swapon --show 我这里是默认的 swap 文件&#xff0c;大小为 2G。 2、安装 zram Ubuntu 下&#xff1a; sudo apt install zram-tools安装后默认会启动&#xff1a; 3、关闭默认的 swap 文件 sudo swapoff /swapfile 其次是关闭 /etc/fstab 中的 …...

TypeScript简介

&#x1f31f; TypeScript入门 TypeScript 是 JavaScript 的超集&#xff0c;由微软开发并维护&#xff0c;通过静态类型检查和现代语言特性&#xff0c;让大型应用开发变得更加可靠和高效。 // 一个简单的 TypeScript 示例 interface User {name: string;age: number;greet():…...

Facebook如何运用AI实现元宇宙的无限可能?

引言 元宇宙&#xff08;Metaverse&#xff09;是一个虚拟的、由多个 3D 虚拟世界组成的网络&#xff0c;用户可以在其中进行互动、游戏、工作和社交。Facebook 作为全球最大的社交媒体平台之一&#xff0c;对元宇宙的构建和实现有着深远的影响。通过运用人工智能&#xff08;…...

【大语言模型ChatGPT+Python】近红外光谱数据分析及机器学习与深度学习建模(近红外光谱数据分析、定性/定量分析模型代码自动生成等)

近红外光谱数据分析是一种重要的分析技术&#xff0c;广泛应用于化学、食品、制药、农业、环境科学等领域。以下是关于近红外光谱数据分析的详细介绍&#xff1a; 一、基本原理 近红外光谱的范围 近红外光谱是指波长范围在780 - 2500纳米的电磁辐射。在这个波段&#xff0c;分子…...

ArcGIS Pro图斑属性自动联动更新-在线卫星底图图斑采集

今天介绍一下在ArcGIS Pro图斑属性自动联动更新 主要介绍内容&#xff1a; 1、ArcGIS Pro数据设计中属性规则的设置&#xff0c;实现图斑属性的自动更新与联动更新。 2、介绍ArcGIS Pro不能新建要素类图层的原因 3、包括新建字段等内容 4、deepseek查询arcade计算图斑面积语…...

OpenHarmony GPIO应用开发-LED

学习于&#xff1a; https://docs.openharmony.cn/pages/v5.0/zh-cn/device-dev/driver/driver-platform-gpio-develop.md https://docs.openharmony.cn/pages/v5.0/zh-cn/device-dev/driver/driver-platform-gpio-des.md 通过OpenHarmony官方文档指导可获知&#xff1a;芯片厂…...

C语言结构体内存对齐使用场景

#pragma pack(push, 1) 和 #pragma pack(pop) 的使用场景&#xff08;C语言&#xff09; 这两个预处理指令用于控制结构体成员的内存对齐方式&#xff0c;主要在以下场景中使用&#xff1a; 主要使用场景 网络通信协议处理 当需要精确控制结构体布局以匹配网络协议格式时 确…...

极速轻量,Rust 网络开发新选择:Hyperlane 框架深度解析

极速轻量&#xff0c;Rust 网络开发新选择&#xff1a;Hyperlane 框架深度解析 在高性能网络服务开发领域&#xff0c;Rust 凭借其内存安全与高效并发的特性备受青睐。今天&#xff0c;我们迎来一款专为现代 Web 服务打造的明星框架——Hyperlane&#xff0c;它以“轻量高效、…...

从零开始学习人工智能(Python高级教程)Day6-Python3 正则表达式

一、Python3 正则表达式 正则表达式是一个特殊的字符序列&#xff0c;它能帮助你方便的检查一个字符串是否与某种模式匹配。 在 Python 中&#xff0c;使用 re 模块来处理正则表达式。 re 模块提供了一组函数&#xff0c;允许你在字符串中进行模式匹配、搜索和替换操作。 r…...

UKCC(原OUCC)真题讲解(一)

题目链接&#xff1a;PRACTICE - 2025 Coding Challenge - UK Bebras 1.GreenStar 方法&#xff1a;使用模块 参考答案&#xff1a; 【知识点】 这里的长度100&#xff0c;是指中心到角的距离。 2.Draw a Square 参考答案&#xff1a; 【知识点】 在正多边形的图案中&#…...

Linux——特殊权限管理

Linux中的特殊权限&#xff08;setuid、setgid、sticky bit&#xff09;扩展了基本的文件权限机制&#xff0c;提供了更精细的控制。以下是详细说明&#xff1a; 1. SetUID&#xff08;SUID&#xff09; 作用&#xff1a;允许用户以文件所有者的权限执行文件。 例如&#xff1a…...

Ubuntu18.04搭建samda服务器

一.什么是Samba服务器&#xff1f; Samba服务器是一种基于开源协议实现的网络共享服务软件&#xff0c;主要用于在不同操作系统&#xff08;如Windows、Linux、Unix&#xff09;之间实现文件和打印机共享功能。其核心目标是解决跨平台资源共享的兼容性问题&#xff0c;尤其是在…...

vue3搭建后台管理系统

找menu菜单 上部用height设置高度和背景颜色 需要自己改左侧栏的边线 将el-menu的border设置为0,然后上方设置border-right设置边框 设置右边 创建data.vue input组件 导入icon图标 引入import 一个template 设置个card el-card 定义card 加el-button查询 el-table表单 …...

[学习]RTKLib详解:pntpos.c与postpos.c

文章目录 RTKLib详解&#xff1a;pntpos.c与postpos.cPart A: pntpos.c一、概述二、整体工作流程三、主要函数说明1. pntpos()2. satposs()3. estpos()4. rescode()5. prange()6. ionocorr()7. tropcorr()8. valsol()9. raim_fde()10. estvel() 四、函数调用关系图&#xff08;…...

64.微服务保姆教程 (七) RocketMQ--分布式消息中间件

RocketMQ–分布式消息中间件 一、MQ 1、什么是MQ MQ(Message Queue)消息队列,是基础数据结构中“先进先出”的一种数据结构。指把要传输的数据(消息)放在队列中,用队列机制来实现消息传递——生产者产生消息并把消息放入队列,然后由消费者去处理。消费者可以到指定队…...

邀约媒体,邀请到场 多少钱?

传媒如春雨&#xff0c;润物细无声&#xff0c;大家好&#xff0c;我是51媒体胡老师。 在策划线下活动时&#xff0c;媒体邀约是提升品牌曝光度和影响力的重要环节。不同类型和规模的媒体邀约&#xff0c;其预算需求也各不相同。以下为各类媒体邀约的费用概览及影响因素&#…...

Android数据库全栈开发实战:Room+SQLCipher+Hilt企业级应用构建

简介 在移动应用开发中,数据库作为数据存储的核心组件,其安全性和性能对企业级应用至关重要。本文将从零开始,全面讲解Android数据库开发的最新技术,包括Room框架的深度使用、SQLCipher加密数据库的实现、Hilt依赖注入的集成以及前后端数据同步的完整方案。通过一个加密任…...

Kafka与RocketMQ在事务消息实现上的区别是什么?

一、Kafka事务消息核心实现&#xff08;基于2.8版本&#xff09; // KafkaProducer.java public synchronized Future<RecordMetadata> send(ProducerRecord<K, V> record) {// 事务消息校验&#xff08;第256行&#xff09;if (transactionManager ! null &&…...

Maven 依赖发布与仓库治理

&#x1f9d1; 博主简介&#xff1a;CSDN博客专家&#xff0c;历代文学网&#xff08;PC端可以访问&#xff1a;https://literature.sinhy.com/#/?__c1000&#xff0c;移动端可微信小程序搜索“历代文学”&#xff09;总架构师&#xff0c;15年工作经验&#xff0c;精通Java编…...

hybird接口配置

【sw1】 [sw1]vlan batch 10 20 [sw1]int g 0/0/3 [sw1-GigabitEthernet0/0/1]port hybrid tagged vlan 10 20 [sw1-GigabitEthernet0/0/1]int g 0/0/1 [sw1-GigabitEthernet0/0/2]port hybrid pvid vlan 10 [sw1-GigabitEthernet0/0/2]port hybrid untagged vlan 10 20 …...

AI视频智能分析网关打造社区/工厂/校园/仓库智慧消防实现精准化安全管控

一、背景 随着社区、商业场所对消防安全要求日益提升&#xff0c;传统消防系统已难以满足智能化、精细化管理需求。智能分析网关融合物联网与人工智能技术&#xff0c;具备强大的数据处理与分析能力&#xff0c;可全面升级消防系统。将其融入消防系统各层级&#xff0c;搭建智…...

Web3 应用中常见的数据安全风险及防护措施

随着 Web3 技术的蓬勃发展&#xff0c;我们见证了一个全新的互联网时代的到来。Web3 应用以其去中心化、用户控制数据和透明性的特点&#xff0c;为用户提供了前所未有的体验。然而&#xff0c;这些应用在提供便利的同时&#xff0c;也带来了一系列数据安全风险。本文将探讨 We…...

免费视频压缩软件

一、本地软件&#xff08;支持离线使用&#xff09; 1. HandBrake 平台&#xff1a;Windows / macOS / Linux 特点&#xff1a;开源免费&#xff0c;支持多种格式转换&#xff0c;提供丰富的预设选项&#xff08;如“Fast 1080p”快速压缩&#xff09;&#xff0c;可自定义分…...

Java实用注解篇: @JSONField

前言 在 Java 开发中&#xff0c;进行对象与 JSON 的相互转换是一项常见操作&#xff0c;尤其在前后端分离的架构中显得尤为重要。Fastjson 作为阿里巴巴开源的 JSON 处理框架&#xff0c;因其高性能和强大功能而被广泛使用。JSONField 是 Fastjson 提供的一个注解&#xff0c;…...

浔川AI 第二次内测报告

浔川AI 第二次内测报告 执行社团&#xff1a;浔川社团官方联合会、总社团联合会 同意执行社团&#xff1a;总社团联合会 合作社团&#xff1a;暮烟社团官方联合会 合作分社团&#xff1a;浔川AI分社团、浔川AI分部 被执行内测程序&#xff1a;浔川AI 内测第二代 被执行内…...

React Hooks 深入浅出

目录 引言&#xff1a;React Hooks 的革命基础 Hooks useState&#xff1a;状态管理的新方式useEffect&#xff1a;组件生命周期的替代方案useContext&#xff1a;简化 Context API 额外的 Hooks useReducer&#xff1a;复杂状态逻辑的管理useCallback 与 useMemo&#xff1a;…...

解释 NestJS 的架构理念(例如,模块化、可扩展性、渐进式框架)

一、模块化设计 // user.module.ts Module({controllers: [UserController], // 当前模块的控制器providers: [UserService], // 当前模块的服务exports: [UserService] // 暴露给其他模块使用的服务 }) export class UserModule {}// order.module.ts Module({…...

Caffeine快速入门

依赖 <dependency><groupId>com.github.ben-manes.caffeine</groupId><artifactId>caffeine</artifactId><version>3.2.0</version> </dependency> Cache的基本api操作 Caffeine.newBuilder.build来构建Caffeine .maximumS…...

【踩坑记录】项目Bug分析:一次因 `String.isBlank()` 引发的崩溃(No such instance method: ‘isBlank‘)

项目Bug分析&#xff1a;一次因 String.isBlank() 引发的崩溃 一、前言 在日常的 Java 项目开发中&#xff0c;使用 String 的常见工具方法如 isEmpty()、trim() 等已司空见惯。然而&#xff0c;近期在一次项目中使用了 String.isBlank() 方法&#xff0c;结果竟然直接导致崩…...