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

从 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);}
}

在这里插入图片描述

相关文章:

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

实现功能 用户注册和登录好友列表展示会话列表展示: 显示当前正在进行哪些会话 (单聊 / 群聊) , 选中好友列表中的某个好友, 会生成对应的会话实时通信, A给B发送消息, B的聊天界面 / 会话界面能立刻显示新的消息 TODO: 添加好友功能用户头像显示传输图片 / 表情包历史消息搜…...

Tomcat部署项目的方式

目录 1、Tomcat发布项目的方式 方式1&#xff1a; 直接把项目发布到webapps目录下 方式2&#xff1a;项目发布到ROOT目录 方式3&#xff1a;虚拟路径方式发布项目 方式4&#xff1a;(推荐)虚拟路径&#xff0c;另外的方式&#xff01; 方式5&#xff1a;发布多个网站 1、…...

推荐一个快速开发接私活神器

文章目录 前言一、项目介绍二、项目地址三、功能介绍四、页面显示登录页面菜单管理图表展示定时任务管理用户管理代码生成 五、视频讲解总结 前言 大家好&#xff01;我是智航云科技&#xff0c;今天为大家分享一个快速开发接私活神器。 一、项目介绍 人人开源是一个提供多种…...

输入输出(4)——C++的输入输出运算符

目录 一、输入运算符>> 二、输出运算符<< 三、 输入与输出运算符的重载 &#xff08;一&#xff09;必须重载为类的友元函数 &#xff08;二&#xff09;返回类型应是对象的引用 一、输入运算符>> 输人运算符“>>”也称为流提取运算符,是一个二目…...

[图解]产品经理创新模式01物流变成信息流

1 00:00:01,570 --> 00:00:04,120 有了现状的业务序列图 2 00:00:04,960 --> 00:00:08,490 我们就来改进我们的业务序列图了 3 00:00:08,580 --> 00:00:11,010 把我们要做的系统放进去&#xff0c;改进它 4 00:00:13,470 --> 00:00:15,260 怎么改进&#xff1f;…...

npm 上传包

将自己做好的包做好后上传 1. 切换镜像&#xff08;只能通过官网代理来上传&#xff09; npm config set registry https://registry.npmjs.org/ 2. 添加用户&#xff08;等价登录&#xff09; npm addUser 3. 提交 npm publish 4. 删除 npm unpublish [<pkg>][&…...

Python 小游戏——贪吃蛇

Python 小游戏——贪吃蛇 文章目录 Python 小游戏——贪吃蛇项目介绍环境配置代码设计思路1. 初始化和变量定义2. 创建游戏窗口和FPS控制器3. 初始化贪吃蛇和食物的位置4. 控制贪吃蛇的方向和分数5. 主游戏循环 难点分析源代码呈现代码结果 项目介绍 贪吃蛇游戏是一款通过上下…...

人工智能方面顶会

人工智能 AAAI the National Conference on Artificial Intelligence 美国人工智能协会主办 IJCAJ the International Joint Conference on Artificial Intelligence每年举办 计算机视觉 CVPR IEEE Conference on Computer Vision and Pattern Recognition ECCV European Co…...

JRT1.7发布

JRT1.7连仪器在线演示视频 JRT1.5实现质控主体、1.6基本完成质控&#xff1b;本次版本推进到1.7&#xff0c;1.7集菜单权限、登录、打印导出客户端、初始化、质控、Linux客户端、仪器连接和监控体系各种功能大全&#xff0c;上十年写系统用到的都全了。 这次直接挑战检验最难…...

Python错误集锦:xlwt写入表格时提示exception-unexpected-data-type-class-bytes

原文链接&#xff1a;http://www.juzicode.com/python-error-exception-unexpected-data-type-class-bytes 错误提示&#xff1a; #juzicode.com/VX公众号:juzicode import xlwt wb xlwt.Workbook() ws wb.add_sheet(juzicode) a bjuzicode ws.write(3, 0, 桔子code) ws.wri…...

赶紧收藏!2024 年最常见 20道 Redis面试题(八)

上一篇地址&#xff1a;赶紧收藏&#xff01;2024 年最常见 20道 Redis面试题&#xff08;七&#xff09;-CSDN博客 十五、一个Redis实例最多能存放多少的keys&#xff1f; Redis实例能存放的键&#xff08;keys&#xff09;的数量主要受限于以下几个因素&#xff1a; 物理内…...

Flowable第一次启动MYSQL8.0版本(踩坑)

flowable工作流项目第一次启动报错表不存在&#xff0c;是因为连接mysql数据库的时候没有设置&nullCatalogMeansCurrenttrue&#xff0c;mysql5.0以上该配置默认为flase&#xff0c;即不操作本数据库。因此需要修改为true。datasource:url: jdbc:mysql://127.0.0.1:3306/fl…...

Java基础的语法---StringBuilder

StringBuilder 构造方法 StringBuilder()&#xff1a;创建一个空的StringBuilder实例。 StringBuilder(String str)&#xff1a;创建一个StringBuilder实例&#xff0c;并将其初始化为指定的字符串内容。 StringBuilder(int a): 创建一个StringBuilder实例…...

【微服务】springboot 构建镜像多种模式使用详解

目录 一、前言 二、微服务常用的镜像构建方案 3.1 使用Dockerfile 3.2 使用docker plugin插件 3.3 使用docker compose 编排文件 三、环境准备 3.1 服务器 3.2 安装JDK环境 3.2.1 创建目录 3.2.2 下载安装包 3.2.3 配置环境变量 2.2.4 查看java版本 3.3 安装maven …...

手写tomcat(Ⅲ)——tomcat动态资源的获取

仿写tomcat的Servlet接口体系 之前写过一篇博客&#xff0c;Tomcat的Servlet-GenericServlet-HttpServlet体系的具体结构&#xff0c;以及Servlet的生命周期 Servlet讲解 想要模仿tomcat获取动态资源&#xff0c;就需要我们自己仿写一个Servlet接口体系 主要包括&#xff1a…...

软件测试面试题(四)

一&#xff1a;测试评估的目标&#xff1f; 量化测试进程 生成缺陷和测试覆盖率的总结报告 测试评估的问题 没有把测试覆盖率作为报告测试进程的根据&#xff0c;使得不知测试是否结束&#xff1b; 没有做测试缺陷评估&#xff0c;缺陷评估是量度软件可行性的重要指标&…...

infoq学习笔记-云原生网关当道,三大主流厂商如何“竞 技”?

注基础组件的质量&#xff0c;这些基础组件是用户看不到的。这些组件包括代码质量、自动化的CI/CD、端对端测试、混沌测试等。在APISIX中&#xff0c;我们内置了大 量的测试案例代码&#xff0c;包括单元测试、E2E测试、混沌测试&#xff0c;以及一些基准测试等&#xff0c;从而…...

Python中别再用 ‘+‘ 拼接字符串了!

大家好&#xff0c;在 Python 编程中&#xff0c;我们常常需要对字符串进行拼接。你可能会自然地想到用 操作符将字符串连接起来&#xff0c;毕竟这看起来简单明了。 在 Python 中&#xff0c;字符串是不可变的数据类型&#xff0c;这意味着一旦字符串被创建&#xff0c;它就…...

前端上传heic图片转jpe格式并展示

各大浏览器对 HEIC 格式图片的支持情况&#xff0c;包括上传和显示的支持度 浏览器版本HEIC 上传HEIC 显示Chrome版本 85 及以上支持不支持Firefox所有版本支持不支持Safari版本 11 及以上支持支持Edge版本 18 及以上支持不支持Opera所有版本支持不支持IE不支持不支持不支持 …...

VMware虚拟机-设置系统网络IP、快照、克隆

1.设置网络IP 1.点击右上角开关按钮-》有线 已连接-》有线设置 2.手动修改ip 3.重启或者把开关重新关闭开启 2.快照设置 快照介绍&#xff1a; 通过快照可快速保存虚拟机当前的状态&#xff0c;后续可以使用虚拟机还原到某个快照的状态。 1.添加快照(需要先关闭虚拟机) 2.在…...

通过Wrangler CLI在worker中创建数据库和表

官方使用文档&#xff1a;Getting started Cloudflare D1 docs 创建数据库 在命令行中执行完成之后&#xff0c;会在本地和远程创建数据库&#xff1a; npx wranglerlatest d1 create prod-d1-tutorial 在cf中就可以看到数据库&#xff1a; 现在&#xff0c;您的Cloudfla…...

dedecms 织梦自定义表单留言增加ajax验证码功能

增加ajax功能模块&#xff0c;用户不点击提交按钮&#xff0c;只要输入框失去焦点&#xff0c;就会提前提示验证码是否正确。 一&#xff0c;模板上增加验证码 <input name"vdcode"id"vdcode" placeholder"请输入验证码" type"text&quo…...

SpringTask-03.入门案例

一.入门案例 启动类&#xff1a; package com.sky;import lombok.extern.slf4j.Slf4j; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cache.annotation.EnableCach…...

C++八股 —— 单例模式

文章目录 1. 基本概念2. 设计要点3. 实现方式4. 详解懒汉模式 1. 基本概念 线程安全&#xff08;Thread Safety&#xff09; 线程安全是指在多线程环境下&#xff0c;某个函数、类或代码片段能够被多个线程同时调用时&#xff0c;仍能保证数据的一致性和逻辑的正确性&#xf…...

算法笔记2

1.字符串拼接最好用StringBuilder&#xff0c;不用String 2.创建List<>类型的数组并创建内存 List arr[] new ArrayList[26]; Arrays.setAll(arr, i -> new ArrayList<>()); 3.去掉首尾空格...

基于SpringBoot在线拍卖系统的设计和实现

摘 要 随着社会的发展&#xff0c;社会的各行各业都在利用信息化时代的优势。计算机的优势和普及使得各种信息系统的开发成为必需。 在线拍卖系统&#xff0c;主要的模块包括管理员&#xff1b;首页、个人中心、用户管理、商品类型管理、拍卖商品管理、历史竞拍管理、竞拍订单…...

[ACTF2020 新生赛]Include 1(php://filter伪协议)

题目 做法 启动靶机&#xff0c;点进去 点进去 查看URL&#xff0c;有 ?fileflag.php说明存在文件包含&#xff0c;原理是php://filter 协议 当它与包含函数结合时&#xff0c;php://filter流会被当作php文件执行。 用php://filter加编码&#xff0c;能让PHP把文件内容…...

深入浅出Diffusion模型:从原理到实践的全方位教程

I. 引言&#xff1a;生成式AI的黎明 – Diffusion模型是什么&#xff1f; 近年来&#xff0c;生成式人工智能&#xff08;Generative AI&#xff09;领域取得了爆炸性的进展&#xff0c;模型能够根据简单的文本提示创作出逼真的图像、连贯的文本&#xff0c;乃至更多令人惊叹的…...

LCTF液晶可调谐滤波器在多光谱相机捕捉无人机目标检测中的作用

中达瑞和自2005年成立以来&#xff0c;一直在光谱成像领域深度钻研和发展&#xff0c;始终致力于研发高性能、高可靠性的光谱成像相机&#xff0c;为科研院校提供更优的产品和服务。在《低空背景下无人机目标的光谱特征研究及目标检测应用》这篇论文中提到中达瑞和 LCTF 作为多…...

若依登录用户名和密码加密

/*** 获取公钥&#xff1a;前端用来密码加密* return*/GetMapping("/getPublicKey")public RSAUtil.RSAKeyPair getPublicKey() {return RSAUtil.rsaKeyPair();}新建RSAUti.Java package com.ruoyi.common.utils;import org.apache.commons.codec.binary.Base64; im…...