Protobuf全稱Protocol Buffers,簡稱GPB、PB,是QQ等IM采用的協(xié)議,比XML、XMPP(環(huán)信)、JSON、結(jié)構(gòu)體等所有傳輸效果都高的一種傳輸協(xié)議,由谷歌發(fā)明,其效率一般是XML XMPP的20倍以上,JSON的10倍以上,是一種游戲中普遍采用的IM消息協(xié)議,所以你非常有必要認真讀一下本博文的入門教程,并運行作者的Demo
本Demo以直播聊天室為假設(shè)的Demo,一般消息類型有50到100種左右,簡單起見,這里舉5種消息類型:
enum MsgType : Int {
case join = 0
case leave = 1
case text = 2
case gift = 3
case heartBeat = 8
}
分別表示進入主播室離開主播室 文本消息 禮物圖片或GIF動畫消息及心跳包消息,其他如廣告、系統(tǒng)廣播等不在本Demo演示這是一個代碼區(qū)塊。
(本文要求讀者有一定的socket和 swift3基礎(chǔ),再往下閱讀)
摘要
1、制作協(xié)議格式
syntax = "proto2";
message UserInfo {
required int32 level = 1;
required string name = 2;
required string iconURL = 3;
}
2、制作協(xié)議的對象數(shù)據(jù)
fileprivate lazy var user : UserInfo.Builder = {
let user = UserInfo.Builder()
user.level = Int32(arc4random_uniform(24))
user.name = "targetcloud\(arc4random_uniform(10))"
user.iconUrl = "icon\(arc4random_uniform(2))"
return user
}()
3、剩下的IM核心代碼其實只有兩行,要發(fā)送消息時
let data = (try! user.build()).data()
收到消息時
UserInfo.parseFrom(data : msgData)
詳細使用
1、環(huán)境安裝
找到Github
https://github.com/alexeyxo/protobuf-Swift
在命令行中依次執(zhí)行下面代碼
ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
brew install libtool
brew link libtool
brew install automake
brew install protobuf
brew install protobuf-[swift](http://lib.csdn.net/base/swift)
如下圖
2、導(dǎo)入cocoapod
在podfile中加入
pod 'ProtocolBuffers-Swift'
安裝pod:pod install
如下圖
3、編寫IMMessage.proto
syntax = "proto2";
message UserInfo {
required int32 level = 1;
required string name = 2;
required string iconURL = 3;
}
message ChatMessage {
required UserInfo user = 1;
required string text = 2;
}
message GiftMessage {
required UserInfo user = 1;
required string giftname = 2;
required string giftURL = 3;
required int32 giftcount = 4;
}
4、再去命令行編繹生成
進入到.proto所在文件目錄
cd /Users/targetcloud/Desktop/app/TGClient/TGClient/IMClient
編繹生成XXX.proto.swift
protoc IMMessage.proto --swift_out="./"
5、把生成好的Immessage.proto.swift拖到工程中去(服務(wù)端和客戶端可以同時使用同一個編繹出來的proto.swift)
如果服務(wù)端使用其他語言的話,則需要在第4步這里生成相應(yīng)語言的文件,此例,我們服務(wù)端也是cocoapod集成
6、使用
一般情況下,IM服務(wù)器收到各個客戶端的消息后,是將這些消息再轉(zhuǎn)發(fā)給各個客戶端(聊天室)或某個客戶端(私聊)
心跳包的設(shè)計:服務(wù)端每隔一秒看一下有沒有客戶端的心跳包,而客戶端是每隔10秒發(fā)一個心跳包,服務(wù)端因為連接的客戶端數(shù)量大等壓力原因所以開始子線程,而客戶端就自己一個,所以沒有必要開子線程
服務(wù)器處理各種消息也處理心跳包,針對每個客戶端連接,如果服務(wù)端持續(xù)10秒以上(>10)沒有再得到客戶端的心跳包,那么就從服務(wù)器移除此客戶端的消息監(jiān)聽并停止和銷毀此客戶端監(jiān)聽定時器
removeClientCallback?(self)
self.delegate?.removeClient(self)
timer?.invalidate()
timer = nil
queue = nil
7、代碼如下:
服務(wù)端代碼
(1)
//
// IMServerManager.swift
// TGServer
//
// Created by targetcloud on 2017/3/27.
// Copyright ? 2017年 targetcloud. All rights reserved.
//
import Cocoa
class IMServerManager: NSObject {
fileprivate var tcpServer : TCPServer?
/*
fileprivate lazy var serverSocket : TCPServer = TCPServer(addr:"0.0.0.0",port: 9898)
*/
fileprivate var isServerRunning : Bool = false
fileprivate lazy var clientMgrs : [IMClientManager] = [IMClientManager]()//所有連接上服務(wù)器的客戶端
}
/*
extension IMServerManager {
func startRunning_() {
isServerRunning = true
serverSocket.listen()
DispatchQueue.global().async {
while self.isServerRunning{
let tcpClient = self.serverSocket.accept()
let lengthBytes = (tcpClient?.read(4))!
let data = Data(bytes: lengthBytes, count: 4)
var length : Int = 0
(data as NSData).getBytes(&length, length: 4)
//print(length)
let dataBytes = (tcpClient?.read(length))!
let resultStr = String(bytes: dataBytes, encoding: .utf8)
print("長度 \(length) 內(nèi)容 \(resultStr ?? "")")
}
}
}
}
*/
extension IMServerManager {
func startRunning(_ address : String, _ port : Int) {
tcpServer = TCPServer(addr: address, port: port)
tcpServer?.listen()
isServerRunning = true
DispatchQueue.global().async {
while self.isServerRunning {
if let client = self.tcpServer?.accept() {
DispatchQueue.global().async {
self.handleClient(client)
}
}
}
}
}
func stopRunning() {
isServerRunning = false
}
func handleClient(_ client : TCPClient) {
let clientMgr = IMClientManager(tcpClient: client)
clientMgrs.append(clientMgr)
//clientMgr.delegate = self//MARK:- 代理使用 1 <成為代理>
//IMServerManager(self)->clientMgrs->client->forwardMsgCallback->IMServerManager(self.clientMgrs)
clientMgr.forwardMsgCallback = {[weak self] (client , msgData, isLeave ) in
if isLeave {
if let index = self?.clientMgrs.index(of: client){
client.tcpClient.close()
self?.clientMgrs.remove(at: index)//點客戶端的離開房間會調(diào)用此句,離開消息不會回傳給此客戶端
}
}
for c in (self?.clientMgrs ?? []) {
c.sendMsg(msgData)
}
}
clientMgr.removeClientCallback = {[weak self] (client) in
print(" 客戶端連接數(shù)由 \(self?.clientMgrs.count ?? 0) -> ")
if let index = self?.clientMgrs.index(of: client){
client.tcpClient.close()
self?.clientMgrs.remove(at: index)//斷開后客戶端要做的處理
print(" \(self?.clientMgrs.count ?? 0) ")
}
}
clientMgr.startReadMsg()
}
}
extension IMServerManager : IMClientManagerDelegate {//MARK:- 代理使用 2 <遵守>
//MARK:- 代理使用 3 <實現(xiàn)代理方法>
func removeClient(_ client: IMClientManager) {
guard let index = clientMgrs.index(of: client) else { return }
client.tcpClient.close()
clientMgrs.remove(at: index)
}
func forwardMsg(_ client: IMClientManager, msgData: Data, isLeave: Bool) {
if isLeave {
/*
if let index = clientMgrs.index(of: client) {
clientMgrs.remove(at: index)
}
*/
removeClient(client)
}
for client in clientMgrs {
client.sendMsg(msgData)
}
}
}
(2)
//
// IMClientManager.swift
// TGServer
//
// Created by targetcloud on 2017/3/27.
// Copyright ? 2017年 targetcloud. All rights reserved.
//
import Cocoa
protocol IMClientManagerDelegate : class {//MARK:- 代理 1 <定義協(xié)議>
func removeClient(_ client : IMClientManager)
func forwardMsg(_ client : IMClientManager, msgData : Data, isLeave : Bool)
}
class IMClientManager: NSObject {
weak var delegate : IMClientManagerDelegate?//MARK:- 代理 2 <定義屬性>
//MARK:- 1 也可以使用閉包解決方案來實現(xiàn)服務(wù)器消息轉(zhuǎn)發(fā)
var forwardMsgCallback : ((_ client : IMClientManager,_ data : Data,_ isLeave : Bool)->())?//var forwardMsgCallback : ((IMClientManager,Data,Bool)->())?
var removeClientCallback : ((_ client : IMClientManager) -> ())?
var tcpClient : TCPClient
fileprivate var isClientRunning : Bool = false
fileprivate var beatCounter : Int = 0
fileprivate var queue : DispatchQueue?
fileprivate var timer : Timer?
init(tcpClient : TCPClient) {
self.tcpClient = tcpClient
}
}
extension IMClientManager {
func startReadMsg() {
isClientRunning = true
queue = DispatchQueue.global()
queue?.async {
//timer放在最后面或者包在DispatchQueue.global().async{此處}
self.timer = Timer(fireAt: Date(), interval: 1, target: self, selector: #selector(self.heartBeat), userInfo: nil, repeats: true)
RunLoop.current.add(self.timer!, forMode: RunLoopMode.commonModes)
//timer.fire()
RunLoop.current.run()
}
while self.isClientRunning {
// 1.長度
if let lengthBytes = self.tcpClient.read(4) {
let lengthData = Data(bytes: lengthBytes, count: 4)
var length : Int = 0
(lengthData as NSData).getBytes(&length, length: 4)
//print(length)
// 2.類型
guard let typeBytes = self.tcpClient.read(2) else {
continue
}
var type : Int = 0
let typedata = Data(bytes: typeBytes, count: 2)
(typedata as NSData).getBytes(&type, length: 2)
//print(type)
// 3.消息
guard let dataBytes = self.tcpClient.read(length) else {
continue
}
let msgData = Data(bytes: dataBytes, count: length)
/*
let resultStr = String(bytes: dataBytes, encoding: .utf8)
print(" --- 長度 \(length) 類型 \(type) 內(nèi)容 \(resultStr ?? "") --- ")
*/
//各種Data轉(zhuǎn)proto,反序列化
switch type {
case 0:
let userInfo = try! UserInfo.parseFrom(data : msgData)
print(length,type,userInfo.name, userInfo.level, userInfo.iconUrl)
case 1:
let user = try! UserInfo.parseFrom(data: msgData)
print(length,type,user.name, user.level, user.iconUrl)
case 2:
let chatMsg = try! ChatMessage.parseFrom(data: msgData)
print(length,type,chatMsg.user.name, chatMsg.user.level, chatMsg.user.iconUrl,chatMsg.text)
case 3:
let giftMsg = try! GiftMessage.parseFrom(data: msgData)
print(length,type,giftMsg.user.name, giftMsg.user.level, giftMsg.user.iconUrl,giftMsg.giftUrl, giftMsg.giftname, giftMsg.giftcount)
case 8:
print(" --- 心跳包 長度\(length), 類型 \(type) \(Thread.current)--- ")
queue?.async {
self.beatCounter = 0//有心跳包,計數(shù)器清0
}
//關(guān)鍵代碼
continue//加這一句心跳包不需要轉(zhuǎn)發(fā)給客戶端
default:
print("其他")
}
// 4.消息轉(zhuǎn)發(fā)出去
self.delegate?.forwardMsg(self, msgData: lengthData + typedata + msgData, isLeave: type == 1)//MARK:- 代理 3 <使用代理>
//MARK:- 2 閉包方案
self.forwardMsgCallback?(self,lengthData + typedata + msgData,type == 1)
} else {//當關(guān)閉客戶端時會調(diào)用這里,不是離開房間,是斷線
self.isClientRunning = false
//不離開房間,直接斷線,那么也要移除此客戶端
self.removeClientCallback?(self)
self.delegate?.removeClient(self)
print(" --- 客戶端主動斷開了連接 --- ")
}
}
}
func sendMsg(_ data : Data) {
_ = tcpClient.send(data: data)
}
}
extension IMClientManager {
@objc fileprivate func heartBeat(){
print(" --- server heartBeat \(beatCounter) \(Thread.current)--- ")
beatCounter += 1
if beatCounter>10{
removeClientCallback?(self)
self.delegate?.removeClient(self)
//停止定時器
timer?.invalidate()
timer = nil
queue = nil
}
}
}
(3)
//
// ViewController.swift
// TGServer
//
// Created by targetcloud on 2017/3/27.
// Copyright ? 2017年 targetcloud. All rights reserved.
//
import Cocoa
class ViewController: NSViewController {
@IBOutlet weak var startBtn: NSButton!
@IBOutlet weak var tipLabel: NSTextField!
@IBOutlet weak var stopBtn: NSButton!
fileprivate lazy var mgr : IMServerManager = IMServerManager()
override func viewDidLoad() {
super.viewDidLoad()
stopBtn.isEnabled = false
}
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
@IBAction func startRunning(_ sender: NSButton) {
tipLabel.stringValue = "正在監(jiān)聽客戶端連接請求。。。"
mgr.startRunning("0.0.0.0", 9898)
stopBtn.isEnabled = true
startBtn.isEnabled = false
}
@IBAction func stopRunning(_ sender: NSButton) {
tipLabel.stringValue = "停止監(jiān)聽新的客戶端連接請求。"
mgr.stopRunning()
stopBtn.isEnabled = false
}
}
客戶端代碼
(1)
//
// TGSocketClient.swift
// TGClient
//
// Created by targetcloud on 2017/3/27.
// Copyright ? 2017年 targetcloud. All rights reserved.
//
import UIKit
enum MsgType : Int {
case join = 0
case leave = 1
case text = 2
case gift = 3
case heartBeat = 8
}
protocol TGSocketClientDelegate : class {//傳遞的事件比較多或各種情況分別處理時一般用代理設(shè)計模式
func socket(_ socket : TGSocketClient,joinRoom userInfo :UserInfo)
func socket(_ socket : TGSocketClient,leaveRoom userInfo :UserInfo)
func socket(_ socket : TGSocketClient,sendChatMsg chatMsg :ChatMessage)
func socket(_ socket : TGSocketClient,sendGift giftMsg :GiftMessage)
}
class TGSocketClient {
weak var delegate : TGSocketClientDelegate?
fileprivate var tcpClient : TCPClient
fileprivate var isConnected : Bool = false
fileprivate lazy var user : UserInfo.Builder = {
let user = UserInfo.Builder()
user.level = Int32(arc4random_uniform(24))
user.name = "targetcloud\(arc4random_uniform(10))"
user.iconUrl = "icon\(arc4random_uniform(2))"
return user
}()
init(address:String , prot : Int){
tcpClient = TCPClient(addr: address, port: prot)
}
}
extension TGSocketClient{
func connectServer(_ timeout : Int) -> Bool {
isConnected = tcpClient.connect(timeout: timeout).0
if isConnected {
startReadMsg()
let timer = Timer(fire: Date(), interval: 10, repeats: true, block: { (timer:Timer) in
self.sendHeartBeats()
})
RunLoop.main.add(timer, forMode: .commonModes)
}
return isConnected
}
fileprivate func sendHeartBeats(){
let heartMsg = "this is a heartBeat message "
sendMsg(MsgType.heartBeat.rawValue, msgData: heartMsg.data(using: .utf8)!)
}
func startReadMsg() {//一直讀消息,讀到后由代理處理
DispatchQueue.global().async {//TCPClient.read是阻塞式的,應(yīng)該放在全局中去執(zhí)行
while self.isConnected {
if let lengthMsg = self.tcpClient.read(4) {
let lData = Data(bytes: lengthMsg, count: 4)
var length : Int = 0
(lData as NSData).getBytes(&length, length: 4)
guard let typeMsg = self.tcpClient.read(2) else {
continue
}
var type : Int = 0
let tdata = Data(bytes: typeMsg, count: 2)
(tdata as NSData).getBytes(&type, length: 2)
guard let msg = self.tcpClient.read(length) else {
continue
}
let msgData = Data(bytes: msg, count: length)
DispatchQueue.main.async {//UI處理放main處理(由delegate中轉(zhuǎn))
self.handleMsg(type, msgData: msgData)
}
}else{
print("有情況,服務(wù)器當了")
}
}
}
}
fileprivate func handleMsg(_ type : Int, msgData : Data) {//讀到后由代理處理
switch type {
case MsgType.join.rawValue:
let user = try! UserInfo.parseFrom(data: msgData)//反序列化成對象
print(user.name, user.level, user.iconUrl)
delegate?.socket(self, joinRoom: user)//返回給VC(控制器)顯示等
case MsgType.leave.rawValue:
let user = try! UserInfo.parseFrom(data: msgData)
print(user.name, user.level, user.iconUrl)
delegate?.socket(self, leaveRoom: user)
case MsgType.text.rawValue:
let chatMsg = try! ChatMessage.parseFrom(data: msgData)
print(chatMsg.user.name, chatMsg.user.level, chatMsg.user.iconUrl,chatMsg.text)
delegate?.socket(self, sendChatMsg: chatMsg)
case MsgType.gift.rawValue:
let giftMsg = try! GiftMessage.parseFrom(data: msgData)
print(giftMsg.user.name, giftMsg.user.level, giftMsg.user.iconUrl,giftMsg.giftUrl, giftMsg.giftname, giftMsg.giftcount)
delegate?.socket(self, sendGift: giftMsg)
case MsgType.heartBeat.rawValue:
print("心跳包")
default:
print("其他類型消息")
}
}
}
extension TGSocketClient{//序列化Data發(fā)送
func sendJoinMsg() {
/*
let data = (try! user.build()).data()
*/
guard let user = try? user.build() else {
return
}
let data = user.data()
sendMsg(MsgType.join.rawValue, msgData: data)
}
func sendLeaveMsg() {
let data = (try! user.build()).data()
sendMsg(MsgType.leave.rawValue, msgData: data)
}
func sendTextMsg(_ text : String) {
let chatMsg = ChatMessage.Builder()
chatMsg.text = text
chatMsg.user = try! user.build()
let chatData = (try! chatMsg.build()).data()
sendMsg(MsgType.text.rawValue, msgData: chatData)
}
func sendGiftMsg(_ giftname : String, _ giftURL : String, _ giftcount : Int) {
let giftMsg = GiftMessage.Builder()
giftMsg.giftname = giftname
giftMsg.giftUrl = giftURL
giftMsg.giftcount = Int32(giftcount)
giftMsg.user = try! user.build()
let giftData = (try! giftMsg.build()).data()
sendMsg(MsgType.gift.rawValue, msgData: giftData)
}
fileprivate func sendMsg(_ type : Int , msgData :Data) {//發(fā)送的消息處理后由tcpClient : TCPClient 正式發(fā)送
var length = msgData.count
let lengthData = Data(bytes: &length, count: 4)
var type = type
let typeData = Data(bytes: &type, count: 2)
tcpClient.send(data: lengthData + typeData + msgData)
}
}
(2)IMMessage.proto
syntax = "proto2";
message UserInfo {
required int32 level = 1;
required string name = 2;
required string iconURL = 3;
}
message ChatMessage {
required UserInfo user = 1;
required string text = 2;
}
message GiftMessage {
required UserInfo user = 1;
required string giftname = 2;
required string giftURL = 3;
required int32 giftcount = 4;
}
(3)
//
// ViewController.swift
// TGClient
//
// Created by targetcloud on 2017/3/27.
// Copyright ? 2017年 targetcloud. All rights reserved.
//
import UIKit
class ViewController: UIViewController {
fileprivate lazy var clientSocket : TGSocketClient = TGSocketClient(address: "192.168.1.103", prot: 9898)
override func viewDidLoad() {
super.viewDidLoad()
if clientSocket.connectServer(5){
print("連接到服務(wù)器成功")
//clientSocket.startReadMsg() 此句放 connectServer 里面,連接成功就開始讀
clientSocket.delegate = self
}
}
@IBAction func sendMsg(_ sender: UIButton) {
switch sender.tag {
case MsgType.join.rawValue:
clientSocket.sendJoinMsg()
case MsgType.leave.rawValue:
clientSocket.sendLeaveMsg()
case MsgType.text.rawValue:
clientSocket.sendTextMsg("你好, targetcloud")
case MsgType.gift.rawValue:
clientSocket.sendGiftMsg("游艇", "http://blog.csdn.net/callzjy/article/details/66596736", 99)
default:
print("未識別消息")
}
}
}
extension ViewController : TGSocketClientDelegate {
func socket(_ socket : TGSocketClient,joinRoom userInfo :UserInfo){
//UI 請自由發(fā)揮
}
func socket(_ socket : TGSocketClient,leaveRoom userInfo :UserInfo){
//UI
}
func socket(_ socket : TGSocketClient,sendChatMsg chatMsg :ChatMessage){
//UI
}
func socket(_ socket : TGSocketClient,sendGift giftMsg :GiftMessage){
//UI
}
}
注意點:
1、運行作者代碼前請修改IP
客戶端的ViewController.swift中的
fileprivate lazy var clientSocket : TGSocketClient = TGSocketClient(address: "192.168.1.103", prot: 9898)
先運行服務(wù)端,點界面的啟動,然后開啟客戶端
2、TCPClient.read是阻塞式的,應(yīng)該放在全局線程中去執(zhí)行
3、發(fā)送時都是序列化Data發(fā)送,讀到消息都是反序列成協(xié)議對象再處理,具體場景:某個客戶端送主播一游艇,客戶端把禮物消息序列化發(fā)送給服務(wù)器,服務(wù)器轉(zhuǎn)發(fā)給同一主播室的其他各個客戶端(讓大家看到某人送給主播一游艇),
** 各個客戶端收到消息后,則反序列化這一消息,并解析出其中的各個成員對象及屬性后呈現(xiàn)到界面上(某人的頭像,游艇的圖片,GIF動畫等)**
4、代理換成閉包形式設(shè)計時,注意閉包中的self的循環(huán)引用
clientMgr.forwardMsgCallback = {[weak self] (client , msgData, isLeave ) in
if isLeave {
if let index = self?.clientMgrs.index(of: client){
client.tcpClient.close()
self?.clientMgrs.remove(at: index)//點客戶端的離開房間會調(diào)用此句,離開消息不會回傳給此客戶端
}
}
for c in (self?.clientMgrs ?? []) {
c.sendMsg(msgData)
}
}
clientMgr.removeClientCallback = {[weak self] (client) in
print(" 客戶端連接數(shù)由 \(self?.clientMgrs.count ?? 0) -> ")
if let index = self?.clientMgrs.index(of: client){
client.tcpClient.close()
self?.clientMgrs.remove(at: index)//斷開后客戶端要做的處理
print(" \(self?.clientMgrs.count ?? 0) ")
}
}
5、序列化時 let data =(try! user.build()).data()這樣的強行try 可以換成
guard let user = try? user.build() else {
return
}
let data = user.data()
如果為了簡便也可以使用try!但確保你對你的代碼有自信,新手建議用guard守護
6、代碼執(zhí)行過程
(1)
點擊客戶端的離開房間會調(diào)用閉包中的
self?.clientMgrs.remove(at: index)
clientMgr.forwardMsgCallback = {[weak self](client , msgData, isLeave) in
if isLeave {
if let index = self?.clientMgrs.index(of: client){
self?.clientMgrs.remove**(**at: index**)**//**點客戶端的離開房間會調(diào)用此句
}
}
for c in (self?.clientMgrs?? []) {
c.sendMsg(msgData)
}
}
但是服務(wù)端收到客戶端的心跳包仍然繼續(xù)執(zhí)行,服務(wù)器會打印
** --- 心跳包****長度28,****類型 8 --- **
但不會回發(fā)消息(心跳包)給客戶端
(2)
當人為繼線(下線、關(guān)閉客戶端)時,會調(diào)用startReadMsg中的 isClientRunning = false
else {//當關(guān)閉客戶端時會調(diào)用這里,不是離開房間,是斷線
** isClientRunning = false**
delegate?.removeClient(self)
服務(wù)器也收就收不到此客戶端的心跳包,也不會打印心跳包
提醒:若在服務(wù)端startReadMsg中的心跳包處理處加上不轉(zhuǎn)發(fā)代碼 continue則客戶不顯示心跳包三個字,[測試]期間可以去掉下面的關(guān)鍵代碼
**//關(guān)鍵代碼**
**continue//加這一句心跳包不需要轉(zhuǎn)發(fā)給客戶端**
7、UI處理放main處理
**DispatchQueue.main.async** {
self.handleMsg(type, msgData: msgData)
}
8、定時器一般要放在其他代碼的后面
如服務(wù)端的心跳包
let timer = Timer(fireAt: Date(), interval: 2.0, target: self, selector: #selector(heartBeat), userInfo: nil, repeats: true)
RunLoop.current.add(timer, forMode: RunLoopMode.commonModes)
//timer.fire()
RunLoop.current.run()
放在IMClientManager的startReadMsg的最后面
或者把上面一段代碼包在子線程中去,這樣可以放在startReadMsg的開始處
**DispatchQueue.global().async {**
let timer = Timer(fireAt: Date(), interval: 2.0, target: self, selector: #selector(heartBeat), userInfo: nil, repeats: true)
RunLoop.current.add(timer, forMode: RunLoopMode.commonModes)
//timer.fire()
RunLoop.current.run()
}
10、服務(wù)端在移除某個客戶端監(jiān)聽時
self?.clientMgrs.remove(at: index)
一般需要同時關(guān)閉TCPClient,代碼如下
client.**tcpClient.close()**
11、總結(jié)
客戶端做的事:
(1)建立與服務(wù)器的連接
(2)發(fā)送消息(data()序列化)
(3)收到消息回顯到UI上,進行處理(反序列消息成對象及屬性,呈現(xiàn)到客戶端的界面上)
(4)建立心跳包
服務(wù)端做的事:
轉(zhuǎn)發(fā)所有客戶端的消息,把心跳停止的客戶端移開