Netty-SDK发送聊天消息步骤解析

一、消息发送接口

java 复制代码
    /**
     * 发送消息接口
     *
     * @param messageVO 消息dto
     * @return {@link R }<{@link Boolean }>
     * @Name: sendMessage
     * @Author Macro Chen
     */
    @PostMapping("/sendMessage")
    public R<ChatMessage> 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 文本消息 :::

json 复制代码
{
  "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 图片消息 :::

json 复制代码
{
    "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 文件消息 :::

json 复制代码
{
    "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 引用消息 :::

json 复制代码
{
    "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 链接消息返回格式 :::

json 复制代码
{
    "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

java 复制代码
/**
 * <p>
 *   消息发送器工厂类
 * </p>
 *
 * @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<Byte, AbstractSocketMessagePusher>
     */
    private static final Map<Byte, AbstractSocketMessagePusher> 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);
    }
}
  1. 首先消息发送器工厂定义了一个散列表来存储所有的消息发送器,key为需要发送的消息类型,value为发送器的类
  2. registry方法:这是一个注册消息发送器的方法,接受参数为Byte类型的消息类型和发送器本身
  3. getStrategyNoNull方法:通过需要发送的消息类型寻找合适的消息发送器,找不到则会抛出异常
  4. pushMessage方法:先通过需要发送的消息类型获取到合适的消息发送器,然后执行调用消息发送器的pushMessage()方法发送消息

三、抽象Socket消息发送器 AbstractSocketMessagePusher

java 复制代码
/**
 * <p>
 * 抽象消息发送器
 * </p>
 *
 * @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();
}
  1. 首先它实现了MessagePusher接口并抽象重写了发送消息方法pushMessage()和获取数据类型方法getDataType()
  2. 提供了一个@PostConstruct注解修饰的注册方法registry(),所有继承它的子类都会在Spring启动完成时向消息发送器工厂类MessagePusherFactory注册实例。
  3. 目前它的子类有ChatMessagePusherContext聊天信息发送器上下文、PingMessagePusher向客户端发送Ping操作消息发送器以及SystemNoticeSocketMessagePusher系统通知实时消息发送器。

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

java 复制代码
/**
 * <p>
 *     聊天消息发送器上下文
 * </p>
 *
 * @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<ApplicationStartedEvent> {

    /**
     * 聊天消息发送器散列表
     *
     * @Author Macro Chen
     * @see Map
     */
    private Map<String, AbstractChatMessagePusher> 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<AbstractChatMessagePusher> 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;
    }
}
  1. 毋庸置疑,首先它继承了抽象Socket消息发送器,重写了发送消息PushMessage方法和获取消息数据类型getDataType方法。
  2. 它实现了ApplicationListener接口并重写了onApplicationEvent方法,用于加载所有抽象聊天消息发送器AbstractChatMessagePusher的Bean
  3. 发送消息pushMessage方法中对消息进行一系列校验和过滤操作,然后根据聊天室类型从抽象聊天发送器散列表MessagePusherMap当中获取适用的发送器AbstractChatMessagePusher进行发送。
  4. 过滤操作:消息处理器工厂AbstractMsgHandler会对不同消息进行不同的逻辑处理,例如文本消息需要进行Url解析和敏感词过滤等操作。

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

java 复制代码
/**
 * <p>
 *     抽象消息发送接口
 * </p>
 *
 * @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());
    }
}
  1. 定义了三个抽象方法,分别为发送消息方法push、发送消息成功后置处理回调方法afterProcess以及获取消息发送器支持的聊天室类型方法getChatRoomType
  2. 提供了一个模板方法isSupport来判断消息发送器是否支持指定的聊天室类型。
  3. 目前它的子类有私聊一对一聊天消息发送器privateConversationMessagePusher以及群聊消息发送器ChatGroupConversationMessagePusher

六、私聊信息发送器 PrivateConversationMessagePusher

java 复制代码
/**
 * <p>
 * 私聊信息发送器
 * </p>
 *
 * @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)));
    }
}
相关推荐
2401_8543910810 分钟前
城镇住房保障:SpringBoot系统功能概览
java·spring boot·后端
hummhumm11 分钟前
Oracle 第29章:Oracle数据库未来展望
java·开发语言·数据库·python·sql·oracle·database
wainyz21 分钟前
Java NIO操作
java·开发语言·nio
工业3D_大熊26 分钟前
【虚拟仿真】CEETRON SDK在船舶流体与结构仿真中的应用解读
java·python·科技·信息可视化·c#·制造·虚拟现实
lzb_kkk35 分钟前
【JavaEE】JUC的常见类
java·开发语言·java-ee
爬山算法1 小时前
Maven(28)如何使用Maven进行依赖解析?
java·maven
2401_857439691 小时前
SpringBoot框架在资产管理中的应用
java·spring boot·后端
怀旧6661 小时前
spring boot 项目配置https服务
java·spring boot·后端·学习·个人开发·1024程序员节
李老头探索1 小时前
Java面试之Java中实现多线程有几种方法
java·开发语言·面试
芒果披萨2 小时前
Filter和Listener
java·filter