引言
在现代直播项目中,IM(即时消息)功能是用户互动的重要组成部分。无论是直播间的聊天、弹幕消息,还是系统通知和互动提示,都需要通过 IM 消息传递给用户。而在直播场景中,消息类型多样,格式复杂,如何高效地解析和展示这些消息,成为了开发中的一个重要问题。
为了提高开发效率,减少手动解析的工作量,自动化的 IM 消息解析机制应运而生。通过自动解析,开发者能够快速识别并处理各种类型的消息,而无需重复编写大量解析逻辑,极大地提升了代码的可维护性和扩展性。
在本文中,我们将深入探讨如何在直播项目中实现 IM 消息的自动解析,分析其实现原理、技术细节以及在实际项目中的应用场景。
IM的消息结构
在直播项目中,IM 消息数据可以看作是一个特殊的网络请求返回的数据。与普通网络请求不同的是,IM 消息不需要我们主动发起请求,而是会自动送达给用户。每一条 IM 消息都携带着独一无二的标识,用来表示消息的类型和用途。例如,当直播间内发生 PK 画面暂停时,系统会通过一条特殊的 IM 消息通知所有用户。该消息的数据结构可能如下所示:
{
"body": "{ \"liveId\":16177029795,\"pauseLiveId\":16177029794,\"action\":\"pkStreamPause\"}",
"channel": 0,
"code": 0,
"groupId": 16177029795,
"mType": -1,
"mid": "6000001263252494",
"rid": 16349097,
"role": 0,
"rtm_mid": 7591435857520568,
"rtm_type": 120,
"sendTime": 1736755387085,
"sid": 100,
"source": "rtm",
"streamId": 16177029795,
"streamerId": 0,
"type": 10,
"version": 0
}
整个消息的原始数据很多,但并不是所有的数据我们都需要关心,其中最重要的应该就是body消息体,它是一个json字符串的形式,里面有我们所需要的所有数据。其中包含了一个action字段,该字段由客户端双端,或者客户端与服务端共同定义,表示此条消息数据的用途。
除此之外,在我们的直播间业务场景消息体中可能还会包含operate字段。
{
"body": "{\"operate\":\"quit\",\"liveId\":16177027891,\"action\":\"videoChat\",\"guestUid\":16349097,\"pos\":1,\"quitReason\":\"MasterBlockChat\"}",
"channel": 0,
"code": 0,
"groupId": 16177027891,
"mType": -1,
"mid": "6000000210753483",
"rid": 16349097,
"role": 0,
"rtm_mid": 4915880367602969,
"rtm_type": 106,
"sendTime": 1734079831595,
"sid": 100,
"source": "rtm",
"streamId": 16177027891,
"streamerId": 0,
"type": 10,
"version": 0
}
而与body同级的数据,我们可以认为全都是公共参数,其中rtm_type字段,当消息体内没有action字段且没有operate字段时,我们就使用rtm_type字段当做消息标识,而其公共字段我们几乎不需要关心。
实现消息自动解析
既然每一条消息都有一个我们已定义的唯一标识如(action、operate、rtm_type),我们就可以通过这个标识来提前进行消息的注册。具体来说,在应用启动时,我们可以通过一个字典或映射关系,将每种类型的消息标识与对应的数据模型进行关联。这使得在收到消息时,能够根据消息的标识快速查找对应的数据模型,并进行自动解析。
举个例子,假设我们的应用中有多种类型的消息:直播间 PK 暂停、用户发言、系统通知等,每一种消息类型都会有一个对应的解析模型。收到消息时,我们可以通过消息的唯一标识找到对应的解析类,然后将消息的body部分传入该解析类进行解析,从而将原始数据转换成可供使用的对象或结构。
构建消息唯一标识
在实际开发中,IM 消息的唯一标识并不总是由单一字段来决定。例如,在上面的例子中,不同的消息可能通过action、operate和rtm_type等多个字段组合构成唯一标识。因此,我们需要构建一个能够根据这些字段动态生成唯一标识的结构体。
我们可以定义一个结构体MWMessageIdentifer,并通过自定义构造方法来初始化它。该结构体将接受action、operate和rtm_type等字段,将它们合并成一个唯一的标识符,用于区分不同类型的消息。
具体实现如下:
public struct MWMessageIdentifer : Equatable,Hashable {
let key:String
fileprivate let kind:MWMessageKind
fileprivate var actionString:String?
fileprivate(set) var operateString:String?
fileprivate var bussinessType:MWRtmBussinessType?
public init(rtmType: MWRtmBussinessType?, rtmAction: String?, operate: String?) {
self.kind = .rtm
self.actionString = rtmAction
self.operateString = operate
self.bussinessType = rtmType
self.key = Self.getKey(kind: self.kind, actionString: rtmAction, operateString: operate, bussinessType: rtmType)
}
public static func == (lhs: Self, rhs: Self) -> Bool {
return lhs.key == rhs.key
}
fileprivate static func getKey(kind:MWMessageKind,actionString:String?,operateString:String?,bussinessType:MWRtmBussinessType?) -> String {
return "\(kind.rawValue),\(actionString ?? ""),\(operateString ?? ""),\(bussinessType?.rawValue ?? -999)"
}
public func hash(into hasher: inout Hasher) {
hasher.combine(self.key)
}
}
构建消息体数据
为了保证消息体的数据模型能够与消息的唯一标识一一对应,我们定义一个协议,所有IM的消息体数据模型都应该遵守此协议,并提供协议要求提供的属性。
消息体协议如下:
import ObjectMapper
public protocol MWMessageMapProtocol : Mappable {
static var linkMsgIdentifiers: [MWMessageIdentifer] { get }
}
协议要求所有的IM消息体数据模型都需要提供一个消息的唯一标识数组。
以第一条PK暂停消息为例,该IM消息的数据模型构建应该如下:
import ObjectMapper
class MWPKPauseMessage: MWMessageMapProtocol {
// { \"liveId\":16177029795,\"pauseLiveId\":16177029794,\"action\":\"pkStreamPause\"}
static var linkMsgIdentifiers: [MWLinkModule.MWMessageIdentifer] {
return [MWMessageIdentifer.init(rtmType: nil, rtmAction: "pkStreamPause", operate: nil)]
}
/// 直播间id
var liveId: Int?
/// 暂停直播间id
var pauseLiveId: Int?
required init?(map: ObjectMapper.Map) {
}
func mapping(map: ObjectMapper.Map) {
liveId <- map["liveId"]
pauseLiveId <- map["pauseLiveId"]
}
}
注册IM消息
有了IM消息体的数据模型之后,我们首先执行注册操作,也就是需要在消息分发出构建一张表,将消息的唯一标识和对应的消息体数据模型一一进行映射。注册这一步就是说将已有的数据模型添加到这张映射表中。
let models:[MWMessageMapProtocol.Type] = [
....
// pk直播间暂停消息
MWPKPauseMessage.self,
// pk直播间恢复消息
MWPKResumeMessage.self,
....
]
MWLinkHelper.shared.addDispatchDelegate(delegate: self, registerModels: models,receiveType: .registerdModels)
而在addDispatchDelegate方法中会进行消息的唯一标识提取已经注册操作。
//注册的所有消息模型dict
fileprivate lazy var registeredAllModels:[MWMessageIdentifer:Mappable.Type] = [:]
if let registerModelDict = registerModelDict, registerModelDict.count > 0 {
registerModelDict.forEach { (key, value) in
if let oldValue = self.registeredAllModels[key], "\(value)" != "\(oldValue)" {
MWAssert(false, "对同一条消息使用了不同model去解析")
}
self.registeredAllModels.updateValue(value, forKey: key)
}
}
接收IM消息并自动解析
当我们接收到一条 IM 消息时,首先需要根据消息中的action、operate和rtm_type构建一个MWMessageIdentifer结构体的实例。接着,我们利用该实例来获取对应的数据模型类,并创建数据模型的实例,最后将解析后的数据模型进行分发,供应用中的其他模块使用。
以RTM消息为例:
//接收到rtm消息
fileprivate func receivedRtmMsg(msg:MWImCustomRtmAttachment,fromSource:MWLinkSourceChannel) {
DispatchQueue.global().async {
let newMessage = MWLinkBaseMessage()
newMessage.groupId = msg.groupId
newMessage.sendTime = msg.sendTime * 1000
newMessage.sid = msg.sid
newMessage.rid = 0
newMessage.mid = "\(msg.mid)"
newMessage.content = msg.msg
var source = MWMessageRtmSource()
source.bussinessType = MWRtmBussinessType.init(rawValue: msg.bussinessType) ?? .unknown
source.contentDict = (newMessage.content ?? "").jsonDict()
source.actionString = source.contentDict?["action"] as? String
source.operateString = source.contentDict?["operate"] as? String
newMessage.sourceRtm = source
newMessage.msgKind = .rtm
if newMessage.groupId == 0, let liveId = source.contentDict?["liveId"] as? Int {
newMessage.groupId = liveId //容个错
}
self.receiveAllConvertedMsg(groupId: newMessage.groupId ?? 0, message: newMessage,fromSource: fromSource)
}
}
- 接收到消息后解析消息的公共参数,以及消息的消息体为json格式。
- 设置消息的action、operate以及rtm_type。
- 处理消息。
fileprivate func receiveAllConvertedMsg(groupId:Int,message:MWLinkBaseMessage,fromSource:MWLinkSourceChannel) {
DispatchQueue.global().async {
let message = self.maperObject(from: message)
...
}
/// 消息数据解析
fileprivate func maperObject(from message:MWLinkBaseMessage) -> MWLinkBaseMessage {
guard message.mappedModel == nil else {return message}
var json:[String:Any]?
if message.msgKind == .link {
json = message.sourceLink?.contentDict
}else if message.msgKind == .rtm {
json = message.sourceRtm?.contentDict
}
guard let json = json else {return message}
var identifer:MWMessageIdentifer?
if message.msgKind == .link, let action = message.sourceLink?.actionString {
identifer = MWMessageIdentifer.init(linkAction: action)
let map = self.maperObject(identifer: identifer, json: json)
message.mappedModel = map
.....
fileprivate func maperObject(identifer:MWMessageIdentifer?, json:[String:Any]) -> Mappable? {
guard let identifer = identifer else {return nil}
if let mappableType = self.registeredAllModels[identifer] {
let model = mappableType.init(JSON: json)
return model
}
return nil
}
这一系列流程完成之后我们接收到IM消息的中的消息体就会被构建为一个数据模型,并赋值给message的mappedModel属性。
结语
在本文中,我们深入探讨了如何在直播项目中实现 IM 消息的自动解析。通过构建消息的唯一标识符、映射数据模型以及自动解析消息体,我们有效地提高了消息处理的灵活性和效率。借助于结构化的数据模型和自动化的解析流程,我们能够在面对复杂的 IM 消息时,更加高效地进行处理和扩展。
这种自动解析机制不仅减少了大量的手动解析逻辑,也使得新消息类型的加入变得简单而直观。随着项目的不断扩展和消息类型的增多,使用这样的设计模式能够确保代码的高可维护性和低耦合性,从而提供更加稳定和流畅的用户体验。
在未来,随着 IM 消息种类和复杂度的不断增加,自动解析技术将更加重要。我们可以进一步优化数据模型,增加对多种格式的支持,甚至结合机器学习等技术,智能化地识别和解析消息内容。自动化解析将成为高效、可扩展的应用开发中不可或缺的一部分。
通过本文的实践,希望能够为你在直播项目中实现 IM 消息解析提供一些启发与帮助,也欢迎大家在实际开发中继续探索与优化这一技术。