一、說明
2017年4月份公司瞻赶,在做海外直播產(chǎn)品的時候赛糟,客戶端需要實(shí)時顯示些交互信息 ,剛開始由于產(chǎn)品處于驗(yàn)證階段用戶量少砸逊,就采用了輪尋的方式璧南,每間隔15s(當(dāng)然這也不及時),去服務(wù)器拉取一下信息师逸。由于運(yùn)營速度的加快司倚,用戶量很快就上來了,導(dǎo)致,服務(wù)器在高峰時段对湃,資源被占滿而無法響應(yīng)崖叫。和服務(wù)端小伙伴商議后迅速采用遗淳,長連接的方式拍柒,來處理客戶端和服務(wù)端不定時頻繁發(fā)消息的業(yè)務(wù)。然后屈暗,在網(wǎng)上就找到了Facebook的SRWebSocket框架拆讯,更多信息參考 https://github.com/facebook/SocketRocket
二 、用法
1养叛、 實(shí)現(xiàn)的功能
1.webSocket --- 開啟長連接
2.webSocket --- 關(guān)閉長連接
3.webSocket --- 長連接連接失敗种呐,自動重連(連接10次)
4.webSocket --- 無網(wǎng)的時候網(wǎng)絡(luò)檢測(此時會停止心跳),有網(wǎng)的時候弃甥,自動重連
5.webSocket --- 和服務(wù)端建立連接后發(fā)送心跳
6.webSocket --- 給服務(wù)端發(fā)送數(shù)據(jù)
7.webSocket --- 接收服務(wù)端數(shù)據(jù)
2爽室、以下是封裝 SRWebSocket 的代碼
#import <Foundation/Foundation.h>
#import "SRWebSocket.h"
@interface WebSocketManager : NSObject
@property (nonatomic, strong) SRWebSocket *webSocket;
+ (instancetype)sharedSocketManager;//單例
- (void)connectServer;//建立長連接
- (void)SRWebSocketClose;//關(guān)閉長連接
- (void)sendDataToServer:(id)data;//發(fā)送數(shù)據(jù)給服務(wù)器
@end
主線程異步隊(duì)列
#define dispatch_main_async_safe(block)\
if ([NSThread isMainThread]) {\
block();\
} else {\
dispatch_async(dispatch_get_main_queue(), block);\
}
#import "WebSocketManager.h"
@interface WebSocketManager()<SRWebSocketDelegate>
@property (nonatomic, strong) NSTimer *heartBeatTimer; //心跳定時器
@property (nonatomic, strong) NSTimer *netWorkTestingTimer; //沒有網(wǎng)絡(luò)的時候檢測網(wǎng)絡(luò)定時器
@property (nonatomic, strong) dispatch_queue_t queue; //數(shù)據(jù)請求隊(duì)列(串行隊(duì)列)
@property (nonatomic, assign) NSTimeInterval reConnectTime; //重連時間
@property (nonatomic, strong) NSMutableArray *sendDataArray; //存儲要發(fā)送給服務(wù)端的數(shù)據(jù)
@property (nonatomic, assign) BOOL isActivelyClose; //用于判斷是否主動關(guān)閉長連接,如果是主動斷開連接淆攻,連接失敗的代理中阔墩,就不用執(zhí)行 重新連接方法
@end
@implementation WebSocketManager
//單例
+ (instancetype)sharedSocketManager
{
static WebSocketManager *_instace = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken,^{
_instace = [[self alloc] init];
});
return _instace;
}
- (instancetype)init
{
self = [super init];
if(self)
{
self.reConnectTime = 0;
self.isActivelyClose = NO;
self.queue = dispatch_queue_create("BF",NULL);
self.sendDataArray = [[NSMutableArray alloc] init];
}
return self;
}
#pragma mark - NSTimer
//初始化心跳
- (void)initHeartBeat
{
//心跳沒有被關(guān)閉
if(self.heartBeatTimer)
{
return;
}
[self destoryHeartBeat];
WS(weakSelf);
dispatch_main_async_safe(^{
weakSelf.heartBeatTimer = [NSTimer timerWithTimeInterval:10 target:weakSelf selector:@selector(senderheartBeat) userInfo:nil repeats:true];
[[NSRunLoop currentRunLoop]addTimer:weakSelf.heartBeatTimer forMode:NSRunLoopCommonModes];
});
}
//取消心跳
- (void)destoryHeartBeat
{
WS(weakSelf);
dispatch_main_async_safe(^{
if(weakSelf.heartBeatTimer)
{
[weakSelf.heartBeatTimer invalidate];
weakSelf.heartBeatTimer = nil;
}
});
}
//沒有網(wǎng)絡(luò)的時候開始定時 -- 用于網(wǎng)絡(luò)檢測
- (void)noNetWorkStartTestingTimer
{
WS(weakSelf);
dispatch_main_async_safe(^{
weakSelf.netWorkTestingTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:weakSelf selector:@selector(noNetWorkStartTesting) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:weakSelf.netWorkTestingTimer forMode:NSDefaultRunLoopMode];
});
}
//取消網(wǎng)絡(luò)檢測
- (void)destoryNetWorkStartTesting
{
WS(weakSelf);
dispatch_main_async_safe(^{
if(weakSelf.netWorkTestingTimer)
{
[weakSelf.netWorkTestingTimer invalidate];
weakSelf.netWorkTestingTimer = nil;
}
});
}
#pragma mark - private -- webSocket相關(guān)方法
//發(fā)送心跳
- (void)senderheartBeat
{
//和服務(wù)端約定好發(fā)送什么作為心跳標(biāo)識,盡可能的減小心跳包大小
WS(weakSelf);
dispatch_main_async_safe(^{
if(weakSelf.webSocket.readyState == SR_OPEN)
{
[weakSelf.webSocket sendPing:nil];
}
});
}
//定時檢測網(wǎng)絡(luò)
- (void)noNetWorkStartTesting
{
//有網(wǎng)絡(luò)
if(AFNetworkReachabilityManager.sharedManager.networkReachabilityStatus != AFNetworkReachabilityStatusNotReachable)
{
//關(guān)閉網(wǎng)絡(luò)檢測定時器
[self destoryNetWorkStartTesting];
//開始重連
[self reConnectServer];
}
}
//建立長連接
- (void)connectServer
{
self.isActivelyClose = NO;
if(self.webSocket)
{
self.webSocket = nil;
}
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"ws://ip地址:端口號"]];
self.webSocket = [[SRWebSocket alloc] initWithURLRequest:request];
self.webSocket.delegate = self;
[self.webSocket open];
}
//重新連接服務(wù)器
- (void)reConnectServer
{
if(self.webSocket.readyState == SR_OPEN)
{
return;
}
if(self.reConnectTime > 1024) //重連10次 2^10 = 1024
{
self.reConnectTime = 0;
return;
}
WS(weakSelf);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(self.reConnectTime *NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
if(weakSelf.webSocket.readyState == SR_OPEN && weakSelf.webSocket.readyState == SR_CONNECTING)
{
return;
}
[weakSelf connectServer];
CTHLog(@"正在重連......");
if(weakSelf.reConnectTime == 0) //重連時間2的指數(shù)級增長
{
weakSelf.reConnectTime = 2;
}
else
{
weakSelf.reConnectTime *= 2;
}
});
}
//關(guān)閉連接
- (void)SRWebSocketClose;
{
self.isActivelyClose = YES;
[self webSocketClose];
//關(guān)閉心跳定時器
[self destoryHeartBeat];
//關(guān)閉網(wǎng)絡(luò)檢測定時器
[self destoryNetWorkStartTesting];
}
//關(guān)閉連接
- (void)webSocketClose
{
if(self.webSocket)
{
[self.webSocket close];
self.webSocket = nil;
}
}
//發(fā)送數(shù)據(jù)給服務(wù)器
- (void)sendDataToServer:(id)data
{
[self.sendDataArray addObject:data];
[self sendeDataToServer];
}
- (void)sendeDataToServer
{
WS(weakSelf);
//把數(shù)據(jù)放到一個請求隊(duì)列中
dispatch_async(self.queue, ^{
//沒有網(wǎng)絡(luò)
if (AFNetworkReachabilityManager.sharedManager.networkReachabilityStatus == AFNetworkReachabilityStatusNotReachable)
{
//開啟網(wǎng)絡(luò)檢測定時器
[weakSelf noNetWorkStartTestingTimer];
}
else //有網(wǎng)絡(luò)
{
if(weakSelf.webSocket != nil)
{
// 只有長連接OPEN開啟狀態(tài)才能調(diào) send 方法瓶珊,不然會Crash
if(weakSelf.webSocket.readyState == SR_OPEN)
{
if (weakSelf.sendDataArray.count > 0)
{
NSString *data = weakSelf.sendDataArray[0];
[weakSelf.webSocket send:data]; //發(fā)送數(shù)據(jù)
[weakSelf.sendDataArray removeObjectAtIndex:0];
if([weakSelf.sendDataArray count] > 0)
{
[weakSelf sendeDataToServer];
}
}
}
else if (weakSelf.webSocket.readyState == SR_CONNECTING) //正在連接
{
CTHLog(@"正在連接中啸箫,重連后會去自動同步數(shù)據(jù)");
}
else if (weakSelf.webSocket.readyState == SR_CLOSING || weakSelf.webSocket.readyState == SR_CLOSED) //斷開連接
{
//調(diào)用 reConnectServer 方法重連,連接成功后 繼續(xù)發(fā)送數(shù)據(jù)
[weakSelf reConnectServer];
}
}
else
{
[weakSelf connectServer]; //連接服務(wù)器
}
}
});
}
#pragma mark - SRWebSocketDelegate -- webSockect代理
//連接成功回調(diào)
- (void)webSocketDidOpen:(SRWebSocket *)webSocket
{
CTHLog(@"webSocket === 連接成功");
[self initHeartBeat]; //開啟心跳
//如果有尚未發(fā)送的數(shù)據(jù),繼續(xù)向服務(wù)端發(fā)送數(shù)據(jù)
if ([self.sendDataArray count] > 0){
[self sendeDataToServer];
}
}
//連接失敗回調(diào)
- (void)webSocket:(SRWebSocket *)webSocket didFailWithError:(NSError *)error
{
//用戶主動斷開連接伞芹,就不去進(jìn)行重連
if(self.isActivelyClose)
{
return;
}
[self destoryHeartBeat]; //斷開連接時銷毀心跳
CTHLog(@"連接失敗忘苛,這里可以實(shí)現(xiàn)掉線自動重連,要注意以下幾點(diǎn)");
CTHLog(@"1.判斷當(dāng)前網(wǎng)絡(luò)環(huán)境唱较,如果斷網(wǎng)了就不要連了扎唾,等待網(wǎng)絡(luò)到來,在發(fā)起重連");
CTHLog(@"3.連接次數(shù)限制南缓,如果連接失敗了稽屏,重試10次左右就可以了");
//判斷網(wǎng)絡(luò)環(huán)境
if (AFNetworkReachabilityManager.sharedManager.networkReachabilityStatus == AFNetworkReachabilityStatusNotReachable) //沒有網(wǎng)絡(luò)
{
[self noNetWorkStartTestingTimer];//開啟網(wǎng)絡(luò)檢測定時器
}
else //有網(wǎng)絡(luò)
{
[self reConnectServer];//連接失敗就重連
}
}
//連接關(guān)閉,注意連接關(guān)閉不是連接斷開,關(guān)閉是 [socket close] 客戶端主動關(guān)閉西乖,斷開可能是斷網(wǎng)了狐榔,被動斷開的。
- (void)webSocket:(SRWebSocket *)webSocket didCloseWithCode:(NSInteger)code reason:(NSString *)reason wasClean:(BOOL)wasClean
{
// 在這里判斷 webSocket 的狀態(tài) 是否為 open , 大家估計會有些奇怪 获雕,因?yàn)槲覀兊姆?wù)器都在海外薄腻,會有些時間差,經(jīng)過測試届案,我們在進(jìn)行某次連接的時候庵楷,上次重連的回調(diào)剛好回來,而本次重連又成功了,就會誤以為尽纽,本次沒有重連成功咐蚯,而再次進(jìn)行重連,就會出現(xiàn)問題弄贿,所以在這里做了一下判斷
if(self.webSocket.readyState == SR_OPEN || self.isActivelyClose)
{
return;
}
CTHLog(@"被關(guān)閉連接春锋,code:%ld,reason:%@,wasClean:%d",code,reason,wasClean);
[self destoryHeartBeat]; //斷開連接時銷毀心跳
//判斷網(wǎng)絡(luò)環(huán)境
if (AFNetworkReachabilityManager.sharedManager.networkReachabilityStatus == AFNetworkReachabilityStatusNotReachable) //沒有網(wǎng)絡(luò)
{
[self noNetWorkStartTestingTimer];//開啟網(wǎng)絡(luò)檢測
}
else //有網(wǎng)絡(luò)
{
[self reConnectServer];//連接失敗就重連
}
}
//該函數(shù)是接收服務(wù)器發(fā)送的pong消息,其中最后一個參數(shù)是接受pong消息的
-(void)webSocket:(SRWebSocket *)webSocket didReceivePong:(NSData*)pongPayload
{
NSString* reply = [[NSString alloc] initWithData:pongPayload encoding:NSUTF8StringEncoding];
CTHLog(@"reply === 收到后臺心跳回復(fù) Data:%@",reply);
}
//收到服務(wù)器發(fā)來的數(shù)據(jù)
- (void)webSocket:(SRWebSocket *)webSocket didReceiveMessage:(id)message
{
NSMutableDictionary *dataDic = [NSMutableDictionary dictionaryWithJsonString:message];
/*根據(jù)具體的業(yè)務(wù)做具體的處理*/
}
@end
三差凹、注意事項(xiàng)
1期奔、每次重連接的時候需要重新建立連接通道
2、由于按home鍵APP進(jìn)入后臺危尿,仍然要保持長連接通道不被系統(tǒng)立即斷掉呐萌,需要在 AppDelegate 中做以下處理,向系統(tǒng)申請資源谊娇,不過這個資源申請也是有限的肺孤,最多只有 10分鐘。也可以實(shí)現(xiàn)無限后臺機(jī)制济欢,這里不做介紹
- (void)applicationDidEnterBackground:(UIApplication *)application{
UIApplication* app = [UIApplication sharedApplication];
__block UIBackgroundTaskIdentifier bgTask;
bgTask = [app beginBackgroundTaskWithExpirationHandler:^{
dispatch_async(dispatch_get_main_queue(), ^{
if (bgTask != UIBackgroundTaskInvalid)
{
bgTask = UIBackgroundTaskInvalid;
}
});
}];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
dispatch_async(dispatch_get_main_queue(), ^{
if (bgTask != UIBackgroundTaskInvalid)
{
bgTask = UIBackgroundTaskInvalid;
}
});
});
}
四赠堵、總結(jié)
這個框架使用起來不難,只是在使用的時候需要考慮的情況有些多,理清思路就好了船逮,如有不足之處顾腊,希望多多指教 。