iOS开发】之CocoaAsyncSocket使用
本文介绍了CocoaAsyncSocket库中GCDAsyncSocket类的使用、粘包处理以及时间延迟测试.
一.CocoaAsyncSocket介绍
CocoaAsyncSocket中主要包含两个类:
1.GCDAsyncSocket.
1
2
|
用GCD搭建的基于TCP/IP协议的socket网络库 GCDAsyncSocket is a TCP/IP socket networking library built atop Grand Central Dispatch. -- 引自CocoaAsyncSocket. |
2.GCDAsyncUdpSocket.
1
2
|
用GCD搭建的基于UDP/IP协议的socket网络库. GCDAsyncUdpSocket is a UDP/IP socket networking library built atop Grand Central Dispatch..-- 引自CocoaAsyncSocket. |
二.下载CocoaAsyncSocket
首先,需要到这里下载CocoaAsyncSocket.
下载后可以看到文件所在位置.
文件路径
这里只要拷贝以下两个文件到项目中.
TCP开发使用的文件
三.客户端
因为,大部分项目已经有服务端socket,所以,先讲解客户端创建过程.
步骤:
1.继承GCDAsyncSocketDelegate协议.
2.声明属性
1
2
|
// 客户端socket @property (strong, nonatomic) GCDAsyncSocket *clientSocket; |
3.创建socket并指定代理对象为self,代理队列必须为主队列.
1
|
self.clientSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue()]; |
4.连接指定主机的对应端口.
1
2
|
NSError *error = nil; self.connected = [self.clientSocket connectToHost:self.addressTF.text onPort:[self.portTF.text integerValue] viaInterface:nil withTimeout:-1 error:&error]; |
5.成功连接主机对应端口号.
1
2
3
4
5
6
7
8
9
10
11
12
|
- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port { // NSLog(@"连接主机对应端口%@", sock); [self showMessageWithStr:@ "链接成功" ]; [self showMessageWithStr:[NSString stringWithFormat:@ "服务器IP: %@-------端口: %d" , host,port]]; // 连接成功开启定时器 [self addTimer]; // 连接后,可读取服务端的数据 [self.clientSocket readDataWithTimeout:- 1 tag:0]; self.connected = YES; } |
注意:
The host parameter will be an IP address, not a DNS name. -- 引自GCDAsyncSocket
连接的主机为IP地址,并非DNS名称.
6.发送数据给服务端
1
2
3
4
5
6
7
8
|
// 发送数据 - (IBAction)sendMessageAction:(id)sender { NSData *data = [self.messageTextF.text dataUsingEncoding:NSUTF8StringEncoding]; // withTimeout -1 : 无穷大,一直等 // tag : 消息标记 [self.clientSocket writeData:data withTimeout:- 1 tag:0]; } |
注意:
发送数据主要通过- (void)writeData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag写入数据的.
7.读取服务端数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
/** 读取数据 @param sock 客户端socket @param data 读取到的数据 @param tag 本次读取的标记 */ - (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag { NSString *text = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding]; [self showMessageWithStr:text]; // 读取到服务端数据值后,能再次读取 [self.clientSocket readDataWithTimeout:- 1 tag:0]; } |
注意:
有的人写好代码,而且第一次能够读取到数据,之后,再也接收不到数据.那是因为,在读取到数据的代理方法中,需要再次调用[self.clientSocket readDataWithTimeout:- 1 tag:0];方法,框架本身就是这么设计的.
8.客户端socket断开连接.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
/** 客户端socket断开 @param sock 客户端socket @param err 错误描述 */ - (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err { [self showMessageWithStr:@ "断开连接" ]; self.clientSocket.delegate = nil; self.clientSocket = nil; self.connected = NO; [self.connectTimer invalidate]; } |
注意:
sokect断开连接时,需要清空代理和客户端本身的socket.
1
2
|
self.clientSocket.delegate = nil; self.clientSocket = nil; |
9.建立心跳连接.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
// 计时器 @property (nonatomic, strong) NSTimer *connectTimer; // 添加定时器 - (void)addTimer { // 长连接定时器 self.connectTimer = [NSTimer scheduledTimerWithTimeInterval:5.0 target:self selector:@selector(longConnectToSocket) userInfo:nil repeats:YES]; // 把定时器添加到当前运行循环,并且调为通用模式 [[NSRunLoop currentRunLoop] addTimer:self.connectTimer forMode:NSRunLoopCommonModes]; } // 心跳连接 - (void)longConnectToSocket { // 发送固定格式的数据,指令@"longConnect" float version = [[UIDevice currentDevice] systemVersion].floatValue; NSString *longConnect = [NSString stringWithFormat:@ "123%f" ,version]; NSData *data = [longConnect dataUsingEncoding:NSUTF8StringEncoding]; [self.clientSocket writeData:data withTimeout:- 1 tag:0]; } |
注意:
心跳连接中发送给服务端的数据只是作为测试代码,根据你们公司需求,或者和后台商定好心跳包的数据以及发送心跳的时间间隔.因为这个项目的服务端socket也是我写的,所以,我自定义心跳包协议.客户端发送心跳包,服务端也需要有对应的心跳检测,以此检测客户端是否在线.
四.服务端
步骤:
1.继承GCDAsyncSocketDelegate协议.
2.声明属性
1
2
|
// 服务端socket(开放端口,监听客户端socket的连接) @property (strong, nonatomic) GCDAsyncSocket *serverSocket; |
3.创建socket并指定代理对象为self,代理队列必须为主队列.
1
2
|
// 初始化服务端socket self.serverSocket = [[GCDAsyncSocket alloc]initWithDelegate:self delegateQueue:dispatch_get_main_queue()]; |
4.开放服务端的指定端口.
1
|
BOOL result = [self.serverSocket acceptOnPort:[self.portF.text integerValue] error:&error]; |
5.连接上新的客户端socket
1
2
3
4
5
6
7
8
9
10
11
12
13
|
// 连接上新的客户端socket - (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(nonnull GCDAsyncSocket *)newSocket { // 保存客户端的socket [self.clientSockets addObject: newSocket]; // 添加定时器 [self addTimer]; [self showMessageWithStr:@ "链接成功" ]; [self showMessageWithStr:[NSString stringWithFormat:@ "客户端的地址: %@ -------端口: %d" , newSocket.connectedHost, newSocket.connectedPort]]; [newSocket readDataWithTimeout:- 1 tag:0]; } |
6.发送数据给客户端
1
2
3
4
5
6
7
8
9
10
11
|
// socket是保存的客户端socket, 表示给这个客户端socket发送消息 - (IBAction)sendMessage:(id)sender { if (self.clientSockets == nil) return ; NSData *data = [self.messageTextF.text dataUsingEncoding:NSUTF8StringEncoding]; // withTimeout -1 : 无穷大,一直等 // tag : 消息标记 [self.clientSockets enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { [obj writeData:data withTimeout:-1 tag:0]; }]; } |
7.读取客户端的数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
/** 读取客户端发送的数据 @param sock 客户端的Socket @param data 客户端发送的数据 @param tag 当前读取的标记 */ - (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag { NSString *text = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding]; [self showMessageWithStr:text]; // 第一次读取到的数据直接添加 if (self.clientPhoneTimeDicts.count == 0) { [self.clientPhoneTimeDicts setObject:[self getCurrentTime] forKey:text]; } else { // 键相同,直接覆盖,值改变 [self.clientPhoneTimeDicts enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) { [self.clientPhoneTimeDicts setObject:[self getCurrentTime] forKey:text]; }]; } [sock readDataWithTimeout:- 1 tag:0]; } |
8.建立检测心跳连接.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
// 检测心跳计时器 @property (nonatomic, strong) NSTimer *checkTimer; // 添加计时器 - (void)addTimer { // 长连接定时器 self.checkTimer = [NSTimer scheduledTimerWithTimeInterval:10.0 target:self selector:@selector(checkLongConnect) userInfo:nil repeats:YES]; // 把定时器添加到当前运行循环,并且调为通用模式 [[NSRunLoop currentRunLoop] addTimer:self.checkTimer forMode:NSRunLoopCommonModes]; } // 检测心跳 - (void)checkLongConnect { [self.clientPhoneTimeDicts enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) { // 获取当前时间 NSString *currentTimeStr = [self getCurrentTime]; // 延迟超过10秒判断断开 if (([currentTimeStr doubleValue] - [obj doubleValue]) > 10.0) { [self showMessageWithStr:[NSString stringWithFormat:@ "%@已经断开,连接时差%f" ,key,[currentTimeStr doubleValue] - [obj doubleValue]]]; [self showMessageWithStr:[NSString stringWithFormat:@ "移除%@" ,key]]; [self.clientPhoneTimeDicts removeObjectForKey:key]; } else { [self showMessageWithStr:[NSString stringWithFormat:@ "%@处于连接状态,连接时差%f" ,key,[currentTimeStr doubleValue] - [obj doubleValue]]]; } }]; } |
心跳检测方法只提供部分思路:
1.懒加载一个可变字典,字典的键作为客户端的标识.如:客户端标识为13123456789.
2.在- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag方法中,将读取到的数据或者数据中的部分字符串作为键.字典的值为系统当前时间.服务端第一次读取数据时,字典中没有数据,所以,直接添加到可变字典中,之后每次读取数据时,都用字典的setObject: forKey:方法添加字典,若存储的键相同,即客户端标识相同,键会被覆盖,再使用系统的当前时间作为值.
3.在- (void)checkLongConnect中,获取此时的当前时间,遍历字典,将每个键的值和当前时间进行比较即可.判断的延迟时间可以写8秒.时间自定.之后,再根据自己的需求进行后续处理.
五.数据粘包处理.
1.粘包情况.
例如:包数据为:abcd.
2.粘包解决思路.
思路1:
发送方将数据包加上包头和包尾,包头、包体以及包尾用字典形式包装成json字符串,接收方,通过解析获取json字符串中的包体,便可进行进一步处理.
例如:
1
2
3
4
5
6
7
|
{ // head:包头,body:包体,end:包尾 NSDictionary *dict = @{ @ "head" : @ "phoneNum" , @ "body" : @(13133334444), @ "end" : @(11)}; } |
思路2:
添加前缀.和包内容拼接成同一个字符串.
例如:当发送数据是13133334444,如果出现粘包情况只属于完整型:
13133334444
1313333444413133334444
131333344441313333444413133334444...
可以将ab作为前缀.则接收到的数据出现的粘包情况:
ab13133334444
ab13133334444ab13133334444
ab13133334444ab13133334444ab13133334444...
使用componentsSeparatedByString:方法,以ab为分隔符,将每个包内容存入数组中,再取对应数组中的数据操作即可.
思路3:
如果最终要得到的数据的长度是个固定长度,用一个字符串作为缓冲池,每次收到数据,都用字符串拼接对应数据,每当字符串的长度和固定长度相同时,便得到一个完整数据,处理完这个数据并清空字符串,再进行下一轮的字符拼接.
例如:处理上面的不完整型.创建一个长度是4的tempData字符串作为数据缓冲池.第1次收到数据,数据是:ab,tempData拼接上ab,tempData中只能再存储2个字符,第2次收到数据,将数据长度和2进行比较,第2次的数据是:cda,截取前两位字符,即cd,tempData继续拼接cd,此时,tempData为abcd,就是我们想要的数据,我们可以处理这个数据,处理之后并清空tempData,将第2次收到数据的剩余数据,即cda中的a,再与tempData拼接.之后,再进行类似操作.
核心代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
/** 处理数据粘包 @param readData 读取到的数据 */ - (void)dealStickPackageWithData:(NSString *)readData { // 缓冲池还需要存储的数据个数 NSInteger tempCount; if (readData.length > 0) { // 还差tempLength个数填满缓冲池 tempCount = 4 - self.tempData.length; if (readData.length <= tempCount) { self.tempData = [self.tempData stringByAppendingString:readData]; if (self.tempData.length == 4) { [self.mutArr addObject:self.tempData]; self.tempData = @ "" ; } } else { // 下一次的数据个数比要填满缓冲池的数据个数多,一定能拼接成完整数据,剩余的继续 self.tempData = [self.tempData stringByAppendingString:[readData substringToIndex:tempCount]]; [self.mutArr addObject:self.tempData]; self.tempData = @ "" ; // 多余的再执行一次方法 [self dealStickPackageWithData:[readData substringFromIndex:tempCount]]; } } } |
调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
// 存储处理后的每次返回数据 @property (nonatomic, strong) NSMutableArray *mutArr; // 数据缓冲池 @property (nonatomic, copy) NSString *tempData; /** 第四次测试 -- 混合型**/ self.mutArr = nil; /* 第1次 : abc 第2次 : da 第3次 : bcdabcd 第4次 : abcdabcd 第5次 : abcdabcdab */ // 数组中的数据代表每次接收的数据 NSArray *testArr4 = [NSArray arrayWithObjects:@ "abc" ,@ "da" ,@ "bcdabcd" ,@ "abcdabcd" ,@ "abcdabcdab" , nil]; self.tempData = @ "" ; for (NSInteger i = 0; i < testArr4.count; i++) { [self dealStickPackageWithData:testArr4[i]]; } NSLog(@ "testArr4 = %@" ,self.mutArr); |
结果:
1
2
3
4
5
6
7
8
9
|
2017-06-09 00:49:12.932976+0800 StickPackageDealDemo[10063:3430118] testArr4 = ( abcd, abcd, abcd, abcd, abcd, abcd, abcd ) |
数据粘包处理Demo在文末.
六.测试.
1.测试配置.
测试时,两端需要处于同一WiFi下.客户端中的IP地址为服务端的IP地址,具体信息进入Wifi设置中查看.
IP和端口描述
2.测试所需环境.
将客户端程序安装在每个客户端,让一台服务端测试机和一台客户端测试机连接mac并运行,这两台测试机可以看到打印结果,所有由服务端发送到客户端的数据,通过客户端再回传给服务端,在服务端看打印结果.
当年的图
3.进行延迟差测试.
延迟差即服务端发送数据到第一台客户端和服务端发送数据到最后一台客户端的时间差.根据服务端发送数据给不同数量的客户端进行测试.而且,发送数据时,是随机发送.
延迟差测试结果:
延迟差测试
由图所知,延迟差在200毫秒以内的比例基本保持在99%以上.所以符合开发需求(延迟在200毫秒以内).
4.单次信息收发测试.
让服务端给每个客户端随机发送200次数据.并计算服务端发送数据到某一客户端,完整的一次收发时间情况.
单次信息收发测试结果:
单次信息收发测试
由图所知,一次收发时间基本在95%以上,这个时间会根据网络状态和数据包大小波动.不过,可以直观看到数据从服务端到客户端的时间.
GitHub:
如果对您有帮助,请点个赞,如有不足之处还望指正.
iOS开发】之CocoaAsyncSocket使用的更多相关文章
- IOS 开发中要注意的事项
1.关于拍摄 TGCameraViewController – 基于 AVFoundation 的自定义相机.样式漂亮,轻量并且可以很容易地集成到 iOS 项目中.不会内存吃紧 2.block 中对控 ...
- iOS - 开发类库
开发类库 UI 项目名称 项目信息 1.MJRefresh 仅需一行代码就可以为UITableView或者CollectionView加上下拉刷新或者上拉刷新功能.可以自定义上下拉刷新的文字说明. ...
- iOS开发--iOS及Mac开源项目和学习资料
文/零距离仰望星空(简书作者)原文链接:http://www.jianshu.com/p/f6cdbc8192ba著作权归作者所有,转载请联系作者获得授权,并标注“简书作者”. 原文出处:codecl ...
- XMPPFrameWork IOS 开发(二)- xcode配置
原始地址:XMPPFrameWork IOS 开发(二) 译文地址: Getting started using XMPPFramework on iOS 介绍 ios上的XMPPFramewor ...
- iOS开发常用第三方库
UI 动画 网络相关 Model 其他 数据库 缓存处理 PDF 图像浏览及处理 摄像照相视频音频处理 响应式框架 消息相关 版本新API的Demo 代码安全与密码 测试及调试 AppleWatch ...
- iOS开发-常用第三方开源框架介绍(你了解的ios只是冰山一角)--(转)
图像: 1.图片浏览控件MWPhotoBrowser 实现了一个照片浏览器类似 iOS 自带的相册应用,可显示来自手机的图片或者是网络图片,可自动从网络下载图片并进行缓存.可对图片进行缩放等操作. 下 ...
- iOS开发--开源库
图像: 1.图片浏览控件MWPhotoBrowser 实现了一个照片浏览器类似 iOS 自带的相册应用,可显示来自手机的图片或者是网络图片,可自动从网络下载图片并进行缓存.可对图片进行缩 ...
- iOS开发-常用第三方开源框架介绍
iOS开发-常用第三方开源框架介绍 图像: 1.图片浏览控件MWPhotoBrowser 实现了一个照片浏览器类似 iOS 自带的相册应用,可显示来自手机的图片或者是网络图片,可自动从网 ...
- iOS开发系列--Swift语言
概述 Swift是苹果2014年推出的全新的编程语言,它继承了C语言.ObjC的特性,且克服了C语言的兼容性问题.Swift发展过程中不仅保留了ObjC很多语法特性,它也借鉴了多种现代化语言的特点,在 ...
- iOS开发系列--打造自己的“美图秀秀”
--绘图与滤镜全面解析 概述 在iOS中可以很容易的开发出绚丽的界面效果,一方面得益于成功系统的设计,另一方面得益于它强大的开发框架.今天我们将围绕iOS中两大图形.图像绘图框架进行介绍:Quartz ...
随机推荐
- 微信小程序 客服自动回复图片
产品需求是,在客服对话框里,发送特定的文字,回复我们的二维码: 小城程开发完成后,这个自动回复图片的功能就摆在了眼前.刚开始我们想到的是:在线客服功能的设置里设置好自动回复的图片,但是目前设置不支持自 ...
- Mybatis学习总结一
一.Mybatis架构 JAR包下载地址 1. mybatis配置 SqlMapConfig.xml,此文件作为mybatis的全局配置文件,配置了mybatis的运行环境等信息. mapper. ...
- 搭建Cookie池
很多时候我们在对网站进行数据抓取的时候,可以抓取一部分页面或者接口,这部分可能没有设置登录限制.但是如果要抓取大规模数据的时候,没有登录进行爬取会出现一些弊端.对于一些设置登录限制的页面,无法爬取对于 ...
- Vue组件传值方法调用
1.子组件改变父组件的值 <father label="云盘快照" name="name2"> <son :props='rows' @ ...
- 「 Luogu P2230 」X 「 Vijos 1142 」 HXOS系统
题目描述可能稍有偏差,但实质上是一样的. 看下面 题目大意 题面这么长,先说说题意吧. 就是有一个操作系统,他的存储方式是树形的.其中分为文件和目录(文件夹)每一个子目录下只能存储 $K$ 个文件或目 ...
- POJ3616 Milking Time【dp】
Description Bessie is such a hard-working cow. In fact, she is so focused on maximizing her producti ...
- Loading class `com.mysql.jdbc.Driver'. This is deprecated. The new driver class is `com.mysql.cj.jdbc.Driver'.
在连接数据库时,使用了最新版本的mysql-Connector,所以导致老版本的“com.mysql.jdbc.Drive”不可行,要改为“com.mysql.cj.jdbc.Driver”
- 前端面试题总结一(js变量和函数声明提前相关)
好久没有更新博客了,^_^写写博客吧!下面是我总结的一些面试题,希望对大家有所帮助 (1)题目如下: alert(a) var a=1 function a(){ alert(a) } 好多 ...
- Java基础学习总结(77)——Java枚举再总结
在Java SE5之前,我们要使用枚举类型时,通常会使用static final 定义一组int常量来标识,代码如下 public static final int MAN = 0; public s ...
- 多项式输出 2009年NOIP全国联赛普及组
时间限制: 1 s 空间限制: 128000 KB 题目等级 : 黄金 Gold 题目描述 Description 一元 n 次多项式可用如下的表达式表示:数,请按照如下规定的格式要求输出该多项式 ...