本文介绍了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使用的更多相关文章

  1. IOS 开发中要注意的事项

    1.关于拍摄 TGCameraViewController – 基于 AVFoundation 的自定义相机.样式漂亮,轻量并且可以很容易地集成到 iOS 项目中.不会内存吃紧 2.block 中对控 ...

  2. iOS - 开发类库

    开发类库   UI 项目名称 项目信息 1.MJRefresh 仅需一行代码就可以为UITableView或者CollectionView加上下拉刷新或者上拉刷新功能.可以自定义上下拉刷新的文字说明. ...

  3. iOS开发--iOS及Mac开源项目和学习资料

    文/零距离仰望星空(简书作者)原文链接:http://www.jianshu.com/p/f6cdbc8192ba著作权归作者所有,转载请联系作者获得授权,并标注“简书作者”. 原文出处:codecl ...

  4. XMPPFrameWork IOS 开发(二)- xcode配置

    原始地址:XMPPFrameWork IOS 开发(二) 译文地址:   Getting started using XMPPFramework on iOS 介绍 ios上的XMPPFramewor ...

  5. iOS开发常用第三方库

    UI 动画 网络相关 Model 其他 数据库 缓存处理 PDF 图像浏览及处理 摄像照相视频音频处理 响应式框架 消息相关 版本新API的Demo 代码安全与密码 测试及调试 AppleWatch ...

  6. iOS开发-常用第三方开源框架介绍(你了解的ios只是冰山一角)--(转)

    图像: 1.图片浏览控件MWPhotoBrowser 实现了一个照片浏览器类似 iOS 自带的相册应用,可显示来自手机的图片或者是网络图片,可自动从网络下载图片并进行缓存.可对图片进行缩放等操作. 下 ...

  7. iOS开发--开源库

    图像: 1.图片浏览控件MWPhotoBrowser        实现了一个照片浏览器类似 iOS 自带的相册应用,可显示来自手机的图片或者是网络图片,可自动从网络下载图片并进行缓存.可对图片进行缩 ...

  8. iOS开发-常用第三方开源框架介绍

    iOS开发-常用第三方开源框架介绍 图像: 1.图片浏览控件MWPhotoBrowser        实现了一个照片浏览器类似 iOS 自带的相册应用,可显示来自手机的图片或者是网络图片,可自动从网 ...

  9. iOS开发系列--Swift语言

    概述 Swift是苹果2014年推出的全新的编程语言,它继承了C语言.ObjC的特性,且克服了C语言的兼容性问题.Swift发展过程中不仅保留了ObjC很多语法特性,它也借鉴了多种现代化语言的特点,在 ...

  10. iOS开发系列--打造自己的“美图秀秀”

    --绘图与滤镜全面解析 概述 在iOS中可以很容易的开发出绚丽的界面效果,一方面得益于成功系统的设计,另一方面得益于它强大的开发框架.今天我们将围绕iOS中两大图形.图像绘图框架进行介绍:Quartz ...

随机推荐

  1. SkiaSharp drawText中文乱码问题

    var fontManager = SKFontManager.Default; var emojiTypeface = fontManager.MatchCharacter('时'); var te ...

  2. SpringMVC接收多参数的处理方法

    问题:依赖SpringMVC自带的机制解析多对象参数往往出现解析不了的问题,使用较为复杂. 解决思路:前端 JS 先把传递到后台的对象转换为 JSON 字符串,后台直接使用字符串类型接收,再使用 st ...

  3. js实现字符串反转

    方案1: var str = "abcdef"; console.log( str.split("").reverse().join("") ...

  4. A4. JVM 内存分配及回收策略

    [概述] Java 技术体系中所提倡的自动内存管理最终可以归结为自动化地解决两个问题:给对象分配内存以及回收分配给对象的内存. 对象的内存分配,往大方向讲,就是在堆上分配,对象主要分配在新生代的 Ed ...

  5. qemu vm setup network(ssh) with buildroot

    1, build buildroot with buildroot.config, that is 'make qemu_x86_64_defconfig' + some packages, sshd ...

  6. 暴力搜索+散列--P1008 三连击

    题目描述 将1,2, ⋯,9共9个数分成3组,分别组成3个三位数,且使这3个三位数构成1:2:3的比例,试求出所有满足条件的3个三位数. 输入输出格式 输入格式: 木有输入 输出格式: 若干行,每行3 ...

  7. Anaconda安装xgboost的过程和踩过的坑

    win10下安装xgb,安装的过程波折起伏,花了5个小时,给后来人做参考喽 第一次尝试 利用以下两个软件 Git for Windows.MINGW进行安装. 安装可以参考:(https://blog ...

  8. enote笔记语言(2)(ver0.4)

    why not(whyn't)                    为什么不(与“why”相反对应,是它的反面)   how对策 how设计   key-memo:                 ...

  9. springboot的jsp热部署

    使用springboot每次修改jsp都需要重新启动是不是很麻烦呢?以下是解决办法! yml格式 server: servlet: jsp: init-parameters: development: ...

  10. buf.writeFloatBE()函数详解

    buf.writeFloatBE(value, offset[, noAssert]) buf.writeFloatLE(value, offset[, noAssert]) value {Numbe ...