接觸WebSocket
最近公司的項目中有一個功能 需要服務器主動推數據到APP。
考慮到普通的HTTP 通信方式只能由客戶端主動拉取,服務器不能主動推給客戶端 。然后就想出的2種解決方案。
1.和后臺溝通了一下 他們那里使用的是WebSocket ,所以就使用WebSocket讓我們app端和服務器建立長連接。這樣就可以事實接受他發過來的消息
2.使用推送,也可以實現接收后臺發過來的一些消息
最后還是選擇了WebSocket,找到了facebook的 SocketRocket 框架。下面是接入過程中的一些記錄
WebSocket
WebSocket 是 HTML5 一種新的協議。它實現了瀏覽器與服務器全雙工通信,能更好的節省服務器資源和帶寬并達到實時通訊,它建立在 TCP 之上,同 HTTP 一樣通過 TCP 來傳輸數據,但是它和 HTTP 最大不同是:
WebSocket 是一種雙向通信協議,在建立連接后,WebSocket 服務器和 Browser/Client Agent 都能主動的向對方發送或接收數據,就像 Socket 一樣;
WebSocket 需要類似 TCP 的客戶端和服務器端通過握手連接,連接成功后才能相互通信。
具體在這兒 WebSocket 是什么原理?為什么可以實現持久連接?
用法
我使用的是pod管理庫 所以在podfile中加入
pod 'SocketRocket'
在使用命令行工具cd到當前工程 安裝
pod install
如果是copy的工程中的 SocketRocket庫的github地址:SocketRocket
導入庫到工程中以后首先封裝一個SocketRocketUtility單例
SocketRocketUtility.m文件中的寫法如下:
#import "SocketRocketUtility.h"
#import <SocketRocket.h>
NSString * const kNeedPayOrderNote = @"kNeedPayOrderNote";//發送的通知名稱
@interface SocketRocketUtility()<SRWebSocketDelegate>
{
int _index;
NSTimer * heartBeat;
NSTimeInterval reConnectTime;
}
@property (nonatomic,strong) SRWebSocket *socket;
@end
@implementation SocketRocketUtility
+ (SocketRocketUtility *)instance {
static SocketRocketUtility *Instance = nil;
static dispatch_once_t predicate;
dispatch_once(&predicate, ^{
Instance = [[SocketRocketUtility alloc] init];
});
return Instance;
}
//開啟連接
-(void)SRWebSocketOpenWithURLString:(NSString *)urlString {
if (self.socket) {
return;
}
if (!urlString) {
return;
}
//SRWebSocketUrlString 就是websocket的地址 寫入自己后臺的地址
self.socket = [[SRWebSocket alloc] initWithURLRequest:
[NSURLRequest requestWithURL:[NSURL URLWithString:urlString]]];
self.socket.delegate = self; //SRWebSocketDelegate 協議
[self.socket open]; //開始連接
}
//關閉連接
- (void)SRWebSocketClose {
if (self.socket){
[self.socket close];
self.socket = nil;
//斷開連接時銷毀心跳
[self destoryHeartBeat];
}
}
#pragma mark - socket delegate
- (void)webSocketDidOpen:(SRWebSocket *)webSocket {
NSLog(@"連接成功,可以與服務器交流了,同時需要開啟心跳");
//每次正常連接的時候清零重連時間
reConnectTime = 0;
//開啟心跳 心跳是發送pong的消息 我這里根據后臺的要求發送data給后臺
[self initHeartBeat];
[[NSNotificationCenter defaultCenter] postNotificationName:kWebSocketDidOpenNote object:nil];
}
- (void)webSocket:(SRWebSocket *)webSocket didFailWithError:(NSError *)error {
NSLog(@"連接失敗,這里可以實現掉線自動重連,要注意以下幾點");
NSLog(@"1.判斷當前網絡環境,如果斷網了就不要連了,等待網絡到來,在發起重連");
NSLog(@"2.判斷調用層是否需要連接,例如用戶都沒在聊天界面,連接上去浪費流量");
NSLog(@"3.連接次數限制,如果連接失敗了,重試10次左右就可以了,不然就死循環了。)";
_socket = nil;
//連接失敗就重連
[self reConnect];
}
- (void)webSocket:(SRWebSocket *)webSocket didCloseWithCode:(NSInteger)code reason:(NSString *)reason wasClean:(BOOL)wasClean {
NSLog(@"被關閉連接,code:%ld,reason:%@,wasClean:%d",code,reason,wasClean);
//斷開連接 同時銷毀心跳
[self SRWebSocketClose];
}
/*
該函數是接收服務器發送的pong消息,其中最后一個是接受pong消息的,
在這里就要提一下心跳包,一般情況下建立長連接都會建立一個心跳包,
用于每隔一段時間通知一次服務端,客戶端還是在線,這個心跳包其實就是一個ping消息,
我的理解就是建立一個定時器,每隔十秒或者十五秒向服務端發送一個ping消息,這個消息可是是空的
*/
-(void)webSocket:(SRWebSocket *)webSocket didReceivePong:(NSData *)pongPayload{
NSString *reply = [[NSString alloc] initWithData:pongPayload encoding:NSUTF8StringEncoding];
NSLog(@"reply===%@",reply);
}
- (void)webSocket:(SRWebSocket *)webSocket didReceiveMessage:(id)message {
//收到服務器發過來的數據 這里的數據可以和后臺約定一個格式 我約定的就是一個字符串 收到以后發送通知到外層 根據類型 實現不同的操作
NSLog(@"%@",message);
[[NSNotificationCenter defaultCenter] postNotificationName:kNeedPayOrderNote object:message];
}
#pragma mark - methods
//重連機制
- (void)reConnect
{
[self SRWebSocketClose];
//超過一分鐘就不再重連 所以只會重連5次 2^5 = 64
if (reConnectTime > 64) {
return;
}
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(reConnectTime * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
self.socket = nil;
[self SRWebSocketOpen];
NSLog(@"重連");
});
//重連時間2的指數級增長
if (reConnectTime == 0) {
reConnectTime = 2;
}else{
reConnectTime *= 2;
}
}
//初始化心跳
- (void)initHeartBeat
{
dispatch_main_async_safe(^{
[self destoryHeartBeat];
__weak typeof(self) weakSelf = self;
//心跳設置為3分鐘,NAT超時一般為5分鐘
heartBeat = [NSTimer scheduledTimerWithTimeInterval:3*60 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"heart");
//和服務端約定好發送什么作為心跳標識,盡可能的減小心跳包大小
[weakSelf sendData:@"heart"];
}];
[[NSRunLoop currentRunLoop]addTimer:heartBeat forMode:NSRunLoopCommonModes];
})
}
//取消心跳
- (void)destoryHeartBeat
{
dispatch_main_async_safe(^{
if (heartBeat) {
[heartBeat invalidate];
heartBeat = nil;
}
})
}
//pingPong機制
- (void)ping{
[self.socket sendPing:nil];
}
#define WeakSelf(ws) __weak __typeof(&*self)weakSelf = self
- (void)sendData:(id)data {
WeakSelf(ws);
dispatch_queue_t queue = dispatch_queue_create("zy", NULL);
dispatch_async(queue, ^{
if (weakSelf.socket != nil) {
// 只有 SR_OPEN 開啟狀態才能調 send 方法,不然要崩
if (weakSelf.socket.readyState == SR_OPEN) {
[weakSelf.socket send:data]; // 發送數據
} else if (weakSelf.socket.readyState == SR_CONNECTING) {
NSLog(@"正在連接中,重連后其他方法會去自動同步數據");
// 每隔2秒檢測一次 socket.readyState 狀態,檢測 10 次左右
// 只要有一次狀態是 SR_OPEN 的就調用 [ws.socket send:data] 發送數據
// 如果 10 次都還是沒連上的,那這個發送請求就丟失了,這種情況是服務器的問題了,小概率的
[self reConnect];
} else if (weakSelf.socket.readyState == SR_CLOSING || weakSelf.socket.readyState == SR_CLOSED) {
// websocket 斷開了,調用 reConnect 方法重連
[self reConnect];
}
} else {
NSLog(@"沒網絡,發送失敗,一旦斷網 socket 會被我設置 nil 的");
}
});
}
-(void)dealloc{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
然后在需要開啟socket的地方調用
[[SocketRocketUtility instance] SRWebSocketOpenWithURLString:@"寫入自己后臺的地址"];
在需要斷開連接的時候調用
[[SocketRocketUtility instance] SRWebSocketClose];
使用這個框架最后一個很重要的 需要注意的一點
這個框架給我們封裝的webscoket在調用它的sendPing senddata方法之前,一定要判斷當前scoket是否連接,如果不是連接狀態,程序則會crash。
結語
這里簡單的實現了連接和收發數據 后續看項目需求在加上后續的改進 希望能夠幫助第一次寫的iOSer 。 希望有更好的方法的童鞋可以有進一步的交流 : )
4月10日 更新:
/// Creates and returns a new NSTimer object initialized with the specified block object and schedules it on the current run loop in the default mode.
/// - parameter: ti The number of seconds between firings of the timer. If seconds is less than or equal to 0.0, this method chooses the nonnegative value of 0.1 milliseconds instead
/// - parameter: repeats If YES, the timer will repeatedly reschedule itself until invalidated. If NO, the timer will be invalidated after it fires.
/// - parameter: block The execution body of the timer; the timer itself is passed as the parameter to this block when executed to aid in avoiding cyclical references
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
上面發送心跳包的方法是iOS10才可以用的 其他版本會崩潰 要適配版本 要選擇 這個方法
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
8月10日 更新demo地址
demo地址
可以下載下來看看哦 :)
注
demo中的后臺地址未設置 所以很多同學直接運行就報錯了 設置一個自己后臺的地址就ok了 :)