GCDAsyncSocket 简单使用

子车凯泽
2023-12-01

原文地址

项目中monitor数据上报,消息推送均使用了socket长连接,技术上使用GCDAsyncSocket 并做了二次封装。

  • CocoaAsyncSocket为Mac和iOS提供了易于使用且强大的异步通信库。CocoaAsyncSocket是支持tcp和udp的,利用它可以轻松实现建立连接、断开连接、发送socket业务请求、重连这四个基本功能。

一、GCDAsyncSocket 总结

在Podfile文件中,只要加上这句话就可以导入了

pod 'CocoaAsyncSocket'

1)首先初始化socket 源码提供了四种初始化方法

- (instancetype)init;
- (instancetype)initWithSocketQueue:(nullable dispatch_queue_t)sq;
- (instancetype)initWithDelegate:(nullable id<GCDAsyncSocketDelegate>)aDelegate delegateQueue:(nullable dispatch_queue_t)dq;
- (instancetype)initWithDelegate:(nullable id<GCDAsyncSocketDelegate>)aDelegate delegateQueue:(nullable dispatch_queue_t)dq socketQueue:(nullable dispatch_queue_t)sq;
  • aDelegate就是socket的代理 dq是delegate的线程

You MUST set a delegate AND delegate dispatch queue before attempting to use the socket, or you will get an error

这里的delegate和dq是必须要有的。

  • sq是socket的线程,这个是可选的设置,如果你写null,GCDAsyncSocket内部会帮你创建一个它自己的socket线程,如果你要自己提供一个socket线程的话,千万不要提供一个并发线程,在频繁socket通信过程中,可能会阻塞掉,个人建议是不用创建

If you pass NULL, GCDAsyncSocket will automatically create it's own socket queue.
If you choose to provide a socket queue, the socket queue must not be a concurrent queue.

2)初始化socket之后,需要跟服务器建立连接

- (BOOL)connectToHost:(NSString *)host onPort:(uint16_t)port error:(NSError **)errPtr;
  • host是主机地址,port是端口号

如果建连成功之后,会收到socket成功的回调

- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port;

如果失败了,会受到以下回调

- (void)socketDidDisconnect:(GCDAsyncSocket*)sock withError:(NSError*)err

3)发送数据

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

发送数据的回调

- (void)socket:(GCDAsyncSocket*)sock didWriteDataWithTag:(long)tag;

4)读取数据回调

- (void)socket:(GCDAsyncSocket*)sock didReadData:(NSData*)data withTag:(long)tag;

5)断开连接、重连

[self.socket disconnect];
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err
{
//这里可以做重连操作
}

二.采坑攻略

1)主动读取消息

在发送消息后,需要主动调取didReadDataWithTimeOut方法读取消息
,这样才能收到你发出请求后从服务器那边收到的数据

- (void)socket:(GCDAsyncSocket*)sock didWriteDataWithTag:(long)tag
{
    [self.socket readDataWithTimeout:-1 tag:tag];
}

2)tag 参数的理解

tag 参数,乍一看可能会以为在writeData到readData一次传输过程中保持一致。看似结果是这样,但是tag参数并没有加在数据传输中。
tag 是为了在回调方法中匹配发起调用的方法的,不会加在传输数据中

调用write方法,收到didWriteData 回调 调用writeDataWithTimeOut 读取数据。收到消息后,会回调didReadData的delegate方法。这是一次数据发送,在接受服务端回应的过程。

- (void)writeData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag;
- (void)onSocket:(AsyncSocket *)sock didWriteDataWithTag:(long)tag;

writeData方法中的tag 和 DidWriteData代理回调中的tag是对应的。源码中tag的传递是包含在当前写的数据包 GCDAsyncWritePacket currentWrite 中。

同理

- (void)readDataWithTimeout:(NSTimeInterval)timeout tag:(long)tag;
- (void)onSocket:(AsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag;

readData 方法中的tag 和 readDataWithTimeout 代理回调中的tag是一致的
tag 传递包含在GCDAsyncReadPacket * currentRead 数据包中。

需要注意:根据tag做消息回执的标识,可能会出现错乱的问题

以read为例分析:

- (void)readDataWithTimeout:(NSTimeInterval)timeout tag:(long)tag;

上面的方法会生成一个数据类:AsyncReadPacket,此类中包含tag,并把此对象放入数组 readQueue中。
(先进先出,比如read了了三次,分别为1,2,3,那么回调的tag会依次是1,2,3)
在CFStream中的回调方法中,会取readQueue最新的一个,在回调方法中取得tag,并将tag传给回调方法:

- (void)onSocket:(AsyncSocket *)sock didReadData:(long)tag;

这样看似tag 传递了下去。但是看下面的读取数据部分源码:

//用偏移量 maxLength 读取数据
- (void)readDataWithTimeout:(NSTimeInterval)timeout
                     buffer:(NSMutableData *)buffer
               bufferOffset:(NSUInteger)offset
                  maxLength:(NSUInteger)length
                        tag:(long)tag
{
    if (offset > [buffer length]) {
        LogWarn(@"Cannot read: offset > [buffer length]");
        return;
    }
    
    GCDAsyncReadPacket *packet = [[GCDAsyncReadPacket alloc] initWithData:buffer
                                                              startOffset:offset
                                                                maxLength:length
                                                                  timeout:timeout
                                                               readLength:0
                                                               terminator:nil
                                                                      tag:tag];
    
    dispatch_async(socketQueue, ^{ @autoreleasepool {
        
        LogTrace();
        
        if ((flags & kSocketStarted) && !(flags & kForbidReadsWrites))
        {
            //往读的队列添加任务,任务是包的形式
            [readQueue addObject:packet];
            [self maybeDequeueRead];
        }
    }});
    
    // Do not rely on the block being run in order to release the packet,
    // as the queue might get released without the block completing.
}

读取数据时的packet实际是是根据readDataWithTimeOut方法传进来的tag重新alloc出来的消息,假如服务端回执消息异常,相同tag对应的消息回执就会不匹配。这一点需要注意。实际业务中,上报消息后会根据服务端的回执消息做逻辑处理,倘若回执消息丢失,根据tag匹配到消息回执就会造成错乱。

官方解释

In addition to this you've probably noticed the tag parameter. The tag you pass during the read/write operation is passed back to you via the delegate method once the read/write operation completes. It does not get sent over the socket or read from the socket. It is designed to help simplify the code in your delegate method. For example, your delegate method might look like this:

#define TAG_WELCOME 10
#define TAG_CAPABILITIES 11
#define TAG_MSG 12

... 

- (void)socket:(AsyncSocket *)sender didReadData:(NSData *)data withTag:(long)tag
{
    if (tag == TAG_WELCOME)
    {
        // Ignore welcome message
    }
    else if (tag == TAG_CAPABILITIES)
    {
        [self processCapabilities:data];
    }
    else if (tag == TAG_MSG)
    {
        [self processMessage:data];
    }
}

 

 类似资料: