TCP/UDP
进行Socket编程, 常见使用的协议UDP/TCP
TCP:传输控制协议 。是专门设计用于在不可靠的因特网上提供可靠的,端到端的字节流通信的协议。它是一种面向连接的协议。TCP连接是字节流而非报文流。
UDP:用户数据报协议 。不需要建立连接,不可靠。
长连接和短连接
长连接,指在一个TCP连接上可以连续发送多个数据包,在TCP连接保持期间,如果没有数据包发送,需要双方发检测包以维持此连接,一般需要自己做在线维持。
短连接是指通信双方有数据交互时,就建立一个TCP连接,数据发送完成后,则断开此TCP连接。
通常的短连接操作步骤是:
连接→数据传输→关闭连接或在没有数据传输时直接关闭;
而长连接通常就是:
连接→数据传输→保持连接(心跳)→数据传输→保持连接(心跳)→……→关闭连接;
Socket框架
- BSD Socket
BSD Socket 是UNIX系统中通用的网络接口,它不仅支持各种不同的网络类型,而且也是一种内部进程之间的通信机制。在我们iOS中也可以使用,但是它所有的函数都是基于C语言的,所有在实际的项目开发中,我们都是使用封装好的。 - CFSocket
CFSocket是苹果官方提供给我们进行Socket编程,里面还是有很多C语言的东西,大家有时间可以研究一下,在实际项目中,通常我们不会自己直接使用。 - AsyncSocket
AsyncSocket是一个开源的库,用来进行iOS的Socket编程就非常方便, 但是目前只有OC版本, 并且长时间没有更新 - ysocket
目前使用Swift进行Socket编程时,常用的一个库
消息数据类型
- 直接传递C/C++语言中一字节对齐的结构体数据,只要结构体的声明为定长格式,那么该方式对于C/C++程序而言就非常方便了, 但是对于java这种不常用结构体的语言, 处理起来就相当麻烦
- 使用SOAP协议(WebService)作为消息报文的格式载体,由该方式生成的报文是基于文本格式的,同时还存在大量的XML描述信息,因此将会大大增加网络IO的负担。又由于XML解析的复杂性,这也会大幅降低报文解析的性能。总之,使用该设计方式将会使系统的整体运行性能明显下降。
- Json交换格式, 目前比较理想的一种通信格式, 也是在Http请求数据时, 最常见的用法(Demo程序)
- ProtocolBuffer(也称PB/GPB): google 的一种数据交换的格式, 可以实现跨平台, 方便的序列化&反序列化, 并且数据量相对json
ProtocolBuffer
1、简介
- 跨平台
ProtoBuf支持多平台和语言, 包括C++/Java/Python等等 - 序列化&反序列号
ProtoBuf支持直接将对象序列化成Data, 也支持直接将Data序列化为对象类型 - 消息大小
一条消息数据,用protobuf序列化后的大小是json的10分之一,xml格式的20分之一,是二进制序列化的10分之一
2、安装Protobuf编译器
ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
brew install automake
brew install libtool
brew install protobuf
git clone [email protected]:alexeyxo/protobuf-swift.git
// cd protobuf-swift,反正一定要到scripts的父文件夹位置
./scripts/build.sh
Add ./src/ProtocolBuffers/ProtocolBuffers.xcodeproj
in your project.
或使用Cocoapod:pod 'ProtocolBuffers-Swift'
注意:Mac项目貌似不能直接使用cocoapods集成
详情请看:https://github.com/alexeyxo/protobuf-swift
3、编码xxx.proto文件,并生成对应的swift代码
protoc xxx.proto --swift_out="./"
如果前面在终端没有成功执行./scripts/build.sh
,该句将报错。
实际开发中的异同
- 心跳包
心跳包是什么呢?我们首先要弄清楚,我们每一个客户端连接到服务器之后,服务器会怎么来处理我们的连接。
我们知道每一个客户端在连接到服务器的时候,都会有一个单独的线程来处理。比如我们前面给大家看的那个房间的例子,一个房间2000人,那就要有2000个连接保持。这对服务器的负荷是非常大的,但是如果说客户端断开了,服务器还保持着这个线程,就非常的耗资源。 - 数据我们和服务器沟通的时候究竟发送什么数据?
在实际开发中,我们不可能仅仅简单的给服务器发一个字符串,服务器给我们回一个字符串,这是不现实的。
下面将使用ysocket、protocolbuffer实现一个简单的IM项目。
基本环境搭建
- 创建一个iOS工程作为客户端-Client,创建一个mac工程作为服务器-Server
- 集成SwiftSocket、ProtocolBuffers-Swift
- 编写IM.proto,并生成IM.proto.swift
IM编程
- 客户端
创建clientSocket;
连接服务器;
读取服务器可能发来的信息,即不断读取。而且读取信息是阻塞式的,两方面考虑都需要另开线程。代码之后再补充;
发送信息给服务器。代码之后再补充
import SwiftSocket
class ViewController: UIViewController {
var client: TCPClient = TCPClient(address: "127.0.0.1", port: 7878)
var isConnected: Bool = false
override func viewDidLoad() {
super.viewDidLoad()
switch client.connect(timeout: 10) {
case .success:
isConnected = true
self.readServerMsg()
case .failure(let error):
print("连接服务器失败:\(error.localizedDescription)")
}
}
// 读取服务器发送来的信息
func readServerMsg() {
DispatchQueue.global().async {
while self.isConnected {
}
}
}
// 发送信息给服务器
override func touchesBegan(_ touches: Set, with event: UIEvent?) {
}
}
- 服务器
创建serverSocket;
创建clientSocket数组。数组的元素与客户端的clientSocket一一对应,并相互交流;
serverSocket开启监听;
不断接收客户端的clientSocket,并生成对应的clientSocket。需要另开线程;
clientSocket不断读取客户端的clientSocket发来的信息。需要另开线程。代码之后再补充
import SwiftSocket
class ViewController: NSViewController {
var server: TCPServer = TCPServer(address: "127.0.0.1", port: 7878)
var clients: [TCPClient] = [TCPClient]()
var serverRunning: Bool = false
override func viewDidLoad() {
super.viewDidLoad()
switch server.listen() {
case .success:
serverRunning = true
print("服务器已启动...")
DispatchQueue.global().async {
while self.serverRunning {
if let client = self.server.accept() {
print("服务器接收到客户端: \(client.address)")
self.readClientMsg(client)
}
}
}
case .failure(let error):
print("服务器监听失败:\(error.localizedDescription)")
}
}
func stopRun() {
serverRunning = false
}
// 读取服务器发送来的信息
func readClientMsg(_ client: TCPClient) {
clients.append(client)
DispatchQueue.global().async {
while self.serverRunning {
}
}
}
}
此时代码基本完成,也可以运行看到基本效果。只是客户端与服务端都缺少发送与接收信息的代码。
发送与接收 .protocolbuffer 数据
1、创建 IM.proto 文件,生成 swift 源码文件。将源码文件分别添加到客户端程序(Client)与服务器(Server)中。
syntax = "proto2";
message UserInfo {
required string name = 1;
required int64 level = 2;
required string iconUrl = 3;
}
2、补充之前,客户端与服务端发送与接收数据的代码
- 客户端发送信息给服务器
这里发送一个消息采用一次性发送的方式
// 发送信息给服务器
override func touchesBegan(_ touches: Set, with event: UIEvent?) {
// 要发送的信息
let userInfo = UserInfo.Builder()
userInfo.name = "阿文\(arc4random_uniform(10))"
userInfo.level = 20
userInfo.iconUrl = "icon\(arc4random_uniform(5))"
var data = (try! userInfo.build()).data()
// 将消息长度添加进要发送的信息中
var length = data.count
data = data + Data(bytes: &length, count: 4)
client.send(data: data)
}
- 服务器接收客户端发来的信息
// 读取客户端发送来的信息
func readClientMsg(_ client: TCPClient) {
clients.append(client)
DispatchQueue.global().async {
while self.serverRunning {
// 读取信息长度
if let lengthBytes = client.read(4) {
let lengthData = Data(bytes: lengthBytes, count: 4)
var length = 0
(lengthData as NSData).getBytes(&length, length: 4)
// 读取信息
if let dataBytes = client.read(length) {
let data = Data(bytes: dataBytes, count: length)
let userInfo = try! UserInfo.parseFrom(data: data)
print("用户名:\(userInfo.name) 用户级别:\(userInfo.level)")
client.send(data: lengthData + data)
}
}
}
}
}
- 客户端接收服务器发来的信息
// 读取服务器发送来的信息
func readServerMsg() {
DispatchQueue.global().async {
while self.isConnected {
// 读取信息长度
if let lengthBytes = self.client.read(4) {
let lengthData = Data(bytes: lengthBytes, count: 4)
var length = 0
(lengthData as NSData).getBytes(&length, length: 4)
// 读取信息
if let dataBytes = self.client.read(length) {
let data = Data(bytes: dataBytes, count: length)
let userInfo = try! UserInfo.parseFrom(data: data)
print("用户名:\(userInfo.name) 用户级别:\(userInfo.level)")
}
}
}
}
}
心跳包
心跳包用于服务器检测客户端是否还在,其内在原理仍是客户端发送信息,服务器接收信息。不过心跳包信息必须表明独有的类型,所以发送一次信息除了信息长度与信息内容外,至少还包含一个标识符表明信息的类型。
如果服务器在一定时间内检测不到客户端的心跳包,则断开该客户端的链接。
代码:https://github.com/taoGod/IM-MG4