实现功能
- 用户注册和登录
- 好友列表展示
- 会话列表展示: 显示当前正在进行哪些会话 (单聊 / 群聊) , 选中好友列表中的某个好友, 会生成对应的会话
- 实时通信, 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 friendName
from user
where 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 message
where sessionId = #{sessionId}
order by postTime desc
limit 1
</select>
<select id="getMessagesBySessionId" resultType="com.example.java_chatroom.model.Message">
select
messageId, sessionId, fromId, content, username as fromName
from
message, user
where
sessionId = #{sessionId}
and fromId = userId
order by
postTime desc
limit 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. 新增会话记录, 返回会话 id
int 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_session
where sessionId in
( select sessionId from message_session_user
where userId = #{userId} )
order by lastTime desc
</select>
<select id="getFriendsBySessionId" resultType="com.example.java_chatroom.model.Friend">
select userId as friendId, username as friendName
from user
where userId in
( select userId from message_session_user
where 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 {
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
// 该方法会在 websocket 连接建立之后, 被自动调用
System.out.println("Test 连接成功!");
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
// 该方法会在 websocket 收到消息的时候, 被自动调用
System.out.println("Test 收到消息!" + message.toString());
// session 是个会话, 里面记录通信双方的信息 (session 中持有 websocket 的通信连接)
session.sendMessage(message);
}
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
// 这个方法实在 连接出现异常的时候, 被自动调用
System.out.println("Test 连接异常!");
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
// 这个方法是在连接正常关闭后, 会被自动调用
System.out.println("Test 连接关闭!");
}
}
下面是本项目中后端使用 WebSocket 实现网络通信
创建 Handler 对象
@Slf4j
@Component
public class WebSocketAPI extends TextWebSocketHandler {
@Autowired
private OnlineUserMapper onlineUserMapper;
@Autowired
private MessageSessionMapper messageSessionMapper;
@Autowired
private MessageMapper messageMapper;
// 自己创建对象也行, 使用 @Autowired 注入也行, spring 本身就有内置对象 ObjectMapper
private ObjectMapper objectMapper = new ObjectMapper();
@Override
public 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
*/
@Override
protected 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 {
// 先构造一个待转发的响应对象. MessageResponse
MessageResponse 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);
}
@Override
public 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);
}
}
@Override
public 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 {
@Autowired
private TestWebSocketAPI testWebSocketAPI;
@Autowired
private WebSocketAPI webSocketAPI;
@Override
public 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 {
@Resource
private 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 {
@Resource
private 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 {
@Resource
private FriendMapper friendMapper;
@RequestMapping("/friendList")
public Object getFriendList(HttpServletRequest req) {
// 1. 先从会话中, 获取到 userId
HttpSession 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 {
@Resource
private MessageSessionMapper messageSessionMapper;
@Resource
private 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 查询数据库, 查出包含该用户的 会话 id
List<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 {
@Resource
private MessageSessionMapper messageSessionMapper;
@Resource
private 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 {
@Resource
private 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 {
@Autowired
private OnlineUserMapper onlineUserMapper;
@Autowired
private MessageSessionMapper messageSessionMapper;
@Autowired
private MessageMapper messageMapper;
// 自己创建对象也行, 使用 @Autowired 注入也行, spring 本身就有内置对象 ObjectMapper
private ObjectMapper objectMapper = new ObjectMapper();
/**
* 数据处理
* @param session
* @param message
* @throws Exception
*/
@Override
protected 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 {
// 先构造一个待转发的响应对象. MessageResponse
MessageResponse 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);
}
}