本文主要介紹
CocoaAsyncSocket的 讀/寫 操作,以及如何處理TCP粘包的問題,CocoaAsyncSocket基本使用的請查看上一篇文章Socket & CocoaAsyncSocket介紹與使用
排隊 讀/寫 操作
CocoaAsyncSocket庫的最佳功能之一是“排隊 讀/寫 操作”。
-
寫操作: 套接字未連接,但我還是可以開始寫它,庫將排隊我的所有寫操作,在套接字連接后,它將自動開始執(zhí)行我的寫操作!
NSError * err = nil ;
NSError *err = nil;
if (![self.clientSocket connectToHost:@"ip地址" onPort: "端口號" error:&err]) //異步!
{
NSLog(@"I goofed: %@", err);
}
//此時套接字未連接。
//但我還是可以開始寫它!
//庫將排隊我的所有寫操作,
//在套接字連接后,它將自動開始執(zhí)行我的寫操作!
[socket writeData: data1 withTimeout: - 1 tag: 1 ];
[socket writeData: data2 withTimeout: - 1 tag: 2 ];
-
排隊讀: 我們可以通過長度獲取到相應長度的數(shù)據(jù),可以很好解決粘包問題
[socket readDataToLength: datalength withTimeout: -1 tag: 0];
Tag參數(shù)了解
CocoaAsyncSocket中的tag參數(shù)是不通過套接字發(fā)送的,也不是從套接字讀取的。tag參數(shù)只需通過各種委托方法回顯給您。它旨在幫助簡化委托方法中的代碼。
[socket writeData: data1 withTimeout: - 1 tag: 1 ];
[socket writeData: data2 withTimeout: - 1 tag: 2 ];
// 當我們發(fā)送數(shù)據(jù)時候使用 tag 標記后,發(fā)送后可以在委托方法中根據(jù) tag 看到那條數(shù)據(jù)已經(jīng)發(fā)送出去了。
- (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag :( long)tag{
if(tag == 1)
NSLog(@"First request sent ");
else if(tag == 2)
NSLog(@"Second request sent ");
}
在讀取時數(shù)據(jù)時,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 processCapabilities:data];
}
else if (tag == TAG_MSG)
{
[self processMessage: data];
}
}
Tcp 粘包
1. 什么是tcp粘包?
TCP是面向連接的,面向流的,提供高可靠性服務。收發(fā)兩端(客戶端和服務器端)都要有一一成對的socket,因此,發(fā)送端為了將多個發(fā)往接收端的包,更有效的發(fā)到對方,使用了優(yōu)化方法(Nagle算法),將多次間隔較小且數(shù)據(jù)量小的數(shù)據(jù),合并成一個大的數(shù)據(jù)塊,然后進行封包。這樣,接收端,就難于分辨出來了,就會出現(xiàn)粘包現(xiàn)象
2. TCP粘包解決方案
目前應用最廣泛的是在消息的頭部添加數(shù)據(jù)包長度,接收方根據(jù)消息長度進行接收;在一條TCP連接上,數(shù)據(jù)的流式傳輸在接收緩沖區(qū)里是有序的,其主要的問題就是第一個包的包尾與第二個包的包頭共存接收緩沖區(qū),所以根據(jù)長度讀取是十分合適的;
2.1 解決發(fā)送方粘包
方案一: 發(fā)送產(chǎn)生是因為Nagle算法合并小數(shù)據(jù)包,那么可以禁用掉該算法;
方案二: TCP提供了強制數(shù)據(jù)立即傳送的操作指令push,當填入數(shù)據(jù)后調(diào)用操作指令就可以立即將數(shù)據(jù)發(fā)送,而不必等待發(fā)送緩沖區(qū)填充自動發(fā)送;
方案三: 數(shù)據(jù)包中加頭,頭部信息為整個數(shù)據(jù)的長度(最廣泛最常用);
// `方案三`發(fā)送方解決粘包的代碼部分:
- (void)sendData:(NSData *)data{
NSMutableData *sendData = [NSMutableData data];
// 獲取數(shù)據(jù)長度
NSInteger datalength = data.length;
// NSInteger長度轉(zhuǎn) NSData
NSData *lengthData = [NSData dataWithBytes:&datalength length:sizeof(datalength)];
// 長度幾個字節(jié)和服務器協(xié)商好。這里我們用的是4個字節(jié)存儲長度信息
NSData *newLengthData = [lengthData subdataWithRange:NSMakeRange(0, 4)];
// 拼接長度信息
[sendData appendData:newLengthData];
//拼接數(shù)據(jù)
[sendData appendData:data];
// 發(fā)送加了長度信息的包
[self.clientSocket writeData:[sendData copy] withTimeout:-1 tag:0];
}
2.2解決接收方粘包
- 解析數(shù)據(jù)包頭部信息,根據(jù)長度來接收;
(最廣泛最常用)
/**
數(shù)據(jù)緩沖區(qū)
*/
@property (nonatomic, strong) NSMutableData *dataBuffer;;
// 讀取客戶端發(fā)送的數(shù)據(jù),通過包頭長度進行拆包
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {
// 數(shù)據(jù)存入緩沖區(qū)
[self.dataBuffer appendData:data];
// 如果長度大于4個字節(jié),表示有數(shù)據(jù)包。4字節(jié)為包頭,存儲包內(nèi)數(shù)據(jù)長度
while (self.dataBuffer.length >= 4) {
NSInteger datalength = 0;
// 獲取包頭,并獲取長度
[[self.dataBuffer subdataWithRange:NSMakeRange(0, 4)] getBytes:&datalength length:sizeof(datalength)];
// 判斷緩存區(qū)內(nèi)是否有包
if (self.dataBuffer.length >= (datalength+4)) {
// 獲取去掉包頭的數(shù)據(jù)
NSData *realData = [[self.dataBuffer subdataWithRange:NSMakeRange(4, datalength)] copy];
// 解析處理
[self handleData:realData socket:sock];
// 移除已經(jīng)拆過的包
self.dataBuffer = [NSMutableData dataWithData:[self.dataBuffer subdataWithRange:NSMakeRange(datalength+4, self.dataBuffer.length - (datalength+4))]];
}else{
break;
}
}
[sock readDataWithTimeout:-1 tag:0];
}
- 自定義數(shù)據(jù)格式:在數(shù)據(jù)中放入開始、結(jié)束標識;解析時根據(jù)格式抓取數(shù)據(jù),缺點是數(shù)據(jù)內(nèi)不能含有開始或結(jié)束標識;
- 短連接傳輸,建立一次連接只傳輸一次數(shù)據(jù)就關(guān)閉;(不推薦)
注:以上代碼僅提供粘包的解決思路,具體如何解包以及包頭數(shù)據(jù)結(jié)構(gòu)可以和服務器進行商定