NettySDK发送聊天消息步骤解析

2023年 10月 9日 52.3k 0

一、消息发送接口

    /**
     * 发送消息接口
     *
     * @param messageVO 消息dto
     * @return {@link R }
     * @Name: sendMessage
     * @Author Macro Chen
     */
    @PostMapping("/sendMessage")
    public R sendMessage(@Validated @RequestBody MessageVO messageVO) {
        return R.success(chatMessageService.sendMessage(messageVO));
    }



// chatMessageService

    /**
     * 发送消息
     *
     * @param messageVO 消息签证官
     * @return {@link Boolean }
     * @Name: sendMessage
     * @Author Macro Chen
     */
    @Override
    public ChatMessage sendMessage(MessageVO messageVO) {
        ChatMessage chatMessage = chatMessageConvert.dtoToBo(messageVO);
        MessagePusherFactory.pushMessage(chatMessage);
        return chatMessage;
    }

这是发送聊天消息的接口,这里逻辑比较简单只做了两件事,一为转换前端的传参,二为把转换过后的消息交由消息发送器工厂发送。

消息格式

:::info
文本消息
:::

{
  "senderId": 29,
  "receiverId": 3,
  "conversationId": "",
  "content": "hello world",
  "msgType": 1,
  "timestamp": 1689728005019,
  "msgFormat": "0",
  "msgExtra": {
    "senderInfo": {
      "id": 29,
      "username": "admin",
      "nickName": "权限管理员",
      "icon": "https://property-acquisition.oss-cn-guangzhou.aliyuncs.com/acquisition/images/20230527/16851506513731.jpg",
      "macroOpenId": "1234"
    }
  },
  "chatRoomType": "1",
}

:::info
图片消息
:::

{
    "senderId": 29,
    "receiverId": 3,
    "conversationId": "",
    "content": "",
    "msgType": 4,
    "timestamp": 1689728109964,
    "msgFormat": "0",
    "msgExtra": {
        "imgExtra": {
            "url": "https://property-acquisition.oss-cn-guangzhou.aliyuncs.com/acquisition/images/20230719/16897281099941688373316292微信图片_20221204143943.jpg",
            "size": 135582,
            "width": 1170,
            "height": 1655
        },
        "senderInfo": {
            "id": 29,
            "username": "admin",
            "nickName": "权限管理员",
            "icon": "https://property-acquisition.oss-cn-guangzhou.aliyuncs.com/acquisition/images/20230527/16851506513731.jpg",
            "macroOpenId": "1234"
        }
    },
    "chatRoomType": "1",
}

:::info
文件消息
:::

{
    "senderId": 29,
    "receiverId": 3,
    "conversationId": "",
    "content": "",
    "msgType": 5,
    "timestamp": 1689728200130,
    "msgFormat": "0",
    "msgExtra": {
        "fileExtra": {
            "fileName": "Macro Chen.pdf",
            "url": "https://property-acquisition.oss-cn-guangzhou.aliyuncs.com/acquisition/images/20230719/1689728200168Macro Chen.pdf",
            "size": 277255
        },
        "senderInfo": {
            "id": 29,
            "username": "admin",
            "nickName": "权限管理员",
            "icon": "https://property-acquisition.oss-cn-guangzhou.aliyuncs.com/acquisition/images/20230527/16851506513731.jpg",
            "macroOpenId": "1234"
        }
    },
    "chatRoomType": "1",
    "isFail": false,
    "loading": true
}

:::info
引用消息
:::

{
    "senderId": 29,
    "receiverId": 3,
    "conversationId": "",
    "content": "这是一条引用消息",
    "msgType": 1,
    "timestamp": 1689728363658,
    "msgFormat": "0",
    "msgExtra": {
        "senderInfo": {
            "id": 29,
            "username": "admin",
            "nickName": "权限管理员",
            "icon": "https://property-acquisition.oss-cn-guangzhou.aliyuncs.com/acquisition/images/20230527/16851506513731.jpg",
            "macroOpenId": "1234"
        },
        "quoteMessage": {
            "id": 2516,
            "content": "嘿嘿",
            "fromUserName": "权限管理员"
        }
    },
    "chatRoomType": "1",
}

:::info
链接消息返回格式
:::

{
    "msgType": 1,
    "content": "https://www.baidu.com",
    "senderId": "29",
    "receiverId": "3",
    "msgFormat": "0",
    "id": null,
    "conversationId": "8062aa06981a407c99e33b6c78420efb",
    "groupId": null,
    "readFlag": null,
    "chatRoomType": "1",
    "readCount": null,
    "delFlag": null,
    "msgExtra": {
        "urlTitle": {
            "https://www.baidu.com": {
                "title": "百度一下,你就知道",
                "icon": "https://www.baidu.com/favicon.ico",
                "desc": "全球领先的中文搜索引擎、致力于让网民更便捷地获取信息,找到所求。百度超过千亿的中文网页数据库,可以瞬间找到相关的搜索结果。"
            }
        },
        "senderInfo": {
            "id": 29,
            "username": "admin",
            "nickName": "权限管理员",
            "icon": "https://property-acquisition.oss-cn-guangzhou.aliyuncs.com/acquisition/images/20230527/16851506513731.jpg",
            "macroOpenId": "1234",
            "conversationId": null,
            "onlineStatus": null
        }
    },
    "createTime": "2023-07-19 09:01:02",
    "updateTime": "2023-07-19 09:01:02"
}

二、消息发送器工厂类 MessagePusherFactory

/**
 * 

* 消息发送器工厂类 *

* * @Title: MessagePusherFactory * @Author Macro Chen * @PACKAGE com.netty.server.handler.message.push.base * @Date 2023/7/10 9:26 */ @Slf4j public class MessagePusherFactory { /** * 消息发送器散列表 * * @Author Macro Chen * @see Map */ private static final Map PUSHER_MAP = new HashMap(DataType.values().length); /** * 注册消息发送器 * * @param dataType 数据类型 * @param pusher 推杆式 * @Name: registry * @Author Macro Chen */ public static void registry(Byte dataType, AbstractSocketMessagePusher pusher) { log.info("注册消息发送器:{}", pusher.getClass()); PUSHER_MAP.put(dataType, pusher); } /** * 根据消息类型获取消息发送器 * * @param dataType 数据类型 * @return {@link AbstractSocketMessagePusher } * @Name: getStrategyNoNull * @Author Macro Chen */ public static AbstractSocketMessagePusher getStrategyNoNull(Byte dataType) { AbstractSocketMessagePusher pusher = PUSHER_MAP.get(dataType); At.isNotNull(pusher, "no pusher to handler this dataType"); return pusher; } /** * 推送消息 * * @param abstractSocketMessageBase 抽象套接字信息基础 * @Name: pushMessage * @Author Macro Chen */ public static void pushMessage(AbstractSocketMessageBase abstractSocketMessageBase) { DataType dataType = abstractSocketMessageBase.getType(); At.isNotNull(dataType, "message dataType violation error"); getStrategyNoNull(dataType.getValue()).pushMessage(abstractSocketMessageBase); } }
  • 首先消息发送器工厂定义了一个散列表来存储所有的消息发送器,key为需要发送的消息类型,value为发送器的类
  • registry方法:这是一个注册消息发送器的方法,接受参数为Byte类型的消息类型和发送器本身
  • getStrategyNoNull方法:通过需要发送的消息类型寻找合适的消息发送器,找不到则会抛出异常
  • pushMessage方法:先通过需要发送的消息类型获取到合适的消息发送器,然后执行调用消息发送器的pushMessage()方法发送消息
  • 三、抽象Socket消息发送器 AbstractSocketMessagePusher

    /**
     * 

    * 抽象消息发送器 *

    * * @Title: AbstractSocketMessagePusher * @Author Macro Chen * @PACKAGE com.netty.server.handler.message.push.base * @Date 2023/7/10 8:42 */ public abstract class AbstractSocketMessagePusher implements MessagePusher{ @PostConstruct private void registry() { MessagePusherFactory.registry(getDataType().getValue(), this); } /** * 推送消息 * * @param abstractSocketMessageBase 抽象套接字信息基础 * @Name: pushMessage * @Author Macro Chen */ @Override public abstract void pushMessage(AbstractSocketMessageBase abstractSocketMessageBase); /** * 获取消息数据类型 * * @return {@link DataType } * @Name: getDataType * @Author Macro Chen */ @Override public abstract DataType getDataType(); }
  • 首先它实现了MessagePusher接口并抽象重写了发送消息方法pushMessage()和获取数据类型方法getDataType()
  • 提供了一个@PostConstruct注解修饰的注册方法registry(),所有继承它的子类都会在Spring启动完成时向消息发送器工厂类MessagePusherFactory注册实例。
  • 目前它的子类有ChatMessagePusherContext聊天信息发送器上下文、PingMessagePusher向客户端发送Ping操作消息发送器以及SystemNoticeSocketMessagePusher系统通知实时消息发送器。
  • image.png

    四、聊天消息发送器上下文 ChatMessagePusherContext

    /**
     * 

    * 聊天消息发送器上下文 *

    * * @Title: ChatMessagePusherContext * @Author Macro Chen * @PACKAGE com.netty.server.handler.message.push.base.chat * @Date 2023/7/10 10:15 */ @Slf4j @RequiredArgsConstructor public class ChatMessagePusherContext extends AbstractSocketMessagePusher implements ApplicationListener { /** * 聊天消息发送器散列表 * * @Author Macro Chen * @see Map */ private Map messagePusherMap = null; @Override public void onApplicationEvent(@NotNull ApplicationStartedEvent event) { messagePusherMap = event.getApplicationContext().getBeansOfType(AbstractChatMessagePusher.class); } /** * 过滤器 可抽取一个过滤器集合 注入到ioc时设置过滤器集合 方便日后拓展过滤器 * * @param message 聊天信息 * @Name: filter * @Author Macro Chen */ private void filters(ChatMessage message) { // 消息工厂根据不同消息类型处理不同逻辑 AbstractMsgHandlerFactory.getStrategyNoNull(message.getMsgType()) .handler(message); } /** * 推送消息 * * @param abstractSocketMessageBase 抽象套接字信息基础 * @Name: pushMessage * @Author Macro Chen */ @Override public final void pushMessage(AbstractSocketMessageBase abstractSocketMessageBase) { log.info("发送消息:{}", abstractSocketMessageBase); // 判断是否支持此类消息 At.isTrue(!(abstractSocketMessageBase instanceof ChatMessage), "Unsupported messages"); // 校验消息 At.allCheckValidateThrow(abstractSocketMessageBase); // 处理消息发送器为空 At.isNotNull(messagePusherMap, "message pusher is empty"); ChatMessage chatMessage = Convert.convert(ChatMessage.class, abstractSocketMessageBase); // 消息处理器为空 Optional optional = messagePusherMap.values().stream() .filter(pusher -> pusher.isSupport(chatMessage.getChatRoomType())) .findFirst(); optional.orElseThrow(() -> new MessageException("no message pusher handler")); AbstractChatMessagePusher messagePusher = optional.get(); // 过滤器过滤 this.filters(chatMessage); // 由子类去发送消息 messagePusher.push(chatMessage); // 后置处理 messagePusher.afterProcess(chatMessage); } /** * 获取消息数据类型 * * @return {@link DataType } * @Name: getDataType * @Author Macro Chen */ @Override public DataType getDataType() { return DataType.MESSAGE; } }
  • 毋庸置疑,首先它继承了抽象Socket消息发送器,重写了发送消息PushMessage方法和获取消息数据类型getDataType方法。
  • 它实现了ApplicationListener接口并重写了onApplicationEvent方法,用于加载所有抽象聊天消息发送器AbstractChatMessagePusher的Bean
  • 发送消息pushMessage方法中对消息进行一系列校验和过滤操作,然后根据聊天室类型从抽象聊天发送器散列表MessagePusherMap当中获取适用的发送器AbstractChatMessagePusher进行发送。
  • 过滤操作:消息处理器工厂AbstractMsgHandler会对不同消息进行不同的逻辑处理,例如文本消息需要进行Url解析和敏感词过滤等操作。
  • image.png

    五、抽象聊天信息发送器 AbstractChatMessagePusher

    /**
     * 

    * 抽象消息发送接口 *

    * * @Title: IMMessagePusher * @Author Macro Chen * @PACKAGE com.netty.server.handler.socket.push * @Date 2023/4/4 11:08 * @Description: 抽象消息发送接口 */ @Slf4j public abstract class AbstractChatMessagePusher { /** * 抽象发送消息 由子类去执行 * * @param chatMessage 聊天信息 * @Name: push * @Author Macro Chen */ protected abstract void push(ChatMessage chatMessage); /** * 抽象发送消息后置处理 由子类执行 * * @param chatMessage 聊天信息 * @Name: afterProcess * @Author Macro Chen */ protected void afterProcess(ChatMessage chatMessage){ log.info("default message push afterProcess"); }; /** * 得到聊天室类型 * * @return {@link ChatRoomType } * @Name: getChatRoomType * @Author Macro Chen */ protected abstract ChatRoomType getChatRoomType(); /** * 是否支持发送此消息 * * @return boolean */ protected boolean isSupport(String roomType) { At.isNotNull(roomType, "roomType is not allow null"); ChatRoomType chatRoomType = this.getChatRoomType(); At.isNotNull(chatRoomType, "roomType is not allow null"); return roomType.equals(chatRoomType.getRoomType()); } }
  • 定义了三个抽象方法,分别为发送消息方法push、发送消息成功后置处理回调方法afterProcess以及获取消息发送器支持的聊天室类型方法getChatRoomType
  • 提供了一个模板方法isSupport来判断消息发送器是否支持指定的聊天室类型。
  • 目前它的子类有私聊一对一聊天消息发送器privateConversationMessagePusher以及群聊消息发送器ChatGroupConversationMessagePusher
  • image.png

    六、私聊信息发送器 PrivateConversationMessagePusher

    /**
     * 

    * 私聊信息发送器 *

    * * @Title: PrivateConversationMessagePusher * @Author Macro Chen * @PACKAGE com.macro.mall.chat.pusher * @Date 2023/5/25 15:49 */ @Slf4j @RequiredArgsConstructor @Component public class PrivateConversationMessagePusher extends AbstractChatMessagePusher { /** * 会话组 * * @Author Macro Chen * @see SessionService */ private final SessionService sessionService; /** * 私聊信息服务类 * * @Author Macro Chen * @see ChatPrivateConversationMapper */ private final ChatPrivateConversationService chatPrivateConversationService; /** * 聊天消息服务类 * * @Author Macro Chen * @see ChatMessageService */ private final ChatMessageService chatMessageService; /** * 发送消息 * * @param chatMessage 聊天信息 * @Name: push * @Author Macro Chen */ @Override @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class) protected void push(ChatMessage chatMessage) { AssertUtil.isTrue(isSupport(chatMessage.getChatRoomType()), () -> { AssertUtil.isNotEmpty(chatMessage.getReceiverId(), "接收方信息有误!"); // 设置会话信息后新增私聊会话 AssertUtil.isTrue(StrUtil.isBlank(chatMessage.getConversationId()), () -> chatPrivateConversationService.getOrSavePrivateConversation(chatMessage, chatPrivateConversationService::savePrivateConversation)); // 保存消息 saveMessage(chatMessage); // 异步查询对方的在线列表并发送消息 sessionService.asyncWrite(chatMessage.getReceiverId(), chatMessage); }); } /** * 得到聊天室类型 * * @return {@link ChatRoomType } * @Name: getChatRoomType * @Author Macro Chen */ @Override protected ChatRoomType getChatRoomType() { return ChatRoomType.PRIVATE; } /** * 保存信息 * * @param message 聊天信息 * @Name: saveMessage * @Author Macro Chen */ public void saveMessage(ChatMessage message) { CompletableFuture.runAsync(() -> At.isTrue(Objects.nonNull(message.getId()), () -> chatMessageService.updateById(message), () -> chatMessageService.save(message))); } }

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论