Featured image of post 关于Socket推送处理的改进

关于Socket推送处理的改进

面临的问题

GBPartySocketGBGlobalSocket 中,每增加一种业务 Action (继承自一个基类 Action ),就要在 didHandleMessageData(messageData: ) 方法中增加如下代码:

1
2
3
4
        if GBEscortCPAction.isCurrentAction(actionName) {
            let action = GBEscortCPAction()
            action.configType(with: actionName, jsonData: messageData)
        }

如此,经过不停迭代,上面的(didHandleMessageData(messageData:))代码就会变的越来越多,如下图非高亮区域所示(高亮区为第一期改进后的,下文后讲到)!

但碍于项目一直在紧锣密鼓中进行着迭代,虽然一直想着去改进,又没有合适的思路,于是一次又一次地跃跃欲试,又一次又一次地放弃!,一切又在前两天出现了转机:马上中秋➕十一放假,节前的最后一次迭代提测后,有了点空闲时间(测试有问题就改一改,没问题就专心改进),一头扎进改进中,连下班都在想怎么改进(哈😄哈😄哈😄),经过2次的调整,总算是有个满意的结果!


分析问题,找准痛点

想要解决问题,一定要弄清楚问题的根源(痛点)在哪!项目中的 Socket 推送信息结构,如下图所示,想要解析数据并做出响应,就一定要现根据 action 名,确定是哪种业务场景,即:根据 action 名确定要用哪个 Action!这一点,大家都明白,也处理了,但是处理的不是很满意:每增加一种业务,就增加上面提到的 isCurrentAction 方法判断,导致 Socket 中重复代码颇多。 Socket推送数据示例   有没有一种自动映射的方案呢?   我第一时间想到了反射:NSClassFromString 以及 Mirror ,经过调研,发现 NSClassFromString 还是有可能满足的,但 Mirror 就不行。

NSClassFromString 虽然可行,但又不行

这里说,是因为从代码层面来说,确实可行! 但又不行,是因为 action 的名字没有按照统一的格式返回,毕竟项目一开始没有考虑过这个(App 端没有考虑过、后端也没有考虑过),如今为了 App 端( iOS 端),让后台再去把所有的 action 名字按照统一的格式整理一下,“不可能,绝对不可能”!

1
2
3
4
5
6
7
/// 房间斗地主业务的action名
"fight_landlord_start_mode.notice"
"fight_landlord_close_mode.notice"

/// 开黑CP业务的action名
"escort_cp.success"
"escort_cp.upgrade"

想要的 action

1
2
3
4
5
6
7
/// 房间斗地主业务的action名
"FightLandlord.start_mode.notice"
"FightLandlord.close_mode.notice"

/// 开黑CP业务的action名
"EscortCp.success.notice"
"EscortCp.upgrade.notice"

如果后台推送的 action 名,按照想要的统一格式,我们就可以如下处理:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
 private func handle(name: String, data: PBMessageData?) {
        guard let actionClsName = name.components(separatedBy: ".").last, !actionClsName.isEmpty else { return }
        guard let nameSpace = Bundle.main.infoDictionary!["CFBundleExecutable"] as? String else {
            assert(false, "⚠️ 请注意:获取命名空间失败")
            return
        }

        guard let Cls: AnyObject.Type = NSClassFromString("\(nameSpace).\(actionClsName)") else {
            assert(false, "⚠️ 请注意:无法根据action类名字符串获取对应的action类")
            return
        }

        guard let actionCls = Cls as? GBSocketAction.Type else{
            assert(false, "⚠️ 请注意:未能成功转换成GBSocketAction类型")
            return
        }

        let action = actionCls.init()
        action.configType(with: action, jsonData: data)
    }

解决问题

第一期改进

捋一捋思路:

  • 一一匹配:根据 action 名确定要用哪个 Handler,始终是重点,避不开要一一去匹配!
  • action 名字符串只定义一次:为了方便匹配 action 名,可采用 let 方式的 action 名,或者枚举类型的 action 名,这里选择后者。
  • 业务独立:全局用一个 Handler ,后台返回的所有 action 名放到一个类里,无论 let 方式的 action 名,或者枚举类型的 action 名,都不重要了,但又回到了根本问题上,Handler 又变的无比复杂!
  • 各个自定义 Handler 类有哪些共同性:处理各自独立的业务逻辑   鉴于该思路的思考和不停地尝试,第一期改进以枚举嵌套 + 协议落地:

1.1 声明协议 + 定义枚举

  • PBSocketHandlerProtocol:各种 Handlerhandle 推送信息的协议,由各个业务的 Handler 类遵循并实现
  • PBSocketGetHandlerProtocol:获取各自业务的具体 Handler 实例
  • HandlerProtocolPBSocketGetHandlerProtocolCaseIterable (其实没用到) 的别名  

1.2 创建领导型 Handler 和 员工型 Handler

  • PBSocketHandlerPBBilliardsHandler :前者相当于领导,后者相当于员工,当推送消息来了,领导根据 action 名,找到具体员工,让该员工去处理。

1.3 编写代码

  • 创建 PBSocketNoticeScene 枚举(见上图),并在其 extension 中创建各种细分业务的枚举,这里以桌球游戏业务为例,即:Billiards 枚举,并遵循 HandlerProtocol 协议,handler 的获取只有在真正需要的时候才去创建,见下图:
  • 对具体业务 Action 类改造:修改 GBBilliardsAction (继承自 GBSocketAction ) 为 PBBilliardsHandler (遵守 PBSocketHandlerProtocol ) 老版本的代码(仅供参考):

第二期改进

在第一期改进中,还有令人不满意的问题:

  • GBPartySocket 中的 didHandleMessageData(messageData: ) 里的众多 XXActionisCurrentAction: ,并没有得到很好的改善,只是转移到了所谓的领导型 PBSocketHandler 中;
  • PBSocketHandler 中改动太多,每增加一种业务的同时需要新增一个匹配判断和一个业务 case ,依然比较繁琐,并且这种方案,引入了中间商(也即:PBSocketHandler,所谓的领导)

那如何更进一步优化呢?经过反复地思考与尝试、反复地尝试与思考,本着一个能简单绝不麻烦➕不重复的原则,迎来了第二期的改进,终令人满意!这里迫不及待地想展示这一期的结果:

2.1 首先,PBPartySocket 中的 didHandleMessageData(messageData:) 方法,只剩下 2 行代码:

2.2 声明SocketActProtocol 协议,用于具体业务 Action 处理业务

说明:该协议,不仅具体的业务 Action 遵循,PBPartySocketPBGlobalSocket 也遵循(而这里也是个巧妙之处)。

SocketActProtocol
1
2
3
protocol SocketActProtocol {
   func act(_ name: String, data: PBMessageData?)
}

所谓的巧妙分析:在第一期改进中提到了领导型 Handler 和员工型 Handler,到了第二期改进时,我发现领导型 Handler 成了讨人厌的“中间商”,所以当 PBPartySocket 或者 PBGlobalSocket 收到后台 Socket 推送时,也可以通过 SocketActProtocolact(_ ,data) 方法处理,而这相比于具体的业务 Actionact(_ ,data) 方法处理(这个也,具体见下文),只是里面的实现方式不一样!

2.3 声明 SocketActionable 协议,用于 Socket 子类(如:GBPartySocket 或者 GBGlobalSocket)获取自己所有的业务 Action 类型,以及让具体的业务 Action 实例执行 Socket 推送信息处理

SocketActionable
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
protocol SocketActionable: SocketActProtocol {
    var actions: [SocketAction.Type] { get }
}
  
extension SocketActionable {
    func act(_ name: String, data: PBMessageData?) {
        guard !actions.isEmpty else {
            return
        }
        guard let Action = actions.first(where: { $0.isHit(name)}) else { return }
       Action.action.act(name, data: data)
    }
}

上文提到 PBPartySocket 也遵循 SocketActProtocol,对于 act(_, data:) 方法的实现,可以用 SocketActionableextension 默认实现,如此下来,后续如果有新的业务需求,只需要定义对应的业务 Action ,并在 GBPartySocket 或者 GBPartySocket 中的 actionsgetter 属性中新增如下代码即可( 总结:这里只需要增加1 行代码):

1
2
3
4
5
6
7
8
9
extension PBPartySocket: SocketActionable {
    var actions: [SocketAction.Type] {
        [
            RoomMagicAction.self,
            BilliardsAction.self,
            EscortCPAction.self
        ]
    }
}

2.4 声明 SocketActionProtocol 协议,用于获取 action 名对应的业务 Action

获取到 names 就可以通过 SocketActionProtocol 的扩展实现 isHit 方法

SocketActionProtocol
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
protocol SocketActionProtocol {
    static var names: [String] { get }
    static func isHit(_ name: String) -> Bool
    static var action: SocketActProtocol { get }
}

extension SocketActionProtocol {
    static func isHit(_ name: String) -> Bool {
        let names = self.names
        guard !name.isEmpty, !names.isEmpty else {
            return false
        }
        return names.contains(name)
    }
}

业务 Action 的核心当然还是要处理 Socket 推送信息,所以业务 Action 要同时遵守 SocketActionProtocolSocketActProtocol,这里采用别名形式加以整理:

SocketAction
1
typealias SocketAction = SocketActionProtocol & SocketActProtocol

2.5 声明具体业务 Action ,并实现相关协议

示例 EscortCPAction
  • 定义 EscortCPAction 结构体,并嵌套 EscortCP: String, CaseIterableRoomQuality: String, CaseIterable 枚举 (这里也可以合并为一个枚举,但个人更喜欢各自业务细分)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
/// 开黑CP推送 & 开黑房质量差关播通知
struct EscortCPAction {
    enum EscortCP: String, CaseIterable {
        /// 原有的`escort_cp.request`(已删除)新代码上去后就不推送了
        case escort_cp_request_v2 = "escort_cp.request.v2"
        /// 组成CP推送
        case escort_cp_success = "escort_cp.success"
        /// CP升级推送
        case escort_cp_upgrade = "escort_cp.upgrade"
        /// 陪陪cp进行官宣cp特效推送(推送目标:全平台所有房间内的用户)
        case escort_cp_official_announce_notice = "escort_cp_official_announce.notice"
        /// 连麦时长达标后邀请进入家族推送
        case escort_family_invite_join_online_mic = "escort_family.invite_join.online_mic"
    }

    enum RoomQuality: String, CaseIterable {
        /// 开黑房质量差[关播]通知
        case gang_up_room_quality_punish_notice = "gang_up_room_quality_punish.notice"
        /// 开黑房质量差[警告]通知
        case gang_up_room_quality_warn_notice = "gang_up_room_quality_warn.notice"
    }
}
  • 扩展 EscortCPAction ,并遵循 SocketAction
  • ① 实现 SocketActionProtocol 中的 namesaction
  • ② 实现 SocketActProtocol 中的 act(_, data:)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
extension EscortCPAction: SocketAction {
   static var names: [String] {
        EscortCP.allCases.map({ $0.rawValue }) + RoomQuality.allCases.map({ $0.rawValue })
    }

   static var action: SocketActProtocol {
        Self.init()
    }

   func act(_ name: String, data: PBMessageData?) {
        guard let data = data else { return }
            act(ec: enumName, data: data)
        }

       else if let enumName = RoomQuality(rawValue: name) {
            act(rq: enumName, data: data)
        }
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
extension EscortCPAction {
    fileprivate func act(ec: EscortCP, data: PBMessageData) {
        if ec == .escort_cp_request_v2 {
            /// 处理业务
        }

        else if ec == .escort_cp_success {
            /// 处理业务
        }

        /// ... other
    }

    fileprivate func act(rq: RoomQuality, data: PBMessageData) {
        if rq == .gang_up_room_quality_punish_notice {
            /// 处理业务
        }

        else if rq == .gang_up_room_quality_warn_notice {
            /// 处理业务
        }
    }
}

总结

  1. 创建业务 Action 类,并遵守并实现 SocketAction 协议
  2. 在对应的 Socket 类中的 SocketActionable 协议的中 actions 新增上面的 Action
  3. 在业务 Action 类中的 act(_, data: ) 方法中处理具体业务 最后,整理里一下大概流程,如下图:
Built with Hugo
主题 StackJimmy 设计