GCDAsyncSocket 簡單使用

weixin_34247155發表於2018-08-25

註冊了這麼久簡書賬號,今天終於決定把自己的總結髮出來。第一篇文章誕生了!

專案中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];
    }
}