CocoaAsyncSocket使用

作者: xiaofu666 | 来源:发表于2021-08-17 15:41 被阅读0次

    本文介绍了CocoaAsyncSocket库中GCDAsyncSocket类的使用、粘包处理以及时间延迟测试.

    一.CocoaAsyncSocket介绍

    CocoaAsyncSocket中主要包含两个类:

    1.GCDAsyncSocket.

    用GCD搭建的基于TCP/IP协议的socket网络库
    GCDAsyncSocket is a TCP/IP socket networking library built atop Grand Central Dispatch. -- 引自CocoaAsyncSocket.
    

    2.GCDAsyncUdpSocket.

    用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.声明属性

    // 客户端socket
    @property (strong, nonatomic) GCDAsyncSocket *clientSocket;
    

    3.创建socket并指定代理对象为self,代理队列必须为主队列.

    self.clientSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue()];
    

    4.连接指定主机的对应端口.

    NSError *error = nil;
    self.connected = [self.clientSocket connectToHost:self.addressTF.text onPort:[self.portTF.text integerValue] viaInterface:nil withTimeout:-1 error:&error];
    

    5.成功连接主机对应端口号.

    - (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.发送数据给服务端

    // 发送数据
    - (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.读取服务端数据

    /**
     读取数据
    
     @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断开连接.

    /**
     客户端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.

    self.clientSocket.delegate = nil;
    self.clientSocket = nil;
    

    9.建立心跳连接.

     // 计时器
    @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.声明属性

    // 服务端socket(开放端口,监听客户端socket的连接)
    @property (strong, nonatomic) GCDAsyncSocket *serverSocket;
    

    3.创建socket并指定代理对象为self,代理队列必须为主队列.

    // 初始化服务端socket
    self.serverSocket = [[GCDAsyncSocket alloc]initWithDelegate:self delegateQueue:dispatch_get_main_queue()];
    

    4.开放服务端的指定端口.

    BOOL result = [self.serverSocket acceptOnPort:[self.portF.text integerValue] error:&error];
    

    5.连接上新的客户端socket

    // 连接上新的客户端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.发送数据给客户端

    // 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.读取客户端的数据

    /**
     读取客户端发送的数据
    
     @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.建立检测心跳连接.

    // 检测心跳计时器
    @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.

    [图片上传失败...(image-91e3ea-1629185588357)]

    2.粘包解决思路.

    • 思路1:

    发送方将数据包加上包头和包尾,包头、包体以及包尾用字典形式包装成json字符串,接收方,通过解析获取json字符串中的包体,便可进行进一步处理.

    例如:

    {
    // 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拼接.之后,再进行类似操作.

    • 核心代码
    /**
    处理数据粘包
    
    @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]];
          }
      }
    }
    
    • 调用
    // 存储处理后的每次返回数据
    @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);
    
    • 结果:
    2017-06-09 00:49:12.932976+0800 StickPackageDealDemo[10063:3430118] testArr4 = (
      abcd,
      abcd,
      abcd,
      abcd,
      abcd,
      abcd,
      abcd
    )
    

    六.配置测试.

    测试时,两端需要处于同一WiFi下.客户端中的IP地址为服务端的IP地址,具体信息进入Wifi设置中查看.

    IP和端口描述

    数据粘包处理Demo在文末.

    GitHub:

    本文来自 枣泥布丁【iOS开发】之CocoaAsyncSocket使用

    相关文章

      网友评论

        本文标题:CocoaAsyncSocket使用

        本文链接:https://www.haomeiwen.com/subject/qfkibltx.html