SpringBoot + Netty-SocketIO 聊天消息持久化

2,590 阅读4分钟

SpringBoot 聊天消息持久化

单聊 - 实现功能

  • 发送文本消息
  • 聊天列表
  • 聊天消息详情

通过上一篇 SpringBoot 集成 netty-socketio 客户端可以把消息发送给服务端,服务端也可以正常接收

将接收的消息进行持久化存储,使用 mysql 数据库进行数据存储,mybatis 操作数据库

对数据库不了解,可以看这

MySql 初识

MySql 进阶

聊天消息列表存储

创建消息记录列表表

CREATE TABLE `t_message_record` (
  `msg_id` bigint(20) NOT NULL COMMENT '消息Id',
  `send_id` bigint(20) NOT NULL COMMENT '发送者Id',
  `receive_id` bigint(20) NOT NULL COMMENT '接受者Id',
  `type` char(1) NOT NULL COMMENT '消息类型',
  `chat_type` char(1) NOT NULL COMMENT '聊天类型',
  `content` varchar(500) NOT NULL COMMENT '聊天内容',
  `send_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '消息发送时间',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`msg_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='消息记录表';

编写 MessageRecordMapper 操作数据库


/**
 * @author taohua
 */

@Mapper
public interface MessageRecordMapper {

  /**
   * 单聊
   * <p>
   * 插入一条单聊消息
   *
   * @param messageRecordDO 消息实体
   * @return 添加成功
   */
  int insertMessageRecord(MessageRecordDO messageRecordDO);
}

编写 MessageRecordMapper xml 映射文件

<?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.moose.operator.mapper.MessageRecordMapper">

  <insert id="insertMessageRecord"
      parameterType="com.moose.operator.model.domain.message.MessageRecordDO">
    INSERT INTO `t_message_record` (msg_id, send_id, receive_id, type, chat_type, content)
    VALUES (#{msgId}, #{sendId}, #{receiveId}, #{type}, #{chatType}, #{content})
  </insert>

</mapper>

在 SingleMessageServiceImpl 中使用

  • saveMessage 主要逻辑
  @Resource
  private SocketConnection socketConnection;

  // 注入 MessageRecordMapper
  @Resource
  private MessageRecordMapper messageRecordMapper;

  @Override public void saveMessage(MessageTemplate template) {

    // TODO: 消息 ack
    MessageRecordDO messageRecord = null;
    try {
      // mock
      //String fromUserId = template.getFromUserId();
      //SocketIOClient fromSocket = socketIOClientMap.get(fromUserId);
      //if ("786600935907659776".equals(toUserId)) {
      //  if (null != fromSocket && fromSocket.isChannelOpen()) {
      //    fromSocket.sendEvent(ChatConstant.SINGLE_CHAT, "你们不是好友!!");
      //  }
      //  return;
      //}

      //log.info("{}", request);

      // 检查 消息模版 参数 ...

      // 判断用户是否存在 ...

      // 是否是好友 ...

      /////////////////////////// save message record ///////////////////////////

      // 发送者ID
      Long sendId = template.getSendId();

      // 接受者 ID
      Long receiveId = template.getReceiveId();

      // 构建消息
      messageRecord = new MessageRecordDO();
      messageRecord.setMsgId(snowflakeIdWorker.nextId());
      messageRecord.setSendId(sendId);
      messageRecord.setReceiveId(receiveId);
      messageRecord.setContent(template.getContent());
      messageRecord.setType(MessageUtils.diffMessageType(template.getType()));
      messageRecord.setChatType(MessageUtils.diffChatType(template.getChatType()));
      // 保存消息
      messageRecordMapper.insertMessageRecord(messageRecord);

    } catch (Exception exception) {
      exception.printStackTrace();
      log.info("消息解析: 错误 [{}] 消息 [{}]", exception.getMessage(), template);
    }
  }

服务单元测试用例


@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest
public class SingleMessageServiceTest {

  @Resource
  private SingleMessageService singleMessageService;

  @Test
  public void testSaveMessage() {
    MessageTemplate template = new MessageTemplate();
    mockJiangJing(template);
    mockTaohua(template);
    mockWangZhaoJun(template);
  }

  private void mockWangZhaoJun(MessageTemplate template) {
    // 王昭君
    template.setSendId(775113183131074560L);
    // 江景
    template.setReceiveId(775113183131074580L);
    template.setType("MS:TEXT");
    template.setChatType("CT:SINGLE");
    template.setContent("你好,江景,我是王昭君。");
    singleMessageService.saveMessage(template);
  }

  private void mockTaohua(MessageTemplate template) {

    // 桃花
    template.setSendId(786600935907659776L);
    // 王昭君
    template.setReceiveId(775113183131074560L);
    template.setType("MS:TEXT");
    template.setChatType("CT:SINGLE");
    template.setContent("你好,王昭君,我是桃花。");
    singleMessageService.saveMessage(template);

  }

  private void mockJiangJing(MessageTemplate template) {

    // 江景
    template.setSendId(775113183131074580L);

    // 桃花
    template.setReceiveId(786600935907659776L);
    template.setType("MS:TEXT");
    template.setChatType("CT:SINGLE");
    template.setContent("你好,桃花,我是江景。");
    singleMessageService.saveMessage(template);

    // 王昭君
    template.setReceiveId(775113183131074560L);
    template.setType("MS:TEXT");
    template.setChatType("CT:SINGLE");
    template.setContent("你好,王昭君,我是江景。");
    singleMessageService.saveMessage(template);
  }
}

单聊实现

  • 消息保存之后再发送消息??
  • socketConnection 中获取,需要判断时候在线
  • 使用 SocketIOClient sendEvent 发送消息

代码


// TODO: 消息传输过程失败情况
// TODO: 并发情况
// 发送消息
// 判断是否在线
SocketIOClient receiveSocket = socketConnection.getSocketIOClient(String.valueOf(receiveId));
if (null != receiveSocket && receiveSocket.isChannelOpen()) {
  // 客户端监听单聊事件
  receiveSocket.sendEvent(ChatConstant.SINGLE_CHAT, MapperUtils.obj2json(template));
  // TODO: un read count compute --> 0
  // messageRecord.setUnReadCount(0);
} else {
  // TODO: un read count compute --> redis increment or decrement
  // messageRecord.setUnReadCount(0);
}

调试

先使用 socket_io_client 调试; dart 语言模块, 后面集成 Flutter 方便

具体代码参考 gitee.com/shizidada/d…

实现效果

已经长连接,接受到数据可以在 UI 上进行渲染

获取聊天列表

MessageRecordMapper java 添加查询方法

  /**
   * 聊天列表
   *
   * @param userId 当前用户
   * @return 聊天列表
   */
  List<MessageRecordDO> selectMessageRecordList(Long userId);

  /**
   * 查询聊天列表详情
   *
   * @param sendId    发送者 Id
   * @param receiveId 接收者 Id
   * @return 聊天详情
   */
  List<MessageRecordDO> selectChatMessageList(@Param("sendId") Long sendId,
      @Param("receiveId") Long receiveId);

MessageRecordMapper xml 添加查询映射

  <select id="selectMessageRecordList" resultMap="BaseResultMap">

    <![CDATA[
    SELECT t.* FROM (
      SELECT
        msg_id,
        send_id,
        receive_id,
        type,
        chat_type,
        content,
        send_time
    FROM (
      SELECT
        receive_id AS receiver,
        msg_id,
        send_id,
        receive_id,
        type,
        chat_type,
        content,
        send_time
    FROM
      t_message_record
    WHERE
      send_id = #{userId} AND receive_id <> #{userId}
    UNION
      SELECT
        send_id AS receiver,
        msg_id,
        send_id,
        receive_id,
        type,
        chat_type,
        content,
        send_time
      FROM
        t_message_record
      WHERE
        send_id <> #{userId} AND receive_id = #{userId}
      ORDER BY send_time DESC ) AS newTable
    GROUP BY
      receiver
    ORDER BY
      send_time DESC
    ) AS t
  ]]>
  </select>

  <select id="selectChatMessageList" resultMap="BaseResultMap">
    SELECT
    <include refid="Base_Column"/>
    FROM
    t_message_record
    WHERE
    ( send_id = #{sendId} AND receive_id = #{receiveId} )
    OR
    ( send_id = #{receiveId} AND receive_id = #{sendId} )
    ORDER BY send_time ASC
  </select>

单元测试

  @Test
  public void testSelectMessageRecordList() {
    // 江景
    Long userId = 775113183131074580L;
    List<MessageRecordDO> messageRecordDOS = messageRecordMapper.selectMessageRecordList(userId);
    log.info("{}", messageRecordDOS);
  }

编写接口

获取聊天列表服务 MessageRecordService

public interface MessageRecordService {

  /**
   * 获取聊天消息列表
   *
   * @return List<ChatMessageRecord>
   */
  List<MessageRecordDO> listMessageRecord();
    
  /**
   * 查询聊天列表
   *
   * @param userId  对方 Id
   * @param pageNum 页码
   * @return 聊天列表详细
   */
  List<MessageRecordDO> listMessageChat(Long userId, Integer pageNum);
}

服务实现 MessageRecordServiceImpl

@Slf4j
@Service
public class MessageRecordServiceImpl implements MessageRecordService {

  @Resource
  private UserInfoService userInfoService;

  @Resource
  private MessageRecordMapper messageRecordMapper;

  @Override public List<MessageRecordDO> listMessageRecord() {
    Long userId = userInfoService.getCurrentUserId();
    return messageRecordMapper.selectMessageRecordList(userId);
  }

  @Override public List<MessageRecordDO> listMessageChat(Long userId, Integer pageNum) {
    Long currentUserId = userInfoService.getCurrentUserId();
    PageHelper.startPage(pageNum, 10);
    return messageRecordMapper.selectChatMessageList(currentUserId, userId);
  }
}

PageHelper 进行分页查询, 根据分页一次只查询 10 条数据

PageHelper.startPage(pageNum, 10);

消息接口 MessageController


@RestController
@RequestMapping("/api/v1/message")
public class MessageController {

  @Resource
  private MessageRecordService messageRecordService;

 /**
   * 获取当前登录用户聊天列表
   *
   * @return R<?> 聊天列表
   */
  @PostMapping(value = "/record/list")
  public R<?> getChatMessageList() {
    return R.ok(messageRecordService.listMessageRecord());
  }
    
  /**
   * 获取单聊记录 对方 Id
   *
   * @return R<?> 聊天记录
   */
  @PostMapping(value = "/chat/list")
  public R<?> getMessageChatList(
      @RequestParam("chatId") Long chatId,
      @RequestParam(value = "pageNum", defaultValue = "1") Integer pageNum
  ) {
    return R.ok(messageRecordService.listMessageChat(chatId, pageNum));
  }
}

启动服务访问

获取聊天记录列表 http://localhost:7000/api/v1/message/record/list

获取对应聊天记录列表 http://localhost:7000/api/v1/message/chat/list

TODO 待完善

  • 群聊、发送二进制消息(图片消息、视频消息...)
  • 检查消息模版参数
  • 判断用户是否存在
  • 是否是好友
  • 主动推送?还是拉去消息列表
  • 消息存储扩散,扩散
  • 分布式情况下,消息推送
  • ....

关注公众号 「全栈技术部」,不断学习更多有趣的技术知识。