目錄
一、socket是什么,socket和HTTP的區(qū)別
二、如何建立一個(gè)socket連接
三、使用CocoaAsyncSocket實(shí)現(xiàn)socket編程
?1、基本實(shí)現(xiàn)
?2、數(shù)據(jù)粘包
?3、心跳保活
?4、斷線重連
一、socket是什么,socket和HTTP的區(qū)別
- HTTP是應(yīng)用層的一個(gè)協(xié)議,而socket不是協(xié)議、它只是操作系統(tǒng)提供給我們程序員用來操作傳輸層TCP協(xié)議和UDP協(xié)議的一套接口,使用socket其實(shí)就是在面向傳輸層的TCP協(xié)議和UDP協(xié)議編程,說白了使用socket編程我們就是在直接發(fā)送一個(gè)TCP請(qǐng)求或UDP請(qǐng)求;(如果直接使用TCP協(xié)議和UDP協(xié)議編程的話,我們程序員就需要按照這兩個(gè)協(xié)議的標(biāo)準(zhǔn)去嚴(yán)格組織數(shù)據(jù)格式,并且還有很多其它的事情要做,這會(huì)非常復(fù)雜,而socket這套接口就方便了我們程序員開發(fā),我們只需要調(diào)用簡單的接口即可,它內(nèi)部幫我們做了組織數(shù)據(jù)格式等復(fù)雜的事情。這就好比我們發(fā)送一個(gè)HTTP請(qǐng)求,如果我們直接使用HTTP協(xié)議編程,那它的數(shù)據(jù)格式也是很復(fù)雜的,包括請(qǐng)求行、請(qǐng)求頭、請(qǐng)求體等,但是好在我們各個(gè)系統(tǒng)都有系統(tǒng)級(jí)別的網(wǎng)絡(luò)框架,我們只需要使用它們的接口,它們內(nèi)部會(huì)做好數(shù)據(jù)格式轉(zhuǎn)化等復(fù)雜的事情,這樣看起來socket跟網(wǎng)絡(luò)框架倒更像是同一級(jí)別的概念)
- HTTP協(xié)議在傳輸層對(duì)應(yīng)的是TCP協(xié)議,所以我們常說的HTTP連接其實(shí)就是一個(gè)TCP連接,HTTP連接可以是短連接也可以是長連接,HTTP/1.0采用的是短連接,HTTP/1.1采用的是長連接;而socket是TCP協(xié)議和UDP協(xié)議的一套接口,所以我們常說socket連接其實(shí)也是一個(gè)TCP連接,同樣socket連接可以是短連接也可以是長連接,這取決于我們怎么使用,當(dāng)然socket基于UDP時(shí)甚至都不存在連接,所以不要把socket連接和長連接劃等號(hào);(順便說一下長連接和短連接,長連接和短連接是應(yīng)用層或者傳輸層應(yīng)用的一個(gè)概念,而不是傳輸層的概念,傳輸層只有連接這個(gè)概念,它本身沒有長短之分,只是因?yàn)閼?yīng)用層或者傳輸層應(yīng)用才導(dǎo)致了這個(gè)連接有長短之分。比如HTTP/1.0采用的是短連接,也就是說TCP三次握手之后,建立了一個(gè)連接,客戶端發(fā)送一個(gè)請(qǐng)求,服務(wù)器給一個(gè)響應(yīng),此時(shí)TCP就要四次揮手了,連接就斷開了,想再傳數(shù)據(jù)的話就得再三次握手建立連接四次揮手?jǐn)嚅_連接,也就是說短連接一次只處理一個(gè)輸入流和輸出流;而HTTP/1.1采用的是長連接,TCP三次握手之后,建立了一個(gè)連接,客戶端發(fā)送一個(gè)請(qǐng)求,服務(wù)器給一個(gè)響應(yīng),TCP并不揮手,連接也沒斷開,雙方還可以繼續(xù)傳遞下一次數(shù)據(jù)、下一次數(shù)據(jù)數(shù)據(jù)......,直到雙方等了某個(gè)時(shí)間段發(fā)現(xiàn)雙方都不需要傳遞數(shù)據(jù)了,才會(huì)四次揮手?jǐn)嚅_連接,也就是說長連接一次可以處理若干個(gè)輸入流和輸出流。又比如socket可以建立一個(gè)長連接,這就意味著TCP的那個(gè)連接要處理若干個(gè)輸入流和輸出流,socket也可以建立短連接,這就意味著TCP的那個(gè)連接只處理一個(gè)輸入流和輸出流。長連接和短連接不是靠時(shí)間長短來界定的,而是靠一次處理的輸入流和輸出流個(gè)數(shù)來界定的)
- HTTP主要用來做客戶端發(fā)一個(gè)請(qǐng)求、服務(wù)端就給一個(gè)響應(yīng)這樣的場景,而socket主要用來做客戶端和服務(wù)端都能主動(dòng)給對(duì)方發(fā)數(shù)據(jù)的場景,通常情況下,單臺(tái)服務(wù)器可以支持十萬個(gè)socket連接的并發(fā)。
二、如何建立一個(gè)socket連接

服務(wù)端socket要做五件事,客戶端socket要做三件事:
- 服務(wù)端socket初始化、客戶端socket初始化
- 服務(wù)端socket調(diào)用
bind()函數(shù),綁定IP地址和端口 -
服務(wù)端socket調(diào)用
listen()函數(shù),設(shè)置監(jiān)聽隊(duì)列有多長(比如說我們設(shè)置監(jiān)聽隊(duì)列的長度為10,這就意味著最多可以有10個(gè)客戶端可以嘗試連接服務(wù)器,而第11個(gè)客戶端會(huì)被告知服務(wù)器太忙了) - 服務(wù)端socket調(diào)用
accept()函數(shù),等待來自客戶端socket的連接請(qǐng)求 - 客戶端socket調(diào)用
connect()函數(shù),向指定IP地址和端口的服務(wù)器發(fā)起連接請(qǐng)求 - 服務(wù)端socket的
accept()函數(shù)返回用于傳輸?shù)膕ocket的文件描述符,連接建立成功
接下來雙方就可以通過read()和write()函數(shù)通信了,雙方也都可以通過close()函數(shù)主動(dòng)斷開連接。
三、使用CocoaAsyncSocket實(shí)現(xiàn)socket編程
1、基本實(shí)現(xiàn)
- 服務(wù)端socket
#import "ViewController.h"
#import "GCDAsyncSocket.h"
#define kServerIPAddress @"192.168.148.63"
#define kServerPort 8080
@interface ViewController () <GCDAsyncSocketDelegate>
/// 服務(wù)端socket
@property (strong, nonatomic) GCDAsyncSocket *serverSocket;
/// 所有的客戶端socket
@property (strong, nonatomic) NSMutableArray *clientSockets;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
}
#pragma mark - GCDAsyncSocketDelegate
/// 連接客戶端成功的回調(diào)
- (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(nonnull GCDAsyncSocket *)newSocket {
NSLog(@"===========>連接客戶端成功");
// 保存客戶端socket
[self.clientSockets addObject:newSocket];
// 連接成功后,立馬開始讀取客戶端的數(shù)據(jù),調(diào)用這個(gè)讀取方法后,當(dāng)客戶端有數(shù)據(jù)發(fā)過來時(shí)就會(huì)觸發(fā)“讀取客戶端數(shù)據(jù)成功的回調(diào)”
[newSocket readDataWithTimeout:-1 tag:0];
}
/// 讀取客戶端數(shù)據(jù)成功的回調(diào)
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {
// 此處我們不對(duì)數(shù)據(jù)做過多的處理,只是把它轉(zhuǎn)換成JSON字符串打印出來看看
NSString *jsonString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"%@", [NSString stringWithFormat:@"===========>讀取客戶端數(shù)據(jù)成功:%@", jsonString]);
// 注意:讀取客戶端數(shù)據(jù)成功后,在這里需要再調(diào)用一次讀取客戶端數(shù)據(jù)的方法,框架本身就是這么設(shè)計(jì)的,否則我們就只能接收一次數(shù)據(jù),之后再也接收不到數(shù)據(jù)
[sock readDataWithTimeout:-1 tag:0];
}
/// 與客戶端斷開連接的回調(diào)
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err {
NSLog(@"%@", [NSString stringWithFormat:@"===========>與客戶端斷開連接:%@", err]);
if (err == nil) { // 代表是服務(wù)端主動(dòng)斷開連接,我們不做處理,客戶端那邊收到回調(diào)后會(huì)走斷線重連的邏輯
NSLog(@"===========>服務(wù)端主動(dòng)斷開連接");
} else { // 代表是客戶端斷開連接,移除斷開的客戶端
[self.clientSockets removeObject:sock];
}
}
#pragma mark - action
/// 服務(wù)端開始監(jiān)聽來自客戶端的連接請(qǐng)求,收到請(qǐng)求后就建立連接,連接成功后會(huì)觸發(fā)“連接客戶端成功的回調(diào)”
- (IBAction)listen:(id)sender {
NSError *error = nil;
BOOL success = [self.serverSocket acceptOnPort:kServerPort error:&error];
if (error == nil && success) {
NSLog(@"===========>服務(wù)端開始監(jiān)聽成功");
} else {
NSLog(@"%@", [NSString stringWithFormat:@"==========>服務(wù)端開始監(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": @"江南憶,其次憶吳宮。吳酒一杯春竹葉,吳娃雙舞醉芙蓉。早晚復(fù)相逢?",
};
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:消息標(biāo)記)
[clientSocket writeData:data1 withTimeout:-1 tag:0];
// 給客戶端發(fā)送數(shù)據(jù)2(tag:消息標(biāo)記)
[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
/// 連接服務(wù)端成功的回調(diào)
- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port {
NSLog(@"===========>連接服務(wù)端成功");
// 連接成功后,立馬開始讀取服務(wù)端的數(shù)據(jù),調(diào)用這個(gè)讀取方法后,當(dāng)服務(wù)端有數(shù)據(jù)發(fā)過來時(shí)就會(huì)觸發(fā)“讀取服務(wù)端數(shù)據(jù)成功的回調(diào)”
[sock readDataWithTimeout:-1 tag:0];
}
/// 讀取服務(wù)端數(shù)據(jù)成功的回調(diào)
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {
// 此處我們不對(duì)數(shù)據(jù)做過多的處理,只是把它轉(zhuǎn)換成JSON字符串打印出來看看
NSString *jsonString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"%@", [NSString stringWithFormat:@"===========>讀取服務(wù)端數(shù)據(jù)成功:%@", jsonString]);
// 注意:讀取服務(wù)端數(shù)據(jù)成功后,在這里需要再調(diào)用一次讀取服務(wù)端數(shù)據(jù)的方法,框架本身就是這么設(shè)計(jì)的,否則我們就只能接收一次數(shù)據(jù),之后再也接收不到數(shù)據(jù)
[sock readDataWithTimeout:-1 tag:0];
}
/// 與服務(wù)端斷開連接的回調(diào)
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err {
NSLog(@"%@", [NSString stringWithFormat:@"===========>與服務(wù)端斷開連接:%@", err]);
if (err == nil) { // 代表是客戶端主動(dòng)斷開連接,我們不做處理,服務(wù)端那邊收到回調(diào)后,會(huì)移除跟指定客戶端的連接
NSLog(@"===========>客戶端主動(dòng)斷開連接");
} else { // 其它情況下斷開連接,暫時(shí)不做處理
}
}
#pragma mark - action
/// 向服務(wù)端發(fā)起連接請(qǐng)求
- (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) {
// 連接服務(wù)端成功后會(huì)觸發(fā)“連接服務(wù)端成功的回調(diào)”
} else {
NSLog(@"%@", [NSString stringWithFormat:@"===========>連接服務(wù)端失?。?@", error]);
}
}
/// 向服務(wù)端發(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];
// 給服務(wù)端發(fā)送數(shù)據(jù)1(tag:消息標(biāo)記)
[self.clientSocket writeData:data1 withTimeout:-1 tag:0];
// 給服務(wù)端發(fā)送數(shù)據(jù)2(tag:消息標(biāo)記)
[self.clientSocket writeData:data2 withTimeout:-1 tag:0];
}
/// 斷開與服務(wù)端的連接
- (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
以上我們就基本實(shí)現(xiàn)了socket編程,客戶端和服務(wù)端之間就可以相互通信了。但是在實(shí)際開發(fā)中,我們還需要考慮三個(gè)問題:數(shù)據(jù)粘包、心跳?;詈蛿嗑€重連。
2、數(shù)據(jù)粘包
2.1 數(shù)據(jù)粘包是什么
上面的例子中,我們預(yù)期的效果是客戶端點(diǎn)擊一次發(fā)送,給服務(wù)端發(fā)送兩條數(shù)據(jù),服務(wù)端觸發(fā)兩次“收到客戶端數(shù)據(jù)的回調(diào)”,然后分別打印:
===========>讀取客戶端數(shù)據(jù)成功:{
"data" : "水光瀲滟晴方好,山色空蒙雨亦奇。 "
}
===========>讀取客戶端數(shù)據(jù)成功:{
"data" : "欲把西湖比西子,淡妝濃抹總相宜。"
}
但實(shí)際上兩條數(shù)據(jù)被合并成一條數(shù)據(jù)發(fā)送給服務(wù)端了,服務(wù)端只觸發(fā)了一次“收到客戶端數(shù)據(jù)的回調(diào)”,也只打印了一次:
===========>讀取客戶端數(shù)據(jù)成功:{
"data" : "水光瀲滟晴方好,山色空蒙雨亦奇。 "
}{
"data" : "欲把西湖比西子,淡妝濃抹總相宜。"
}
這就是數(shù)據(jù)粘包——多條數(shù)據(jù)被合并成了一條數(shù)據(jù)傳輸。
2.2 為什么會(huì)出現(xiàn)數(shù)據(jù)粘包
我們知道TCP有個(gè)發(fā)送緩存,有些情況下TCP并不是有一條數(shù)據(jù)就發(fā)一條數(shù)據(jù),而是等發(fā)送緩存滿了,再把發(fā)送緩存里的多條數(shù)據(jù)一起發(fā)送出去,這就會(huì)導(dǎo)致數(shù)據(jù)粘包。
此外TCP還采用了Nagle優(yōu)化算法來打包數(shù)據(jù),它會(huì)將多次間隔較小且數(shù)據(jù)量較小的數(shù)據(jù)自動(dòng)合并成一個(gè)比較大的數(shù)據(jù)一塊兒傳輸,這也會(huì)導(dǎo)致數(shù)據(jù)粘包。
2.3 怎么處理數(shù)據(jù)粘包
處理數(shù)據(jù)粘包也很簡單,核心思路就是:發(fā)送方在發(fā)送數(shù)據(jù)的時(shí)候先給每條數(shù)據(jù)都添加一個(gè)包頭,包頭里存放的關(guān)鍵信息就是真實(shí)數(shù)據(jù)的長度,當(dāng)然也可以存放更多的業(yè)務(wù)信息,此外包頭的尾部還需要拼接一個(gè)包頭結(jié)束標(biāo)識(shí)——回車換行符,以便將來接收方讀取數(shù)據(jù)時(shí)可以根據(jù)這個(gè)包頭結(jié)束標(biāo)識(shí)優(yōu)先讀取到包頭數(shù)據(jù)。接收方調(diào)用指定的讀取方法優(yōu)先讀取到包頭數(shù)據(jù),然后根據(jù)包頭里的長度信息再去精準(zhǔn)讀取指定長度的真實(shí)數(shù)據(jù),這樣就可以讀取到一條完整的數(shù)據(jù)了,然后再讀取下一條數(shù)據(jù)就不會(huì)粘包了。
- 服務(wù)端socket(相對(duì)于上面的例子,變化的代碼)
// 讀取處理
/// 包頭
@property (strong, nonatomic) NSDictionary *headerDictionary;
#pragma mark - GCDAsyncSocketDelegate
/// 連接客戶端成功的回調(diào)
- (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(nonnull GCDAsyncSocket *)newSocket {
NSLog(@"===========>連接客戶端成功");
// 保存客戶端socket
[self.clientSockets addObject:newSocket];
// 連接成功后,立馬開始讀取客戶端的數(shù)據(jù),調(diào)用這個(gè)讀取方法后,當(dāng)客戶端有數(shù)據(jù)發(fā)過來時(shí)就會(huì)觸發(fā)“讀取客戶端數(shù)據(jù)成功的回調(diào)”
// [newSocket readDataWithTimeout:-1 tag:0];
// 連接成功后,立馬開始讀取客戶端的數(shù)據(jù),調(diào)用這個(gè)讀取方法后,當(dāng)客戶端有數(shù)據(jù)發(fā)過來時(shí)就會(huì)觸發(fā)“讀取客戶端數(shù)據(jù)成功的回調(diào)”,但跟上面方法不同的是這個(gè)方法只會(huì)從數(shù)據(jù)的開頭起讀取到包頭結(jié)束標(biāo)識(shí)為止,也就是說“讀取客戶端數(shù)據(jù)成功的回調(diào)”的data只會(huì)讀取到包頭數(shù)據(jù)
[newSocket readDataToData:[GCDAsyncSocket CRLFData] withTimeout:-1 tag:0];
}
/// 讀取客戶端數(shù)據(jù)成功的回調(diào)
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {
if (self.headerDictionary == nil) { // 如果包頭為空,就代表之前沒讀取到過包頭,那本次回調(diào)的觸發(fā)肯定就是讀取到包頭了,因?yàn)樯厦娌捎玫氖莚eadDataToData讀取方法讀取到包頭結(jié)束標(biāo)識(shí)為止
self.headerDictionary = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:nil];
if (self.headerDictionary == nil) {
NSLog(@"===========>數(shù)據(jù)格式出錯(cuò)了:包頭數(shù)據(jù)為空");
return;
}
// 獲取包頭的內(nèi)容1——真實(shí)數(shù)據(jù)的長度
NSInteger size = [self.headerDictionary[@"size"] integerValue];
// 此時(shí)再調(diào)用這個(gè)讀取方法去讀取指定長度的真實(shí)數(shù)據(jù),讀取到真實(shí)數(shù)據(jù)后還是會(huì)觸發(fā)當(dāng)前這個(gè)回調(diào)
[sock readDataToLength:size withTimeout:-1 tag:0];
return;
}
// 如果走到這里,代表是讀取到了真實(shí)數(shù)據(jù)
NSInteger size = [self.headerDictionary[@"size"] integerValue];
// 說明數(shù)據(jù)有問題
if (size <= 0 || data.length != size) {
NSLog(@"===========>數(shù)據(jù)格式出錯(cuò)了:真實(shí)數(shù)據(jù)大小不正確");
return;
}
// 此處我們不對(duì)數(shù)據(jù)做過多的處理,只是把它轉(zhuǎn)換成JSON字符串打印出來看看
NSString *jsonString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"%@", [NSString stringWithFormat:@"===========>讀取客戶端數(shù)據(jù)成功:%@", jsonString]);
// 置位包頭
self.headerDictionary = nil;
// 注意:讀取客戶端數(shù)據(jù)成功后,在這里需要再調(diào)用一次讀取客戶端數(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": @"江南憶,其次憶吳宮。吳酒一杯春竹葉,吳娃雙舞醉芙蓉。早晚復(fù)相逢?",
};
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:消息標(biāo)記)
[clientSocket writeData:finalData1 withTimeout:-1 tag:0];
// 給客戶端發(fā)送數(shù)據(jù)2(tag:消息標(biāo)記)
[clientSocket writeData:finalData2 withTimeout:-1 tag:0];
}];
}
#pragma mark - private method
/// 獲取拼接了包頭后的數(shù)據(jù)
- (NSData *)dataWithHeader:(NSData *)data {
// 包頭
NSMutableDictionary *headerDictionary = [NSMutableDictionary dictionary];
// 包頭里的內(nèi)容1——真實(shí)數(shù)據(jù)的長度
[headerDictionary setObject:[NSString stringWithFormat:@"%ld", data.length] forKey:@"size"];
// 把字典格式的包頭轉(zhuǎn)化成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];
// 包頭里的內(nèi)容2——包頭結(jié)束標(biāo)識(shí),回車換行符
[headerData appendData:[GCDAsyncSocket CRLFData]];
NSMutableData *finalData = headerData;
// 在包頭數(shù)據(jù)后面拼接上真實(shí)數(shù)據(jù)
[finalData appendData:data];
return finalData;
}
- 客戶端socket(相對(duì)于上面的例子,變化的代碼)
// 讀取處理
/// 包頭
@property (strong, nonatomic) NSDictionary *headerDictionary;
#pragma mark - GCDAsyncSocketDelegate
/// 連接服務(wù)端成功的回調(diào)
- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port {
NSLog(@"===========>連接服務(wù)端成功");
// 連接成功后,立馬開始讀取服務(wù)端的數(shù)據(jù),調(diào)用這個(gè)讀取方法后,當(dāng)服務(wù)端有數(shù)據(jù)發(fā)過來時(shí)就會(huì)觸發(fā)“讀取服務(wù)端數(shù)據(jù)成功的回調(diào)”
// [sock readDataWithTimeout:-1 tag:0];
// 連接成功后,立馬開始讀取服務(wù)端的數(shù)據(jù),調(diào)用這個(gè)讀取方法后,當(dāng)服務(wù)端有數(shù)據(jù)發(fā)過來時(shí)就會(huì)觸發(fā)“讀取服務(wù)端數(shù)據(jù)成功的回調(diào)”,但跟上面方法不同的是這個(gè)方法只會(huì)從數(shù)據(jù)的開頭起讀取到包頭結(jié)束標(biāo)識(shí)為止,也就是說“讀取服務(wù)端數(shù)據(jù)成功的回調(diào)”的data只會(huì)讀取到包頭數(shù)據(jù)
[sock readDataToData:[GCDAsyncSocket CRLFData] withTimeout:-1 tag:0];
}
/// 讀取服務(wù)端數(shù)據(jù)成功的回調(diào)
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {
if (self.headerDictionary == nil) { // 如果包頭為空,就代表之前沒讀取到過包頭,那本次回調(diào)的觸發(fā)肯定就是讀取到包頭了,因?yàn)樯厦娌捎玫氖莚eadDataToData讀取方法讀取到包頭結(jié)束標(biāo)識(shí)為止
self.headerDictionary = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:nil];
if (self.headerDictionary == nil) {
NSLog(@"===========>數(shù)據(jù)格式出錯(cuò)了:包頭數(shù)據(jù)為空");
return;
}
// 獲取包頭的內(nèi)容1——真實(shí)數(shù)據(jù)的長度
NSInteger size = [self.headerDictionary[@"size"] integerValue];
// 此時(shí)再調(diào)用這個(gè)讀取方法去讀取指定長度的真實(shí)數(shù)據(jù),讀取到真實(shí)數(shù)據(jù)后還是會(huì)觸發(fā)當(dāng)前這個(gè)回調(diào)
[sock readDataToLength:size withTimeout:-1 tag:0];
return;
}
// 如果走到這里,代表是讀取到了真實(shí)數(shù)據(jù)
NSInteger size = [self.headerDictionary[@"size"] integerValue];
// 說明數(shù)據(jù)有問題
if (size <= 0 || data.length != size) {
NSLog(@"===========>數(shù)據(jù)格式出錯(cuò)了:真實(shí)數(shù)據(jù)大小不正確");
return;
}
// 此處我們不對(duì)數(shù)據(jù)做過多的處理,只是把它轉(zhuǎn)換成JSON字符串打印出來看看
NSString *jsonString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"%@", [NSString stringWithFormat:@"===========>讀取服務(wù)端數(shù)據(jù)成功:%@", jsonString]);
// 置位包頭
self.headerDictionary = nil;
// 注意:讀取服務(wù)端數(shù)據(jù)成功后,在這里需要再調(diào)用一次讀取服務(wù)端數(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
/// 向服務(wù)端發(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];
// 給服務(wù)端發(fā)送數(shù)據(jù)1(tag:消息標(biāo)記)
[self.clientSocket writeData:finalData1 withTimeout:-1 tag:0];
// 給服務(wù)端發(fā)送數(shù)據(jù)2(tag:消息標(biāo)記)
[self.clientSocket writeData:finalData2 withTimeout:-1 tag:0];
}
#pragma mark - private method
/// 獲取拼接了包頭后的數(shù)據(jù)
- (NSData *)dataWithHeader:(NSData *)data {
// 包頭
NSMutableDictionary *headerDictionary = [NSMutableDictionary dictionary];
// 包頭里的內(nèi)容1——真實(shí)數(shù)據(jù)的長度
[headerDictionary setObject:[NSString stringWithFormat:@"%ld", data.length] forKey:@"size"];
// 把字典格式的包頭轉(zhuǎn)化成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];
// 包頭里的內(nèi)容2——包頭結(jié)束標(biāo)識(shí),回車換行符
[headerData appendData:[GCDAsyncSocket CRLFData]];
NSMutableData *finalData = headerData;
// 在包頭數(shù)據(jù)后面拼接上真實(shí)數(shù)據(jù)
[finalData appendData:data];
return finalData;
}
3、心跳?;?/h4>
正常來說,socket連接一旦建立之后就會(huì)一直掛在那里,直到某一端主動(dòng)斷開連接。但實(shí)際上,運(yùn)營商在檢測到鏈路上有一段時(shí)間無數(shù)據(jù)傳輸時(shí),就會(huì)自動(dòng)斷開這種處于非活躍狀態(tài)的連接,這就是所謂的運(yùn)營商N(yùn)AT超時(shí),超時(shí)時(shí)間為5分鐘。因此我們就需要做心跳保活——即客戶端每隔一定的時(shí)間間隔就向服務(wù)端發(fā)送一個(gè)心跳數(shù)據(jù)包,用來保證當(dāng)前socket連接處于活躍狀態(tài),避免運(yùn)營商把我們的連接中斷,這個(gè)時(shí)間間隔我們?nèi)〉氖?分鐘,服務(wù)器在收到心跳包時(shí)不當(dāng)做真實(shí)數(shù)據(jù)處理即可。
- 服務(wù)端socket(相對(duì)于上面的例子,變化的代碼)
#pragma mark - GCDAsyncSocketDelegate
/// 讀取客戶端數(shù)據(jù)成功的回調(diào)
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {
if (self.headerDictionary == nil) { // 如果包頭為空,就代表之前沒讀取到過包頭,那本次回調(diào)的觸發(fā)肯定就是讀取到包頭了,因?yàn)樯厦娌捎玫氖莚eadDataToData讀取方法讀取到包頭結(jié)束標(biāo)識(shí)為止
self.headerDictionary = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:nil];
if (self.headerDictionary == nil) {
NSLog(@"===========>數(shù)據(jù)格式出錯(cuò)了:包頭數(shù)據(jù)為空");
return;
}
// 獲取包頭的內(nèi)容1——真實(shí)數(shù)據(jù)的長度
NSInteger size = [self.headerDictionary[@"size"] integerValue];
// 此時(shí)再調(diào)用這個(gè)讀取方法去讀取指定長度的真實(shí)數(shù)據(jù),讀取到真實(shí)數(shù)據(jù)后還是會(huì)觸發(fā)當(dāng)前這個(gè)回調(diào)
[sock readDataToLength:size withTimeout:-1 tag:0];
return;
}
// 如果走到這里,代表是讀取到了真實(shí)數(shù)據(jù)
NSInteger size = [self.headerDictionary[@"size"] integerValue];
// 說明數(shù)據(jù)有問題
if (size <= 0 || data.length != size) {
NSLog(@"===========>數(shù)據(jù)格式出錯(cuò)了:真實(shí)數(shù)據(jù)大小不正確");
return;
}
NSDictionary *dictionary = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
if ([dictionary[@"data"] isEqual:@"com.zhangshuo.alive"]) { // 如果是心跳包,我們不做處理
NSLog(@"===========>我是心跳包");
} else { // 如果不是心跳包,我們再做處理
// 此處我們不對(duì)數(shù)據(jù)做過多的處理,只是把它轉(zhuǎn)換成JSON字符串打印出來看看
NSString *jsonString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"%@", [NSString stringWithFormat:@"===========>讀取客戶端數(shù)據(jù)成功:%@", jsonString]);
}
// 置位包頭
self.headerDictionary = nil;
// 注意:讀取客戶端數(shù)據(jù)成功后,在這里需要再調(diào)用一次讀取客戶端數(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(相對(duì)于上面的例子,變化的代碼)
#define kHeartBeatTimeInterval 60 * 3 // 心跳?;顣r(shí)間間隔:3分鐘
/// 心跳保活定時(shí)器
@property (nonatomic, strong) NSTimer *heartBeatTimer;
#pragma mark - GCDAsyncSocketDelegate
/// 連接服務(wù)端成功的回調(diào)
- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port {
NSLog(@"===========>連接服務(wù)端成功");
// 連接成功后,立馬開始讀取服務(wù)端的數(shù)據(jù),調(diào)用這個(gè)讀取方法后,當(dāng)服務(wù)端有數(shù)據(jù)發(fā)過來時(shí)就會(huì)觸發(fā)“讀取服務(wù)端數(shù)據(jù)成功的回調(diào)”
// [sock readDataWithTimeout:-1 tag:0];
// 連接成功后,立馬開始讀取服務(wù)端的數(shù)據(jù),調(diào)用這個(gè)讀取方法后,當(dāng)服務(wù)端有數(shù)據(jù)發(fā)過來時(shí)就會(huì)觸發(fā)“讀取服務(wù)端數(shù)據(jù)成功的回調(diào)”,但跟上面方法不同的是這個(gè)方法只會(huì)從數(shù)據(jù)的開頭起讀取到包頭結(jié)束標(biāo)識(shí)為止,也就是說“讀取服務(wù)端數(shù)據(jù)成功的回調(diào)”的data只會(huì)讀取到包頭數(shù)據(jù)
[sock readDataToData:[GCDAsyncSocket CRLFData] withTimeout:-1 tag:0];
// 添加心跳?;? [self addHeartBeat];
}
/// 與服務(wù)端斷開連接的回調(diào)
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err {
NSLog(@"%@", [NSString stringWithFormat:@"===========>與服務(wù)端斷開連接:%@", err]);
// 移除心跳?;? [self removeHeartBeat];
}
#pragma mark - 心跳?;?
// 添加心跳?;?- (void)addHeartBeat {
[self removeHeartBeat];
// 心跳時(shí)間設(shè)置為3分鐘,NAT超時(shí)一般為3~5分鐘
self.heartBeatTimer = [NSTimer scheduledTimerWithTimeInterval:kHeartBeatTimeInterval repeats:YES block:^(NSTimer * _Nonnull timer) {
// 和服務(wù)端約定好心跳保活數(shù)據(jù)包的內(nèi)容,以便它們讀取,盡可能減小心跳?;顢?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];
// 給服務(wù)端發(fā)送數(shù)據(jù)1(tag:消息標(biāo)記)
[self.clientSocket writeData:finalData withTimeout:-1 tag:0];
}];
[[NSRunLoop currentRunLoop]addTimer:self.heartBeatTimer forMode:NSRunLoopCommonModes];
}
// 移除心跳保活
- (void)removeHeartBeat {
[self.heartBeatTimer invalidate];
self.heartBeatTimer = nil;
}
4、斷線重連
客戶端主動(dòng)斷開連接時(shí)(如App退出登錄或者App進(jìn)入后臺(tái)等場景),我們不需要做斷線重連;其它情況下如果連接斷開了(如服務(wù)器出了問題或者網(wǎng)斷了等場景),我們就需要做斷線重連,來盡量使連接處于正常連接的狀態(tài),這樣才能保證業(yè)務(wù)的正常運(yùn)行。具體做法就是,當(dāng)客戶端檢測到跟服務(wù)端斷開連接時(shí)就啟動(dòng)第一次斷線重連,2秒后啟動(dòng)第二次斷線重連,再隔4秒后啟動(dòng)第三次斷線重連,如果三次斷線重連還沒成功,就認(rèn)為是服務(wù)器出了問題,不再重連。
/// 斷線重連總時(shí)長
@property (assign, nonatomic) NSTimeInterval reconnectDuration;
#pragma mark - GCDAsyncSocketDelegate
/// 連接服務(wù)端成功的回調(diào)
- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port {
NSLog(@"===========>連接服務(wù)端成功");
// 連接成功后,立馬開始讀取服務(wù)端的數(shù)據(jù),調(diào)用這個(gè)讀取方法后,當(dāng)服務(wù)端有數(shù)據(jù)發(fā)過來時(shí)就會(huì)觸發(fā)“讀取服務(wù)端數(shù)據(jù)成功的回調(diào)”
// [sock readDataWithTimeout:-1 tag:0];
// 連接成功后,立馬開始讀取服務(wù)端的數(shù)據(jù),調(diào)用這個(gè)讀取方法后,當(dāng)服務(wù)端有數(shù)據(jù)發(fā)過來時(shí)就會(huì)觸發(fā)“讀取服務(wù)端數(shù)據(jù)成功的回調(diào)”,但跟上面方法不同的是這個(gè)方法只會(huì)從數(shù)據(jù)的開頭起讀取到包頭結(jié)束標(biāo)識(shí)為止,也就是說“讀取服務(wù)端數(shù)據(jù)成功的回調(diào)”的data只會(huì)讀取到包頭數(shù)據(jù)
[sock readDataToData:[GCDAsyncSocket CRLFData] withTimeout:-1 tag:0];
// 添加心跳?;? [self addHeartBeat];
// 斷線重連成功后,置位斷線重連總時(shí)長
self.reconnectDuration = 0;
}
/// 與服務(wù)端斷開連接的回調(diào)
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err {
NSLog(@"%@", [NSString stringWithFormat:@"===========>與服務(wù)端斷開連接:%@", err]);
// 移除心跳?;? [self removeHeartBeat];
if (err == nil) { // 代表是客戶端主動(dòng)斷開連接,我們不做處理,服務(wù)端那邊收到回調(diào)后,會(huì)移除跟指定客戶端的連接
NSLog(@"===========>客戶端主動(dòng)斷開連接");
} else { // 其它情況下斷開連接,啟動(dòng)斷線重連
[self reconnect];
}
}
#pragma mark - 斷線重連
// 斷線重連
- (void)reconnect {
if (self.clientSocket.isConnected) {
[self.clientSocket disconnect];
}
// 第一次斷線重連:0
// 第二次斷線重連:2
// 第三次斷線重連:4
if (self.reconnectDuration > 6) { // 已經(jīng)三次斷線重連了,還沒成功
NSLog(@"===========>服務(wù)器出小差了,請(qǐng)稍后重試");
return;
}
NSLog(@"===========>斷線重連總時(shí)長:%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;
// 如果沒重連成,還會(huì)觸發(fā)“與服務(wù)端斷開連接的回調(diào)”
BOOL success = [self.clientSocket connectToHost:kServerIPAddress onPort:kServerPort viaInterface:nil withTimeout:-1 error:&error];
if (error == nil && success) {
// 連接服務(wù)端成功后會(huì)觸發(fā)“連接服務(wù)端成功的回調(diào)”
} else {
NSLog(@"%@", [NSString stringWithFormat:@"===========>連接服務(wù)端失?。?@", error]);
}
}
});
// 斷線重連時(shí)間以2指數(shù)級(jí)增長
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即時(shí)通訊,從入門到“放棄”?
2、即時(shí)通訊下數(shù)據(jù)粘包、斷包處理實(shí)例(基于CocoaAsyncSocket)