Protobuf全称Protocol Buffers,简称GPB、PB,是QQ等IM采用的协议,比XML、XMPP(环信)、JSON、结构体等所有传输效果都高的一种传输协议,由谷歌发明,其效率一般是XML XMPP的20倍以上,JSON的10倍以上,是一种游戏中普遍采用的IM消息协议,所以你非常有必要认真读一下本博文的入门教程,并运行作者的Demo
本Demo以直播聊天室为假设的Demo,一般消息类型有50到100种左右,简单起见,这里举5种消息类型:
enum MsgType : Int {
case join = 0
case leave = 1
case text = 2
case gift = 3
case heartBeat = 8
}
分别表示进入主播室离开主播室 文本消息 礼物图片或GIF动画消息及心跳包消息,其他如广告、系统广播等不在本Demo演示这是一个代码区块。
(本文要求读者有一定的socket和 swift3基础,再往下阅读)
摘要
1、制作协议格式
syntax = "proto2";
message UserInfo {
required int32 level = 1;
required string name = 2;
required string iconURL = 3;
}
2、制作协议的对象数据
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核心代码其实只有两行,要发送消息时
let data = (try! user.build()).data()
收到消息时
UserInfo.parseFrom(data : msgData)
详细使用
1、环境安装
找到Github
https://github.com/alexeyxo/protobuf-Swift
在命令行中依次执行下面代码
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、导入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拖到工程中去(服务端和客户端可以同时使用同一个编绎出来的proto.swift)
如果服务端使用其他语言的话,则需要在第4步这里生成相应语言的文件,此例,我们服务端也是cocoapod集成
6、使用
一般情况下,IM服务器收到各个客户端的消息后,是将这些消息再转发给各个客户端(聊天室)或某个客户端(私聊)
心跳包的设计:服务端每隔一秒看一下有没有客户端的心跳包,而客户端是每隔10秒发一个心跳包,服务端因为连接的客户端数量大等压力原因所以开始子线程,而客户端就自己一个,所以没有必要开子线程
服务器处理各种消息也处理心跳包,针对每个客户端连接,如果服务端持续10秒以上(>10)没有再得到客户端的心跳包,那么就从服务器移除此客户端的消息监听并停止和销毁此客户端监听定时器
removeClientCallback?(self)
self.delegate?.removeClient(self)
timer?.invalidate()
timer = nil
queue = nil
7、代码如下:
服务端代码
(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]()//所有连接上服务器的客户端
}
/*
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) 内容 \(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)//点客户端的离开房间会调用此句,离开消息不会回传给此客户端
}
}
for c in (self?.clientMgrs ?? []) {
c.sendMsg(msgData)
}
}
clientMgr.removeClientCallback = {[weak self] (client) in
print(" 客户端连接数由 \(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 <实现代理方法>
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 <定义协议>
func removeClient(_ client : IMClientManager)
func forwardMsg(_ client : IMClientManager, msgData : Data, isLeave : Bool)
}
class IMClientManager: NSObject {
weak var delegate : IMClientManagerDelegate?//MARK:- 代理 2 <定义属性>
//MARK:- 1 也可以使用闭包解决方案来实现服务器消息转发
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) 内容 \(resultStr ?? "") --- ")
*/
//各种Data转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//有心跳包,计数器清0
}
//关键代码
continue//加这一句心跳包不需要转发给客户端
default:
print("其他")
}
// 4.消息转发出去
self.delegate?.forwardMsg(self, msgData: lengthData + typedata + msgData, isLeave: type == 1)//MARK:- 代理 3 <使用代理>
//MARK:- 2 闭包方案
self.forwardMsgCallback?(self,lengthData + typedata + msgData,type == 1)
} else {//当关闭客户端时会调用这里,不是离开房间,是断线
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 = "正在监听客户端连接请求。。。"
mgr.startRunning("0.0.0.0", 9898)
stopBtn.isEnabled = true
startBtn.isEnabled = false
}
@IBAction func stopRunning(_ sender: NSButton) {
tipLabel.stringValue = "停止监听新的客户端连接请求。"
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 {//传递的事件比较多或各种情况分别处理时一般用代理设计模式
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是阻塞式的,应该放在全局中去执行
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中转)
self.handleMsg(type, msgData: msgData)
}
}else{
print("有情况,服务器当了")
}
}
}
}
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发送
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) {//发送的消息处理后由tcpClient : TCPClient 正式发送
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("连接到服务器成功")
//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 请自由发挥
}
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)
先运行服务端,点界面的启动,然后开启客户端
2、TCPClient.read是阻塞式的,应该放在全局线程中去执行
3、发送时都是序列化Data发送,读到消息都是反序列成协议对象再处理,具体场景:某个客户端送主播一游艇,客户端把礼物消息序列化发送给服务器,服务器转发给同一主播室的其他各个客户端(让大家看到某人送给主播一游艇),
** 各个客户端收到消息后,则反序列化这一消息,并解析出其中的各个成员对象及属性后呈现到界面上(某人的头像,游艇的图片,GIF动画等)**
4、代理换成闭包形式设计时,注意闭包中的self的循环引用
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)//点客户端的离开房间会调用此句,离开消息不会回传给此客户端
}
}
for c in (self?.clientMgrs ?? []) {
c.sendMsg(msgData)
}
}
clientMgr.removeClientCallback = {[weak self] (client) in
print(" 客户端连接数由 \(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、代码执行过程
(1)
点击客户端的离开房间会调用闭包中的
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**)**//**点客户端的离开房间会调用此句
}
}
for c in (self?.clientMgrs?? []) {
c.sendMsg(msgData)
}
}
但是服务端收到客户端的心跳包仍然继续执行,服务器会打印
** --- 心跳包****长度28,****类型 8 --- **
但不会回发消息(心跳包)给客户端
(2)
当人为继线(下线、关闭客户端)时,会调用startReadMsg中的 isClientRunning = false
else {//当关闭客户端时会调用这里,不是离开房间,是断线
** isClientRunning = false**
delegate?.removeClient(self)
服务器也收就收不到此客户端的心跳包,也不会打印心跳包
提醒:若在服务端startReadMsg中的心跳包处理处加上不转发代码 continue则客户不显示心跳包三个字,[测试]期间可以去掉下面的关键代码
**//关键代码**
**continue//加这一句心跳包不需要转发给客户端**
7、UI处理放main处理
**DispatchQueue.main.async** {
self.handleMsg(type, msgData: msgData)
}
8、定时器一般要放在其他代码的后面
如服务端的心跳包
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、服务端在移除某个客户端监听时
self?.clientMgrs.remove(at: index)
一般需要同时关闭TCPClient,代码如下
client.**tcpClient.close()**
11、总结
客户端做的事:
(1)建立与服务器的连接
(2)发送消息(data()序列化)
(3)收到消息回显到UI上,进行处理(反序列消息成对象及属性,呈现到客户端的界面上)
(4)建立心跳包
服务端做的事:
转发所有客户端的消息,把心跳停止的客户端移开