WebSocket:SocketRocket封裝

WebSocket

  • WebSocket 是 HTML5 開始提供的一種在單個(gè) TCP 連接上進(jìn)行全雙工通訊的協(xié)議呻粹。
  • HTTP 協(xié)議是一種無狀態(tài)的奢方、無連接的侣监、單向的應(yīng)用層協(xié)議。它采用了請求/響應(yīng)模型齿诉。通信請求只能由客戶端發(fā)起,服務(wù)端對請求做出應(yīng)答處理晌姚。這種通信模型有一個(gè)弊端:HTTP 協(xié)議無法實(shí)現(xiàn)服務(wù)器主動向客戶端發(fā)起消息粤剧。這種單向請求的特點(diǎn),注定了如果服務(wù)器有連續(xù)的狀態(tài)變化挥唠,客戶端要獲知就非常麻煩抵恋。大多數(shù) Web 應(yīng)用程序?qū)⑼ㄟ^頻繁的異步JavaScript和XML(AJAX)請求實(shí)現(xiàn)長輪詢。輪詢的效率低猛遍,非常浪費(fèi)資源(因?yàn)楸仨毑煌_B接馋记,或者 HTTP 連接始終打開)。
  • WebSocket 連接允許客戶端和服務(wù)器之間進(jìn)行全雙工通信懊烤,以便任一方都可以通過建立的連接將數(shù)據(jù)推送到另一端梯醒。WebSocket 只需要建立一次連接,就可以一直保持連接狀態(tài)腌紧。這相比于輪詢方式的不停建立連接顯然效率要大大提高茸习。

WebSocket與Socket的關(guān)系

Socket其實(shí)并不是一個(gè)協(xié)議,而是為了方便使用TCP或UDP而抽象出來的一層壁肋,是位于應(yīng)用層和傳輸控制層之間的一組接口号胚。是應(yīng)用層與TCP/IP協(xié)議族通信的中間軟件抽象層,它是一組接口浸遗。在設(shè)計(jì)模式中猫胁,Socket其實(shí)就是一個(gè)門面模式,它把復(fù)雜的TCP/IP協(xié)議族隱藏在Socket接口后面跛锌,對用戶來說弃秆,一組簡單的接口就是全部,讓Socket去組織數(shù)據(jù)髓帽,以符合指定的協(xié)議菠赚。當(dāng)兩臺主機(jī)通信時(shí),必須通過Socket連接郑藏,Socket則利用TCP/IP協(xié)議建立TCP連接衡查。TCP連接則更依靠于底層的IP協(xié)議,IP協(xié)議的連接則依賴于鏈路層等更低層次必盖。
WebSocket則是一個(gè)典型的應(yīng)用層協(xié)議拌牲。
區(qū)別是Socket是傳輸控制層協(xié)議俱饿,WebSocket是應(yīng)用層協(xié)議。

WebSocket的框架SocketRocket

SocketRocket

  • 工具類封裝
#import <Foundation/Foundation.h>
#import <SocketRocket.h>

extern NSString * const GQNotification_SocketRocketDidOpen;
extern NSString * const GQNotification_SocketRocketDidClose;
extern NSString * const GQNotification_SocketRocketDidReceive;

@interface GQSocketRocketManager : NSObject
+ (instancetype)gq_shareInstance;

/** 獲取連接狀態(tài) */
@property (nonatomic, assign, readonly) SRReadyState socketReadyState;

/** 開啟連接 */
- (void)gq_openWithURLString:(NSString *)urlString;
/** 關(guān)閉連接 */
- (void)gq_close;
/** 發(fā)送數(shù)據(jù) */
- (void)gq_sendData:(id)data;

@end
#import "GQSocketRocketManager.h"
#import "GQKit.h"
NSString * const GQNotification_SocketRocketDidOpen = @"GQNotification_SocketRocketDidOpen";
NSString * const GQNotification_SocketRocketDidClose = @"GQNotification_SocketRocketDidClose";
NSString * const GQNotification_SocketRocketDidReceive = @"GQNotification_SocketRocketDidReceive";

@interface GQSocketRocketManager()<SRWebSocketDelegate>

@property (nonatomic,strong) SRWebSocket *socket;

@property (nonatomic,strong) NSTimer *heartBeat;

@property (nonatomic,assign) NSTimeInterval reConnectTime;

@property (nonatomic,copy) NSString *urlString;
@end

@implementation GQSocketRocketManager

+ (instancetype)gq_shareInstance {
    static GQSocketRocketManager *instance;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [[GQSocketRocketManager alloc] init];
    });
    return instance;
}

- (void)gq_openWithURLString:(NSString *)urlString {
    if (self.socket) {
        return;
    }
    if (!urlString) {
        return;
    }
    self.urlString = urlString;
    self.socket = [[SRWebSocket alloc] initWithURLRequest:
                   [NSURLRequest requestWithURL:[NSURL URLWithString:urlString]]];
    
    self.socket.delegate = self;
    [self.socket open];
}

- (void)gq_close {
    if (self.socket){
        [self.socket close];
        self.socket = nil;
        [self destoryHeartBeat];
    }
}

- (void)gq_sendData:(id)data {
    __weak __typeof(self) weakSelf = self;
    dispatch_queue_t queue =  dispatch_queue_create("zy", NULL);
    dispatch_async(queue, ^{
        if (weakSelf.socket != nil) {
            // 只有 SR_OPEN 開啟狀態(tài)才能調(diào) send 方法啊塌忽,不然要崩
            if (weakSelf.socket.readyState == SR_OPEN) {
                [weakSelf.socket send:data];    // 發(fā)送數(shù)據(jù)
                
            } else if (weakSelf.socket.readyState == SR_CONNECTING) {
                GQLog(@"正在連接中");
                [self reConnect];
            } else if (weakSelf.socket.readyState == SR_CLOSING || weakSelf.socket.readyState == SR_CLOSED) {
                [self reConnect];
            }
        } else {
            
        }
    });
}

//重連機(jī)制
- (void)reConnect {
    [self gq_close];

    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(self.reConnectTime * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        self.socket = nil;
        [self gq_openWithURLString:self.urlString];
    });
    
    if (self.reConnectTime == 0) {
        self.reConnectTime = 2;
    }
}

//取消心跳
- (void)destoryHeartBeat {
    GQ_dispatch_main_async_safe(^{
        if (self.heartBeat) {
            if ([self.heartBeat respondsToSelector:@selector(isValid)]){
                if ([self.heartBeat isValid]){
                    [self.heartBeat invalidate];
                    self.heartBeat = nil;
                }
            }
        }
    })
}

//初始化心跳
- (void)initHeartBeat {
    GQ_dispatch_main_async_safe(^{
        [self destoryHeartBeat];
        self.heartBeat = [NSTimer timerWithTimeInterval:3 target:self selector:@selector(sentheart) userInfo:nil repeats:YES];
        [[NSRunLoop currentRunLoop] addTimer:self.heartBeat forMode:NSRunLoopCommonModes];
    })
}

-(void)sentheart {
    //發(fā)送心跳
    [self gq_sendData:[@"1" dataUsingEncoding:NSUTF8StringEncoding]];
}

#pragma mark - delegate
- (void)webSocketDidOpen:(SRWebSocket *)webSocket {
    //每次正常連接的時(shí)候清零重連時(shí)間
    self.reConnectTime = 0;
    //開啟心跳
    [self initHeartBeat];
    if (webSocket == self.socket) {
        GQLog(@"socket連接成功");
        [[NSNotificationCenter defaultCenter] postNotificationName:GQNotification_SocketRocketDidOpen object:nil];
    }
}

- (void)webSocket:(SRWebSocket *)webSocket didFailWithError:(NSError *)error {
    if (webSocket == self.socket) {
        GQLog(@"socket連接失敗");
        _socket = nil;
        [self reConnect];
    }
}

- (void)webSocket:(SRWebSocket *)webSocket didCloseWithCode:(NSInteger)code reason:(NSString *)reason wasClean:(BOOL)wasClean {
    if (webSocket == self.socket) {
        GQLog(@"socket連接斷開 被關(guān)閉連接稍途,code:%ld,reason:%@,wasClean:%d",(long)code,reason,wasClean);
        [self reConnect];
    }
}

/*該函數(shù)是接收服務(wù)器發(fā)送的pong消息,其中最后一個(gè)是接受pong消息的砚婆,
 在這里就要提一下心跳包械拍,一般情況下建立長連接都會建立一個(gè)心跳包,
 用于每隔一段時(shí)間通知一次服務(wù)端装盯,客戶端還是在線坷虑,這個(gè)心跳包其實(shí)就是一個(gè)ping消息,
 */
- (void)webSocket:(SRWebSocket *)webSocket didReceivePong:(NSData *)pongPayload {
//    NSString *reply = [[NSString alloc] initWithData:pongPayload encoding:NSUTF8StringEncoding];
    
}

- (void)webSocket:(SRWebSocket *)webSocket didReceiveMessage:(id)message {
    if (webSocket == self.socket) {
        GQLog(@"socket收到數(shù)據(jù)message:%@",message);
        [[NSNotificationCenter defaultCenter] postNotificationName:GQNotification_SocketRocketDidReceive object:message];
    }
}

- (SRReadyState)socketReadyState {
    return self.socket.readyState;
}

@end
封裝的步驟邏輯
  • 1.開啟連接
    - (void)gq_openWithURLString:(NSString *)urlString;
  • 2.開啟心跳

像心跳一樣每隔固定時(shí)間發(fā)一次埂奈,以此來告訴服務(wù)器迄损,這個(gè)客戶端還活著。事實(shí)上這是為了保持長連接账磺,至于這個(gè)包的內(nèi)容芹敌,是沒有什么特別規(guī)定的,不過一般都是很小的包垮抗,或者只包含包頭的一個(gè)空包氏捞。所以心跳就是一個(gè)定時(shí)器heartBeat,每3秒發(fā)送一次冒版。
心跳的每一次發(fā)送其實(shí)就是一個(gè)ping消息液茎,其服務(wù)器返回的是pong消息,-(void)webSocket:(SRWebSocket *)webSocket didReceivePong:(NSData *)pongPayload辞嗡,接收不到pong那就是心跳斷開了捆等。

  • 3.重連機(jī)制

當(dāng)自己網(wǎng)絡(luò)斷開時(shí)候,需要重連续室;
當(dāng)后臺主動斷開栋烤,正常斷開,會調(diào)用- (void)webSocket:(SRWebSocket *)webSocket didCloseWithCode:(NSInteger)code reason:(NSString *)reason wasClean:(BOOL)wasClean挺狰,處理可以是關(guān)閉連接SRWebSocketClose明郭,也可以是重連,一般是后者她渴;
當(dāng)后臺是網(wǎng)絡(luò)不好达址,或者崩潰蔑祟,調(diào)用- (void)webSocket:(SRWebSocket *)webSocket didFailWithError:(NSError *)error趁耗,會重新連接;

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末疆虚,一起剝皮案震驚了整個(gè)濱河市苛败,隨后出現(xiàn)的幾起案子满葛,更是在濱河造成了極大的恐慌,老刑警劉巖罢屈,帶你破解...
    沈念sama閱讀 218,682評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件嘀韧,死亡現(xiàn)場離奇詭異,居然都是意外死亡缠捌,警方通過查閱死者的電腦和手機(jī)锄贷,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來曼月,“玉大人谊却,你說我怎么就攤上這事⊙魄郏” “怎么了炎辨?”我有些...
    開封第一講書人閱讀 165,083評論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長聪姿。 經(jīng)常有香客問我碴萧,道長,這世上最難降的妖魔是什么末购? 我笑而不...
    開封第一講書人閱讀 58,763評論 1 295
  • 正文 為了忘掉前任破喻,我火速辦了婚禮,結(jié)果婚禮上盟榴,老公的妹妹穿的比我還像新娘低缩。我一直安慰自己,他們只是感情好曹货,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,785評論 6 392
  • 文/花漫 我一把揭開白布咆繁。 她就那樣靜靜地躺著,像睡著了一般顶籽。 火紅的嫁衣襯著肌膚如雪玩般。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,624評論 1 305
  • 那天礼饱,我揣著相機(jī)與錄音坏为,去河邊找鬼。 笑死镊绪,一個(gè)胖子當(dāng)著我的面吹牛匀伏,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播蝴韭,決...
    沈念sama閱讀 40,358評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼够颠,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了榄鉴?” 一聲冷哼從身側(cè)響起履磨,我...
    開封第一講書人閱讀 39,261評論 0 276
  • 序言:老撾萬榮一對情侶失蹤蛉抓,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后剃诅,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體巷送,經(jīng)...
    沈念sama閱讀 45,722評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年矛辕,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了笑跛。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,030評論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡聊品,死狀恐怖堡牡,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情杨刨,我是刑警寧澤晤柄,帶...
    沈念sama閱讀 35,737評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站妖胀,受9級特大地震影響芥颈,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜赚抡,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,360評論 3 330
  • 文/蒙蒙 一爬坑、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧涂臣,春花似錦盾计、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至岩四,卻和暖如春哭尝,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背剖煌。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評論 1 270
  • 我被黑心中介騙來泰國打工材鹦, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人耕姊。 一個(gè)月前我還...
    沈念sama閱讀 48,237評論 3 371
  • 正文 我出身青樓桶唐,卻偏偏與公主長得像,于是被迫代替她去往敵國和親茉兰。 傳聞我的和親對象是個(gè)殘疾皇子尤泽,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,976評論 2 355

推薦閱讀更多精彩內(nèi)容