iOS中socket通信---基于CocoaAsyncSocket實現

前言

最近項目中涉及到socket通信這塊;所以有幸有時間大概看了一下這一塊;目前還在實現階段,因此現在還不能去些具體的實現過程;現在只大概描述一下這幾天看的資料和自己的一點心得吧;等項目實現之后會將具體的實現流程寫出來以供大家參考;

Socket通信基礎

Socket起源于Unix,而Unix/Linux基本哲學之一就是“一切皆文件”,都可以用“打開open –> 讀寫read/write –> 關閉close”模式來操作。我的理解就是Socket就是該模式的一個實現,socket即是一種特殊的文件,一些socket函數就是對其進行的操作(讀/寫IO、打開、關閉);

套接字(socket)是通信的基石,是支持TCP/IP協議的網絡通信的基本操作單元。它是網絡通信過程中端點的抽象表示,包含進行網絡通信必須的五種信息:連接使用的協議,本地主機的IP地址,本地進程的協議端口,遠地主機的IP地址,遠地進程的協議端口。因為TCP協議+端口號可以唯一標識一臺計算機中的進程;

創建Socket連接時,可以指定使用的傳輸層協議,Socket可以支持不同的傳輸層協議(TCP或UDP),當使用TCP協議進行連接時,該Socket連接就是一個TCP連接。

由于通常情況下Socket連接就是TCP連接,因此Socket連接一旦建立,通信雙方即可開始相互發送數據內容,直到雙方連接斷開。但在實際網絡應用中,客戶端到服務器之間的通信往往需要穿越多個中間節點,例如路由器、網關、防火墻等,大部分防火墻默認會關閉長時間處于非活躍狀態的連接而導致 Socket 連接斷連,因此需要通過輪詢告訴網絡,該連接處于活躍狀態。

TCP/IP協議

再者就是已經被講爛了的TCP/IP協議了,但是我們大多數人對其還不是很熟悉,只是整天在各種資料上看到聽到,但是我們似乎從來都沒有用心去細看;但是這里還是不得不說??;首先就是最基本的TCP的三次握手:
第一次握手:客戶端發送syn包(syn=j)到服務器,并進入SYN_SEND狀態,等待服務器確認;
第二次握手:服務器收到syn包,確認客戶端的syn(ack=j+1),同時自己也發送一個syn包(syn=k),即syn+ack包,此時服務器進入SYN_RECV狀態;
第三次握手:客戶端收到服務器的syn+ack包,向服務器發送確認包ack(ack=k+1),此包發送完畢,客戶端和服務器進入ESTABLISHED狀態,完成三次握手;

安全的tcp連接.png

CocoaAsyncSocket 分類

CocoaAsyncSocket 主要分GCDAsyncSocket.h 和 GCDAsyncUdpSocket.h這兩個類;前者是針對TCP進行通信的,而后者是針對UDP進行通信;

UDP廣播接收
NSError *error;
//綁定本地端口,端口號和后臺協商指定;
[self.asyncUdpSocket bindToPort:8989 error:&error];
if (error) {SALog(@"綁定端口:%@",error);return;}

//啟用廣播;
[self.asyncUdpSocket enableBroadcast:YES error:&error];
if (error) {SALog(@"啟用廣播:%@",error);return;}

//開啟接收數據,不開啟的話會接收不到數據;
[self.asyncUdpSocket beginReceiving:&error];
if (error) {SALog(@"開啟接收數據:%@",error);return;}

//重復發送廣播
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(broadcast) userInfo:nil repeats:YES];
[timer fire];

- (void)broadcast{
NSString *msg = @"Hello world!";
[self.socket sendData:[msg dataUsingEncoding:NSUTF8StringEncoding] toHost:@"192.168.0.31" port:9999 withTimeout:-1 tag:100];
}
/** 在廣播的時候IP地址192.168.0.31是一個廣播地址,這個地址可以通過自己手機的IP地址和子網掩碼相與進行計算出來,實在不知道可以放一個所有地址的廣播地址255.255.255.255 。 這樣只要在這個局域網中的所有設備都可以接收這個廣播 */

然后設置代理,監聽接收的數據:
- (GCDAsyncUdpSocket *)asyncUdpSocket{
if (!_asyncUdpSocket) {
_asyncUdpSocket = [[GCDAsyncUdpSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_global_queue(0, 0)];
}
return _asyncUdpSocket;
}

代理方法接收數據
- (void)udpSocket:(GCDAsyncUdpSocket *)sock didReceiveData:(NSData *)data fromAddress:(NSData *)address withFilterContext:(id)filterContext{
SALog(@"UDP接收數據……………………………………………………");
if (!self.isConnect) {
NSString *msg = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSArray *msgArray = [msg componentsSeparatedByString:@"|"];
NSData *msgData = [msgArray.lastObject dataUsingEncoding:NSUTF8StringEncoding];
NSError *error;
NSDictionary *dataDict = [NSJSONSerialization JSONObjectWithData:msgData options:NSJSONReadingMutableContainers error:&error];
if (!error) {
self.serverHost = dataDict[@"ip"];
self.port = [dataDict[@"port"] intValue];
//連接廣播的IP地址
[self openConnection];
}
}
}

- (void)udpSocket:(GCDAsyncUdpSocket *)sock didNotConnect:(NSError *)error{
  SALog(@"斷開連接");
}

- (void)udpSocket:(GCDAsyncUdpSocket *)sock didSendDataWithTag:(long)tag{
  SALog(@"發送的消息");
}

- (void)udpSocket:(GCDAsyncUdpSocket *)sock didConnectToAddress:(NSData *)address{
  SALog(@"已經連接");
}

- (void)udpSocketDidClose:(GCDAsyncUdpSocket *)sock withError:(NSError *)error{
  SALog(@"斷開連接");
}

- (void)udpSocket:(GCDAsyncUdpSocket *)sock didNotSendDataWithTag:(long)tag dueToError:(NSError *)error{
  SALog(@"沒有發送數據");
}
注意事項:
(1)監聽的端口(綁定端口)和發送的目的端口要一致。否則接收不到數據;
(2)如果要進行廣播數據,必須只能使用[socket bindToPort:9999 error:&error] 方法來綁定端口,不能綁定IP地址并且啟用廣播,否則不能廣播數據。在發送廣播的時候可以綁定地址也可以設置為所有地址的廣播地址;
(3)該實例沒有被銷毀前,只能被創建一次,因為端口正在被使用,不能被重復創建,可以用一個懶加載優化下。廣播發送之后就會一直發送除非斷開連接;
(4)注意 NSTimer 的銷毀防止內存泄露。
心跳包
心跳包機制

跳包之所以叫心跳包是因為:它像心跳一樣每隔固定時間發一次,以此來告訴服務器,這個客戶端還活著。事實上這是為了保持長連接,至于這個包的內容,是沒有什么特別規定的,不過一般都是很小的包,或者只包含包頭的一個空包。

在TCP的機制里面,本身是存在有心跳包的機制的,也就是TCP的選項:SO_KEEPALIVE。系統默認是設置的2小時的心跳頻率。但是它檢查不到機器斷電、網線拔出、防火墻這些斷線。而且邏輯層處理斷線可能也不是那么好處理。一般,如果只是用于保活還是可以的。 心跳包一般來說都是在邏輯層發送空的echo包來實現的。下一個定時器,在一定時間間隔下發送一個空包給客戶端,然后客戶端反饋一個同樣的空包回來,服務器如果在一定時間內收不到客戶端發送過來的反饋包,那就只有認定說掉線了。 其實,要判定掉線,只需要send或者recv一下,如果結果為零,則為掉線。但是,在長連接下,有可能很長一段時間都沒有數據往來。理論上說,這個連接是一直保持連接的,但是實際情況中,如果中間節點出現什么故障是難以知道的。更要命的是,有的節點(防火墻)會自動把一定時間之內沒有數據交互的連接給斷掉。在這個時候,就需要我們的心跳包了,用于維持長連接,保活。 在獲知了斷線之后,服務器邏輯可能需要做一些事情,比如斷線后的數據清理呀,重新連接呀……當然,這個自然是要由邏輯層根據需求去做了。

總的來說,心跳包主要也就是用于長連接的保活和斷線處理。一般的應用下,判定時間在30-40秒比較不錯。如果實在要求高,那就在6-9秒。

實現方法:
由應用程序自己發送心跳包來檢測連接是否正常,大致的方法是:服務器在一個 Timer事件中定時向客戶端發送一個短小精悍的數據包,
然后啟動一個低級別的線程,在該線程中不斷檢測客戶端的回應, 如果在一定時間內沒有收到客戶端的回應,即認為客戶端已經掉線;同
樣,如果客戶端在一定時間內沒 有收到服務器的心跳包,則認為連接不可用。

心跳檢測步驟:

1.客戶端每隔一個時間間隔發生一個探測包給服務器
2.客戶端發包時啟動一個超時定時器
3.服務器端接收到檢測包,應該回應一個包
4.如果客戶機收到服務器的應答包,則說明服務器正常,刪除超時定時器
5.如果客戶端的超時定時器超時,依然沒有收到應答包,則說明服務器掛了
GCDAsyncSocket
- (instancetype)init
{
    if (self = [super init]) {
        _asyncSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue()];
    }
    return self;
}

- (void)dealloc
{
    _asyncSocket.delegate = nil;
    _asyncSocket = nil;
}
//連接主機
- (void)connectWithHost:(NSString *)hostName port:(int)port
{
   
}

- (void)disconnect
{
    [_asyncSocket disconnect];
}

- (BOOL)isConnected
{
    return [_asyncSocket isConnected];
}

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

}

- (void)writeData:(NSData *)data timeout:(NSTimeInterval)timeout tag:(long)tag
{

}

#pragma mark -
#pragma mark GCDAsyncSocketDelegate method
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err
{
   
}
//連接成功
- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port
{
}

- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag
{
    //一直請求讀取否則會接收不到數據
    [sock readDataWithTimeout:-1 tag:tag];
}

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

數據粘包的處理

在使用socket通信的時候是避免不了會遇到在接收數據的時候出現數據粘包的問題,因此在客戶端處理數據粘包的問題是不可避免的;本項目中對于數據粘包的處理是和后臺協商制定包尾結束符,對接收到的數據進行便利拼接成完成的數據包以供解析;

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容