当前位置: 首页 > 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.在…...

ffmpeg(四):滤镜命令

FFmpeg 的滤镜命令是用于音视频处理中的强大工具&#xff0c;可以完成剪裁、缩放、加水印、调色、合成、旋转、模糊、叠加字幕等复杂的操作。其核心语法格式一般如下&#xff1a; ffmpeg -i input.mp4 -vf "滤镜参数" output.mp4或者带音频滤镜&#xff1a; ffmpeg…...

自然语言处理——Transformer

自然语言处理——Transformer 自注意力机制多头注意力机制Transformer 虽然循环神经网络可以对具有序列特性的数据非常有效&#xff0c;它能挖掘数据中的时序信息以及语义信息&#xff0c;但是它有一个很大的缺陷——很难并行化。 我们可以考虑用CNN来替代RNN&#xff0c;但是…...

.Net Framework 4/C# 关键字(非常用,持续更新...)

一、is 关键字 is 关键字用于检查对象是否于给定类型兼容,如果兼容将返回 true,如果不兼容则返回 false,在进行类型转换前,可以先使用 is 关键字判断对象是否与指定类型兼容,如果兼容才进行转换,这样的转换是安全的。 例如有:首先创建一个字符串对象,然后将字符串对象隐…...

dify打造数据可视化图表

一、概述 在日常工作和学习中&#xff0c;我们经常需要和数据打交道。无论是分析报告、项目展示&#xff0c;还是简单的数据洞察&#xff0c;一个清晰直观的图表&#xff0c;往往能胜过千言万语。 一款能让数据可视化变得超级简单的 MCP Server&#xff0c;由蚂蚁集团 AntV 团队…...

Linux C语言网络编程详细入门教程:如何一步步实现TCP服务端与客户端通信

文章目录 Linux C语言网络编程详细入门教程&#xff1a;如何一步步实现TCP服务端与客户端通信前言一、网络通信基础概念二、服务端与客户端的完整流程图解三、每一步的详细讲解和代码示例1. 创建Socket&#xff08;服务端和客户端都要&#xff09;2. 绑定本地地址和端口&#x…...

免费数学几何作图web平台

光锐软件免费数学工具&#xff0c;maths,数学制图&#xff0c;数学作图&#xff0c;几何作图&#xff0c;几何&#xff0c;AR开发,AR教育,增强现实,软件公司,XR,MR,VR,虚拟仿真,虚拟现实,混合现实,教育科技产品,职业模拟培训,高保真VR场景,结构互动课件,元宇宙http://xaglare.c…...

Python 实现 Web 静态服务器(HTTP 协议)

目录 一、在本地启动 HTTP 服务器1. Windows 下安装 node.js1&#xff09;下载安装包2&#xff09;配置环境变量3&#xff09;安装镜像4&#xff09;node.js 的常用命令 2. 安装 http-server 服务3. 使用 http-server 开启服务1&#xff09;使用 http-server2&#xff09;详解 …...

FFmpeg avformat_open_input函数分析

函数内部的总体流程如下&#xff1a; avformat_open_input 精简后的代码如下&#xff1a; int avformat_open_input(AVFormatContext **ps, const char *filename,ff_const59 AVInputFormat *fmt, AVDictionary **options) {AVFormatContext *s *ps;int i, ret 0;AVDictio…...

Spring AOP代理对象生成原理

代理对象生成的关键类是【AnnotationAwareAspectJAutoProxyCreator】&#xff0c;这个类继承了【BeanPostProcessor】是一个后置处理器 在bean对象生命周期中初始化时执行【org.springframework.beans.factory.config.BeanPostProcessor#postProcessAfterInitialization】方法时…...

聚六亚甲基单胍盐酸盐市场深度解析:现状、挑战与机遇

根据 QYResearch 发布的市场报告显示&#xff0c;全球市场规模预计在 2031 年达到 9848 万美元&#xff0c;2025 - 2031 年期间年复合增长率&#xff08;CAGR&#xff09;为 3.7%。在竞争格局上&#xff0c;市场集中度较高&#xff0c;2024 年全球前十强厂商占据约 74.0% 的市场…...