本文介紹了CocoaAsyncSocket庫中GCDAsyncSocket類的使用、粘包處理以及時間延遲測試.
一.CocoaAsyncSocket介紹
CocoaAsyncSocket中主要包含兩個類:
1.GCDAsyncSocket.
用GCD搭建的基于TCP/IP協議的socket網絡庫
GCDAsyncSocket is a TCP/IP socket networking library built atop Grand Central Dispatch. -- 引自CocoaAsyncSocket.
2.GCDAsyncUdpSocket.
用GCD搭建的基于UDP/IP協議的socket網絡庫.
GCDAsyncUdpSocket is a UDP/IP socket networking library built atop Grand Central Dispatch..-- 引自CocoaAsyncSocket.
二.下載CocoaAsyncSocket
- 首先,需要到這里下載CocoaAsyncSocket.
- 下載后可以看到文件所在位置.
- 這里只要拷貝以下兩個文件到項目中.
三.客戶端
因為,大部分項目已經有服務端socket,所以,先講解客戶端創建過程.
步驟:
1.繼承GCDAsyncSocketDelegate
協議.
2.聲明屬性
// 客戶端socket
@property (strong, nonatomic) GCDAsyncSocket *clientSocket;
3.創建socket并指定代理對象為self,代理隊列必須為主隊列.
self.clientSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue()];
4.連接指定主機的對應端口.
NSError *error = nil;
self.connected = [self.clientSocket connectToHost:self.addressTF.text onPort:[self.portTF.text integerValue] viaInterface:nil withTimeout:-1 error:&error];
5.成功連接主機對應端口號.
- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port
{
// NSLog(@"連接主機對應端口%@", sock);
[self showMessageWithStr:@"鏈接成功"];
[self showMessageWithStr:[NSString stringWithFormat:@"服務器IP: %@-------端口: %d", host,port]];
// 連接成功開啟定時器
[self addTimer];
// 連接后,可讀取服務端的數據
[self.clientSocket readDataWithTimeout:- 1 tag:0];
self.connected = YES;
}
注意:
*The host parameter will be an IP address, not a DNS name. -- *引自GCDAsyncSocket
連接的主機為IP地址,并非DNS名稱.
6.發送數據給服務端
// 發送數據
- (IBAction)sendMessageAction:(id)sender
{
NSData *data = [self.messageTextF.text dataUsingEncoding:NSUTF8StringEncoding];
// withTimeout -1 : 無窮大,一直等
// tag : 消息標記
[self.clientSocket writeData:data withTimeout:- 1 tag:0];
}
注意:
發送數據主要通過- (void)writeData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag
寫入數據的.
7.讀取服務端數據
/**
讀取數據
@param sock 客戶端socket
@param data 讀取到的數據
@param tag 本次讀取的標記
*/
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag
{
NSString *text = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];
[self showMessageWithStr:text];
// 讀取到服務端數據值后,能再次讀取
[self.clientSocket readDataWithTimeout:- 1 tag:0];
}
注意:
有的人寫好代碼,而且第一次能夠讀取到數據,之后,再也接收不到數據.那是因為,在讀取到數據的代理方法中,需要再次調用[self.clientSocket readDataWithTimeout:- 1 tag:0];
方法,框架本身就是這么設計的.
8.客戶端socket斷開連接.
/**
客戶端socket斷開
@param sock 客戶端socket
@param err 錯誤描述
*/
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err
{
[self showMessageWithStr:@"斷開連接"];
self.clientSocket.delegate = nil;
self.clientSocket = nil;
self.connected = NO;
[self.connectTimer invalidate];
}
注意:
sokect斷開連接時,需要清空代理和客戶端本身的socket.
self.clientSocket.delegate = nil;
self.clientSocket = nil;
9.建立心跳連接.
// 計時器
@property (nonatomic, strong) NSTimer *connectTimer;
// 添加定時器
- (void)addTimer
{
// 長連接定時器
self.connectTimer = [NSTimer scheduledTimerWithTimeInterval:5.0 target:self selector:@selector(longConnectToSocket) userInfo:nil repeats:YES];
// 把定時器添加到當前運行循環,并且調為通用模式
[[NSRunLoop currentRunLoop] addTimer:self.connectTimer forMode:NSRunLoopCommonModes];
}
// 心跳連接
- (void)longConnectToSocket
{
// 發送固定格式的數據,指令@"longConnect"
float version = [[UIDevice currentDevice] systemVersion].floatValue;
NSString *longConnect = [NSString stringWithFormat:@"123%f",version];
NSData *data = [longConnect dataUsingEncoding:NSUTF8StringEncoding];
[self.clientSocket writeData:data withTimeout:- 1 tag:0];
}
注意:
心跳連接中發送給服務端的數據只是作為測試代碼,根據你們公司需求,或者和后臺商定好心跳包的數據以及發送心跳的時間間隔.因為這個項目的服務端socket也是我寫的,所以,我自定義心跳包協議.客戶端發送心跳包,服務端也需要有對應的心跳檢測,以此檢測客戶端是否在線.
四.服務端
步驟:
1.繼承GCDAsyncSocketDelegate
協議.
2.聲明屬性
// 服務端socket(開放端口,監聽客戶端socket的連接)
@property (strong, nonatomic) GCDAsyncSocket *serverSocket;
3.創建socket并指定代理對象為self,代理隊列必須為主隊列.
// 初始化服務端socket
self.serverSocket = [[GCDAsyncSocket alloc]initWithDelegate:self delegateQueue:dispatch_get_main_queue()];
4.開放服務端的指定端口.
BOOL result = [self.serverSocket acceptOnPort:[self.portF.text integerValue] error:&error];
5.連接上新的客戶端socket
// 連接上新的客戶端socket
- (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(nonnull GCDAsyncSocket *)newSocket
{
// 保存客戶端的socket
[self.clientSockets addObject: newSocket];
// 添加定時器
[self addTimer];
[self showMessageWithStr:@"鏈接成功"];
[self showMessageWithStr:[NSString stringWithFormat:@"客戶端的地址: %@ -------端口: %d", newSocket.connectedHost, newSocket.connectedPort]];
[newSocket readDataWithTimeout:- 1 tag:0];
}
6.發送數據給客戶端
// socket是保存的客戶端socket, 表示給這個客戶端socket發送消息
- (IBAction)sendMessage:(id)sender
{
if(self.clientSockets == nil) return;
NSData *data = [self.messageTextF.text dataUsingEncoding:NSUTF8StringEncoding];
// withTimeout -1 : 無窮大,一直等
// tag : 消息標記
[self.clientSockets enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
[obj writeData:data withTimeout:-1 tag:0];
}];
}
7.讀取客戶端的數據
/**
讀取客戶端發送的數據
@param sock 客戶端的Socket
@param data 客戶端發送的數據
@param tag 當前讀取的標記
*/
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag
{
NSString *text = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];
[self showMessageWithStr:text];
// 第一次讀取到的數據直接添加
if (self.clientPhoneTimeDicts.count == 0)
{
[self.clientPhoneTimeDicts setObject:[self getCurrentTime] forKey:text];
}
else
{
// 鍵相同,直接覆蓋,值改變
[self.clientPhoneTimeDicts enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) {
[self.clientPhoneTimeDicts setObject:[self getCurrentTime] forKey:text];
}];
}
[sock readDataWithTimeout:- 1 tag:0];
}
8.建立檢測心跳連接.
// 檢測心跳計時器
@property (nonatomic, strong) NSTimer *checkTimer;
// 添加計時器
- (void)addTimer
{
// 長連接定時器
self.checkTimer = [NSTimer scheduledTimerWithTimeInterval:10.0 target:self selector:@selector(checkLongConnect) userInfo:nil repeats:YES];
// 把定時器添加到當前運行循環,并且調為通用模式
[[NSRunLoop currentRunLoop] addTimer:self.checkTimer forMode:NSRunLoopCommonModes];
}
// 檢測心跳
- (void)checkLongConnect
{
[self.clientPhoneTimeDicts enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) {
// 獲取當前時間
NSString *currentTimeStr = [self getCurrentTime];
// 延遲超過10秒判斷斷開
if (([currentTimeStr doubleValue] - [obj doubleValue]) > 10.0)
{
[self showMessageWithStr:[NSString stringWithFormat:@"%@已經斷開,連接時差%f",key,[currentTimeStr doubleValue] - [obj doubleValue]]];
[self showMessageWithStr:[NSString stringWithFormat:@"移除%@",key]];
[self.clientPhoneTimeDicts removeObjectForKey:key];
}
else
{
[self showMessageWithStr:[NSString stringWithFormat:@"%@處于連接狀態,連接時差%f",key,[currentTimeStr doubleValue] - [obj doubleValue]]];
}
}];
}
心跳檢測方法只提供部分思路:
1.懶加載一個可變字典
,字典的鍵
作為客戶端的標識
.如:客戶端標識
為13123456789
.
2.在- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag
方法中,將讀取到的數據
或者數據中的部分字符串
作為鍵.字典的值
為系統當前時間
.服務端第一次讀取數據時,字典中沒有數據,所以,直接添加到可變字典中,之后每次讀取數據時,都用字典的setObject: forKey:
方法添加字典,若存儲的鍵相同
,即客戶端標識相同
,鍵會被覆蓋,再使用系統的當前時間
作為值.
3.在- (void)checkLongConnect
中,獲取此時的當前時間
,遍歷字典,將每個鍵的值和當前時間進行比較即可.判斷的延遲時間可以寫8秒.時間自定.之后,再根據自己的需求進行后續處理.
五.數據粘包處理.
1.粘包情況.
例如:包數據為:abcd
.
接收類型 | 第1次接收 | 第2次接收 | 第3次接收 |
---|---|---|---|
完整型 | abcd | abcdabcd | abcdabcdabcd |
多余型 | abcdab | cdabcdab | cdabcdabcdab |
不完整型 | ab | cda | bcdabc |
2.粘包解決思路.
- 思路1:
發送方將數據包加上包頭
和包尾
,包頭、包體以及包尾
用字典
形式包裝成json字符串
,接收方,通過解析獲取json字符串
中的包體,便可進行進一步處理.
例如:
{
// head:包頭,body:包體,end:包尾
NSDictionary *dict = @{
@"head" : @"phoneNum",
@"body" : @(13133334444),
@"end" : @(11)};
}
-
思路2:
添加前綴.和包內容拼接成同一個字符串.例如:當發送數據是
13133334444
,如果出現粘包情況只屬于完整型
:
13133334444
1313333444413133334444
131333344441313333444413133334444
...
可以將ab
作為前綴.則接收到的數據出現的粘包情況:
ab13133334444
ab13133334444ab13133334444
ab13133334444ab13133334444ab13133334444
...
使用componentsSeparatedByString:
方法,以ab為分隔符,將每個包內容存入數組中,再取對應數組中的數據操作即可. -
思路3:
如果最終要得到的數據的長度是個固定長度
,用一個字符串
作為緩沖池
,每次收到數據,都用字符串
拼接對應數據,每當字符串的長度
和固定長度
相同時,便得到一個完整數據
,處理完這個數據并清空字符串
,再進行下一輪的字符拼接
.例如:處理上面的
不完整型
.創建一個長度是4
的tempData
字符串作為
數據緩沖池.第1次
收到數據,數據是:ab
,tempData
拼接上ab
,tempData
中只能再存儲2個字符,第2次
收到數據,將數據長度
和2
進行比較,第2次的數據是:cda
,截取前兩位字符,即cd
,tempData
繼續拼接cd
,此時,tempData
為abcd
,就是我們想要的數據,我們可以處理這個數據,處理之后并清空tempData
,將第2次收到數據
的剩余數據
,即cda
中的a
,再與tempData
拼接.之后,再進行類似操作. 核心代碼
/**
處理數據粘包
@param readData 讀取到的數據
*/
- (void)dealStickPackageWithData:(NSString *)readData
{
// 緩沖池還需要存儲的數據個數
NSInteger tempCount;
if (readData.length > 0)
{
// 還差tempLength個數填滿緩沖池
tempCount = 4 - self.tempData.length;
if (readData.length <= tempCount)
{
self.tempData = [self.tempData stringByAppendingString:readData];
if (self.tempData.length == 4)
{
[self.mutArr addObject:self.tempData];
self.tempData = @"";
}
}
else
{
// 下一次的數據個數比要填滿緩沖池的數據個數多,一定能拼接成完整數據,剩余的繼續
self.tempData = [self.tempData stringByAppendingString:[readData substringToIndex:tempCount]];
[self.mutArr addObject:self.tempData];
self.tempData = @"";
// 多余的再執行一次方法
[self dealStickPackageWithData:[readData substringFromIndex:tempCount]];
}
}
}
- 調用
// 存儲處理后的每次返回數據
@property (nonatomic, strong) NSMutableArray *mutArr;
// 數據緩沖池
@property (nonatomic, copy) NSString *tempData;
/** 第四次測試 -- 混合型**/
self.mutArr = nil;
/*
第1次 : abc
第2次 : da
第3次 : bcdabcd
第4次 : abcdabcd
第5次 : abcdabcdab
*/
// 數組中的數據代表每次接收的數據
NSArray *testArr4 = [NSArray arrayWithObjects:@"abc",@"da",@"bcdabcd",@"abcdabcd",@"abcdabcdab", nil];
self.tempData = @"";
for (NSInteger i = 0; i < testArr4.count; i++)
{
[self dealStickPackageWithData:testArr4[i]];
}
NSLog(@"testArr4 = %@",self.mutArr);
- 結果:
2017-06-09 00:49:12.932976+0800 StickPackageDealDemo[10063:3430118] testArr4 = (
abcd,
abcd,
abcd,
abcd,
abcd,
abcd,
abcd
)
數據粘包處理Demo在文末.
六.測試.
1.測試配置.
測試時,兩端需要處于同一WiFi下.客戶端中的IP地址為服務端的IP地址,具體信息進入Wifi設置中查看.
2.測試所需環境.
將客戶端程序安裝在每個客戶端,讓一臺服務端測試機和一臺客戶端測試機連接mac并運行,這兩臺測試機可以看到打印結果,所有由服務端發送到客戶端的數據,通過客戶端再回傳給服務端,在服務端看打印結果.
3.進行延遲差測試.
延遲差
即服務端發送數據到第一臺客戶端
和服務端發送數據到最后一臺客戶端
的時間差.根據服務端發送數據給不同數量的客戶端進行測試.而且,發送數據時,是隨機發送.
延遲差測試結果:
由圖所知,延遲差在200毫秒以內的比例基本保持在99%以上.所以符合開發需求(延遲在200毫秒以內).
4.單次信息收發測試.
讓服務端給每個客戶端隨機發送200
次數據.并計算服務端發送數據到某一客戶端,完整的一次收發時間
情況.
單次信息收發測試結果:
由圖所知,一次收發時間基本在95%以上,這個時間會根據網絡狀態和數據包大小波動.不過,可以直觀看到數據從服務端到客戶端的時間.
CSDN
個人博客
GitHub
數據粘包處理Demo
CocoaAsyncSocket客戶端Demo
CocoaAsyncSocket服務端Demo
CocoaAsyncSocket客戶端Demo(含粘包解決和測試)
CocoaAsyncSocket服務端Demo(含粘包解決和測試)