介紹
Protocol Buffers (ProtocolBuffer/ protobuf )是Google公司開發的一種數據描述語言,類似於XML能夠將結構化數據序列化,比起XML它更簡單,文件更小,傳輸解析更快,原生支持java、c++、python,如果要在 iOS 上使用,可以直接使用 C++.但是編譯過程很麻煩,因此這裏使用的是第三方的庫.
Swift : https://github.com/alexeyxo/protobuf-swift
ObjC : https://github.com/aerofs/protobuf-objc
優勢
-
直接傳遞C/C++語言中一字節對齊的結構體數據,只要結構體的聲明爲定長格式,那麼該方式對於C/C++程序而言就非常方便了,僅需將接收到的數據按照結構體類型強行轉換即可。事實上對於變長結構體也不會非常麻煩。在發送數據時,也只需定義一個結構體變量並設置各個成員變量的值之後,再以char*的方式將該二進制數據發送到遠端。反之,該方式對於Java開發者而言就會非常繁瑣,首先需要將接收到的數據存於ByteBuffer之中,再根據約定的字節序逐個讀取每個字段,並將讀取後的值再賦值給另外一個值對象中的域變量,以便於程序中其他代碼邏輯的編寫。對於該類型程序而言,聯調的基準是必須客戶端和服務器雙方均完成了消息報文構建程序的編寫後才能展開,而該設計方式將會直接導致Java程序開發的進度過慢。即便是Debug階段,也會經常遇到Java程序中出現各種域字段拼接的小錯誤。
-
使用SOAP協議(WebService)作爲消息報文的格式載體,由該方式生成的報文是基於文本格式的,同時還存在大量的XML描述信息,因此將會大大增加網絡IO的負擔。又由於XML解析的複雜性,這也會大幅降低報文解析的性能。總之,使用該設計方式將會使系統的整體運行性能明顯下降。
ProtocolBuffer環境安裝
環境安裝
ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
brew install automake
brew install libtool
brew install protobuf
客戶端集成(通過cocoapods)
use_frameworks!
pod 'ProtocolBuffers-Swift'
服務器集成 因爲服務器使用Mac編寫,不能直接使用cocoapods集成 因爲需要將工程編譯爲靜態庫來集成 到Git中下載整個庫 執行腳本: ./scripts/build.sh 添加: ProtocolBuffers.xcodeproj到項目中 路徑protobuf-swift-master/plugin/ProtocolBuffers
ProtocolBuffer的使用
創建.proto文件
- 在項目中, 創建一個(或多個).proto文件
- 之後會通過該文件, 自動幫我們生成需要的源文件(比如C++生成.cpp源文件, 比如java生成.java源文件, Swift就生成.swift源文件)
源碼規範
syntax = "proto2";
message Person {
required int64 id = 1;
required string name = 2;
optional string email = 3;
}
具體說明
- syntax = "proto2"; 爲定義使用的版本號, 目前常用版本proto2/proto3
- message是消息定義的關鍵字,等同於C++/Swift中的struct/class,或是Java中的class
- Person爲消息的名字,等同於結構體名或類名
- required前綴表示該字段爲必要字段,既在序列化和反序列化之前該字段必須已經被賦值
- optional前綴表示該字段爲可選字段, 既在序列化和反序列化時可以沒有被賦值
- repeated通常被用在數組字段中
- int64和string分別表示整型和字符串型的消息字段
- id和name和email分別表示消息字段名,等同於Swift或是C++中的成員變量名
- 標籤數字1和2則表示不同的字段在序列化後的二進制數據中的佈局位置, 需要注意的是該值在同一message中不能重複
定義有定義結構體用於生成Model
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;
}
代碼編寫完成後, 生成對應語言代碼 protoc Immessage.proto --swift_out="./"
ysocket
socket在即時通訊解決方案中是比較常見的,由於Swift與C混編並不那麼友好,我們用一個封裝好的庫ysocket,當然還有SwiftSocket
創建一個服務類
import Cocoa
class ServerManager: NSObject {
fileprivate lazy var serverSocket : TCPServer = TCPServer(addr: "0.0.0.0", port: 8787)//或者127.0.0.1
fileprivate var isServerRunning : Bool = false
fileprivate lazy var clientMrgs : [ClientManager] = [ClientManager]()
}
extension ServerManager {
func startRunning() {
// 1.開啓監聽
serverSocket.listen()
isServerRunning = true
// 2.開始接受客戶端
DispatchQueue.global().async {
while self.isServerRunning {
if let client = self.serverSocket.accept() {
DispatchQueue.global().async {
self.handlerClient(client)
}
}
}
}
}
func stopRunning() {
isServerRunning = false
}
}
extension ServerManager {
fileprivate func handlerClient(_ client : TCPClient) {
// 1.用一個ClientManager管理TCPClient
let mgr = ClientManager(tcpClient: client)
mgr.delegate = self
// 2.保存客戶端
clientMrgs.append(mgr)
// 3.用client開始接受消息
mgr.startReadMsg()
}
}
extension ServerManager : ClientManagerDelegate {
func sendMsgToClient(_ data: Data) {
for mgr in clientMrgs {
mgr.tcpClient.send(data: data)
}
}
func removeClient(_ client: ClientManager) {
guard let index = clientMrgs.index(of: client) else { return }
clientMrgs.remove(at: index)
}
}
客戶端數據處理類
import Cocoa
protocol ClientManagerDelegate : class {
func sendMsgToClient(_ data : Data)
func removeClient(_ client : ClientManager)
}
class ClientManager: NSObject {
var tcpClient : TCPClient
weak var delegate : ClientManagerDelegate?
fileprivate var isClientConnected : Bool = false
fileprivate var heartTimeCount : Int = 0
init(tcpClient : TCPClient) {
self.tcpClient = tcpClient
}
}
extension ClientManager {
func startReadMsg() {
isClientConnected = true
let timer = Timer(fireAt: Date(), interval: 1, target: self, selector: #selector(checkHeartBeat), userInfo: nil, repeats: true)
RunLoop.current.add(timer, forMode: .commonModes)
timer.fire()
while isClientConnected {
if let lMsg = tcpClient.read(4) {
// 1.讀取長度的data
let headData = Data(bytes: lMsg, count: 4)
var length : Int = 0
(headData as NSData).getBytes(&length, length: 4)
// 2.讀取類型
guard let typeMsg = tcpClient.read(2) else {
return
}
let typeData = Data(bytes: typeMsg, count: 2)
var type : Int = 0
(typeData as NSData).getBytes(&type, length: 2)
// 2.根據長度, 讀取真實消息
guard let msg = tcpClient.read(length) else {
return
}
let data = Data(bytes: msg, count: length)
if type == 1 {
tcpClient.close()
delegate?.removeClient(self)
} else if type == 100 {
heartTimeCount = 0
print("心跳包")
continue
}
let totalData = headData + typeData + data
delegate?.sendMsgToClient(totalData)
} else {
self.removeClient()
}
}
}
@objc fileprivate func checkHeartBeat() {
heartTimeCount += 1
if heartTimeCount >= 10 {
self.removeClient()
}
}
private func removeClient() {
delegate?.removeClient(self)
isClientConnected = false
print("客戶端斷開了連接")
tcpClient.close()
}
}
客服端
由於涉及隱私此處貼出關鍵代碼 SocketTool
fileprivate var tcp : TCPClient
//用戶model
fileprivate var userInfo : UserInfo.Builder = {
let userInfo = UserInfo.Builder()
userInfo.name = "Royce\(arc4random_uniform(10))"
userInfo.level = 20
userInfo.iconUrl = "icon\(arc4random_uniform(5))"
return userInfo
}()
init(addr : String, port : Int) {
tcp = TCPClient(addr: addr, port: port)
}
func connectServer() -> Bool {
return tcp.connect(timeout: 5).0
}
//開始讀取消息
func startReadMsg(){
DispatchQueue.global().async {
while true {
//檢查是否有數據
guard let MsgLenght = self.tcp.read(4) else { continue }
//獲取headerdata長度
let headData = Data(bytes: MsgLenght, count: 4)
var length : Int = 0
(headData as NSData).getBytes(&length, length: 4)
//讀取類型
guard let typeMsg = self.tcp.read(2) else {
return
}
let typeData = Data(bytes: typeMsg, count: 2)
var type : Int = 0
(typeData as NSData).getBytes(&type, length: 2)
// 2.根據長度, 讀取真實消息
guard let msg = self.tcp.read(length) else {
return
}
let data = Data(bytes: msg, count: length)
// 3.處理消息
DispatchQueue.main.async {
self.handleMsg(type: type, data: data)
}
}
}
}
//心跳包
func sendHeartBeat() {
// 1.獲取心跳包中的數據
let heartString = "I am is heart beat;"
let heartData = heartString.data(using: .utf8)!
// 2.發送數據
sendMsg(data: heartData, type: 100)
}
//發送普通消息
func sendMsg(data : Data, type : Int) {
// 1.將消息長度, 寫入到data
var length = data.count
let headerData = Data(bytes: &length, count: 4)
// 2.消息類型
var tempType = type
let typeData = Data(bytes: &tempType, count: 2)
// 3.發送消息
let totalData = headerData + typeData + data
tcp.send(data: totalData)
}
使用
fileprivate lazy var socket : SocketTool = SocketTool(addr: "192.168.10.159", port: 8787)
fileprivate var heartBeatTimer : Timer?
func startContectSever() -> () {
//鏈接成功
if socket.connectServer() {
//開始讀取消息
socket.startReadMsg()
//發送心跳包
addHeartBeatTimer()
}
}
fileprivate func addHeartBeatTimer() {
heartBeatTimer = Timer(fireAt: Date(), interval: 9, target: self, selector: #selector(sendHeartBeat), userInfo: nil, repeats: true)
RunLoop.main.add(heartBeatTimer!, forMode: .commonModes)
}
@objc fileprivate func sendHeartBeat() {
最終效果