protocolBuffer + ysocket 实现即使通讯
- ysocket : 在Swift中使用socket编程使用的库,基于BSD-socket(一个C语言写的)
- protocolBuffer(PB) : 一种数据交换方式,在即时通讯传输数据的过程当中使用JSON因为反序列化和序列化比较繁琐,使用这种方式可以直接由data->对象,对象->data,并且消息大小只有json的十分之一.并且通过proto文件可以跨平台生成其它多种类型对象文件.
-
服务端
class ServerManager { // 传入IP和端口号,控制服务开启接受的类 fileprivate lazy var serverSocket = TCPServer(addr: "0.0.0.0", port: 7878) // 一个服务对应多个客户端,要有一个存储客户端的数组 fileprivate lazy var clientMrs = [ClientManager]() // 服务器是否开启 fileprivate var isServerRunning = false }
开启服务的函数
func startRunning() {
serverSocket.listen()
isServerRunning = true
DispatchQueue.global().async {
while self.isServerRunning {
if let client = self.serverSocket.accept() {
DispatchQueue.global().async {
let clientManager = ClientManager(client: client)
self.clientMrs.append(clientManager)
}
}
}
}
}
1.serverSocket.listen()
2.self.serverSocket.accept() 此方法要放到一个死循环里,时刻接收连接到服务器的客户端TCPClient.并且加入到数组中..注意这里一定要在gloabl并发队列里,因为还要继续接收其它的客户端的接入.
3.拿到clint.调用client.read(expectlen: Int)
按照字节长度获取信息..第一次获取到的是[UInt8],然后转换为Data,再然后是String.
-
客户端
在客户端要创建一个socket类来管理发送与接收消息的方法
class HYSocket { fileprivate var tcpClient : TCPClient init(addr : String, port : Int) { tcpClient = TCPClient(addr: addr, port: port) } }
TCPClient有一个方法 tcpClient.connect(timeout: 5)
返回一个布尔值,如果端口和IP存在,即可连接成功.
发送消息可以通过string也可以data
tcpClient.send(data:Data)
在客户端调用这个方法的时候,服务端的accept中就会监听到发送消息的client,read所携带的信息.
这样最简单的一个字符串就可以做到了.但是实际项目中的情况要复杂很多.主要是要传输的数据复杂,并且在服务器接受到消息之后还要再发送给客户端.一个用户加入了直播间,其它用户也要看到..在用户加入直播间时候还要告诉服务器是哪一个用户,用户的姓名,等级,头像等等.发礼物时候要告诉服务器礼物的名称,个数.
这些信息用一个json转成data当然可以进行传输,但是序列化与反序列化以及再用json转对象的过程复杂而且信息量大.
ProtocolBuffer使用
接下来这件事不一定是由前端开发人员自己来做的.最终我们需要一个关于IM中的对象以及对应属性的swift文件.
新建一个后缀名为.proto的文件,protocolBuffer有其规范来声明数据类型,方便转换传输,此次直播项目中的proto文件中是这样的.
syntax = "proto2";
message UserInfo {
required int64 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 person.proto --swift_out="./"
生成一个.swift文件. 加入到工程中..打开一看1000多行代码.
接下来在其它类中我们可以就有了UserInfo.ChatMessage以及GiftMessage这三个类并且可以对其属性赋值了.
还有一个点就是: 服务端在读取socket传来的消息的时候需要指定读取的字节长度,读少了会读取不全,读多了会产生多余消耗.所以在客户端发送消息的时候回传三个东西: "消息长度" + "消息类型(文本/礼物/进入房间)" + "消息对象"
在服务端读取的时候也要按照这个规定进行有序读取,消息长度和类型一般固定字节数,这与客户端发送消息时候的字节数要统一.
发送消息(客户端):
封装一个统一的发送函数
private func sendMsgToServer(data:Data, type: Int) {
// 消息长度
var length = data.count
let lengthData = Data(bytes: &length, count: 4)
//消息类型
var type = type
let typeData = Data(bytes: &type, count: 2)
//总消息
let totalData = lengthData + typeData + data
client.send(data: totalData)
}
接下来是发送文本消息的函数
func sendTextMsg(text: String) {
let chatMsg = ChatMessage.Builder()
chatMsg.user = (try! userinfo.build())
chatMsg.text = text
let msgData = (try! chatMsg.build()).data()
sendMsgToServer(data: msgData, type: 2)
}
步骤:
- 声明一个在Proto文件中声明的ChatMsg消息对象,对其赋值(requeired声明的属性必须要赋值.不然转data会失败. 2.protocolBuffer提供给了我们直接转化为Data的方法,再调用client.send,发送到服务器就可以了.
接收消息(客户端):
服务器会将某一用户进入房间的消息全部返回给这个房间里的客户端.
所以在客户端Socket类中也要有一个方法时刻接收服务器发来的消息
func startReadMsg() {
DispatchQueue.global().async {
while true {
guard let lMsg = self.tcpClient.read(4) else {
continue
}
// 1.读取长度的data
let headData = Data(bytes: lMsg, count: 4)
var length : Int = 0
(headData as NSData).getBytes(&length, length: 4)
// 2.读取类型
guard let typeMsg = self.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 = self.tcpClient.read(length) else {
return
}
let data = Data(bytes: msg, count: length)
// 3.处理消息
DispatchQueue.main.async {
self.handleMsg(type: type, data: data)
}
}
}
}
接受到的消息我们需要根据传过来的type进行区分是哪种对象.例如刚才传的ChatMessage的类型是2.那么判断在type==2的时候直接获取chatMessage对象.
let chatMsg = try! ChatMessage.parseFrom(data: data)
放在proto中的对象会有这个反序列的函数..
由此: 发送消息和接收消息都通过protocolBuffer方便的完成了.从data->对象,对象->data,接下来根据具体需求做一些对应的封装,将消息用代理发送到控制器里就好了
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'
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中不能重复
定义有枚举类型Protocol Buffer消息
enum UserStatus {
OFFLINE = 0; //表示处于离线状态的用户
ONLINE = 1; //表示处于在线状态的用户
}
message UserInfo {
required int64 acctID = 1;
required string name = 2;
required UserStatus status = 3;
}
定义有类型嵌套
enum UserStatus {
OFFLINE = 0;
ONLINE = 1;
}
message UserInfo {
required int64 acctID = 1;
required string name = 2;
required UserStatus status = 3;
}
message LogonRespMessage {
required LoginResult logonResult = 1;
required UserInfo userInfo = 2;
}
代码编写完成后, 生成对应语言代码
protoc person.proto --swift_out="./"
服务器集成
因为服务器使用Mac编写,不能直接使用cocoapods集成
因为需要将工程编译为静态库来集成
到Git中下载整个库
执行脚本: ./scripts/build.sh 添加: ./src/ProtocolBuffers/ProtocolBuffers.xcodeproj到项目中