# 私聊功能实现 **Repository Path**: wangzhi430/ChatSystem ## Basic Information - **Project Name**: 私聊功能实现 - **Description**: 用户可以进行私聊 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 6 - **Forks**: 1 - **Created**: 2022-11-11 - **Last Updated**: 2023-11-08 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README @[TOC] # 1. 效果展示 项目源码: https://gitee.com/wangzhi430/ChatSystem ![在这里插入图片描述](https://img-blog.csdnimg.cn/9fbed1a5af1f48f8ad61ffc3c7774c84.png) ![在这里插入图片描述](https://img-blog.csdnimg.cn/2ad51f7edfa64893a4e754bd6d1d9576.png) ![在这里插入图片描述](https://img-blog.csdnimg.cn/d58b6601ce004a58b53d3095f4300548.png) ![在这里插入图片描述](https://img-blog.csdnimg.cn/748d76fa7f7d4d33b379400e3a8826a1.png) ![在这里插入图片描述](https://img-blog.csdnimg.cn/db9dac35f83148b197b8986837eb2024.png) # 2. 基础准备 ## 2.1 项目创建 ![在这里插入图片描述](https://img-blog.csdnimg.cn/7fd6c17591a4460b95cf960476510079.png) ![在这里插入图片描述](https://img-blog.csdnimg.cn/02e2100350204eb6ac4fdcf646b50f97.png) ![在这里插入图片描述](https://img-blog.csdnimg.cn/70a2aa8445db4efd98cc95a2e26cb3b2.png) ![在这里插入图片描述](https://img-blog.csdnimg.cn/2ac364f9932b47cd82f9806f0c08ef18.png) ## 2.2 配置文件 ```xml spring.datasource.url=jdbc:mysql://localhost:3306/ChatSystem?characterEncoding=utf8&useSSL=true spring.datasource.username=root spring.datasource.password=0000 spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver mybatis.mapper-locations=classpath:mapper/**Mapper.xml debug=true logging.level.root=INFO logging.level.com.example.onlinemusic.mapper=debug logging.level.druid.sql.Statement=DEBUG logging.level.com.example=DEBUG ``` # 3. 数据库的设计与实现 数据表分为三个表,用户表、聊天关系表、聊天列表。 用户表用来存储用户的信息。 >这里设计三个字段,用户Id,用户账户,用户密码。 聊天关系表用来存储聊天的两个用户的关系。 > 这里设计三个字段,关系Id,发送者Id,接收者Id。 聊天列表用来存储对应的聊天的信息,根据关系Id,来识别是哪两者的用户。 >这里设计5个字段,列表Id,关系Id,发送用户Id,发送内容,发送时间。 实现代码: ```sql create database if not exists ChatSystem; use ChatSystem; drop table if exists user; -- 创建一个用户信息表 create table user ( userId int primary key auto_increment, username varchar(128) unique, password varchar(128) not null ); drop table if exists user_link; -- 聊天关系表 create table user_link ( `linkId` int primary key auto_increment, `from` int not null, `to` int not null ); drop table if exists chat_list; -- 聊天列表 create table chat_list( listId int primary key auto_increment, linkId int, userId int, content varchar(128) not null, createtime datetime ); ``` # 4. 登录注册模块的设计与实现 ## 4.1 登录注册统一响应类 这里登录注册后端返回的数据,统一是这个格式。 ```java import lombok.Data; @Data public class ResponseBodyMessage { private int status; private String message; private T data; public ResponseBodyMessage(int status,String message,T data) { this.status = status; this.message = message; this.data = data; } } ``` ## 4.2 BCrypt加密 ### 4.2.1 添加依赖类 在pom.xml中添加BCrypt的依赖类 ```xml org.springframework.security spring-security-web org.springframework.security spring-security-config ``` ### 4.2.2 在启动类中添加代码 ```java @SpringBootApplication(exclude = {org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration.class}) public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } } ``` ### 4.2.3 在AppConfig类中注入Bean对象 ```java @Configuration public class AppConfig { @Bean public BCryptPasswordEncoder getBCryptPasswordEncoder() { return new BCryptPasswordEncoder(); } } ``` ## 4.3 添加拦截器 防止未登录用户进入非法界面. ### 4.3.1 LoginInterceptor 类 ```java public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session = request.getSession(false); if (session != null && session.getAttribute("user") != null){ return true; } response.setStatus(403); response.sendRedirect("/login.html"); return false; } } ``` ### 4.3.2 AppConfig 类 ```java @Configuration public class AppConfig implements WebMvcConfigurer{ @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor()) .addPathPatterns("/**") .excludePathPatterns("/**/*.js") .excludePathPatterns("/**/*.jpg") .excludePathPatterns("/**/*.css") .excludePathPatterns("/**/*.png") .excludePathPatterns("/**/login.html") .excludePathPatterns("/**/register.html") .excludePathPatterns("/**/login") .excludePathPatterns("/**/register"); } @Bean public BCryptPasswordEncoder getBCryptPasswordEncoder() { return new BCryptPasswordEncoder(); } } ``` ## 4.4 具体代码实现 之前代码中, 登录注册流程很清楚, 可以查看之前博客. ```java @RestController public class UserController { @Resource private UserService userService; @Autowired private BCryptPasswordEncoder bCryptPasswordEncoder; @RequestMapping("/login") public ResponseBodyMessage userLogin(@RequestBody User user, HttpServletRequest request) { User truUser = userService.selectByUserName(user.getUsername()); if (truUser == null) { System.out.println("登录失败!"); return new ResponseBodyMessage<>(-1, "用户名密码错误!", false); } else { boolean flg = bCryptPasswordEncoder.matches(user.getPassword(), truUser.getPassword()); if (!flg) { return new ResponseBodyMessage<>(-1, "用户名密码错误!", false); } System.out.println("登录成功!"); HttpSession session = request.getSession(true); System.out.println(session); session.setAttribute("user", truUser); return new ResponseBodyMessage<>(1, "登录成功!", true); } } @RequestMapping("/register") public ResponseBodyMessage register(@RequestBody User user) { if(user.getUsername() == null || "".equals(user.getUsername().trim()) || user.getPassword() == null || "".equals(user.getPassword().trim())){ return new ResponseBodyMessage<>(-1,"输入内容为空!",null); } User truUser = userService.selectByUserName(user.getUsername()); if (truUser != null) { return new ResponseBodyMessage<>(-1,"当前用户名已经存在!",null); } else{ String password = bCryptPasswordEncoder.encode(user.getPassword()); user.setPassword(password); userService.addUser(user); return new ResponseBodyMessage<>(1,"注册成功!",user); } } @RequestMapping("/logout") public void userLogout(HttpServletRequest request, HttpServletResponse response) throws IOException { HttpSession session = request.getSession(false); // 拦截器的拦截, 所以不可能出现session为空的情况 session.removeAttribute("user"); response.sendRedirect("login.html"); } } ``` # 5. 私信模块的设计与实现 这里的私信功能, 主要运用到了WebSocket ## 5.1 基础配置 ### 5.1.1 依赖类 ```xml org.springframework.boot spring-boot-starter-websocket ``` ### 5.1.2 在AppConfig中配置 ```java @Configuration @EnableWebSocket public class AppConfig implements WebMvcConfigurer, WebSocketConfigurer { @Autowired private ChatController chatController; @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(chatController,"/intoChat") .addInterceptors(new HttpSessionHandshakeInterceptor()); } } ``` ## 5.2 设计思路 在成功建立websocket连接的时候, 后端发送请求给前端, 让用户根据请求来绘制好友列表. ``` { status: 1, // 这里的1为成功请求, -1 为失败的请求 message: "getUser", users: "", // 列表用户 fromusername: "", // 当前用户 } ``` 在绘制好好友列表之后, 可以选择好友来进行对话.在点击好友之后, 发送请求给服务器, 服务器根据两者的用户账户进行建立会话, 读取之前的聊天记录. 请求 ``` { from: , 发送用户账户 to: , 接收用户账户 message: "loadMessage", } ``` 响应 ``` { status: 1, messages: "", // 以前的聊天记录, 每一条包含, 用户Id, 内容, 是否是发送者. message: "loadMessage", tousername: "", 接收用户账户 } ``` 在进入聊天界面之后. 可以输入对应的消息, 然后点击发送, 进行发送消息.服务器就将消息发送给当前两者用户, 谁在线, 就更新列表. 不在线就不更新. 请求 ``` { from: "", // 发送用户账户 to: "", // 接收用户账户 content: "", // 发送的信息内容 message: "sendMessage" } ``` 响应 ``` { status: 1, message: "sendMessage", messages: "", // 发送的消息 } ``` 用户异常退出或者用户退出的时候, 在在线状态中设置离线, 在进入的时候设置上线. ## 5.3 用户在线状态管理器 这里使用 `ConcurrentHashMap` 来进行存储, `key`为用户Id, `value`为`WebSocketSession` 主要是三个功能 ① 进入私聊界面添加用户状态到哈希表中 ② 退出私聊界面删除哈希表中的用户状态 ③ 获取当前用户的状态. ```java @Component public class OnlineUserManager { // 哈希表存储的是用户的当前的状态,在线就存储到哈希表中 private ConcurrentHashMap userState = new ConcurrentHashMap<>(); public void enterHall(int userId, WebSocketSession webSocketSession) { userState.put(userId,webSocketSession); } public void exitHall(int userId) { userState.remove(userId); } public WebSocketSession getState(int userId) { return userState.get(userId); } } ``` ## 5.4 设计数据库操作 > 主要是两个功能: > 1. 在点击用户头像的时候, 加载聊天界面, 并且读取聊天记录. > 2. 在点击发送按钮的时候, 发送消息, 并且加载消息到在线用户的窗口中. 实现功能1 需要去数据中, 查找两个用户的关系, 是否聊过天, 如果没有聊过天, 查询到的记录就是null. 如果聊过天, 根据关系去聊天列表中查询对应的记录. > 功能1 涉及的数据库操作: > 1. 根据两个用户的Id, 在聊天关系表中查找linkId > 2. 根据linkId, 在聊天列表中查找对应的聊天记录 实习功能2, 需要去数据中, 查询是否两个用户聊过天, 是否存在linkId, 如果不存在, 就需要建立关系, 创建一个linkId, 并且根据linkId, 去插入数据, 如果存在, 直接根据linkId插入数据. > 功能2 涉及的数据库操作: > 1. 在聊天关系表中, 插入一条数据, from, to分别为两者用户的Id > 2. 在聊天列表中, 根据linkId 插入一条数据, 添加发送该消息的用户Id,消息内容, 创建时间. ### 5.4.1 创建实体类 ChatList 实体类 ```java @Data public class ChatList { private int listId; private int linkId; private int userId; private String content; private Timestamp createtime; } ``` UserLink 实体类 ```java @Data public class UserLink { private int linkId; private int from; private int to; } ``` ### 5.4.2 在mapper文件夹下创建对应xml ChatListMapper.xml ```xml insert into chat_list(linkId,userId,content,createtime) values (#{linkId},#{fromId},#{content},#{timestamp}); ``` ### 5.4.3 对应Mapper接口 ChatListMapper 接口 ```java @Mapper public interface ChatListMapper { List selectChat(int linkId); void insertChat(Integer linkId, Integer fromId, String content, Timestamp timestamp); } ``` UserLinkMapper 接口 ```java @Mapper public interface UserLinkMapper { Integer selectLinkId(int fromId,int toId); void insertLink(int min, int max); } ``` ### 5.4.4 对应的Service类 ChatListService 类 ```java @Service public class ChatListService { @Autowired private ChatListMapper chatListMapper; public List selectChat(int linkId){ return chatListMapper.selectChat(linkId); } public void insertChat(Integer linkId, Integer fromId, String content, Timestamp timestamp) { chatListMapper.insertChat(linkId,fromId,content,timestamp); } } ``` UserLinkService ```java @Service public class UserLinkService { @Autowired private UserLinkMapper userLinkMapper; public Integer selectLinkId(int fromId,int toId){ return userLinkMapper.selectLinkId(fromId,toId); } public void insertLink(int min, int max) { userLinkMapper.insertLink(min,max); } } ``` ## 5.5 Controller类的实现 这里采用了webSocket方法, 主要是四个类来实现, 1. `afterConnectionEstablished` 连接成功的时候调用 2. `handleTextMessage` 接收请求的时候调用 3. `handleTransportError` 连接异常断开的时候调用 4. `afterConnectionClosed` 连接断开的时候调用 ### 5.5.1 连接成功的时候调用类 > 1. 首先判断当前用户是否已经登录, 防止用户多开 > 2. 将用户的在线状态设置为在线 > 3. 从数据库中查找所有的用户 > 4. 设置响应类, 并添加对应的信息 > 5. 返回响应. ```java // 连接成功调用 @Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { ResponseMessage responseMessage = new ResponseMessage(); // 1. 首先判断当前用户是否已经登录, 防止用户多开 User user = (User) session.getAttributes().get("user"); if(onlineUserManager.getState(user.getUserId()) != null) { responseMessage.setStatus(-1); responseMessage.setMessage("当前用户已经登录了, 不要重复登录"); session.sendMessage(new TextMessage(objectMapper.writeValueAsBytes(responseMessage))); return; } // 2. 将用户的在线状态设置为在线 onlineUserManager.enterHall(user.getUserId(),session); // 3. 从数据库中查找所有的用户 // 4. 设置响应类, 并添加对应的信息 responseMessage.setStatus(1); responseMessage.setMessage("getUser"); responseMessage.setFromusername(user.getUsername()); responseMessage.setUsers(userService.selectAllUser(user.getUsername())); // 5. 返回响应 session.sendMessage(new TextMessage(objectMapper.writeValueAsBytes(responseMessage))); } ``` ### 5.5.2 接收请求的时候调用类 > 1. 解析请求的内容 > 2. 判断是加载消息记录, 还是发送消息 > 加载消息 >      2.a.1 根据两者的用户Id 查看 linkId, 这里让from始终最小,to始终最大,方便查找 >      2.a.2 判断当前的linkId是否存在, 不存在就不需要加载聊天记录了 >      2.a.3 存在聊天记录,需要加载 >      2.a.4 设置对应的响应, 并返回 > 发送消息 >      2.b.1 查找对应的linkId >      2.b.2 判断当前linkId是否为空, 为空需要创建linkId >      2.b.3 根据linkId, 在聊天列表中添加数据 >      2.b.4 设置对应的响应信息 >      2.b.5 获取两者用户的session, 并判断是否在线, 给在线的用户返回响应,刷新聊天框 ```java // 连接成功收到的响应 @Override protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { ResponseMessage responseMessage = new ResponseMessage(); User user = (User) session.getAttributes().get("user"); // 1. 解析请求的内容 String payload = message.getPayload(); System.out.println(payload); RequestMessage requestMessage = objectMapper.readValue(payload,RequestMessage.class); // 2. 判断是加载消息记录, 还是发送消息 if(requestMessage.getMessage().equals("loadMessage")){ // 2.a.1 根据两者的用户Id 查看 linkId, 这里让from始终最小,to始终最大,方便查找 responseMessage.setMessage("loadMessage"); responseMessage.setTousername(requestMessage.getTo()); Integer fromId = userService.selectUserId(requestMessage.getFrom()); Integer toId = userService.selectUserId(requestMessage.getTo()); int min = Math.min(fromId,toId); int max = Math.max(fromId,toId); Integer linkId = userLinkService.selectLinkId(min,max); // 2.a.2 判断当前的linkId是否存在, 不存在就不需要加载聊天记录了 if(linkId == null || linkId == 0) { responseMessage.setStatus(1); responseMessage.setMessages(null); }else{ // 2.a.3 存在聊天记录,需要加载, responseMessage.setStatus(1); List chatLists = chatListService.selectChat(linkId); List messages = new ArrayList<>(); for(ChatList chatList : chatLists) { Message message1 = new Message(); message1.setMessage(chatList.getContent()); message1.setUserId(chatList.getUserId()); message1.setSender(chatList.getUserId() == user.getUserId()); messages.add(message1); } responseMessage.setMessages(messages); } // 2.a.4 设置对应的响应, 并返回 session.sendMessage(new TextMessage(objectMapper.writeValueAsBytes(responseMessage))); } if(requestMessage.getMessage().equals("sendMessage")) { // 2.b.1 查找对应的linkId Integer fromId = userService.selectUserId(requestMessage.getFrom()); Integer toId = userService.selectUserId(requestMessage.getTo()); int min = Math.min(fromId,toId); int max = Math.max(fromId,toId); Integer linkId = userLinkService.selectLinkId(min,max); // 2.b.2 判断当前linkId是否为空, 为空需要创建linkId if(linkId == null || linkId == 0) { responseMessage.setStatus(1); userLinkService.insertLink(min,max); linkId = userLinkService.selectLinkId(min,max); } // 2.b.3 根据linkId, 在聊天列表中添加数据 String content = requestMessage.getContent(); chatListService.insertChat(linkId,fromId,content,new Timestamp(System.currentTimeMillis())); // 2.b.4 设置对应的响应信息 responseMessage.setStatus(1); responseMessage.setMessage("sendMessage"); Message message1 = new Message(); // 2.b.5 获取两者用户的session, 并判断是否在线, 给在线的用户返回响应, 刷新聊天框 WebSocketSession session1 = onlineUserManager.getState(fromId); WebSocketSession session2 = onlineUserManager.getState(toId); if(session1 != null) { message1.setSender(true); message1.setUserId(fromId); message1.setMessage(content); List list = new ArrayList<>(); list.add(message1); responseMessage.setMessages(list); session1.sendMessage(new TextMessage(objectMapper.writeValueAsBytes(responseMessage))); } if(session2 != null) { message1.setSender(false); message1.setUserId(toId); message1.setMessage(content); List list = new ArrayList<>(); list.add(message1); responseMessage.setMessages(list); session2.sendMessage(new TextMessage(objectMapper.writeValueAsBytes(responseMessage))); } } } ``` ### 5.5.3 连接异常断开的时候调用 ```java // 连接异常调用 @Override public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception { User user = (User) session.getAttributes().get("user"); WebSocketSession webSocketSession = onlineUserManager.getState(user.getUserId()); if(webSocketSession == session) { // 2. 设置在线状态 onlineUserManager.exitHall(user.getUserId()); } System.out.println("用户"+user.getUsername()+"退出"); } ``` ### 5.5.4 连接断开的时候调用 ```java // 连接关闭调用 @Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { User user = (User) session.getAttributes().get("user"); WebSocketSession webSocketSession = onlineUserManager.getState(user.getUserId()); if(webSocketSession == session) { // 2. 设置在线状态 onlineUserManager.exitHall(user.getUserId()); } System.out.println("用户"+user.getUsername()+"退出"); } ``` ## 5.5 前端代码 ### 5.5.1 html文件 ```html 好友界面
1234124124
``` ### 5.5.2 css文件 ```css * { margin: 0; padding: 0; box-sizing: border-box; /* background-image: url(../image/1.jpg); */ } html,body{ height: 100%; background-image: url(../image/1.jpg); background-position: center center; background-size: cover; background-repeat: no-repeat; } .parent{ display: flex; justify-content: center; align-items: center; height: 100%; width: 100%; } .left{ display: flex; flex-direction: column; justify-content: center; align-items: center; height: 80%; width: 300px; } .titleList{ background-color: #2e2e2e; width: 100%; height: 75px; padding-left: 10px; display: flex; justify-content: flex-start; align-items: center; border-bottom: 1px solid white; } .chatList{ background-color: #bdb0b0; width: 100%; height: calc(100% - 75px); } .right{ background-color: rgba(235, 229, 229, 0.9); height: 80%; width: 700px; display: flex; flex-direction: column; justify-content: flex-start; } .pg{ border-radius: 50%; height: 50px; width: 50px; } .username{ color: white; margin-left: 10px; } .touser{ padding: 10px; display: flex; justify-content: flex-start; align-items: center; border-bottom: 1px solid white; } .tousername{ color: white; } .preview{ color: white; } .information{ margin-left: 10px; display: flex; flex-direction: column; justify-content: center; align-items: flex-start; } #textareaContent{ display: block; width: 97%; background: rgba(248,249,251,0.8); border: none; padding: 0 16px; border-radius: 4px; resize: none; height: 88px; font-size: 14px; line-height: 22px; margin: 8px; } .commentText{ padding: 0 16px; display: flex; justify-content: space-between; align-items: center; } .sendComment{ display: block; width: 77px; height: 24px; background: #fc5531; color: #fff; border-radius: 16px; font-size: 14px; text-align: center; line-height: 24px; border: none; } .sendComment:active{ color: #fc5531; background: #fff; } .textLine{ font-size: 8px; } em{ color: #222226; margin: 0 4px; font-style: normal; } .touserName{ height: 64px; width: 100%; border-bottom: 2px solid black; } .touserName span{ margin-left: 14px; line-height: 64px; } .MessageList{ margin: 10px; height: 66%; overflow:auto; } .inputList{ border-top: 2px solid black; height: calc(34% - 64px); } .tous{ display: flex; align-items: center; justify-content: flex-start; } .fromus{ display: flex; align-items: center; flex-direction: row-reverse; } .sendMes{ margin: 10px; line-height: 28px; padding: 4px 12px; color: #222226; background: #cad9ff; border-radius: 5px; } ``` # 6. 群聊功能 这里页面没设计, 主要是通过在用户在线状态管理器中添加几个类, 返回在线的所有用户, 在界面的时候一个用户发送消息, 直接返回给所有的用户. ![在这里插入图片描述](https://img-blog.csdnimg.cn/04f5ded4304d4d15ac925f0c0e18e539.png) ![在这里插入图片描述](https://img-blog.csdnimg.cn/78dc197e48664ca5ac6540756250e9d3.png) ## 6.1 用户状态管理器 ```java public WebSocketSession getState(int userId) { return userState.get(userId); } public int getOnlinePeople() { return userState.size(); } ``` ## 6.2 代码实现 ### 6.2.1 后端代码 ```java package com.example.demo.controller; import com.example.demo.model.User; import com.example.demo.room.OnlineUserManager; import com.example.demo.room.RequestMessage; import com.example.demo.room.ResponseMessage; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.web.socket.CloseStatus; import org.springframework.web.socket.TextMessage; import org.springframework.web.socket.WebSocketSession; import org.springframework.web.socket.handler.TextWebSocketHandler; import java.util.Collection; @Component public class RoomController extends TextWebSocketHandler { private ObjectMapper objectMapper = new ObjectMapper(); @Autowired private OnlineUserManager onlineUserManager; // 连接成功调用 @Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { User user = (User) session.getAttributes().get("user"); onlineUserManager.enterHall(user.getUserId(),session); System.out.println("当前人数: " + onlineUserManager.getOnlinePeople()); ResponseMessage responseMessage = new ResponseMessage(); responseMessage.setMessage("people"); responseMessage.setNumber(onlineUserManager.getOnlinePeople()); Collection collection = onlineUserManager.getAllSession(); for(WebSocketSession s : collection) { s.sendMessage(new TextMessage(objectMapper.writeValueAsBytes(responseMessage))); } } // 连接成功收到的响应 @Override protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { User user = (User) session.getAttributes().get("user"); String payload = message.getPayload(); System.out.println(payload); RequestMessage requestMessage = objectMapper.readValue(payload,RequestMessage.class); ResponseMessage responseMessage = new ResponseMessage(); responseMessage.setMessage("chatMessage"); responseMessage.setContent(requestMessage.getContent()); Collection collection = onlineUserManager.getAllSession(); for(WebSocketSession s : collection) { s.sendMessage(new TextMessage(objectMapper.writeValueAsBytes(responseMessage))); } } // 连接异常调用 @Override public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception { User user = (User) session.getAttributes().get("user"); WebSocketSession webSocketSession = onlineUserManager.getState(user.getUserId()); if(webSocketSession == session) { // 2. 设置在线状态 onlineUserManager.exitHall(user.getUserId()); } System.out.println("用户"+user.getUsername()+"退出"); } // 连接关闭调用 @Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { User user = (User) session.getAttributes().get("user"); WebSocketSession webSocketSession = onlineUserManager.getState(user.getUserId()); if(webSocketSession == session) { // 2. 设置在线状态 onlineUserManager.exitHall(user.getUserId()); } System.out.println("用户"+user.getUsername()+"退出"); } } ``` ### 6.2.2 前端代码 ```html Document
当前在线用户: 0
```