iOS高级理论:CocoaAsyncSocket 介绍与使用

时间:2024-03-01 09:55:07

一、简介

CocoaAsyncSocket为Mac和iOS提供了易于使用和功能强大的异步套接字库,主要包含两个类:

GCDAsyncSocket:用GCD搭建的基于TCP/IP协议的socket网络库

GCDAsyncUdpSocket:用GCD搭建的基于UDP/IP协议的socket网络库.

本文主要介绍 GCDAsyncSocket的使用,他是一个TCP库,建在Grand Central Dispatch上面的。

二 客户端的使用

2.1 常用的API方法
2.1.1 初始化
@property (nonatomic, strong) GCDAsyncSocket *clientSocket;

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

需要delegate和delegate_queue才能使GCDAsyncSocket调用您的委托方法。提供delegateQueue是一个新概念。delegateQueue要求必须是一个串行队列,使得委托方法在delegateQueue队列中执行。

2.1.2 连接服务器
NSError *err = nil;
if (![self.clientSocket connectToHost:@"ip地址" onPort: "端口号" error:&err])  //异步!
{
    //  如果有错误,很可能是"已经连接"或"没有委托集"
    NSLog(@"I goofed: %@", err);
}

连接方法是异步的。这意味着当您调用connect方法时,它们会启动后台操作以连接到所需的主机/端口。

2.1.3 发送数据
//  发送数据
- (void)sendData:(NSData *)data{
     // -1表示超时时间无限大
     // tag:消息标记
     [self.clientSocket writeData:data withTimeout:-1 tag:0];
}

通过调用writeData: withTimeout: tag:方法,即可发送数据给服务器。

2.2 常用的委托方法
2.2.1 连接成功
// socket连接成功会执行该方法
- (void)socket:(GCDAsyncSocket*)sock didConnectToHost:(NSString*)host port:(UInt16)port{
    NSLog(@"--连接成功--");
    [sock readDataWithTimeout:-1 tag:0];
}
2.2.2 收到服务端数据
// 收到服务器发送的数据会执行该方法
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag{
    NSString *serverStr = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];
    NSLog(@"服务端回包了--回包内容--%@---长度%lu",serverStr,(unsigned long)data.length);
    [sock readDataWithTimeout:-1 tag:0];
}
2.2.3 断开
// 断开连接会调取该方法
- (void)socketDidDisconnect:(GCDAsyncSocket*)sock withError:(NSError*)err{
    NSLog(@"--断开连接--");
    //  sokect断开连接时,需要清空代理和客户端本身的socket.
    self.clientSocket.delegate = nil;
    self.clientSocket = nil;
}

以上几个委托方法,使我们比较使用比较频繁的委托代理方法。

5、心跳包

心跳包就是在客户端和服务器间定时通知对方自己状态的一个自己定义的命令,按照一定的时间间隔发送,类似于心跳
用来判断对方(设备,进程或其它网元)是否正常运行,采用定时发送简单的通讯包,如果在指定时间段内未收到对方响应,则判断对方已经离线。

@property(nonatomic, strong) NSTimer *heartbeatTimer;

- (void)beginSendHeartbeat{
    // 创建心跳定制器
    self.heartbeatTimer = [NSTimer scheduledTimerWithTimeInterval:5.0 target:self selector:@selector(sendHeartbeat:) userInfo:nil repeats:YES];
    [[NSRunLoop mainRunLoop] addTimer:self.heartbeatTimer forMode:NSRunLoopCommonModes];
}

- (void)sendHeartbeat:(NSTimer *)timer {
    if (timer != nil) {
        char heartbeat[4] = {0xab,0xcd,0x00,0x00}; // 心跳字节,和服务器协商
        NSData *heartbeatData = [NSData dataWithBytes:&heartbeat length:sizeof(heartbeat)];
        [self.clientSocket writeData:heartbeatData withTimeout:-1 tag:0];
    }
}
2.2 服务端

GCDAsyncSocket还允许您创建服务器,并接受传入的连接; 服务端使用基本和客户类似,只不过需要开启端口进行监听客户端连接。

1、初始化

@property(nonatomic, strong) GCDAsyncSocket *serverSocket;

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

2、开放服务端的指定端口

 //  开放服务端的指定端口.
    NSError *error = nil;
    BOOL result = [self.serverSocket acceptOnPort:port error:&error];
    if (result) {
        NSLog(@"端口开启成功,并监听客户端请求连接...");
    }else {
        NSLog(@"端口开启失...");
    }

3、发送数据给客户端

 //  发送数据,和客户端使用一致
- (void)sendData:(NSData *)data{
     // -1表示超时时间无限大
     // tag:消息标记
    [self.serverSocket writeData:data withTimeout:-1 tag:0];
}

4、委托方法

(1) 监听到新的客户端socket连接委托:

 /* 存储所有连接的客户端 socket*/
@property(nonatomic, strong) NSMutableArray *arrayClient;

//  监听到新的客户端socket连接,会执行该方法
- (void)socket:(GCDAsyncSocket *)serveSock didAcceptNewSocket:(GCDAsyncSocket *) newSocket{
    
    NSLog(@"%@ IP: %@: %hu 客户端请求连接...",newSocket,newSocket.connectedHost,newSocket.localPort);
    // 将客户端socket保存起来
    if (![self.arrayClient containsObject:newSocket]) {
        [self.arrayClient addObject:newSocket];
    }
    [newSocket readDataWithTimeout:-1 tag:0];
}

监听到客户端连接,将客户端 socket 保存起来,因为服务器可能会收到很多客户端连接。

(2) 读取客户端数据:

 //  读取客户端发送的数据
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {
  //  记录客户端心跳
    char heartbeat[4] = {0xab,0xcd,0x00,0x00}; // 心跳
    NSData *heartbeatData = [NSData dataWithBytes:&heartbeat length:sizeof(heartbeat)];
    if ([data isEqualToData:heartbeatData]) {
        NSLog(@"*************心跳**************");
        self.heartbeatDateDict[sock.connectedHost] = [NSDate date];
    }
    [sock readDataWithTimeout:-1 tag:0];
}

(3) 断开连接:

 //  断开连接
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err{    
    NSLog(@"断开连接");
}

(4) 监听心跳包:

 // 子线程用于监听心跳包
@property(nonatomic, strong) NSThread *checkThread;
// 记录每个心跳缓存
@property (nonatomic, strong) NSMutableDictionary *heartbeatDateDict;
// 初始化子线程,并启动
self.checkThread = [[NSThread alloc]initWithTarget:sharedInstance selector:@selector(checkClientOnline) object:nil];
[self.checkThread start];

#pragma checkTimeThread

//  这里设置10检查一次 数组里所有的客户端socket 最后一次通讯时间,这样的话会有周期差(最多差10s),可以设置为1s检查一次,这样频率快
//  开启线程 启动runloop 循环检测客户端socket最新time
- (void)checkClientOnline{
    
    @autoreleasepool {
        [NSTimer scheduledTimerWithTimeInterval:10 target:self selector:@selector(repeatCheckClinetOnline) userInfo:nil repeats:YES];
        [[NSRunLoop currentRunLoop] run];
    }
}

//  移除 超过心跳时差的 client
- (void)repeatCheckClinetOnline{
    
    if (self.arrayClient.count == 0) {
        return;
    }
    NSDate *date = [NSDate date];
    for (GCDAsyncSocket *socket in self.arrayClient ) {
        if ([date timeIntervalSinceDate:self.heartbeatDateDict[socket.connectedHost]]>10) {
            [self.arrayClient removeObject:socket];
        }
    }
}

三、CocoaAsyncSocket 读/写操作

3.1 排队 读/写 操作

CocoaAsyncSocket库的最佳功能之一是“排队 读/写 操作”。

写操作: 套接字未连接,但我还是可以开始写它,库将排队我的所有写操作,在套接字连接后,它将自动开始执行我的写操作!

NSError * err = nil ;
NSError *err = nil;
if (![self.clientSocket connectToHost:@"ip地址" onPort: "端口号" error:&err])  //异步!
{
    NSLog(@"I goofed: %@", err);
}

//此时套接字未连接。
//但我还是可以开始写它!
//库将排队我的所有写操作,
//在套接字连接后,它将自动开始执行我的写操作!
   [socket writeData: data1 withTimeout: - 1  tag: 1 ];
   [socket writeData: data2 withTimeout: - 1  tag: 2 ];

排队读: 我们可以通过长度获取到相应长度的数据,可以很好解决粘包问题。

[socket readDataToLength: datalength withTimeout: -1  tag: 0];
3.2 Tag参数了解

CocoaAsyncSocket中的tag参数是不通过套接字发送的,也不是从套接字读取的。tag参数只需通过各种委托方法回显给您。它旨在帮助简化委托方法中的代码。

  [socket writeData: data1 withTimeout: - 1  tag: 1 ];
  [socket writeData: data2 withTimeout: - 1  tag: 2 ];
//  当我们发送数据时候使用 tag 标记后,发送后可以在委托方法中根据 tag 看到那条数据已经发送出去了。
- (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag :( long)tag{
    if(tag == 1)
         NSLog(@"First request sent ");
    else  if(tag == 2)
         NSLog(@"Second request sent ");
}

在读取时数据时,tag 也很有帮助:

[socket readDataWithTimeout:-1 tag:0];
// 读取 tag 与上面方法的 tag 值是一一对应的。
#define TAG_WELCOME 10
#define TAG_CAPABILITIES 11
#define TAG_MSG 12
- (void)socket:(GCDAsyncSocket *)sender didReadData :( NSData *)data withTag :( long)tag{
    if(tag == TAG_WELCOME)
    {
        //  忽略欢迎信息
    }
    else  if(tag == TAG_CAPABILITIES)
    {
        [self  proce*abilities:data];
    }
    else if (tag == TAG_MSG)
    {
        [self  processMessage: data];
    }
}

四、Tcp 粘包

4.1 什么是tcp粘包?

TCP是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了,就会出现粘包现象。

4.2 TCP粘包解决方案

目前应用最广泛的是在消息的头部添加数据包长度,接收方根据消息长度进行接收;在一条TCP连接上,数据的流式传输在接收缓冲区里是有序的,其主要的问题就是第一个包的包尾与第二个包的包头共存接收缓冲区,所以根据长度读取是十分合适的;

4.2.1 解决发送方粘包

方案一: 发送产生是因为Nagle算法合并小数据包,那么可以禁用掉该算法;

方案二: TCP提供了强制数据立即传送的操作指令push,当填入数据后调用操作指令就可以立即将数据发送,而不必等待发送缓冲区填充自动发送;

方案三: 数据包中加头,头部信息为整个数据的长度(最广泛最常用);

//  `方案三`发送方解决粘包的代码部分:
- (void)sendData:(NSData *)data{
        
        NSMutableData *sendData = [NSMutableData data];
        // 获取数据长度
        NSInteger datalength = data.length;
        //  NSInteger长度转 NSData
        NSData *lengthData = [NSData dataWithBytes:&datalength length:sizeof(datalength)];
        // 长度几个字节和服务器协商好。这里我们用的是4个字节存储长度信息
        NSData *newLengthData = [lengthData subdataWithRange:NSMakeRange(0, 4)];
        // 拼接长度信息
        [sendData appendData:newLengthData];
        //拼接数据
        [sendData appendData:data];
        // 发送加了长度信息的包
        [self.clientSocket writeData:[sendData copy] withTimeout:-1 tag:0];
}

4.2.2 解决接收方粘包

1、解析数据包头部信息,根据长度来接收;(最广泛最常用)

/**
 数据缓冲区
 */
@property (nonatomic, strong) NSMutableData *dataBuffer;;

//  读取客户端发送的数据,通过包头长度进行拆包
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {
    
        //  数据存入缓冲区
        [self.dataBuffer appendData:data];
        
        // 如果长度大于4个字节,表示有数据包。4字节为包头,存储包内数据长度
        while (self.dataBuffer.length >= 4) {
            
            NSInteger  datalength = 0;
            // 获取包头,并获取长度
            [[self.dataBuffer subdataWithRange:NSMakeRange(0, 4)] getBytes:&datalength length:sizeof(datalength)];
            //  判断缓存区内是否有包
            if (self.dataBuffer.length >= (datalength+4)) {
                // 获取去掉包头的数据
                NSData *realData = [[self.dataBuffer subdataWithRange:NSMakeRange(4, datalength)] copy];
                // 解析处理
                [self handleData:realData socket:sock];
                
                // 移除已经拆过的包
                self.dataBuffer = [NSMutableData dataWithData:[self.dataBuffer subdataWithRange:NSMakeRange(datalength+4, self.dataBuffer.length - (datalength+4))]];
            }else{
                break;
            }
        }
        [sock readDataWithTimeout:-1 tag:0];
}

自定义数据格式:在数据中放入开始、结束标识;解析时根据格式抓取数据,缺点是数据内不能含有开始或结束标识;
短连接传输,建立一次连接只传输一次数据就关闭;(不推荐)
注:以上代码仅提供粘包的解决思路,具体如何解包以及包头数据结构可以和服务器进行商定。