iOS websocket接入

接觸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了 :)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,345評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,494評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,283評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,953評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,714評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,186評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,255評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,410評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,940評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,776評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,976評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,518評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,210評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,642評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,878評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,654評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,958評論 2 373