//
// WebSocketManager.swift
// SurveillanceHome
//
// Created by duxuying on 2023/10/26.
// Copyright © 2023 tplink. All rights reserved.
//
import Foundation
import Starscream
import SwiftyJSON
public enum AppnetworkStatus: Int {
case unknow = 0
case wan
case wlan
case none
}
public enum CloudWSEventType: Int {
case roleChange = 0
case siteChange
}
// MARK: - WebSocket代理
//这里即设置代理,稍后还会发通知.使用情况不一样.
protocol WebSocketManagerDelegate: AnyObject {
/// 建立连接成功通知
func webSocketManagerDidConnect(manager: WebSocketManager)
/// 断开链接通知,参数 `isReconnecting` 表示是否处于等待重新连接状态。
func webSocketManagerDidDisconnect(manager: WebSocketManager, error: Error?)
/// 接收到消息后的回调(String)
func webSocketManagerDidReceiveMessage(manager: WebSocketManager, text: String)
/// 接收到消息后的回调(Data)
func webSocketManagerDidReceiveData(manager: WebSocketManager, data: Data)
}
enum WebSocketConnectType {
case closed //初始状态,未连接
case connect //已连接
case disconnect //连接后断开
case reconnecting //重连中...
}
class WebSocketManager: NSObject {
/// 单例,可以使用单例,也可以使用[alloc]init 根据情况自己选择
static let shard = WebSocketManager()
/// WebSocket对象
private var webSocket : Starscream.WebSocket?
/// 是否连接
var isConnected : Bool = false {
didSet {
if isConnected != oldValue {
if (!isConnected && !isActivelyClose && isHaveNet) {
self.reConnectSocket()
}
}
}
}
/// 代理
weak var delegate: WebSocketManagerDelegate?
private var heartbeatInterval: TimeInterval = 15
/// 重连次数
private var reConnectCount: Int = 0
private var reConnectVmsCount: Int = 0
//存储要发送给服务端的数据,本案例不实现此功能,如有需求自行实现
// private var sendDataArray = [String]()
private let appContext = TPAppContextFactory.shared()
public var connectDic: [String : Any]
public var subscribeDic: [String : String]
public var deviceStatusSubscribeDic: [String: String] = [:]
public var deviceShareSubscribeDic: [String: String] = [:]
// private let onceToken = "创建单例"
public var receiveMessageList:[TPSSMessage] = []
///心跳包定时器
var heartBeatTimer: Timer?
///网络监听定时器
var netWorkTimer:Timer?
var connectType : WebSocketConnectType = .closed
/// 用于判断是否主动关闭长连接,如果是主动断开连接,连接失败的代理中,就不用执行 重新连接方法
private var isActivelyClose:Bool = false
/// 当前是否有网络,
private var isHaveNet:Bool = true
//短时收到多次通知,只执行一次
private var isNotificationPost = false
public var urlStrng: String?
private var retryCount = 0
override private init() {
self.connectDic = ["method":"connect","data":["csrfToken":"\(appContext.loginHistories[0].vmsToken)"]] as [String : Any]
self.subscribeDic = ["method":"subscribe","topic":"/v2user/queue/vms/\(appContext.loginHistories[0].vmsId)/device-message-inform"]
// webSocket.advancedDelegate = self
}
// MARK: - 公开方法,外部调用
public func updatenetWorkStatus(networkStatus: AppnetworkStatus){
switch(networkStatus){
case .wan:
if !self.isConnected {
self.isHaveNet = true
}
case .wlan:
if !self.isConnected {
self.isHaveNet = true
}
case .none:
if self.isConnected {
self.isHaveNet = false
}
case .unknow:
break
}
}
public func update(cloudEventType: CloudWSEventType = .roleChange){
if appContext.loginType == .personal {
guard let url = URL(string: "https://aps1-vms-api-beta.tplinkcloud.com"), let ip = url.host else {
return
}
self.urlStrng = String(format: "wss://%@/api/app/personal/accounts/%@/ws/status",ip ,appContext.accountInfo.pcAccountId)
// self.connectDic = ["method":"connect","data":["csrfToken":"3530618e30e74c68847e817f77114141"]] as [String : Any]
self.connectDic = ["method":"connect","data":["csrfToken":"\(appContext.accountInfo.vmsToken)"]] as [String : Any]
// self.connectDic = ["method":"connect","data":["csrfToken":"TPEC_SID=a1-iam-d3b65df38e864e55905372bcc623cdcd"]] as [String : Any]
self.subscribeDic = ["method":"subscribe","topic":"/topic/omada/\(appContext.accountInfo.accountID)/device-info"]
self.deviceStatusSubscribeDic = ["method":"subscribe","topic":"/topic/omada/\(appContext.accountInfo.pcAccountId)/device-info"]
self.deviceShareSubscribeDic = ["method":"subscribe","topic":"/topic/personal/accounts/\(appContext.accountInfo.pcAccountId)/share-message-notify"]
} else if appContext.loginType == .cloudLocalAccess {
guard let url = URL(string: appContext.loginHistories[0].vmsURL), let ip = url.host else {
return
}
self.urlStrng = String(format: "wss://%@/api/v1/vms/cloud-access/%@/websocket/api/app/vms/%@/ws/status",ip ,appContext.loginHistories[0].vmsId, appContext.loginHistories[0].localVmsId)
self.connectDic = ["method":"connect","data":["csrfToken":"\(appContext.loginHistories[0].vmsToken)"]] as [String : Any]
self.subscribeDic = ["method":"subscribe","topic":"/v2user/queue/vms/\(appContext.loginHistories[0].localVmsId)/users/\(appContext.loginHistories[0].vmsOwnerAccountId)/device-message-inform"]
} else if appContext.loginType == .local{
self.urlStrng = String(format: "wss://%@:%d/api/app/vms/%@/ws/status",appContext.loginHistories[0].vmsIp, appContext.loginHistories[0].vmsPort, appContext.loginHistories[0].vmsId)
self.connectDic = ["method":"connect","data":["csrfToken":"\(appContext.loginHistories[0].vmsToken)"]] as [String : Any]
self.subscribeDic = ["method":"subscribe","topic":"/v2user/queue/vms/\(appContext.loginHistories[0].vmsId)/device-message-inform"]
} else if appContext.loginType == .cloud {
guard let url = URL(string: appContext.loginHistories[0].vmsURL), let ip = url.host else {
return
}
self.urlStrng = String(format: "wss://%@/api/app/vms/%@/ws/status",ip, appContext.loginHistories[0].vmsId)
self.connectDic = ["method":"connect","data":["csrfToken":"\(appContext.loginHistories[0].vmsToken)"]] as [String : Any]
if cloudEventType == .siteChange {
self.subscribeDic = ["method":"subscribe","topic":"/topic/vms/\(appContext.loginHistories[0].vmsId)/users/\(appContext.accountInfo.pcUserId)/site-permission" ]
} else {
self.subscribeDic = ["method":"subscribe","topic":"/topic/vms/\(appContext.loginHistories[0].vmsId)/users/\(appContext.accountInfo.pcUserId)/role"]
}
}
}
private func checkIfInfoOk() -> Bool {
if appContext.loginType == .cloudLocalAccess {
if (appContext.loginHistories[0].vmsURL == "" || appContext.loginHistories[0].vmsId == "" || appContext.loginHistories[0].vmsToken == "" || appContext.loginHistories[0].vmsOwnerAccountId == "") {
return false
}
} else if appContext.loginType == .local{
if (appContext.loginHistories[0].vmsIp == "" || appContext.loginHistories[0].vmsId == "" || appContext.loginHistories[0].vmsToken == "") {
return false
}
}
return true
}
public func getMessageList()->Array<TPSSMessage> {
return receiveMessageList
}
public func clearMessageList(){
self.receiveMessageList.removeAll()
}
func connectSocket(_ paremeters: Any?) {
if let url = urlStrng {
guard let url = URL(string: url) else {
return
}
if !checkIfInfoOk() {
return
}
self.isActivelyClose = false
var request = URLRequest(url: url)
request.timeoutInterval = 5
//添加头信息
request.setValue(appContext.accountInfo.vmsCookie, forHTTPHeaderField: "Cookie")
let pinner = FoundationSecurity(allowSelfSigned: true)
if appContext.loginType == .cloudLocalAccess {
//cloudAccess 使用CustomEngine会报错403
webSocket = WebSocket(request: request, certPinner: pinner, useCustomEngine: false)
} else if appContext.loginType == .local{
webSocket = WebSocket(request: request, certPinner: pinner)
} else if appContext.loginType == .cloud {
webSocket = WebSocket(request: request, certPinner: pinner, useCustomEngine: false)
} else {
webSocket = WebSocket(request: request, certPinner: pinner, useCustomEngine: false)
}
webSocket?.delegate = self
webSocket?.connect()
print("WebSocket Request:\nURL:\(String(describing: request.url))\nHeader:\(String(describing: request.allHTTPHeaderFields))\nBody:\(String(describing: request.httpBody))")
// 自定义队列,一般不需要设置,默认主队列
//webSocket?.callbackQueue = DispatchQueue(label: "com.vluxe.starscream.myapp")
}
}
/// 发送消息
func sendMessage(_ text: String) {
if self.isHaveNet {
// 有网络直接发消息
if self.connectType == .connect { //已经连接
self.webSocket?.write(string: text)
}else if self.connectType == .reconnecting {
// self.sendDataArray.append(text)
}else if self.connectType == .disconnect {
reConnectSocket()
}else{
// self.sendDataArray.append(text)
}
} else {
// 无网络的时候的操作
//1.提示无网络
//2.存储消息
// self.sendDataArray.append(text)
//等待来网
guard isActivelyClose else {
initNetWorkTestingTimer()
return
}
}
}
/// 断开链接
func disconnect() {
self.isActivelyClose = true
self.connectType = .disconnect
webSocket?.disconnect()
destoryHeartBeat()
destoryNetWorkStartTesting()
}
/// 重新连接
func reConnectSocket() {
if !checkIfInfoOk() {
return
}
if self.reConnectCount > 10 { //重连10次
self.reConnectCount = 0;
return
}
//重连10次,每两次间隔5s
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 5) {
if self.connectType == .reconnecting {
return
}
/// 连接
self.update()
self.connectSocket(nil)
self.reConnectCount = self.reConnectCount + 1
}
}
func reConnectVms() {
self.disconnect()
if self.reConnectVmsCount > 10 { //重连10次
self.reConnectVmsCount = 0;
return
}
//重连10次,每两次间隔5s
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 5) {
if self.connectType == .reconnecting {
return
}
/// 连接
self.update()
self.connectSocket(nil)
self.reConnectVmsCount = self.reConnectVmsCount + 1
}
}
// MARK: - 网络监听
func networkNotifation() {
//外部最好也传进来一个网络变化的通知
//当断开网络时候,不在进行重新连接
//当网络恢复的时候,重新连接,根据自己业务进行更新.
//更新网络状态
isHaveNet = false
}
// MARK: - 私有方法
/// 初始化心跳
private func initHeartBeat() {
if self.heartBeatTimer != nil {
return
}
self.heartBeatTimer = Timer(timeInterval: 1, target: self, selector: #selector(sendHeartBeat), userInfo: nil, repeats: true)
RunLoop.current.add(self.heartBeatTimer!, forMode: RunLoop.Mode.common)
}
private func initNetWorkTestingTimer() {
if self.netWorkTimer != nil {
return
}
self.netWorkTimer = Timer(timeInterval: 5, target: self, selector: #selector(noNetWorkStartTesting), userInfo: nil, repeats: true)
RunLoop.current.add(self.netWorkTimer!, forMode: RunLoop.Mode.common)
}
/// 心跳
@objc private func sendHeartBeat() {
if self.isConnected {
// let text = "ping的内容,和服务器商定"
let text = "h"
if let data = text.data(using: String.Encoding.utf8) {
webSocket?.write(ping: data)
}
// 我在网上查阅资料显示,也可以使用webSocket?.write(string: "")
// 即: webSocket?.write(string: text)
// write方法中ping和text是一样的,只是传入的枚举不一样,可以参考源代码
}else{
// 发现没有连接,根据需求做判断
}
}
/// 没有网络的时候开始定时 -- 用于网络检测
@objc private func noNetWorkStartTesting() {
//有网络
if isHaveNet {//这里可以根据业务需要修改
//1.关闭网络监测定时器
destoryNetWorkStartTesting()
//2.重新连接
reConnectSocket()
}
}
//关闭心跳定时器
private func destoryHeartBeat() {
self.heartBeatTimer?.invalidate()
self.heartBeatTimer = nil
}
//关闭网络监测定时器
private func destoryNetWorkStartTesting() {
self.netWorkTimer?.invalidate()
self.netWorkTimer = nil
}
private func sendConnectRequest() {
guard let connectJson = try? JSONSerialization.data(withJSONObject: self.connectDic, options: .prettyPrinted) else {
//转换失败
// print("starcream: connect JSON tranlate failed")
return
}
let connectStr = String(data: connectJson, encoding: .utf8) ?? ""
self.webSocket?.write(string:connectStr)
}
private func sendSubscribeRequest() {
guard let subscribeJson = try? JSONSerialization.data(withJSONObject: self.subscribeDic, options: .prettyPrinted) else {
//转换失败
// print("starcream: subscribe JSON tranlate failed")
return
}
let subscribeStr = String(data: subscribeJson, encoding: .utf8) ?? ""
self.webSocket?.write(string: subscribeStr)
if appContext.loginType == .cloud {
update(cloudEventType: .siteChange)
guard let subscribeJson2 = try? JSONSerialization.data(withJSONObject: self.subscribeDic, options: .prettyPrinted) else {
//转换失败
return
}
//cloudVMS 需要订阅角色,和站点改变两个subscribe,第一次订阅的是角色,这里订阅站点
let subscribeStr2 = String(data: subscribeJson2, encoding: .utf8) ?? ""
self.webSocket?.write(string: subscribeStr2)
}
}
private func sendDeviceStatusSubscribeRequest() {
guard let subscribeJson = try? JSONSerialization.data(withJSONObject: self.deviceStatusSubscribeDic, options: .prettyPrinted) else {
//转换失败
print("starcream: subscribe JSON tranlate failed")
return
}
let subscribeStr = String(data: subscribeJson, encoding: .utf8) ?? ""
// print("dxy::Device Status \(subscribeStr)")
self.webSocket?.write(string: subscribeStr)
if appContext.loginType == .cloud {
update(cloudEventType: .siteChange)
guard let subscribeJson2 = try? JSONSerialization.data(withJSONObject: self.deviceStatusSubscribeDic, options: .prettyPrinted) else {
//转换失败
return
}
//cloudVMS 需要订阅角色,和站点改变两个subscribe,第一次订阅的是角色,这里订阅站点
let subscribeStr2 = String(data: subscribeJson2, encoding: .utf8) ?? ""
self.webSocket?.write(string: subscribeStr2)
}
}
private func sendDeviceShareSubscribeRequest() {
guard let subscribeJson = try? JSONSerialization.data(withJSONObject: self.deviceShareSubscribeDic, options: .prettyPrinted) else {
//转换失败
print("starcream: subscribe JSON tranlate failed")
return
}
let subscribeStr = String(data: subscribeJson, encoding: .utf8) ?? ""
// print("dxy::Device Share \(subscribeStr)")
self.webSocket?.write(string: subscribeStr)
if appContext.loginType == .cloud {
update(cloudEventType: .siteChange)
guard let subscribeJson2 = try? JSONSerialization.data(withJSONObject: self.deviceShareSubscribeDic, options: .prettyPrinted) else {
//转换失败
return
}
//cloudVMS 需要订阅角色,和站点改变两个subscribe,第一次订阅的是角色,这里订阅站点
let subscribeStr2 = String(data: subscribeJson2, encoding: .utf8) ?? ""
self.webSocket?.write(string: subscribeStr2)
}
}
private func addMessageToList(message: TPSSMessage) {
if !self.receiveMessageList.contains(message) {
self.receiveMessageList.append(message)
}
}
}
extension WebSocketManager: Starscream.WebSocketDelegate{
func didReceive(event: WebSocketEvent, client: Starscream.WebSocketClient) {
switch event {
// case .connected(let headers):
case .connected(_):
isConnected = true
delegate?.webSocketManagerDidConnect(manager: self)
// _ = "连接成功,在这里处理成功后的逻辑,比如将发送失败的消息重新发送等等..."
// print("dxy::websocket is connected: \(headers)")
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.5, execute: {
self.initHeartBeat()
self.sendConnectRequest()
})
break
case .disconnected(let reason, let code):
isConnected = false
let error = NSError(domain: reason, code: Int(code), userInfo: nil) as Error
delegate?.webSocketManagerDidDisconnect(manager: self, error: error)
self.connectType = .disconnect
if self.isActivelyClose {
self.connectType = .closed
} else {
self.connectType = .disconnect
destoryHeartBeat() //断开心跳定时器
if self.isHaveNet {
reConnectSocket() //重新连接
} else {
initNetWorkTestingTimer()
}
}
// print("dxy::websocket is disconnected: \(reason) with code: \(code)")
break
case .text(let string):
delegate?.webSocketManagerDidReceiveMessage(manager: self, text: string)
let dic = ["text" : string]
// print("dxy::\(dic)")
let jsonData = string.data(using: String.Encoding.utf8, allowLossyConversion: false) ?? Data()
guard let dict = try? JSONSerialization.jsonObject(with: jsonData, options: [JSONSerialization.ReadingOptions.init(rawValue: 0)]) as? [String:AnyObject] else {
//消息获取失败
return
}
if (string.contains("errorCode")) {
//请求部分
if let errorcode = dict["errorCode"] as? Int, let method = dict["method"] as? String {
//请求
if errorcode == 0 {
//请求成功
if "connect" == method{
//链接请求成功
// print("dxy::链接请求成功")
#if APP_VIGI
if appContext.loginType == .cloudLocalAccess || appContext.loginType == .local{
//VIGI cloudVMS无需订阅subscribe
self.sendSubscribeRequest()
}
#else
// self.sendSubscribeRequest()
self.sendDeviceStatusSubscribeRequest()
self.sendDeviceShareSubscribeRequest()
#endif
} else if "subscribe" == method {
//订阅返回成功
// print("dxy::订阅返回成功")
}
} else {
//接口请求失败,重新连接
self.reConnectVms()
}
} else {
//请求失败
self.reConnectVms()
}
} else if let dictTopic = dict["topic"] as? String {
//消息内容
let components = dictTopic.components(separatedBy: "/")
let lastComponent = components.last
switch lastComponent {
case "role":
if let data = dict["data"] as? [String:AnyObject], let orgRole = data["orgRole"] as? String {
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "webSocketManagerDidReceiveRoleChange"), object: orgRole)
}
case "site-permission":
if !isNotificationPost {
isNotificationPost = true
DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) { [weak self] in
// 确保self仍然存在(避免循环引用)
guard let self = self else { return }
// 发送通知
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "webSocketManagerDidReceiveSitePrivilegeChange"), object: dictTopic)
// 重置标志,表示通知已经发送
self.isNotificationPost = false
}
}
case "device-message-inform":
if let messageArray = dict["data"] as? Array<[AnyHashable: Any]> {
var pushEventList:[TPSSPushEvent] = []
for messageDic in messageArray {
let message = TPSSMessage(siteMessageWith: messageDic, isCloud: false)
self.addMessageToList(message: message)
let pushEvent = TPSSPushEvent(vmsPushUserInfo: messageDic, isCloudVms: false)
pushEventList.append(pushEvent)
}
NotificationCenter.default.post(name: TPGuardNewEventMessageNotification, object: pushEventList)
}
case "share-message-notify":
if let messageDictList = dict["data"] as? [[String: Any]] {
for messageDict in messageDictList {
let shareMessage = TPGuardDeviceShareMessage()
shareMessage.messageID = messageDict["msgId"] as? String ?? ""
let msgSubTypes = messageDict["msgSubType"] as? [Int64] ?? []
shareMessage.messageType = TPGuardMessageType(rawValue: (messageDict["msgType"] as? Int ?? 5)) ?? .deviceShareIPC
shareMessage.msgSubType = msgSubTypes.first ?? 0
shareMessage.time = TimeInterval((messageDict["time"] as? Int64 ?? 0) / 1000)
shareMessage.createTime = TimeInterval((messageDict["createTime"] as? Int64 ?? 0) / 1000)
shareMessage.duration = TimeInterval((messageDict["duration"] as? Int64 ?? 0) / 1000)
shareMessage.state = messageDict["state"] as? Bool ?? false
shareMessage.deviceModel = messageDict["deviceModel"] as? String ?? ""
shareMessage.deviceName = messageDict["deviceName"] as? String ?? ""
shareMessage.deviceType = messageDict["deviceType"] as? String ?? ""
shareMessage.deviceId = messageDict["deviceId"] as? String ?? ""
shareMessage.shareId = messageDict["shareId"] as? String ?? ""
shareMessage.sharerUserName = messageDict["sharerUserName"] as? String ?? ""
shareMessage.sharerNickName = messageDict["sharerNickName"] as? String ?? ""
shareMessage.content = messageDict["content"] as? String ?? ""
shareMessage.expiry = TimeInterval((messageDict["expiry"] as? Int64 ?? 0) / 1000)
if msgSubTypes.contains(2) {
let device = appContext.deviceForDeviceList(withPcDeviceID: shareMessage.deviceId, of: .remote)
var userInfo: [String: Any] = ["deviceID": device?.identifier ?? 0]
if shareMessage.messageType == .deviceShareNVR {
userInfo["channels"] = [-1]
} else if shareMessage.messageType == .deviceShareNVR {
userInfo["channels"] = messageDict["channels"] as? [Int]
} else {
userInfo["channels"] = [-1]
}
NotificationCenter.default.post(name: TPGuardPlayerStopPlayNotification, object: nil, userInfo: userInfo)
} else {
TPGuardMessageManager.shard.addDeviceShareMessage(newMessage: shareMessage)
}
}
}
case "device-info":
if let deviceDict = dict["data"] as? [String: Any] {
if let deviceID = deviceDict["deviceId"] as? String, let accountID = deviceDict["accountId"] as? String {
if accountID == appContext.accountInfo.pcAccountId {
guard let deviceIDNumber = TPSSDeviceIdentifier(deviceID) else { return }
var needDelete = false
var needReloadFromClound = false
if let isDeleted = deviceDict["isDeleted"] as? Bool {
if isDeleted == true {
needDelete = true
}
}
if let isBind = deviceDict["isBind"] as? Bool {
if isBind == false {
needDelete = true
}
}
if needDelete {
appContext.setDeviceInvalidByManuallyforDevice(deviceIDNumber, listType: .remote)
} else {
if let deviceStatus = deviceDict["status"] as? Int {
if deviceStatus == 0 {
appContext.setDeviceStateByManually(false, forDevice: deviceIDNumber, listType: .remote)
} else if deviceStatus == 1 {
appContext.setDeviceStateByManually(true, forDevice: deviceIDNumber, listType: .remote)
} else if deviceStatus == 3 {
appContext.setFactoryDefaultByManually(true, forDevice: deviceIDNumber, listType: .remote)
} else {
// appContext.newRequestLoadDeviceList()
needReloadFromClound = true
}
}
if let toDeleteChannelList = deviceDict["shareChannelList"] as? [Int] {
for channel in toDeleteChannelList {
if channel == -1 {
appContext.setDeviceInvalidByManuallyforDevice(deviceIDNumber, listType: .remote)
} else {
appContext.deleteShareChannel(byManuallyforDevice: deviceIDNumber, channelID: channel, listType: .remote)
}
}
}
}
if needReloadFromClound {
NotificationCenter.default.post(name: TPGuardDeviceListNeedUpdateNotification, object: nil)
} else {
NotificationCenter.default.post(name: TPGuardDeviceListDidUpdateNotification, object: nil)
}
}
}
}
default:
break
}
}
// print("dxy::Received text3: \(string)")
break
// case .binary(let data):
case .binary(_):
// print("dxy::Received data4: \(data.count)")
break
case .ping(_):
// print("dxy::ping")
break
case .pong(_):
// print("dxy::pong")
break
case .viabilityChanged(_):
break
case .reconnectSuggested(_):
break
case .cancelled:
isConnected = false
case .error(let error):
isConnected = false
if retryCount < 5 {
retryCount += 1
update()
connectSocket(nil)
}
handleError(error)
case .peerClosed:
isConnected = false
break
}
}
// custom
func handleError(_ error: Error?) {
// if let e = error as? WSError {
// print("dxy::websocket encountered an error: \(e.message) \n")
// } else if let e = error {
// print("dxy::websocket encountered an error: \(e.localizedDescription)\n")
// } else {
// print("dxy::websocket encountered an error\n")
// }
}
}
在这个类中,如何订阅这些类型的topic