一、說明
2017年4月份公司,在做海外直播產品的時候,客戶端需要實時顯示些交互信息 ,剛開始由于產品處于驗證階段用戶量少,就采用了輪尋的方式,每間隔15s(當然這也不及時),去服務器拉取一下信息。由于運營速度的加快,用戶量很快就上來了,導致,服務器在高峰時段,資源被占滿而無法響應。和服務端小伙伴商議后迅速采用,長連接的方式,來處理客戶端和服務端不定時頻繁發消息的業務。然后,在網上就找到了Facebook的SRWebSocket框架,更多信息參考 https://github.com/facebook/SocketRocket
二 、用法
1、 實現的功能
1.webSocket --- 開啟長連接
2.webSocket --- 關閉長連接
3.webSocket --- 長連接連接失敗,自動重連(連接10次)
4.webSocket --- 無網的時候網絡檢測(此時會停止心跳),有網的時候,自動重連
5.webSocket --- 和服務端建立連接后發送心跳
6.webSocket --- 給服務端發送數據
7.webSocket --- 接收服務端數據
2、以下是封裝 SRWebSocket 的代碼
#import <Foundation/Foundation.h>
#import "SRWebSocket.h"
@interface WebSocketManager : NSObject
@property (nonatomic, strong) SRWebSocket *webSocket;
+ (instancetype)sharedSocketManager;//單例
- (void)connectServer;//建立長連接
- (void)SRWebSocketClose;//關閉長連接
- (void)sendDataToServer:(id)data;//發送數據給服務器
@end
主線程異步隊列
#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; //沒有網絡的時候檢測網絡定時器
@property (nonatomic, strong) dispatch_queue_t queue; //數據請求隊列(串行隊列)
@property (nonatomic, assign) NSTimeInterval reConnectTime; //重連時間
@property (nonatomic, strong) NSMutableArray *sendDataArray; //存儲要發送給服務端的數據
@property (nonatomic, assign) BOOL isActivelyClose; //用于判斷是否主動關閉長連接,如果是主動斷開連接,連接失敗的代理中,就不用執行 重新連接方法
@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
{
//心跳沒有被關閉
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;
}
});
}
//沒有網絡的時候開始定時 -- 用于網絡檢測
- (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];
});
}
//取消網絡檢測
- (void)destoryNetWorkStartTesting
{
WS(weakSelf);
dispatch_main_async_safe(^{
if(weakSelf.netWorkTestingTimer)
{
[weakSelf.netWorkTestingTimer invalidate];
weakSelf.netWorkTestingTimer = nil;
}
});
}
#pragma mark - private -- webSocket相關方法
//發送心跳
- (void)senderheartBeat
{
//和服務端約定好發送什么作為心跳標識,盡可能的減小心跳包大小
WS(weakSelf);
dispatch_main_async_safe(^{
if(weakSelf.webSocket.readyState == SR_OPEN)
{
[weakSelf.webSocket sendPing:nil];
}
});
}
//定時檢測網絡
- (void)noNetWorkStartTesting
{
//有網絡
if(AFNetworkReachabilityManager.sharedManager.networkReachabilityStatus != AFNetworkReachabilityStatusNotReachable)
{
//關閉網絡檢測定時器
[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];
}
//重新連接服務器
- (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的指數級增長
{
weakSelf.reConnectTime = 2;
}
else
{
weakSelf.reConnectTime *= 2;
}
});
}
//關閉連接
- (void)SRWebSocketClose;
{
self.isActivelyClose = YES;
[self webSocketClose];
//關閉心跳定時器
[self destoryHeartBeat];
//關閉網絡檢測定時器
[self destoryNetWorkStartTesting];
}
//關閉連接
- (void)webSocketClose
{
if(self.webSocket)
{
[self.webSocket close];
self.webSocket = nil;
}
}
//發送數據給服務器
- (void)sendDataToServer:(id)data
{
[self.sendDataArray addObject:data];
[self sendeDataToServer];
}
- (void)sendeDataToServer
{
WS(weakSelf);
//把數據放到一個請求隊列中
dispatch_async(self.queue, ^{
//沒有網絡
if (AFNetworkReachabilityManager.sharedManager.networkReachabilityStatus == AFNetworkReachabilityStatusNotReachable)
{
//開啟網絡檢測定時器
[weakSelf noNetWorkStartTestingTimer];
}
else //有網絡
{
if(weakSelf.webSocket != nil)
{
// 只有長連接OPEN開啟狀態才能調 send 方法,不然會Crash
if(weakSelf.webSocket.readyState == SR_OPEN)
{
if (weakSelf.sendDataArray.count > 0)
{
NSString *data = weakSelf.sendDataArray[0];
[weakSelf.webSocket send:data]; //發送數據
[weakSelf.sendDataArray removeObjectAtIndex:0];
if([weakSelf.sendDataArray count] > 0)
{
[weakSelf sendeDataToServer];
}
}
}
else if (weakSelf.webSocket.readyState == SR_CONNECTING) //正在連接
{
CTHLog(@"正在連接中,重連后會去自動同步數據");
}
else if (weakSelf.webSocket.readyState == SR_CLOSING || weakSelf.webSocket.readyState == SR_CLOSED) //斷開連接
{
//調用 reConnectServer 方法重連,連接成功后 繼續發送數據
[weakSelf reConnectServer];
}
}
else
{
[weakSelf connectServer]; //連接服務器
}
}
});
}
#pragma mark - SRWebSocketDelegate -- webSockect代理
//連接成功回調
- (void)webSocketDidOpen:(SRWebSocket *)webSocket
{
CTHLog(@"webSocket === 連接成功");
[self initHeartBeat]; //開啟心跳
//如果有尚未發送的數據,繼續向服務端發送數據
if ([self.sendDataArray count] > 0){
[self sendeDataToServer];
}
}
//連接失敗回調
- (void)webSocket:(SRWebSocket *)webSocket didFailWithError:(NSError *)error
{
//用戶主動斷開連接,就不去進行重連
if(self.isActivelyClose)
{
return;
}
[self destoryHeartBeat]; //斷開連接時銷毀心跳
CTHLog(@"連接失敗,這里可以實現掉線自動重連,要注意以下幾點");
CTHLog(@"1.判斷當前網絡環境,如果斷網了就不要連了,等待網絡到來,在發起重連");
CTHLog(@"3.連接次數限制,如果連接失敗了,重試10次左右就可以了");
//判斷網絡環境
if (AFNetworkReachabilityManager.sharedManager.networkReachabilityStatus == AFNetworkReachabilityStatusNotReachable) //沒有網絡
{
[self noNetWorkStartTestingTimer];//開啟網絡檢測定時器
}
else //有網絡
{
[self reConnectServer];//連接失敗就重連
}
}
//連接關閉,注意連接關閉不是連接斷開,關閉是 [socket close] 客戶端主動關閉,斷開可能是斷網了,被動斷開的。
- (void)webSocket:(SRWebSocket *)webSocket didCloseWithCode:(NSInteger)code reason:(NSString *)reason wasClean:(BOOL)wasClean
{
// 在這里判斷 webSocket 的狀態 是否為 open , 大家估計會有些奇怪 ,因為我們的服務器都在海外,會有些時間差,經過測試,我們在進行某次連接的時候,上次重連的回調剛好回來,而本次重連又成功了,就會誤以為,本次沒有重連成功,而再次進行重連,就會出現問題,所以在這里做了一下判斷
if(self.webSocket.readyState == SR_OPEN || self.isActivelyClose)
{
return;
}
CTHLog(@"被關閉連接,code:%ld,reason:%@,wasClean:%d",code,reason,wasClean);
[self destoryHeartBeat]; //斷開連接時銷毀心跳
//判斷網絡環境
if (AFNetworkReachabilityManager.sharedManager.networkReachabilityStatus == AFNetworkReachabilityStatusNotReachable) //沒有網絡
{
[self noNetWorkStartTestingTimer];//開啟網絡檢測
}
else //有網絡
{
[self reConnectServer];//連接失敗就重連
}
}
//該函數是接收服務器發送的pong消息,其中最后一個參數是接受pong消息的
-(void)webSocket:(SRWebSocket *)webSocket didReceivePong:(NSData*)pongPayload
{
NSString* reply = [[NSString alloc] initWithData:pongPayload encoding:NSUTF8StringEncoding];
CTHLog(@"reply === 收到后臺心跳回復 Data:%@",reply);
}
//收到服務器發來的數據
- (void)webSocket:(SRWebSocket *)webSocket didReceiveMessage:(id)message
{
NSMutableDictionary *dataDic = [NSMutableDictionary dictionaryWithJsonString:message];
/*根據具體的業務做具體的處理*/
}
@end
三、注意事項
1、每次重連接的時候需要重新建立連接通道
2、由于按home鍵APP進入后臺,仍然要保持長連接通道不被系統立即斷掉,需要在 AppDelegate 中做以下處理,向系統申請資源,不過這個資源申請也是有限的,最多只有 10分鐘。也可以實現無限后臺機制,這里不做介紹
- (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;
}
});
});
}
四、總結
這個框架使用起來不難,只是在使用的時候需要考慮的情況有些多,理清思路就好了,如有不足之處,希望多多指教 。