iOS中使用GCDAsyncSocket建立长连接

胡元忠
2023-12-01

    在App与服务器需要高频通信,或者服务器主动推送消息到App的情况下,就需要通过长连接来实现。比如聊天和股票软件。

    下面介绍iOS中如何通过GCDAsyncSocket来实现长连接。

GCDAsyncSocket介绍

    GCDAsyncSocket是一个开源库 CocoaAsyncSocket 的一部分,用于建立可靠的TCP连接。 如果想建立UDP连接,可以用GCDAsyncUDPSocket。

建立连接

1、创建socket对象,delegateQueue可以指定代理方法执行的队列。
-(GCDAsyncSocket*)socket
{
    if (_socket == nil)
    {
        _socket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)];
    }

    return _socket;
}

2、连接到指定服务器
NSError *error = nil;
// host:域名或ip,port:端口号,timeout:超时时间
if (![self.socket connectToHost:host onPort:port withTimeout:timeout error:&error])
{
      NSLog(@"socket连接服务器错误:%@", error);
}


#pragma mark- GCDAsyncSocketDelegate
-(void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port
{
    NSLog(@"socket成功建立。");
}


-(void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)error
{
    NSLog(@"链接出错:error:%@", error);
}



3、进行TLS验证(可选)
// 打包到App中的根证书
-(NSData *)certData
{
    if(!_certData)
    {
        _certData = [NSData dataWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"root_cert"
                                                                                  ofType:@"cer"]];
    }
    return _certData;
}


#pragma mark- GCDAsyncSocketDelegate
-(void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port
{
    NSLog(@"socket成功建立。");

    NSMutableDictionary *settings = [NSMutableDictionary dictionary];
    //允许自签名证书手动验证
    [settings setObject:@YES forKey:GCDAsyncSocketManuallyEvaluateTrust];
    //GCDAsyncSocketSSLPeerName
    //[settings setObject:@"example.com" forKey:GCDAsyncSocketSSLPeerName];
    
    // 如果不是自签名证书,而是那种权威证书颁发机构注册申请的证书
    // 那么这个settings字典可不传。
    NSLog(@"socket开始TLS握手");
    [sock startTLS:settings]; 
}

- (void)socket:(GCDAsyncSocket *)sock didReceiveTrust:(SecTrustRef)trust completionHandler:(void (^)(BOOL))completionHandler
{
    NSLog(@"socket TLS开始校验证书。");
    
    OSStatus status = -1;
    SecTrustResultType result = kSecTrustResultDeny;
    
    if(self.certData)
    {
        SecCertificateRef cert = SecCertificateCreateWithData(NULL, (CFDataRef)self.certData);
        // 设置证书用于验证
        SecTrustSetAnchorCertificates(trust, (__bridge CFArrayRef)[NSArray arrayWithObject:(__bridge id)cert]);
        // 验证服务器证书和本地证书是否匹配
        status = SecTrustEvaluate(trust, &result);
        CFRelease(cert);
    }
    else
    {
        NSLog(@"local certificates could not be loaded");
        completionHandler(NO);
    }
    
    if ((status == noErr && (result == kSecTrustResultProceed || result == kSecTrustResultUnspecified)))
    {
        //成功通过验证,证书可信
        completionHandler(YES);
    }
    else
    {
        CFArrayRef arrayRefTrust = SecTrustCopyProperties(trust);
        TPDiskLog(@"error in connection occured\n%@", arrayRefTrust);
        CFRelease(arrayRefTrust);
        completionHandler(NO);
    }
    
    NSLog(@"socket TLS校验证书完毕。");
}

- (void)socketDidSecure:(GCDAsyncSocket *)sock
{
    NSLog(@"socket TLS握手成功,安全通信已经建立连接。");
}


收发数据

1、原理
    我理解socket就像是一个互不干扰的双向水管,数据就像是水(这里的水不会混在一起,是有序的)。为了不混乱,只考虑App接收数据,Server发送数据的情况。即App端是出水口,Server端是入水口。此时的socket可以看成是单一管道。
    在初始状态,出水口是关闭的,入水口是打开的。Server可以一直往水管注水(发数据),App在需要的时候,就拿桶去出水口接水(接收数据)。这个桶的大小(缓冲区),就是App需要数据的大小,每次接满了就关闭出水口,如果没有接满,就等待。
    根据App接水的特性,使得App必须明确知道Server每次注水的大小。否则会出现桶太大,接了好几个批次的水,等待的时间也可能很长;桶太小,同一次的水都还没有接完。
    所以我们需要规范Server注水的行为,每次注水之前,都需要先注入固定大小的,带有本次注水大小信息的特质液体。App在接水时,每次都是先用固定大小的桶,接收特质液体,然后从液体中获取后续水的大小,再用对应大小的桶来接收水。

2、定义头部
    对应回数据,则是将数据格式化:基本头(固定长度)+ 数据(可变长度)。比如下面这样定义,每次App先读取4字节数据,获取数据的长度,然后再读取对应长度的数据,来获取payload真实内容。
typedef struct {
    uint32_t length;// 数据长度
} header_t;

3、读函数
    需要指定读取长度length,tag可以用于区分本次读取是头部,还是数据主体(下面的完整代码会有例子)。这里的超时时间一般要设置成-1,防止socket在指定时间内没有读取够数据,把连接断开。如果业务请求需要设置超时时间,要在外部通过定时器管理。
[self.socket readDataToLength:lenght withTimeout:-1 tag:tag];

#pragma mark- GCDAsyncSocketDelegate
-(void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag
{
    // 读取到数据,通过tag来区分是header还是body
}

4、写函数
    与读函数类似

[self.socket writeData:data withTimeout:-1 tag:0];

#pragma mark- GCDAsyncSocketDelegate
-(void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag
{
    NSLog(@"didWriteDataWithTag:%ld",tag);
}

完整代码

typedef struct {
    uint32_t length;// 数据长度
} header_t;

#define HeaderLength (sizeof(header_t))


// 通信层的超时时间,都设置成infinite
#define SocketTimeOutNone (-1) // 不超时,防止socket断开

#define ReadTagPacketHeader (101)
#define ReadTagPacketBody   (102)


@interface SocketManager() <GCDAsyncSocketDelegate>
@property (nonatomic, strong) GCDAsyncSocket *socket;
@property (nonatomic, strong) NSData *certData;

@end

@implementation SocketManager

-(GCDAsyncSocket*)socket
{
    if (_socket == nil)
    {
        _socket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)];
    }

    return _socket;
}


-(NSData *)certData
{
    if(!_certData)
    {
        _certData = [NSData dataWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"root_cert" ofType:@"cer"]];
    }
    return _certData;
}

#pragma mark- func
-(BOOL)connectToServer
{
    NSLog(@"socket开始连接服务器");
    NSError *error = nil;
    if (![self.socket connectToHost:host onPort:port withTimeout:15 error:&error])
    {
        NSLog(@"socket连接服务器错误:%@", error);
        return NO;
    }
    
    return YES;
}

-(void)disconnect
{
    [self.socket disconnect];
}


-(void)sendData:(NSData*)data
{
    header_t h = {};
    h.length = (uint32_t)data.length;
    
    [self.socket writeData:[NSData dataWithBytes:&h length:HeaderLength] withTimeout:SocketTimeOutNone tag:0];
    [self.socket writeData:data withTimeout:SocketTimeOutNone tag:0];
}

-(void)readDataLenght:(NSUInteger)lenght tag:(long)tag
{
    [self.socket readDataToLength:lenght withTimeout:SocketTimeOutNone tag:tag];
}

#pragma mark- GCDAsyncSocketDelegate

-(void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port
{
    NSLog(@"socket成功建立。");
    
    NSMutableDictionary *settings = [NSMutableDictionary dictionaryWithCapacity:3];
    //允许自签名证书手动验证
    [settings setObject:@YES forKey:GCDAsyncSocketManuallyEvaluateTrust];
    //GCDAsyncSocketSSLPeerName
    //[settings setObject:@"example.com" forKey:GCDAsyncSocketSSLPeerName];
    
    // 如果不是自签名证书,而是那种权威证书颁发机构注册申请的证书
    // 那么这个settings字典可不传。
    NSLog(@"socket开始TLS握手");
    [sock startTLS:settings]; // 开始TLS握手
}

- (void)socket:(GCDAsyncSocket *)sock didReceiveTrust:(SecTrustRef)trust completionHandler:(void (^)(BOOL))completionHandler
{
    NSLog(@"socket TLS开始校验证书。");
    
    OSStatus status = -1;
    SecTrustResultType result = kSecTrustResultDeny;
    
    if(self.certData)
    {
        SecCertificateRef cert = SecCertificateCreateWithData(NULL, (CFDataRef)self.certData);
        // 设置证书用于验证
        SecTrustSetAnchorCertificates(trust, (__bridge CFArrayRef)[NSArray arrayWithObject:(__bridge id)cert]);
        // 验证服务器证书和本地证书是否匹配
        status = SecTrustEvaluate(trust, &result);
        
        CFRelease(cert);
    }
    else
    {
        NSLog(@"local certificates could not be loaded");
        completionHandler(NO);
    }
    
    if ((status == noErr && (result == kSecTrustResultProceed || result == kSecTrustResultUnspecified)))
    {
        //成功通过验证,证书可信
        completionHandler(YES);
    }
    else
    {
        CFArrayRef arrayRefTrust = SecTrustCopyProperties(trust);
        NSLog(@"error in connection occured\n%@", arrayRefTrust);
        CFRelease(arrayRefTrust);
        completionHandler(NO);
    }
    
    NSLog(@"socket TLS校验证书完毕。");
}

- (void)socketDidSecure:(GCDAsyncSocket *)sock
{
    NSLog(@"socket TLS握手成功,安全通信已经建立连接。");

    [self readDataLenght:kHeaderLength tag:ReadTagPacketHeader];
}

-(void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)error
{
    NSLog(@"链接出错:error:%@\n", error);
}

-(void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag
{
    NSLog(@"didWriteDataWithTag:%ld",tag);
}

-(void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag
{
    static header_t h = {};
    if (tag == ReadTagPacketHeader)
    {
        if (data.length == HeaderLength)
        {
            header_t *header = (header_t*)data.bytes;
            h.length = header->length;
            if (header->length == 0)// 只有头部
            {
                [self readDataLenght:HeaderLength tag:ReadTagPacketHeader];
            }
            else
            {
                [self readDataLenght:header->length tag:ReadTagPacketBody];
            }
        }
        else
        {
            NSLog(@"exception occur");
        }
    }
    else if (tag == ReadTagPacketBody)
    {
        // 这里的data,就是server发送的数据
        
        [self readDataLenght:HeaderLength tag:ReadTagPacketHeader];
    }
}

@end






 类似资料: