一、消息发送接口
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);
}
}
- 首先消息发送器工厂定义了一个散列表来存储所有的消息发送器,key为需要发送的消息类型,value为发送器的类
- registry方法:这是一个注册消息发送器的方法,接受参数为
Byte
类型的消息类型和发送器本身 - getStrategyNoNull方法:通过需要发送的消息类型寻找合适的消息发送器,找不到则会抛出异常
- 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();
}
- 首先它实现了
MessagePusher
接口并抽象重写了发送消息方法pushMessage()
和获取数据类型方法getDataType()
。 - 提供了一个
@PostConstruct
注解修饰的注册方法registry()
,所有继承它的子类都会在Spring启动完成时向消息发送器工厂类MessagePusherFactory
注册实例。 - 目前它的子类有
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;
}
}
- 毋庸置疑,首先它继承了抽象Socket消息发送器,重写了发送消息
PushMessage
方法和获取消息数据类型getDataType
方法。 - 它实现了
ApplicationListener
接口并重写了onApplicationEvent
方法,用于加载所有抽象聊天消息发送器AbstractChatMessagePusher
的Bean - 发送消息
pushMessage
方法中对消息进行一系列校验和过滤操作,然后根据聊天室类型从抽象聊天发送器散列表MessagePusherMap
当中获取适用的发送器AbstractChatMessagePusher
进行发送。 - 过滤操作:消息处理器工厂
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());
}
}
- 定义了三个抽象方法,分别为发送消息方法
push
、发送消息成功后置处理回调方法afterProcess
以及获取消息发送器支持的聊天室类型方法getChatRoomType
。 - 提供了一个模板方法
isSupport
来判断消息发送器是否支持指定的聊天室类型。 - 目前它的子类有私聊一对一聊天消息发送器
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)));
}
}