介绍
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() {
最终效果