实现功能
- 用户注册和登录
- 好友列表展示
- 会话列表展示: 显示当前正在进行哪些会话 (单聊 / 群聊) , 选中好友列表中的某个好友, 会生成对应的会话
- 实时通信, A给B发送消息, B的聊天界面 / 会话界面能立刻显示新的消息
TODO:
- 添加好友功能
- 用户头像显示
- 传输图片 / 表情包
- 历史消息搜索
- 消息撤回
- …
相关技术
网络通信: 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);}
}