从 0 开始实现一个网页聊天室 (小型项目)

实现功能

  1. 用户注册和登录
  2. 好友列表展示
  3. 会话列表展示: 显示当前正在进行哪些会话 (单聊 / 群聊) , 选中好友列表中的某个好友, 会生成对应的会话
  4. 实时通信, A给B发送消息, B的聊天界面 / 会话界面能立刻显示新的消息

TODO:

  1. 添加好友功能
  2. 用户头像显示
  3. 传输图片 / 表情包
  4. 历史消息搜索
  5. 消息撤回

相关技术

网络通信: WebSocket
Spring + SpringBoot + SpringMVC + MyBatis
HTML + CSS + JS

数据库设计

在这里插入图片描述

项目的基本框架

在这里插入图片描述

前端页面

注册和登录页面在这里插入图片描述

在这里插入图片描述
聊天界面
在这里插入图片描述

在这里插入图片描述

后端代码

实体类

User

本类表示一个用户的信息, 对应数据库的 user 表

@Data
public class User {private int userId;private String username = "";private String password = "";public User() {}public User(String username, String password) {this.username = username;this.password = password;}
}

Friend

使用一个 Friend 对象表示一个好友

// 使用一个 Friend 对象表示一个好友, 对应数据库的 friend 表
@Data
public class Friend {private int friendId;private String friendName;public Friend() {}public Friend(int friendId, String friendName) {this.friendId = friendId;this.friendName = friendName;}
}

Message

本类表示一条消息的相关信息, 对应数据库的表 message + 字段: fromname
(没有 postTime 是因为: 在查询的时候就是一次性查出所有的时间, 按照时间结果排序后返回, 我们这里就不需要再获取时间了)

// 本类表示一条消息的相关信息
// (没有 postTime 是因为: 在查询的时候就是一次性查出所有的时间, 按照时间结果排序后返回, 我们这里就不需要再获取时间了)
@Data
public class Message {private Integer messageId;private int fromId;private String fromName;private int sessionId;private String content;public Message() {}public Message( int fromId, String fromName, int sessionId, String content) {this.fromId = fromId;this.fromName = fromName;this.sessionId = sessionId;this.content = content;}
}

MessageSession

使用该类表示一个会话, 对应数据库的 message_session + message_session_user

// 使用该类表示一个会话
@Data
public class MessageSession {private int sessionId;private List<Friend> friends;private String lastMessage;
}

MessageSessionUserItem

该类对象表示 message_session_user 表里的一个记录

// 该类对象表示 message_session_user 表里的一个记录
@Data
public class MessageSessionUserItem {private int sessionId;private int userId;public MessageSessionUserItem() {}public MessageSessionUserItem(int sessionId, int userId) {this.sessionId = sessionId;this.userId = userId;}
}

MessageRequest

WebSocket 请求
自定义格式, 用于网络通信中接受请求

// WebSocket请求
@Data
public class MessageRequest {private String type = "message";private int sessionId;private String content;
}

MessageResponse

WebSocket 响应
自定义格式, 用于网络通信中返回响应

// WebSocket响应
@Data
public class MessageResponse {private String type = "message";private int fromId;private String fromName;private int sessionId;private String content;public MessageResponse() {}public MessageResponse(int fromId, String fromName, int sessionId, String content) {this.fromId = fromId;this.fromName = fromName;this.sessionId = sessionId;this.content = content;}
}

数据库

FriendMapper

用户好友的相关操作

@Mapper
public interface FriendMapper {// 查询用户好友列表List<Friend> selectFriendList(@Param("userId") int userId);
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.java_chatroom.model.FriendMapper"><select id="selectFriendList" resultType="com.example.java_chatroom.model.Friend">select userId as friendId, username as friendNamefrom userwhere userId in(select friendId from friend where userId = #{userId})</select>
</mapper>

MessageMapper

消息的相关操作

@Mapper
public interface MessageMapper {// 获取指定会话的最后一条消息String getLastMessageBySessionId(@Param("sessionId") int sessionId);// 获取指定会话的历史消息 (限制100条)List<Message> getMessagesBySessionId(@Param("sessionId") int sessionId);// 插入一条消息到数据库表中void add(@Param("message") Message message);
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.java_chatroom.model.MessageMapper"><select id="getLastMessageBySessionId" resultType="java.lang.String">select content from messagewhere sessionId = #{sessionId}order by postTime desclimit 1</select><select id="getMessagesBySessionId" resultType="com.example.java_chatroom.model.Message">selectmessageId, sessionId, fromId, content, username as fromNamefrommessage, userwheresessionId = #{sessionId}and fromId = userIdorder bypostTime desclimit 100 offset 0</select><insert id="add">insert into message values(null, #{message.fromId}, #{message.sessionId}, #{message.content}, now());</insert>
</mapper>

MessageSessionMapper

会话的相关操作

@Mapper
public interface MessageSessionMapper {// 1.根据 userId 获取到该用户在哪些会话中存在, 返回结果是一组 sessionId.List<Integer> getSessionIdsByUserId(@Param("userId") int userId);// 2. 根据 sessionId 查询这个会话包含哪些用户(刨除掉最初的 user)List<Friend> getFriendsBySessionId(@Param("sessionId") int sessionId,@Param("selfUserId") int selfUserId);// 3. 新增会话记录, 返回会话 idint addMessageSession(@Param("messageSession") MessageSession messageSession);// 4.给 message_session_user 表新增对应记录int addMessageSessionUser(@Param("messageSessionUserItem") MessageSessionUserItem messageSessionUserItem);
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.java_chatroom.model.MessageSessionMapper"><select id="getSessionIdsByUserId" resultType="java.lang.Integer">select sessionId from message_sessionwhere sessionId in( select sessionId from message_session_userwhere userId = #{userId} )order by lastTime desc</select><select id="getFriendsBySessionId" resultType="com.example.java_chatroom.model.Friend">select userId as friendId, username as friendNamefrom userwhere userId in( select userId from message_session_userwhere sessionId = #{sessionId}and userId != #{selfUserId} )</select><insert id="addMessageSession" useGeneratedKeys="true" keyProperty="messageSession.sessionId">insert into message_session values(null, now())</insert><insert id="addMessageSessionUser">insert into message_session_user values(#{messageSessionUserItem.sessionId},#{messageSessionUserItem.userId})</insert>
</mapper>

UserMapper

用户的相关操作

@Mapper
public interface UserMapper {// 把用户插入到数据库中 -> 注册int insert(@Param("user") User user);// 根据用户名查询用户信息 -> 登录@Select("select * from user where username = #{username}")User selectByName(@Param("username") String username);
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.java_chatroom.model.UserMapper"><insert id="insert" useGeneratedKeys="true" keyProperty="userId">insert into user values(null, #{user.username}, #{user.password})</insert>
</mapper>

WebSocket 通讯模块

前端

主要是 JS 中的代码

先放一个 demo

// 编写 js 使用 websocket 的代码. 
// 创建一个 websocket 实例
let websocket = new WebSocket("ws://127.0.0.1:8080/test");// 给这个 websocket 注册上一些回调函数. 
websocket.onopen = function() {// 连接建立完成后, 就会自动执行到. console.log("websocket 连接成功!");
}websocket.onclose = function() {// 连接断开后, 自动执行到. console.log("websocket 连接断开!");
} websocket.onerror = function() {// 连接异常时, 自动执行到console.log("websocket 连接异常!");
}websocket.onmessage = function(e) {// 收到消息时, 自动执行到console.log("websocket 收到消息! " + e.data);
}// 发送消息 (点击发送按钮之后触发的事件)
let messageInput = document.querySelector('#message');
let sendButton = document.querySelector('#send-button');
sendButton.onclick = function() {console.log("websocket 发送消息: " + messageInput.value);websocket.send(messageInput.value);
}

这里就是本项目前端使用 WebSocket 进行网络通信的逻辑

/
// 操作 websocket
/// 创建 websocket 实例
// let websocket = new WebSocket("ws://127.0.0.1:8080/WebSocketMessage");
// let websocket = new WebSocket("ws://152.136.56.110:9090/WebSocketMessage");
let websocket = new WebSocket("ws://" + location.host + "/WebSocketMessage");websocket.onopen = function() {console.log("websocket 连接成功!");
}websocket.onmessage = function(e) {console.log("websocket 收到消息! " + e.data);// 此时收到的 e.data 是个 json 字符串, 需要转成 js 对象let resp = JSON.parse(e.data);if (resp.type == 'message') {// 处理消息响应handleMessage(resp);} else {// resp 的 type 出错!console.log("resp.type 不符合要求!");}
}websocket.onclose = function() {console.log("websocket 连接关闭!");
}websocket.onerror = function() {console.log("websocket 连接异常!");
}function handleMessage(resp) {// 把客户端收到的消息, 给展示出来. // 展示到对应的会话预览区域, 以及右侧消息列表中. // 1. 根据响应中的 sessionId 获取到当前会话对应的 li 标签. //    如果 li 标签不存在, 则创建一个新的let curSessionLi = findSessionLi(resp.sessionId);if (curSessionLi == null) {// 就需要创建出一个新的 li 标签, 表示新会话. curSessionLi = document.createElement('li');curSessionLi.setAttribute('message-session-id', resp.sessionId);// 此处 p 标签内部应该放消息的预览内容. 一会后面统一完成, 这里先置空curSessionLi.innerHTML = '<h3>' + resp.fromName + '</h3>'+ '<p></p>';// 给这个 li 标签也加上点击事件的处理curSessionLi.onclick = function() {clickSession(curSessionLi);}}// 2. 把新的消息, 显示到会话的预览区域 (li 标签里的 p 标签中)//    如果消息太长, 就需要进行截断. let p = curSessionLi.querySelector('p');p.innerHTML = resp.content;if (p.innerHTML.length > 10) {p.innerHTML = p.innerHTML.substring(0, 10) + '...';}// 3. 把收到消息的会话, 给放到会话列表最上面. let sessionListUL = document.querySelector('#session-list');sessionListUL.insertBefore(curSessionLi, sessionListUL.children[0]);// 4. 如果当前收到消息的会话处于被选中状态, 则把当前的消息给放到右侧消息列表中. //    新增消息的同时, 注意调整滚动条的位置, 保证新消息虽然在底部, 但是能够被用户直接看到. if (curSessionLi.className == 'selected') {// 把消息列表添加一个新消息. let messageShowDiv = document.querySelector('.right .message-show');addMessage(messageShowDiv, resp);scrollBottom(messageShowDiv);}// 其他操作, 还可以在会话窗口上给个提示 (红色的数字, 有几条消息未读), 还可以播放个提示音.  // 这些操作都是纯前端的. 实现也不难, 不是咱们的重点工作. 暂时不做了. 
}function findSessionLi(targetSessionId) {// 获取到所有的会话列表中的 li 标签let sessionLis = document.querySelectorAll('#session-list li');for (let li of sessionLis) {let sessionId = li.getAttribute('message-session-id');if (sessionId == targetSessionId) {return li;}}// 啥时候会触发这个操作, 就比如如果当前新的用户直接给当前用户发送消息, 此时没存在现成的 li 标签return null;
}

后端

同样先上 Demo

@Component
public class TestWebSocketAPI extends TextWebSocketHandler {@Overridepublic void afterConnectionEstablished(WebSocketSession session) throws Exception {// 该方法会在 websocket 连接建立之后, 被自动调用System.out.println("Test 连接成功!");}@Overrideprotected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {// 该方法会在 websocket 收到消息的时候, 被自动调用System.out.println("Test 收到消息!" + message.toString());// session 是个会话, 里面记录通信双方的信息 (session 中持有 websocket 的通信连接)session.sendMessage(message);}@Overridepublic void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {// 这个方法实在 连接出现异常的时候, 被自动调用System.out.println("Test 连接异常!");}@Overridepublic void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {// 这个方法是在连接正常关闭后, 会被自动调用System.out.println("Test 连接关闭!");}
}

下面是本项目中后端使用 WebSocket 实现网络通信

创建 Handler 对象

@Slf4j
@Component
public class WebSocketAPI extends TextWebSocketHandler {@Autowiredprivate OnlineUserMapper onlineUserMapper;@Autowiredprivate MessageSessionMapper messageSessionMapper;@Autowiredprivate MessageMapper messageMapper;// 自己创建对象也行, 使用 @Autowired 注入也行, spring 本身就有内置对象 ObjectMapperprivate ObjectMapper objectMapper = new ObjectMapper();@Overridepublic void afterConnectionEstablished(WebSocketSession session) throws Exception {log.info("[WebSocketAPI] 连接成功!");User user = (User) session.getAttributes().get("user");if(user == null) {return;}log.info("获取到的 userId: {}, username: {}",user.getUserId(), user.getUsername());// 连接建立成功之后, 将 上线用户 和 session 进行绑定onlineUserMapper.online(user.getUserId(), session);}/*** 数据处理* @param session* @param message* @throws Exception*/@Overrideprotected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {log.info("[WebSocketAPI] 收到消息! " + message.toString());// 先获取到当前用户的信息, 后续要转发的消息等User user = (User) session.getAttributes().get("user");if(user == null){log.info("[WebSocketAPI] user == null, 未登录用户, 无法进行消息转发");return;}// 针对请求进行解析, 把 json 格式字符串转换成 Java 对象MessageRequest req = objectMapper.readValue(message.getPayload(), MessageRequest.class);if("message".equals(req.getType())) {// 进行消息转发transferMessage(user, req);}else {log.info("[WebSocketAPI] req.type 有误! {}", message.getPayload());}}/*** 通过该方法来完成消息的实际转发过程* @param user 发送消息的对象* @param req 内含 sessionId, content*/private void transferMessage(User user, MessageRequest req) throws IOException {// 先构造一个待转发的响应对象. MessageResponseMessageResponse resp = new MessageResponse(user.getUserId(), user.getUsername(), req.getSessionId(), req.getContent());// 把这个响应对象转换成 JSON 格式字符串,以待备用String respJson = objectMapper.writeValueAsString(resp);log.info("[transferMessage] respJson: {}", respJson);// 根据请求中的 sessionId, 获取到 MessageSession 里有哪些用户 (查询数据库)List<Friend> friends =  messageSessionMapper.getFriendsBySessionId(req.getSessionId(), user.getUserId());// 此处响应返回的对象中, 应该包含发送方Friend myself = new Friend(user.getUserId(), user.getUsername());friends.add(myself);// 循环遍历 friends, 给其中每一个对象都发送一份响应//   这里是为了满足群聊的设定(即使前端还未实现,但是后端接口和数据库都是支持群聊的)for(Friend friend : friends) {// 已知 userId, 进一步查询 OnlineUserMapper, 获取对应的 WebSocketSession, 从而进行消息转发WebSocketSession webSocketSession = onlineUserMapper.getSession(friend.getFriendId());if(webSocketSession != null) {webSocketSession.sendMessage(new TextMessage(respJson));}}// 转发的消息还要在数据库备份Message message = new Message(user.getUserId(), user.getUsername(), req.getSessionId(), resp.getContent());// 自增主键为 null或为空, 数据库会自动生成messageMapper.add(message);}@Overridepublic void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {log.info("[WebSocketAPI] 连接异常! " + exception.toString());User user = (User) session.getAttributes().get("user");if(user != null) {onlineUserMapper.offline(user.getUserId(), session);}}@Overridepublic void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {log.info("[WebSocketAPI] 连接关闭! " + status.toString());User user = (User) session.getAttributes().get("user");if(user != null) {onlineUserMapper.offline(user.getUserId(), session);}}
}

将 Handler 注册到 Config 里面

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {@Autowiredprivate TestWebSocketAPI testWebSocketAPI;@Autowiredprivate WebSocketAPI webSocketAPI;@Overridepublic void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {// 通过本方法, 将创建好的 Handler 类给注册到具体路径上.// 此时浏览器可通过 请求路径, 调用到绑定的 Handler 类.registry.addHandler(testWebSocketAPI, "/test");registry.addHandler(webSocketAPI, "/WebSocketMessage")// 通过注册这个特定的 HttpSession 拦截器, 可以把用户在// HttpSession 中添加的 Attribute 键值对// 往 WebSocketSession 中添加一份.addInterceptors(new HttpSessionHandshakeInterceptor());}
}

OnlineUserMapper

本类用来记录当前用户在线的状态. (维护 userId 和 WebSocketSession 之间的映射)

// 本类用来记录当前用户在线的状态. (维护 userId 和 WebSocketSession 之间的映射)
@Slf4j
@Component
public class OnlineUserMapper {// 此处这个哈希表要考虑 线程安全 问题private ConcurrentHashMap<Integer, WebSocketSession> sessions = new ConcurrentHashMap<>();/*** 用户上线, 给哈希表里插入键值对* @param userId* @param webSocketSession*/public void online(int userId, WebSocketSession webSocketSession) {if(sessions.get(userId) != null) {// 针对用户多开, 这里的处理是不记录后面登录用户的 session, 即后续登录用户做不到消息的收发// (毕竟这里是根据映射关系来实现消息转发的)log.info("[{}] 已登录, 登录失败",userId);return;}sessions.put(userId, webSocketSession);log.info("[{}] 上线!", userId);}/*** 用户下线, 根据 userId 删除键值对* @param userId* @param webSocketSession*/public void offline(int userId, WebSocketSession webSocketSession) {if(sessions.get(userId) == webSocketSession) {// 如果键值对中 session和调用该方法的 session 相同, 才允许删除键值对sessions.remove(userId);log.info("[{}] 下线!", userId);}}/*** 根据 userId 获取键值对** @param userId* @return*/public WebSocketSession getSession(int userId) {return sessions.get(userId);}
}

功能处理

用户注册

调用接口: register

@Slf4j
@RestController
@Controller
@ResponseBody
public class UserAPI {@Resourceprivate UserMapper userMapper;/*** 用户注册* 返回 User 对象* 注册成功, 返回的 User 对象包含用户信息* 注册失败, 返回的 User 对象无内容*/@RequestMapping("/register")public Object register(String username, String password) {User user = new User();// 判空if(!StringUtils.hasLength(username) || !StringUtils.hasLength(password)) {return user;}try {user = new User(username, password);int ret = userMapper.insert(user);log.info("注册 ret :{}", ret);user.setPassword("");} catch (DuplicateKeyException e) {// 抛出该异常说明用户名重复, 注册失败user = new User();log.error("用户名重复, 注册失败");}return user;}
}

在这里插入图片描述

用户登录

调用接口: login

@Slf4j
@RestController
@Controller
@ResponseBody
public class UserAPI {@Resourceprivate UserMapper userMapper;/*** 用户登录* 返回 User 对象* 登录成功, 返回的 User 对象包含用户信息, 并且将 User 对象存储在 session 中* 登录失败, 返回的 User 对象无内容*/@RequestMapping("/login")public Object login(String username, String password, HttpServletRequest request) {// 判空if(!StringUtils.hasLength(username) || !StringUtils.hasLength(password)) {return new User();}// 校验用户名密码User user = userMapper.selectByName(username);if(user == null || !password.equals(user.getPassword())) {return new User();}// 校验成功, 则登陆成功, 创建会话// true 表示会话不存在则创建会话, false 表示会话不存在就返回空HttpSession session = request.getSession(true);session.setAttribute("user",user);user.setPassword("");return user;}
}

在这里插入图片描述

用户登录后, 聊天界面会自动获取登录用户的好友列并展示

调用接口: friendList

// 处理好友信息
@Slf4j
@RestController
public class FriendAPI {@Resourceprivate FriendMapper friendMapper;@RequestMapping("/friendList")public Object getFriendList(HttpServletRequest req) {// 1. 先从会话中, 获取到 userIdHttpSession session = req.getSession(false);if(session == null) {log.info("[getFriendList] session 不存在");return new ArrayList<Friend>();}User user = (User) session.getAttribute("user");if(user == null) {log.info("[getFriendList] user 不存在");return new ArrayList<Friend>();}// 根据 userId 查询数据库List<Friend> list = friendMapper.selectFriendList(user.getUserId());return list;}
}

在这里插入图片描述

用户登录后, 聊天界面会自动获取登录用户的会话列并展示

调用接口: sessionList

@Slf4j
@RestController
public class MessageSessionAPI {@Resourceprivate MessageSessionMapper messageSessionMapper;@Resourceprivate MessageMapper messageMapper;/*** 获取登录用户 的 所有会话信息 (会话id, 最后一条信息)* @param req* @return*/@RequestMapping("/sessionList")public Object getMessageSessionList(HttpServletRequest req) {List<MessageSession> messageSessionList = new ArrayList<>();// 1. 获取当前用户的 userId (从 Spring 的 session 中获取)HttpSession session = req.getSession(false);if(session == null) {log.info("[getMessageSessionList] session == null");return messageSessionList;}User user = (User) session.getAttribute("user");if(user == null) {log.info("[getMessageSessionList] user == null");return messageSessionList;}int userId = user.getUserId();// 2. 根据 userId 查询数据库, 查出包含该用户的 会话 idList<Integer> sessionIdList = messageSessionMapper.getSessionIdsByUserId(user.getUserId());//3. 遍历会话id, 查询出每个会话里涉及的好友有谁for(int sessionId : sessionIdList) {MessageSession messageSession = new MessageSession();messageSession.setSessionId(sessionId);// 查询每个会话涉及的好友有谁List<Friend> friends = messageSessionMapper.getFriendsBySessionId(sessionId, user.getUserId());messageSession.setFriends(friends);// 查询出每个会话的最后一条消息String lastMessage = messageMapper.getLastMessageBySessionId(sessionId);if (lastMessage == null) {lastMessage = "";}messageSession.setLastMessage(lastMessage);messageSessionList.add(messageSession);}// 最终目标是构造出一个 MessageSession 对象数组return messageSessionList;}
}

在这里插入图片描述

好友列表中, 点击某一个好友之后, 会在会话列创建出一个新会话

调用接口: session

@Slf4j
@RestController
public class MessageSessionAPI {@Resourceprivate MessageSessionMapper messageSessionMapper;@Resourceprivate MessageMapper messageMapper;/*** 创建会话, 并给会话表中插入两条信息 -- 我和好友绑定的会话信息* @param toUserId 好友id* @param user 登录用户信息* @return*/@Transactional@RequestMapping("/session")public Object addMessageSession(int toUserId, @SessionAttribute("user") User user) {Map<String, Integer> resp = new HashMap<>();// 先给 message_session 表插入数据, 获取 messageId , messageId 放在 MessionSession 对象里MessageSession messageSession = new MessageSession();messageSessionMapper.addMessageSession(messageSession); //通过先插入一个空的 messageSession, 可以获取自增主键 messionId// 往 message_session_user 表里插入数据 -- 自己MessageSessionUserItem item1 = new MessageSessionUserItem(messageSession.getSessionId(), user.getUserId());messageSessionMapper.addMessageSessionUser(item1);// 往 message_session_user 表里插入数据 -- 好友MessageSessionUserItem item2 = new MessageSessionUserItem(messageSession.getSessionId(), toUserId);messageSessionMapper.addMessageSessionUser(item2);resp.put("sessionId", messageSession.getSessionId());// JSON 对于普通对象和 Map 都能处理
//        return messageSession;return resp;}
}

在这里插入图片描述

会话列表中, 点击某一个会话之后, 右侧消息栏会显示出该会话的最近100条消息

调用接口: message

@RestController
public class MessageAPI {@Resourceprivate MessageMapper messageMapper;@RequestMapping("/message")public Object getMessage(int sessionId) {List<Message> messages = messageMapper.getMessagesBySessionId(sessionId);// 针对查询结果, 进行逆置操作Collections.reverse(messages);return messages;}
}

在这里插入图片描述

编辑消息后, 点击发送按钮会发送消息到对应会话, 该会话的所有用户的消息列表中都会出现新的消息

这里应用的 WebSocket 技术, handleTextMessage 方法能够感知到消息发送, 并获取消息信息进行处理

@Slf4j
@Component
public class WebSocketAPI extends TextWebSocketHandler {@Autowiredprivate OnlineUserMapper onlineUserMapper;@Autowiredprivate MessageSessionMapper messageSessionMapper;@Autowiredprivate MessageMapper messageMapper;// 自己创建对象也行, 使用 @Autowired 注入也行, spring 本身就有内置对象 ObjectMapperprivate ObjectMapper objectMapper = new ObjectMapper();/*** 数据处理* @param session* @param message* @throws Exception*/@Overrideprotected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {log.info("[WebSocketAPI] 收到消息! " + message.toString());// 先获取到当前用户的信息, 后续要转发的消息等User user = (User) session.getAttributes().get("user");if(user == null){log.info("[WebSocketAPI] user == null, 未登录用户, 无法进行消息转发");return;}// 针对请求进行解析, 把 json 格式字符串转换成 Java 对象MessageRequest req = objectMapper.readValue(message.getPayload(), MessageRequest.class);if("message".equals(req.getType())) {// 进行消息转发transferMessage(user, req);}else {log.info("[WebSocketAPI] req.type 有误! {}", message.getPayload());}}/*** 通过该方法来完成消息的实际转发过程* @param user 发送消息的对象* @param req 内含 sessionId, content*/private void transferMessage(User user, MessageRequest req) throws IOException {// 先构造一个待转发的响应对象. MessageResponseMessageResponse resp = new MessageResponse(user.getUserId(), user.getUsername(), req.getSessionId(), req.getContent());// 把这个响应对象转换成 JSON 格式字符串,以待备用String respJson = objectMapper.writeValueAsString(resp);log.info("[transferMessage] respJson: {}", respJson);// 根据请求中的 sessionId, 获取到 MessageSession 里有哪些用户 (查询数据库)List<Friend> friends =  messageSessionMapper.getFriendsBySessionId(req.getSessionId(), user.getUserId());// 此处响应返回的对象中, 应该包含发送方Friend myself = new Friend(user.getUserId(), user.getUsername());friends.add(myself);// 循环遍历 friends, 给其中每一个对象都发送一份响应//   这里是为了满足群聊的设定(即使前端还未实现,但是后端接口和数据库都是支持群聊的)for(Friend friend : friends) {// 已知 userId, 进一步查询 OnlineUserMapper, 获取对应的 WebSocketSession, 从而进行消息转发WebSocketSession webSocketSession = onlineUserMapper.getSession(friend.getFriendId());if(webSocketSession != null) {webSocketSession.sendMessage(new TextMessage(respJson));}}// 转发的消息还要在数据库备份Message message = new Message(user.getUserId(), user.getUsername(), req.getSessionId(), resp.getContent());// 自增主键为 null或为空, 数据库会自动生成messageMapper.add(message);}
}

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/15996.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

禅道密码正确但是登录异常处理

禅道密码正确&#xff0c;但是登录提示密码错误的异常处理 排查内容 # 1、服务器异常&#xff0c;存储空间、数据库异常 # 2、服务异常&#xff0c;文件丢失等异常问题定位 # 1、df -h 排查服务器存储空间 # 2、根据my.php排查数据库连接是否正常 # 3、修改my.pho,debugtrue…

探索切片索引:列表反转的艺术

新书上架~&#x1f447;全国包邮奥~ python实用小工具开发教程http://pythontoolsteach.com/3 欢迎关注我&#x1f446;&#xff0c;收藏下次不迷路┗|&#xff40;O′|┛ 嗷~~ 目录 一、引言&#xff1a;列表反转的挑战 二、切片索引的基本概念 三、切片索引实现列表反转 …

vs2013使用qt Linguist以及tr不生效问题

一、qt Linguist&#xff08;语言家&#xff09;步骤流程 1、创建翻译文件,在qt选项中 2.选择对应所需的语言&#xff0c;得到.ts后缀的翻译文件 3.创建.pro文件&#xff0c;并将.ts配置在.pro文件中 3.使用qt Linguist 打开创建好的以.ts为后缀的翻译文件&#xff0c;按图所示…

细粒度图像分类论文(AAM模型方法)阅读笔记

细粒度图像分类论文阅读笔记 摘要Abstract1. 用于细粒度图像分类的聚合注意力模块1.1 文献摘要1.2 研究背景1.3 本文创新点1.4 计算机视觉中的注意力机制1.5 模型方法1.5.1 聚合注意力模块1.5.2 通道注意力模块通道注意力代码实现 1.5.3 空间注意力模块空间注意力代码实现 1.5.…

【软件设计师】面向对象技术

1.面向对象基础 1.1 基本概念 方法重载是函数名字相同&#xff0c;参数列表不同 组成 即组合&#xff0c;指整体与部分的关系&#xff0c;整体与部分生命周期相同 聚合 关联关系的一个特例&#xff0c;是体现整体与部分&#xff0c;即使has-a的关系&#xff0c;此时整体与部分…

【LakeHouse】Apache Iceberg + Amoro 助力网易构建云原生湖仓

Apache Iceberg Amoro 助力网易构建云原生湖仓 1.云原生湖仓背景与挑战2.Apache Iceberg 、Amoro 与云原生2.1 Apache Iceberg2.2 Amoro 简介 3.Apache Iceberg Amoro 云原生实践3.1 云上湖仓案例一3.2 云上湖仓案例二3.3 云上湖仓案例三 4.Amoro 未来发展规划 出品社区&…

【代码随想录——回溯算法二周目】

1. 组合总和 var (path []intres [][]int )func combinationSum(candidates []int, target int) [][]int {path make([]int, 0)res make([][]int, 0)dfs(candidates,target,0,0)return res }func dfs(candidates []int, target int,tempTarget int,start int) {if tempTarg…

Django-auth组件

Django-auth组件 1 表结构 我们从python manage.py migrate为我们创建的auth组件内置的表开始看 auth_user&#xff1a;用户表存储用户信息&#xff08;登录admin后台&#xff09; 里面的字段分两类&#xff1a;用户基本信息&#xff08;用户名&#xff0c;邮箱&#xff0c;密…

华为OD机试【找出通过车辆最多颜色】(java)(100分)

1、题目描述 在一个狭小的路口&#xff0c;每秒只能通过一辆车&#xff0c;假设车辆的颜色只有 3 种&#xff0c;找出 N 秒内经过的最多颜色的车辆数量。 三种颜色编号为0 &#xff0c;1 &#xff0c;2。 2、输入描述 第一行输入的是通过的车辆颜色信息[0,1,1,2] &#xff0…

Three.js——二维平面、二维圆、自定义二维图形、立方体、球体、圆柱体、圆环、扭结、多面体、文字

个人简介 &#x1f440;个人主页&#xff1a; 前端杂货铺 ⚡开源项目&#xff1a; rich-vue3 &#xff08;基于 Vue3 TS Pinia Element Plus Spring全家桶 MySQL&#xff09; &#x1f64b;‍♂️学习方向&#xff1a; 主攻前端方向&#xff0c;正逐渐往全干发展 &#x1…

RabbitMQ-默认读、写方式介绍

1、RabbitMQ简介 rabbitmq是一个开源的消息中间件&#xff0c;主要有以下用途&#xff0c;分别是&#xff1a; 应用解耦&#xff1a;通过使用RabbitMQ&#xff0c;不同的应用程序之间可以通过消息进行通信&#xff0c;从而降低应用程序之间的直接依赖性&#xff0c;提高系统的…

功率电感的设计步骤

文章目录 1&#xff1a;高导磁气隙&#xff08;铁氧体&#xff09;1.1设计原理1.2 设计步骤 2 铁粉芯2.1&#xff1a;设计原理2.2&#xff1a;设计步骤 TI电感设计 学习视频原链接 截图 1 截图1 截图1 截图 2 截图2 截图2 1&#xff1a;高导磁气隙&#xff08;铁氧体&#…

基于机器学习判断面部微表情发现哪些人更容易诊有帕金森病

1. 概述 帕金森病&#xff08;Parkinson’s disease&#xff0c;PD&#xff09;是一种慢性、进展性的神经退行性疾病&#xff0c;主要影响运动系统。该病症以大脑中黑质致密部多巴胺能神经元的逐渐丧失为特征&#xff0c;导致多巴胺&#xff08;一种重要的神经递质&#xff09…

【Qt】深入探索Qt窗口与对话框:从创建到管理:QDockWidget(浮动窗口)、QDialog(对话框)

文章目录 前言&#xff1a;1. 浮动窗口2. 对话框介绍2.1. 示例&#xff1a;主窗口中&#xff0c;通过点击按钮&#xff0c;弹出一个新的对话框。2.2. 创建自定义对话框2.2.1. 纯代码的方式2.2.2. 图形化界面的方式 3. 模态对话框 和 非模态对话框4. Qt 内置对话框4.1. 消息对话…

Hybrid Block Storage for Efficient Cloud Volume Service——论文泛读

TOS 2023 Paper 论文阅读笔记整理 问题 传统桌面和服务器应用程序向云的迁移给底层云存储带来了高性能、高可靠性和低成本的挑战。由于这些传统应用程序的I/O模式和一致性要求&#xff0c;与采用特定编程模型和范式&#xff08;如MapReduce[22]和RDD[52]&#xff09;的云原生…

香橙派AIpro(OrangePi AIPro)开发板初测评

开发板简介 最近&#xff0c;我拿到手一款Orange Pi AI Pro 开发板&#xff0c;它是香橙派联合华为精心打造的高性能AI 开发板&#xff0c;最早发布于2023年12月&#xff0c;其搭载了昇腾AI 处理器&#xff0c;可提供8TOPS INT8 的计算能力&#xff0c;内存提供了8GB 和16GB两…

基于jeecgboot-vue3的Flowable新建流程定义(一)

因为这个项目license问题无法开源&#xff0c;更多技术支持与服务请加入我的知识星球。 1、vue3版本因为流程分类是动态的&#xff0c;不再固定了&#xff0c;所以新建的时候需要选择建立哪种流程类型的流程 代码如下&#xff1a; <!-- 选择模型的流程类型对话框 -->&…

数据结构(六)图

2024年5月26日一稿(王道P220) 6.1 图的基本概念 6.1.1 图的定义 6.2 图的存储及基本操作 6.2.1邻接矩阵法 6.2.2 邻接表

python web自动化(分布式测试Grid)

Grid介绍 Selenium Grid 是 Selenium 提供的⼀个⼯具&#xff0c;⽤于⽀持在多台计算机上并⾏运⾏测试。 它允许将测试分发到不同的机器和浏览器组合上&#xff0c;同时收集结果。 1.并⾏执⾏测试⽤例&#xff1a;在不同的机器上并⾏执⾏测试⽤例&#xff0c;从⽽加速整个测试过…

Vulhub——adminer

文章目录 一、CVE-2021-21311&#xff08;SSRF&#xff09;二、CVE-2021-43008&#xff08;远程文件读取&#xff09; 一、CVE-2021-21311&#xff08;SSRF&#xff09; Adminer是一个PHP编写的开源数据库管理工具&#xff0c;支持MySQL、MariaDB、PostgreSQL、SQLite、MS SQL…