目錄
一艘绍、socket是什么,socket和HTTP的區(qū)別
二秫筏、如何建立一個socket連接
三鞍盗、使用CocoaAsyncSocket實現(xiàn)socket編程
?1、基本實現(xiàn)
?2跳昼、數(shù)據(jù)粘包
?3般甲、心跳保活
?4鹅颊、斷線重連
一敷存、socket是什么,socket和HTTP的區(qū)別
- HTTP是應用層的一個協(xié)議堪伍,而socket不是協(xié)議锚烦、它只是操作系統(tǒng)提供給我們程序員用來操作傳輸層TCP協(xié)議和UDP協(xié)議的一套接口,使用socket其實就是在面向傳輸層的TCP協(xié)議和UDP協(xié)議編程帝雇,說白了使用socket編程我們就是在直接發(fā)送一個TCP請求或UDP請求涮俄;(如果直接使用TCP協(xié)議和UDP協(xié)議編程的話,我們程序員就需要按照這兩個協(xié)議的標準去嚴格組織數(shù)據(jù)格式尸闸,并且還有很多其它的事情要做彻亲,這會非常復雜孕锄,而socket這套接口就方便了我們程序員開發(fā),我們只需要調用簡單的接口即可苞尝,它內部幫我們做了組織數(shù)據(jù)格式等復雜的事情畸肆。這就好比我們發(fā)送一個HTTP請求,如果我們直接使用HTTP協(xié)議編程宙址,那它的數(shù)據(jù)格式也是很復雜的轴脐,包括請求行、請求頭抡砂、請求體等大咱,但是好在我們各個系統(tǒng)都有系統(tǒng)級別的網絡框架,我們只需要使用它們的接口注益,它們內部會做好數(shù)據(jù)格式轉化等復雜的事情碴巾,這樣看起來socket跟網絡框架倒更像是同一級別的概念)
- HTTP協(xié)議在傳輸層對應的是TCP協(xié)議,所以我們常說的HTTP連接其實就是一個TCP連接聊浅,HTTP連接可以是短連接也可以是長連接餐抢,HTTP/1.0采用的是短連接现使,HTTP/1.1采用的是長連接低匙;而socket是TCP協(xié)議和UDP協(xié)議的一套接口,所以我們常說socket連接其實也是一個TCP連接碳锈,同樣socket連接可以是短連接也可以是長連接顽冶,這取決于我們怎么使用,當然socket基于UDP時甚至都不存在連接售碳,所以不要把socket連接和長連接劃等號强重;(順便說一下長連接和短連接,長連接和短連接是應用層或者傳輸層應用的一個概念贸人,而不是傳輸層的概念间景,傳輸層只有連接這個概念,它本身沒有長短之分艺智,只是因為應用層或者傳輸層應用才導致了這個連接有長短之分倘要。比如HTTP/1.0采用的是短連接,也就是說TCP三次握手之后十拣,建立了一個連接封拧,客戶端發(fā)送一個請求,服務器給一個響應夭问,此時TCP就要四次揮手了泽西,連接就斷開了,想再傳數(shù)據(jù)的話就得再三次握手建立連接四次揮手斷開連接缰趋,也就是說短連接一次只處理一個輸入流和輸出流捧杉;而HTTP/1.1采用的是長連接陕见,TCP三次握手之后,建立了一個連接糠溜,客戶端發(fā)送一個請求淳玩,服務器給一個響應,TCP并不揮手非竿,連接也沒斷開蜕着,雙方還可以繼續(xù)傳遞下一次數(shù)據(jù)、下一次數(shù)據(jù)數(shù)據(jù)......红柱,直到雙方等了某個時間段發(fā)現(xiàn)雙方都不需要傳遞數(shù)據(jù)了承匣,才會四次揮手斷開連接,也就是說長連接一次可以處理若干個輸入流和輸出流锤悄。又比如socket可以建立一個長連接韧骗,這就意味著TCP的那個連接要處理若干個輸入流和輸出流,socket也可以建立短連接零聚,這就意味著TCP的那個連接只處理一個輸入流和輸出流袍暴。長連接和短連接不是靠時間長短來界定的,而是靠一次處理的輸入流和輸出流個數(shù)來界定的)
- HTTP主要用來做客戶端發(fā)一個請求隶症、服務端就給一個響應這樣的場景政模,而socket主要用來做客戶端和服務端都能主動給對方發(fā)數(shù)據(jù)的場景,通常情況下蚂会,單臺服務器可以支持十萬個socket連接的并發(fā)淋样。
二、如何建立一個socket連接
服務端socket要做五件事胁住,客戶端socket要做三件事:
- 服務端socket初始化趁猴、客戶端socket初始化
- 服務端socket調用
bind()
函數(shù)蜗字,綁定IP地址和端口 -
服務端socket調用
listen()
函數(shù)牺丙,設置監(jiān)聽隊列有多長(比如說我們設置監(jiān)聽隊列的長度為10匪凡,這就意味著最多可以有10個客戶端可以嘗試連接服務器齐苛,而第11個客戶端會被告知服務器太忙了) - 服務端socket調用
accept()
函數(shù)鉴嗤,等待來自客戶端socket的連接請求 - 客戶端socket調用
connect()
函數(shù)毁菱,向指定IP地址和端口的服務器發(fā)起連接請求 - 服務端socket的
accept()
函數(shù)返回用于傳輸?shù)膕ocket的文件描述符周拐,連接建立成功
接下來雙方就可以通過read()
和write()
函數(shù)通信了涡戳,雙方也都可以通過close()
函數(shù)主動斷開連接浪规。
三或听、使用CocoaAsyncSocket實現(xiàn)socket編程
1、基本實現(xiàn)
- 服務端socket
#import "ViewController.h"
#import "GCDAsyncSocket.h"
#define kServerIPAddress @"192.168.148.63"
#define kServerPort 8080
@interface ViewController () <GCDAsyncSocketDelegate>
/// 服務端socket
@property (strong, nonatomic) GCDAsyncSocket *serverSocket;
/// 所有的客戶端socket
@property (strong, nonatomic) NSMutableArray *clientSockets;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
}
#pragma mark - GCDAsyncSocketDelegate
/// 連接客戶端成功的回調
- (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(nonnull GCDAsyncSocket *)newSocket {
NSLog(@"===========>連接客戶端成功");
// 保存客戶端socket
[self.clientSockets addObject:newSocket];
// 連接成功后笋婿,立馬開始讀取客戶端的數(shù)據(jù)誉裆,調用這個讀取方法后,當客戶端有數(shù)據(jù)發(fā)過來時就會觸發(fā)“讀取客戶端數(shù)據(jù)成功的回調”
[newSocket readDataWithTimeout:-1 tag:0];
}
/// 讀取客戶端數(shù)據(jù)成功的回調
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {
// 此處我們不對數(shù)據(jù)做過多的處理缸濒,只是把它轉換成JSON字符串打印出來看看
NSString *jsonString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"%@", [NSString stringWithFormat:@"===========>讀取客戶端數(shù)據(jù)成功:%@", jsonString]);
// 注意:讀取客戶端數(shù)據(jù)成功后足丢,在這里需要再調用一次讀取客戶端數(shù)據(jù)的方法粱腻,框架本身就是這么設計的,否則我們就只能接收一次數(shù)據(jù)斩跌,之后再也接收不到數(shù)據(jù)
[sock readDataWithTimeout:-1 tag:0];
}
/// 與客戶端斷開連接的回調
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err {
NSLog(@"%@", [NSString stringWithFormat:@"===========>與客戶端斷開連接:%@", err]);
if (err == nil) { // 代表是服務端主動斷開連接绍些,我們不做處理,客戶端那邊收到回調后會走斷線重連的邏輯
NSLog(@"===========>服務端主動斷開連接");
} else { // 代表是客戶端斷開連接耀鸦,移除斷開的客戶端
[self.clientSockets removeObject:sock];
}
}
#pragma mark - action
/// 服務端開始監(jiān)聽來自客戶端的連接請求柬批,收到請求后就建立連接,連接成功后會觸發(fā)“連接客戶端成功的回調”
- (IBAction)listen:(id)sender {
NSError *error = nil;
BOOL success = [self.serverSocket acceptOnPort:kServerPort error:&error];
if (error == nil && success) {
NSLog(@"===========>服務端開始監(jiān)聽成功");
} else {
NSLog(@"%@", [NSString stringWithFormat:@"==========>服務端開始監(jiān)聽失斝涠:%@", error]);
}
}
/// 向客戶端發(fā)送數(shù)據(jù)(以JSON字符串的格式傳輸)
- (IBAction)send:(id)sender {
if (self.clientSockets == nil) {
return;
}
// 數(shù)據(jù)1
NSDictionary *dictionary1 = @{
@"data": @"江南憶氮帐,最憶是杭州。山寺月中尋桂子洛姑,郡亭枕上看潮頭上沐。何日更重游?",
};
NSData *jsonData1 = [NSJSONSerialization dataWithJSONObject:dictionary1 options:NSJSONWritingPrettyPrinted error:nil];
NSString *jsonString1 = [[NSString alloc] initWithData:jsonData1 encoding:NSUTF8StringEncoding];
NSData *data1 = [[jsonString1 dataUsingEncoding:NSUTF8StringEncoding] mutableCopy];
// 數(shù)據(jù)2
NSDictionary *dictionary2 = @{
@"data": @"江南憶楞艾,其次憶吳宮参咙。吳酒一杯春竹葉,吳娃雙舞醉芙蓉硫眯。早晚復相逢蕴侧?",
};
NSData *jsonData2 = [NSJSONSerialization dataWithJSONObject:dictionary2 options:NSJSONWritingPrettyPrinted error:nil];
NSString *jsonString2 = [[NSString alloc] initWithData:jsonData2 encoding:NSUTF8StringEncoding];
NSData *data2 = [[jsonString2 dataUsingEncoding:NSUTF8StringEncoding] mutableCopy];
[self.clientSockets enumerateObjectsUsingBlock:^(id _Nonnull clientSocket, NSUInteger idx, BOOL * _Nonnull stop) {
// 給客戶端發(fā)送數(shù)據(jù)1(tag:消息標記)
[clientSocket writeData:data1 withTimeout:-1 tag:0];
// 給客戶端發(fā)送數(shù)據(jù)2(tag:消息標記)
[clientSocket writeData:data2 withTimeout:-1 tag:0];
}];
}
/// 斷開與客戶端的連接
- (IBAction)disconnect:(id)sender {
[self.serverSocket disconnect];
self.serverSocket.delegate = nil;
self.serverSocket = nil;
}
#pragma mark - setter, getter
- (GCDAsyncSocket *)serverSocket {
if (_serverSocket == nil) {
_serverSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue()];
}
return _serverSocket;
}
- (NSMutableArray *)clientSockets {
if (_clientSockets == nil) {
_clientSockets = [NSMutableArray array];
}
return _clientSockets;
}
@end
- 客戶端socket
#import "ViewController.h"
#import "GCDAsyncSocket.h"
#define kServerIPAddress @"192.168.148.63"
#define kServerPort 8080
@interface ViewController () <GCDAsyncSocketDelegate>
/// 客戶端socket
@property (strong, nonatomic) GCDAsyncSocket *clientSocket;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
}
#pragma mark - GCDAsyncSocketDelegate
/// 連接服務端成功的回調
- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port {
NSLog(@"===========>連接服務端成功");
// 連接成功后,立馬開始讀取服務端的數(shù)據(jù)舟铜,調用這個讀取方法后戈盈,當服務端有數(shù)據(jù)發(fā)過來時就會觸發(fā)“讀取服務端數(shù)據(jù)成功的回調”
[sock readDataWithTimeout:-1 tag:0];
}
/// 讀取服務端數(shù)據(jù)成功的回調
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {
// 此處我們不對數(shù)據(jù)做過多的處理奠衔,只是把它轉換成JSON字符串打印出來看看
NSString *jsonString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"%@", [NSString stringWithFormat:@"===========>讀取服務端數(shù)據(jù)成功:%@", jsonString]);
// 注意:讀取服務端數(shù)據(jù)成功后谆刨,在這里需要再調用一次讀取服務端數(shù)據(jù)的方法,框架本身就是這么設計的归斤,否則我們就只能接收一次數(shù)據(jù)痊夭,之后再也接收不到數(shù)據(jù)
[sock readDataWithTimeout:-1 tag:0];
}
/// 與服務端斷開連接的回調
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err {
NSLog(@"%@", [NSString stringWithFormat:@"===========>與服務端斷開連接:%@", err]);
if (err == nil) { // 代表是客戶端主動斷開連接,我們不做處理脏里,服務端那邊收到回調后她我,會移除跟指定客戶端的連接
NSLog(@"===========>客戶端主動斷開連接");
} else { // 其它情況下斷開連接,暫時不做處理
}
}
#pragma mark - action
/// 向服務端發(fā)起連接請求
- (IBAction)connect:(id)sender {
NSError *error = nil;
BOOL success = [self.clientSocket connectToHost:kServerIPAddress onPort:kServerPort viaInterface:nil withTimeout:-1 error:&error];
if (error == nil && success) {
// 連接服務端成功后會觸發(fā)“連接服務端成功的回調”
} else {
NSLog(@"%@", [NSString stringWithFormat:@"===========>連接服務端失斊群帷:%@", error]);
}
}
/// 向服務端發(fā)送數(shù)據(jù)(以JSON字符串的格式傳輸)
- (IBAction)send:(id)sender {
if (self.clientSocket == nil) {
return;
}
// 數(shù)據(jù)1
NSDictionary *dictionary1 = @{
@"data": @"水光瀲滟晴方好番舆,山色空蒙雨亦奇。 ",
};
NSData *jsonData1 = [NSJSONSerialization dataWithJSONObject:dictionary1 options:NSJSONWritingPrettyPrinted error:nil];
NSString *jsonString1 = [[NSString alloc] initWithData:jsonData1 encoding:NSUTF8StringEncoding];
NSData *data1 = [[jsonString1 dataUsingEncoding:NSUTF8StringEncoding] mutableCopy];
// 數(shù)據(jù)2
NSDictionary *dictionary2 = @{
@"data": @"欲把西湖比西子矾踱,淡妝濃抹總相宜恨狈。",
};
NSData *jsonData2 = [NSJSONSerialization dataWithJSONObject:dictionary2 options:NSJSONWritingPrettyPrinted error:nil];
NSString *jsonString2 = [[NSString alloc] initWithData:jsonData2 encoding:NSUTF8StringEncoding];
NSData *data2 = [[jsonString2 dataUsingEncoding:NSUTF8StringEncoding] mutableCopy];
// 給服務端發(fā)送數(shù)據(jù)1(tag:消息標記)
[self.clientSocket writeData:data1 withTimeout:-1 tag:0];
// 給服務端發(fā)送數(shù)據(jù)2(tag:消息標記)
[self.clientSocket writeData:data2 withTimeout:-1 tag:0];
}
/// 斷開與服務端的連接
- (IBAction)disconnect:(id)sender {
[self.clientSocket disconnect];
self.clientSocket.delegate = nil;
self.clientSocket = nil;
}
#pragma mark - setter, getter
- (GCDAsyncSocket *)clientSocket {
if (_clientSocket == nil) {
_clientSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue()];
}
return _clientSocket;
}
@end
以上我們就基本實現(xiàn)了socket編程,客戶端和服務端之間就可以相互通信了呛讲。但是在實際開發(fā)中禾怠,我們還需要考慮三個問題:數(shù)據(jù)粘包返奉、心跳保活和斷線重連吗氏。
2芽偏、數(shù)據(jù)粘包
2.1 數(shù)據(jù)粘包是什么
上面的例子中,我們預期的效果是客戶端點擊一次發(fā)送弦讽,給服務端發(fā)送兩條數(shù)據(jù)污尉,服務端觸發(fā)兩次“收到客戶端數(shù)據(jù)的回調”,然后分別打油:
===========>讀取客戶端數(shù)據(jù)成功:{
"data" : "水光瀲滟晴方好十厢,山色空蒙雨亦奇。 "
}
===========>讀取客戶端數(shù)據(jù)成功:{
"data" : "欲把西湖比西子捂齐,淡妝濃抹總相宜蛮放。"
}
但實際上兩條數(shù)據(jù)被合并成一條數(shù)據(jù)發(fā)送給服務端了,服務端只觸發(fā)了一次“收到客戶端數(shù)據(jù)的回調”奠宜,也只打印了一次:
===========>讀取客戶端數(shù)據(jù)成功:{
"data" : "水光瀲滟晴方好包颁,山色空蒙雨亦奇。 "
}{
"data" : "欲把西湖比西子压真,淡妝濃抹總相宜娩嚼。"
}
這就是數(shù)據(jù)粘包——多條數(shù)據(jù)被合并成了一條數(shù)據(jù)傳輸。
2.2 為什么會出現(xiàn)數(shù)據(jù)粘包
我們知道TCP有個發(fā)送緩存滴肿,有些情況下TCP并不是有一條數(shù)據(jù)就發(fā)一條數(shù)據(jù)岳悟,而是等發(fā)送緩存滿了,再把發(fā)送緩存里的多條數(shù)據(jù)一起發(fā)送出去泼差,這就會導致數(shù)據(jù)粘包贵少。
此外TCP還采用了Nagle優(yōu)化算法來打包數(shù)據(jù),它會將多次間隔較小且數(shù)據(jù)量較小的數(shù)據(jù)自動合并成一個比較大的數(shù)據(jù)一塊兒傳輸堆缘,這也會導致數(shù)據(jù)粘包滔灶。
2.3 怎么處理數(shù)據(jù)粘包
處理數(shù)據(jù)粘包也很簡單,核心思路就是:發(fā)送方在發(fā)送數(shù)據(jù)的時候先給每條數(shù)據(jù)都添加一個包頭吼肥,包頭里存放的關鍵信息就是真實數(shù)據(jù)的長度录平,當然也可以存放更多的業(yè)務信息,此外包頭的尾部還需要拼接一個包頭結束標識——回車換行符缀皱,以便將來接收方讀取數(shù)據(jù)時可以根據(jù)這個包頭結束標識優(yōu)先讀取到包頭數(shù)據(jù)斗这。接收方調用指定的讀取方法優(yōu)先讀取到包頭數(shù)據(jù),然后根據(jù)包頭里的長度信息再去精準讀取指定長度的真實數(shù)據(jù)啤斗,這樣就可以讀取到一條完整的數(shù)據(jù)了表箭,然后再讀取下一條數(shù)據(jù)就不會粘包了。
- 服務端socket(相對于上面的例子争占,變化的代碼)
// 讀取處理
/// 包頭
@property (strong, nonatomic) NSDictionary *headerDictionary;
#pragma mark - GCDAsyncSocketDelegate
/// 連接客戶端成功的回調
- (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(nonnull GCDAsyncSocket *)newSocket {
NSLog(@"===========>連接客戶端成功");
// 保存客戶端socket
[self.clientSockets addObject:newSocket];
// 連接成功后燃逻,立馬開始讀取客戶端的數(shù)據(jù)序目,調用這個讀取方法后,當客戶端有數(shù)據(jù)發(fā)過來時就會觸發(fā)“讀取客戶端數(shù)據(jù)成功的回調”
// [newSocket readDataWithTimeout:-1 tag:0];
// 連接成功后伯襟,立馬開始讀取客戶端的數(shù)據(jù)猿涨,調用這個讀取方法后,當客戶端有數(shù)據(jù)發(fā)過來時就會觸發(fā)“讀取客戶端數(shù)據(jù)成功的回調”姆怪,但跟上面方法不同的是這個方法只會從數(shù)據(jù)的開頭起讀取到包頭結束標識為止叛赚,也就是說“讀取客戶端數(shù)據(jù)成功的回調”的data只會讀取到包頭數(shù)據(jù)
[newSocket readDataToData:[GCDAsyncSocket CRLFData] withTimeout:-1 tag:0];
}
/// 讀取客戶端數(shù)據(jù)成功的回調
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {
if (self.headerDictionary == nil) { // 如果包頭為空,就代表之前沒讀取到過包頭稽揭,那本次回調的觸發(fā)肯定就是讀取到包頭了俺附,因為上面采用的是readDataToData讀取方法讀取到包頭結束標識為止
self.headerDictionary = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:nil];
if (self.headerDictionary == nil) {
NSLog(@"===========>數(shù)據(jù)格式出錯了:包頭數(shù)據(jù)為空");
return;
}
// 獲取包頭的內容1——真實數(shù)據(jù)的長度
NSInteger size = [self.headerDictionary[@"size"] integerValue];
// 此時再調用這個讀取方法去讀取指定長度的真實數(shù)據(jù),讀取到真實數(shù)據(jù)后還是會觸發(fā)當前這個回調
[sock readDataToLength:size withTimeout:-1 tag:0];
return;
}
// 如果走到這里溪掀,代表是讀取到了真實數(shù)據(jù)
NSInteger size = [self.headerDictionary[@"size"] integerValue];
// 說明數(shù)據(jù)有問題
if (size <= 0 || data.length != size) {
NSLog(@"===========>數(shù)據(jù)格式出錯了:真實數(shù)據(jù)大小不正確");
return;
}
// 此處我們不對數(shù)據(jù)做過多的處理事镣,只是把它轉換成JSON字符串打印出來看看
NSString *jsonString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"%@", [NSString stringWithFormat:@"===========>讀取客戶端數(shù)據(jù)成功:%@", jsonString]);
// 置位包頭
self.headerDictionary = nil;
// 注意:讀取客戶端數(shù)據(jù)成功后,在這里需要再調用一次讀取客戶端數(shù)據(jù)的方法揪胃,框架本身就是這么設計的璃哟,否則我們就只能接收一次數(shù)據(jù),之后再也接收不到數(shù)據(jù)
// [sock readDataWithTimeout:-1 tag:0];
// 繼續(xù)讀取下一條數(shù)據(jù)
[sock readDataToData:[GCDAsyncSocket CRLFData] withTimeout:-1 tag:0];
}
// 發(fā)送處理
#pragma mark - action
/// 向客戶端發(fā)送數(shù)據(jù)(以JSON字符串的格式傳輸)
- (IBAction)send:(id)sender {
if (self.clientSockets == nil) {
return;
}
// 數(shù)據(jù)1
NSDictionary *dictionary1 = @{
@"data": @"江南憶喊递,最憶是杭州随闪。山寺月中尋桂子,郡亭枕上看潮頭骚勘。何日更重游铐伴?",
};
NSData *jsonData1 = [NSJSONSerialization dataWithJSONObject:dictionary1 options:NSJSONWritingPrettyPrinted error:nil];
NSString *jsonString1 = [[NSString alloc] initWithData:jsonData1 encoding:NSUTF8StringEncoding];
NSData *data1 = [[jsonString1 dataUsingEncoding:NSUTF8StringEncoding] mutableCopy];
NSData *finalData1 = [self dataWithHeader:data1];
// 數(shù)據(jù)2
NSDictionary *dictionary2 = @{
@"data": @"江南憶,其次憶吳宮俏讹。吳酒一杯春竹葉当宴,吳娃雙舞醉芙蓉。早晚復相逢藐石?",
};
NSData *jsonData2 = [NSJSONSerialization dataWithJSONObject:dictionary2 options:NSJSONWritingPrettyPrinted error:nil];
NSString *jsonString2 = [[NSString alloc] initWithData:jsonData2 encoding:NSUTF8StringEncoding];
NSData *data2 = [[jsonString2 dataUsingEncoding:NSUTF8StringEncoding] mutableCopy];
NSData *finalData2 = [self dataWithHeader:data2];
[self.clientSockets enumerateObjectsUsingBlock:^(id _Nonnull clientSocket, NSUInteger idx, BOOL * _Nonnull stop) {
// 給客戶端發(fā)送數(shù)據(jù)1(tag:消息標記)
[clientSocket writeData:finalData1 withTimeout:-1 tag:0];
// 給客戶端發(fā)送數(shù)據(jù)2(tag:消息標記)
[clientSocket writeData:finalData2 withTimeout:-1 tag:0];
}];
}
#pragma mark - private method
/// 獲取拼接了包頭后的數(shù)據(jù)
- (NSData *)dataWithHeader:(NSData *)data {
// 包頭
NSMutableDictionary *headerDictionary = [NSMutableDictionary dictionary];
// 包頭里的內容1——真實數(shù)據(jù)的長度
[headerDictionary setObject:[NSString stringWithFormat:@"%ld", data.length] forKey:@"size"];
// 把字典格式的包頭轉化成JSON字符串格式的包頭
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:headerDictionary options:NSJSONWritingPrettyPrinted error:nil];
NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
// 得到JSON字符串格式的包頭數(shù)據(jù)
NSMutableData *headerData = [[jsonString dataUsingEncoding:NSUTF8StringEncoding] mutableCopy];
// 包頭里的內容2——包頭結束標識即供,回車換行符
[headerData appendData:[GCDAsyncSocket CRLFData]];
NSMutableData *finalData = headerData;
// 在包頭數(shù)據(jù)后面拼接上真實數(shù)據(jù)
[finalData appendData:data];
return finalData;
}
- 客戶端socket(相對于上面的例子定拟,變化的代碼)
// 讀取處理
/// 包頭
@property (strong, nonatomic) NSDictionary *headerDictionary;
#pragma mark - GCDAsyncSocketDelegate
/// 連接服務端成功的回調
- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port {
NSLog(@"===========>連接服務端成功");
// 連接成功后于微,立馬開始讀取服務端的數(shù)據(jù),調用這個讀取方法后青自,當服務端有數(shù)據(jù)發(fā)過來時就會觸發(fā)“讀取服務端數(shù)據(jù)成功的回調”
// [sock readDataWithTimeout:-1 tag:0];
// 連接成功后株依,立馬開始讀取服務端的數(shù)據(jù),調用這個讀取方法后延窜,當服務端有數(shù)據(jù)發(fā)過來時就會觸發(fā)“讀取服務端數(shù)據(jù)成功的回調”恋腕,但跟上面方法不同的是這個方法只會從數(shù)據(jù)的開頭起讀取到包頭結束標識為止,也就是說“讀取服務端數(shù)據(jù)成功的回調”的data只會讀取到包頭數(shù)據(jù)
[sock readDataToData:[GCDAsyncSocket CRLFData] withTimeout:-1 tag:0];
}
/// 讀取服務端數(shù)據(jù)成功的回調
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {
if (self.headerDictionary == nil) { // 如果包頭為空逆瑞,就代表之前沒讀取到過包頭荠藤,那本次回調的觸發(fā)肯定就是讀取到包頭了伙单,因為上面采用的是readDataToData讀取方法讀取到包頭結束標識為止
self.headerDictionary = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:nil];
if (self.headerDictionary == nil) {
NSLog(@"===========>數(shù)據(jù)格式出錯了:包頭數(shù)據(jù)為空");
return;
}
// 獲取包頭的內容1——真實數(shù)據(jù)的長度
NSInteger size = [self.headerDictionary[@"size"] integerValue];
// 此時再調用這個讀取方法去讀取指定長度的真實數(shù)據(jù),讀取到真實數(shù)據(jù)后還是會觸發(fā)當前這個回調
[sock readDataToLength:size withTimeout:-1 tag:0];
return;
}
// 如果走到這里哈肖,代表是讀取到了真實數(shù)據(jù)
NSInteger size = [self.headerDictionary[@"size"] integerValue];
// 說明數(shù)據(jù)有問題
if (size <= 0 || data.length != size) {
NSLog(@"===========>數(shù)據(jù)格式出錯了:真實數(shù)據(jù)大小不正確");
return;
}
// 此處我們不對數(shù)據(jù)做過多的處理吻育,只是把它轉換成JSON字符串打印出來看看
NSString *jsonString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"%@", [NSString stringWithFormat:@"===========>讀取服務端數(shù)據(jù)成功:%@", jsonString]);
// 置位包頭
self.headerDictionary = nil;
// 注意:讀取服務端數(shù)據(jù)成功后,在這里需要再調用一次讀取服務端數(shù)據(jù)的方法淤井,框架本身就是這么設計的布疼,否則我們就只能接收一次數(shù)據(jù),之后再也接收不到數(shù)據(jù)
// [sock readDataWithTimeout:-1 tag:0];
// 繼續(xù)讀取下一條數(shù)據(jù)
[sock readDataToData:[GCDAsyncSocket CRLFData] withTimeout:-1 tag:0];
}
// 發(fā)送處理
#pragma mark - action
/// 向服務端發(fā)送數(shù)據(jù)(以JSON字符串的格式傳輸)
- (IBAction)send:(id)sender {
if (self.clientSocket == nil) {
return;
}
// 數(shù)據(jù)1
NSDictionary *dictionary1 = @{
@"data": @"水光瀲滟晴方好币狠,山色空蒙雨亦奇游两。 ",
};
NSData *jsonData1 = [NSJSONSerialization dataWithJSONObject:dictionary1 options:NSJSONWritingPrettyPrinted error:nil];
NSString *jsonString1 = [[NSString alloc] initWithData:jsonData1 encoding:NSUTF8StringEncoding];
NSData *data1 = [[jsonString1 dataUsingEncoding:NSUTF8StringEncoding] mutableCopy];
NSData *finalData1 = [self dataWithHeader:data1];
// 數(shù)據(jù)2
NSDictionary *dictionary2 = @{
@"data": @"欲把西湖比西子,淡妝濃抹總相宜漩绵。",
};
NSData *jsonData2 = [NSJSONSerialization dataWithJSONObject:dictionary2 options:NSJSONWritingPrettyPrinted error:nil];
NSString *jsonString2 = [[NSString alloc] initWithData:jsonData2 encoding:NSUTF8StringEncoding];
NSData *data2 = [[jsonString2 dataUsingEncoding:NSUTF8StringEncoding] mutableCopy];
NSData *finalData2 = [self dataWithHeader:data2];
// 給服務端發(fā)送數(shù)據(jù)1(tag:消息標記)
[self.clientSocket writeData:finalData1 withTimeout:-1 tag:0];
// 給服務端發(fā)送數(shù)據(jù)2(tag:消息標記)
[self.clientSocket writeData:finalData2 withTimeout:-1 tag:0];
}
#pragma mark - private method
/// 獲取拼接了包頭后的數(shù)據(jù)
- (NSData *)dataWithHeader:(NSData *)data {
// 包頭
NSMutableDictionary *headerDictionary = [NSMutableDictionary dictionary];
// 包頭里的內容1——真實數(shù)據(jù)的長度
[headerDictionary setObject:[NSString stringWithFormat:@"%ld", data.length] forKey:@"size"];
// 把字典格式的包頭轉化成JSON字符串格式的包頭
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:headerDictionary options:NSJSONWritingPrettyPrinted error:nil];
NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
// 得到JSON字符串格式的包頭數(shù)據(jù)
NSMutableData *headerData = [[jsonString dataUsingEncoding:NSUTF8StringEncoding] mutableCopy];
// 包頭里的內容2——包頭結束標識贱案,回車換行符
[headerData appendData:[GCDAsyncSocket CRLFData]];
NSMutableData *finalData = headerData;
// 在包頭數(shù)據(jù)后面拼接上真實數(shù)據(jù)
[finalData appendData:data];
return finalData;
}
3、心跳敝雇拢活
正常來說轰坊,socket連接一旦建立之后就會一直掛在那里,直到某一端主動斷開連接祟印。但實際上肴沫,運營商在檢測到鏈路上有一段時間無數(shù)據(jù)傳輸時,就會自動斷開這種處于非活躍狀態(tài)的連接蕴忆,這就是所謂的運營商NAT超時颤芬,超時時間為5分鐘。因此我們就需要做心跳碧锥欤活——即客戶端每隔一定的時間間隔就向服務端發(fā)送一個心跳數(shù)據(jù)包站蝠,用來保證當前socket連接處于活躍狀態(tài),避免運營商把我們的連接中斷卓鹿,這個時間間隔我們取的是3分鐘菱魔,服務器在收到心跳包時不當做真實數(shù)據(jù)處理即可。
- 服務端socket(相對于上面的例子吟孙,變化的代碼)
#pragma mark - GCDAsyncSocketDelegate
/// 讀取客戶端數(shù)據(jù)成功的回調
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {
if (self.headerDictionary == nil) { // 如果包頭為空澜倦,就代表之前沒讀取到過包頭,那本次回調的觸發(fā)肯定就是讀取到包頭了杰妓,因為上面采用的是readDataToData讀取方法讀取到包頭結束標識為止
self.headerDictionary = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:nil];
if (self.headerDictionary == nil) {
NSLog(@"===========>數(shù)據(jù)格式出錯了:包頭數(shù)據(jù)為空");
return;
}
// 獲取包頭的內容1——真實數(shù)據(jù)的長度
NSInteger size = [self.headerDictionary[@"size"] integerValue];
// 此時再調用這個讀取方法去讀取指定長度的真實數(shù)據(jù)藻治,讀取到真實數(shù)據(jù)后還是會觸發(fā)當前這個回調
[sock readDataToLength:size withTimeout:-1 tag:0];
return;
}
// 如果走到這里,代表是讀取到了真實數(shù)據(jù)
NSInteger size = [self.headerDictionary[@"size"] integerValue];
// 說明數(shù)據(jù)有問題
if (size <= 0 || data.length != size) {
NSLog(@"===========>數(shù)據(jù)格式出錯了:真實數(shù)據(jù)大小不正確");
return;
}
NSDictionary *dictionary = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
if ([dictionary[@"data"] isEqual:@"com.zhangshuo.alive"]) { // 如果是心跳包巷挥,我們不做處理
NSLog(@"===========>我是心跳包");
} else { // 如果不是心跳包桩卵,我們再做處理
// 此處我們不對數(shù)據(jù)做過多的處理,只是把它轉換成JSON字符串打印出來看看
NSString *jsonString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"%@", [NSString stringWithFormat:@"===========>讀取客戶端數(shù)據(jù)成功:%@", jsonString]);
}
// 置位包頭
self.headerDictionary = nil;
// 注意:讀取客戶端數(shù)據(jù)成功后,在這里需要再調用一次讀取客戶端數(shù)據(jù)的方法雏节,框架本身就是這么設計的胜嗓,否則我們就只能接收一次數(shù)據(jù),之后再也接收不到數(shù)據(jù)
// [sock readDataWithTimeout:-1 tag:0];
// 繼續(xù)讀取下一條數(shù)據(jù)
[sock readDataToData:[GCDAsyncSocket CRLFData] withTimeout:-1 tag:0];
}
- 客戶端socket(相對于上面的例子钩乍,變化的代碼)
#define kHeartBeatTimeInterval 60 * 3 // 心跳奔嫒铮活時間間隔:3分鐘
/// 心跳保活定時器
@property (nonatomic, strong) NSTimer *heartBeatTimer;
#pragma mark - GCDAsyncSocketDelegate
/// 連接服務端成功的回調
- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port {
NSLog(@"===========>連接服務端成功");
// 連接成功后件蚕,立馬開始讀取服務端的數(shù)據(jù)孙技,調用這個讀取方法后,當服務端有數(shù)據(jù)發(fā)過來時就會觸發(fā)“讀取服務端數(shù)據(jù)成功的回調”
// [sock readDataWithTimeout:-1 tag:0];
// 連接成功后排作,立馬開始讀取服務端的數(shù)據(jù)牵啦,調用這個讀取方法后,當服務端有數(shù)據(jù)發(fā)過來時就會觸發(fā)“讀取服務端數(shù)據(jù)成功的回調”妄痪,但跟上面方法不同的是這個方法只會從數(shù)據(jù)的開頭起讀取到包頭結束標識為止哈雏,也就是說“讀取服務端數(shù)據(jù)成功的回調”的data只會讀取到包頭數(shù)據(jù)
[sock readDataToData:[GCDAsyncSocket CRLFData] withTimeout:-1 tag:0];
// 添加心跳保活
[self addHeartBeat];
}
/// 與服務端斷開連接的回調
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err {
NSLog(@"%@", [NSString stringWithFormat:@"===========>與服務端斷開連接:%@", err]);
// 移除心跳鄙郎活
[self removeHeartBeat];
}
#pragma mark - 心跳鄙驯瘢活
// 添加心跳保活
- (void)addHeartBeat {
[self removeHeartBeat];
// 心跳時間設置為3分鐘罪针,NAT超時一般為3~5分鐘
self.heartBeatTimer = [NSTimer scheduledTimerWithTimeInterval:kHeartBeatTimeInterval repeats:YES block:^(NSTimer * _Nonnull timer) {
// 和服務端約定好心跳迸砀活數(shù)據(jù)包的內容,以便它們讀取泪酱,盡可能減小心跳迸梢螅活數(shù)據(jù)包的大小
NSDictionary *dictionary = @{
@"data": @"com.zhangshuo.alive",
};
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:dictionary options:NSJSONWritingPrettyPrinted error:nil];
NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
NSData *data = [[jsonString dataUsingEncoding:NSUTF8StringEncoding] mutableCopy];
NSData *finalData = [self dataWithHeader:data];
// 給服務端發(fā)送數(shù)據(jù)1(tag:消息標記)
[self.clientSocket writeData:finalData withTimeout:-1 tag:0];
}];
[[NSRunLoop currentRunLoop]addTimer:self.heartBeatTimer forMode:NSRunLoopCommonModes];
}
// 移除心跳保活
- (void)removeHeartBeat {
[self.heartBeatTimer invalidate];
self.heartBeatTimer = nil;
}
4墓阀、斷線重連
客戶端主動斷開連接時(如App退出登錄或者App進入后臺等場景)毡惜,我們不需要做斷線重連;其它情況下如果連接斷開了(如服務器出了問題或者網斷了等場景)斯撮,我們就需要做斷線重連经伙,來盡量使連接處于正常連接的狀態(tài),這樣才能保證業(yè)務的正常運行勿锅。具體做法就是帕膜,當客戶端檢測到跟服務端斷開連接時就啟動第一次斷線重連,2秒后啟動第二次斷線重連粱甫,再隔4秒后啟動第三次斷線重連泳叠,如果三次斷線重連還沒成功,就認為是服務器出了問題茶宵,不再重連。
/// 斷線重連總時長
@property (assign, nonatomic) NSTimeInterval reconnectDuration;
#pragma mark - GCDAsyncSocketDelegate
/// 連接服務端成功的回調
- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port {
NSLog(@"===========>連接服務端成功");
// 連接成功后宗挥,立馬開始讀取服務端的數(shù)據(jù)乌庶,調用這個讀取方法后种蝶,當服務端有數(shù)據(jù)發(fā)過來時就會觸發(fā)“讀取服務端數(shù)據(jù)成功的回調”
// [sock readDataWithTimeout:-1 tag:0];
// 連接成功后,立馬開始讀取服務端的數(shù)據(jù)瞒大,調用這個讀取方法后螃征,當服務端有數(shù)據(jù)發(fā)過來時就會觸發(fā)“讀取服務端數(shù)據(jù)成功的回調”,但跟上面方法不同的是這個方法只會從數(shù)據(jù)的開頭起讀取到包頭結束標識為止透敌,也就是說“讀取服務端數(shù)據(jù)成功的回調”的data只會讀取到包頭數(shù)據(jù)
[sock readDataToData:[GCDAsyncSocket CRLFData] withTimeout:-1 tag:0];
// 添加心跳倍⒐觯活
[self addHeartBeat];
// 斷線重連成功后,置位斷線重連總時長
self.reconnectDuration = 0;
}
/// 與服務端斷開連接的回調
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err {
NSLog(@"%@", [NSString stringWithFormat:@"===========>與服務端斷開連接:%@", err]);
// 移除心跳毙锏纾活
[self removeHeartBeat];
if (err == nil) { // 代表是客戶端主動斷開連接魄藕,我們不做處理,服務端那邊收到回調后撵术,會移除跟指定客戶端的連接
NSLog(@"===========>客戶端主動斷開連接");
} else { // 其它情況下斷開連接背率,啟動斷線重連
[self reconnect];
}
}
#pragma mark - 斷線重連
// 斷線重連
- (void)reconnect {
if (self.clientSocket.isConnected) {
[self.clientSocket disconnect];
}
// 第一次斷線重連:0
// 第二次斷線重連:2
// 第三次斷線重連:4
if (self.reconnectDuration > 6) { // 已經三次斷線重連了,還沒成功
NSLog(@"===========>服務器出小差了嫩与,請稍后重試");
return;
}
NSLog(@"===========>斷線重連總時長:%f", self.reconnectDuration);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(self.reconnectDuration * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
if (self.clientSocket.isDisconnected) {
NSError *error = nil;
// 如果沒重連成寝姿,還會觸發(fā)“與服務端斷開連接的回調”
BOOL success = [self.clientSocket connectToHost:kServerIPAddress onPort:kServerPort viaInterface:nil withTimeout:-1 error:&error];
if (error == nil && success) {
// 連接服務端成功后會觸發(fā)“連接服務端成功的回調”
} else {
NSLog(@"%@", [NSString stringWithFormat:@"===========>連接服務端失敗:%@", error]);
}
}
});
// 斷線重連時間以2指數(shù)級增長
if (self.reconnectDuration == 0) {
self.reconnectDuration += 2;
} else if (self.reconnectDuration == 2) {
self.reconnectDuration += 4;
} else if (self.reconnectDuration == 6) {
self.reconnectDuration += 8;
}
}
參考
1划滋、iOS即時通訊饵筑,從入門到“放棄”?
2处坪、即時通訊下數(shù)據(jù)粘包翻翩、斷包處理實例(基于CocoaAsyncSocket)