iOS 從HTTP到WebSocket的無縫過渡

什么是WebSocket

  • WebSocket協(xié)議是基于TCP的一種新的網(wǎng)絡(luò)協(xié)議。它實現(xiàn)了瀏覽器與服務(wù)器全雙工(full-duplex)通信——允許服務(wù)器主動發(fā)送信息給客戶端嫩絮。

  • 基于WebSocket,我使用了Facebook提供的框架SocketRocket语盈。GitHub下載地址

需求分析

  • 在項目開發(fā)中榴啸,有一個需求,要用WebSocket替換全部的http請求。

  • 對于http請求穆刻,項目中一般使用AFNetworking夸研,開發(fā)中也會對AFNetworking進(jìn)行一些簡單的封裝棘钞。

  • 對于這個需求含末,我重新封裝了網(wǎng)絡(luò)工具類服协,在保證其它類不進(jìn)行任何代碼修改的前提下,完成了http到WebSocket的過渡唯绍。

功能實現(xiàn)

  • 對于網(wǎng)絡(luò)工具類,對SocketRocket進(jìn)行了封裝枝誊,實現(xiàn)了基本的重連機(jī)制况芒,block回調(diào)。因為目的是從AFNetworking到SocketRocket的無縫過渡叶撒,所以很多地方采用了封裝AFNetworking時留下的名稱和方法绝骚。

1.單例和初始化方法

//單例方法
+ (CCAFNetworking *)sharedManager {
    static CCAFNetworking *sharedAccountManagerInstance = nil;
    static dispatch_once_t predicate;
    dispatch_once(&predicate, ^{
        sharedAccountManagerInstance = [[self alloc] init];
    });
    return sharedAccountManagerInstance;
}
//初始化方法
- (instancetype)init {
    if ((self = [super init])) {
        _callbackBlocks = [[NSMutableArray alloc] init];//用戶存放block
        _queue = dispatch_queue_create("com.Jifen.queue", DISPATCH_QUEUE_CONCURRENT);//用于任務(wù)執(zhí)行
    }
    return self;
}

初始化方法里創(chuàng)建了一個可變數(shù)組用于存放block回調(diào),創(chuàng)建了一個隊列用于任務(wù)的執(zhí)行祠够。

@property (nonatomic, strong)NSMutableArray *callbackBlocks;
@property (nonatomic, strong)dispatch_queue_t queue;

2.建立WebSocket連接

- (void)openForURLString:(NSString *)URLString {
    self.urlString = URLString;
    
    [self.webSocket close];
    self.webSocket.delegate = nil;
    
    self.webSocket = [[SRWebSocket alloc] initWithURLRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:URLString]]];
    self.webSocket.delegate = self;
    [self.webSocket open];
}

3.發(fā)送消息

項目中采用apid作為接口標(biāo)識压汪,將apid和請求參數(shù)寫入字典,生成json發(fā)送給服務(wù)器古瓤,并將block回調(diào)寫入數(shù)組中止剖,在消息接收成功后進(jìn)行移除處理。

//聲明成功和失敗的block
typedef void(^didReceiveMessageBlock) (id message);
typedef void(^didFailWithErrorBlock) (NSError *error);
//block對應(yīng)字典key
static NSString *const receiveCallbackKey = @"receive";
static NSString *const failCallbackKey = @"fail";

對原先AFNetworking的post方法進(jìn)行重寫落君。

- (void)postUrl:(NSString *)url showUIViewController:(UIViewController *)showView postParamentData:(NSDictionary *)data succesData:(userBaseRequest)postRequest failed:(userFailedRequest)postError {
    NSMutableDictionary * parameters = [[NSMutableDictionary alloc] init];
    //apid作為接口標(biāo)識
    [parameters setValue:url forKey:@"apid"];
    NSMutableDictionary * paramsDic = [NSMutableDictionary dictionaryWithDictionary:data];
    //請求參數(shù)寫入字典
    [parameters setValue:paramsDic forKey:@"params"];
    //轉(zhuǎn)換成json
    NSData * jsonData = [NSJSONSerialization dataWithJSONObject:parameters options:NSJSONWritingPrettyPrinted error:nil];
    NSString *jsonString = [[NSString alloc] initWithData:jsonData
                                                 encoding:NSUTF8StringEncoding];
    NSLog(@"jsonstring===%@",jsonString);
    //添加到任務(wù)隊列中進(jìn)行消息發(fā)送
    dispatch_sync(self.queue, ^{
        [self send:jsonString];
    });
    //成功block
    didReceiveMessageBlock receiveMesaage = ^(id message) {
        NSDictionary *jsonDic = [NSDictionary dictionaryWithDictionary:message];
        if ([jsonDic[@"apid"]isEqualToString:url]) {
            postRequest(jsonDic[@"apidata"],nil);
        }
    };
    //失敗block穿香,不會觸發(fā),為適配原先API
    didFailWithErrorBlock failWithError = ^(NSError *error) {
        postError(error);
    };
    //將block添加到數(shù)組中
    NSMutableDictionary * mDic = [[NSMutableDictionary alloc] init];
    mDic[receiveCallbackKey] = [receiveMesaage copy];
    mDic[failCallbackKey] = [failWithError copy];
    mDic[@"apid"] = url;
    [self.callbackBlocks addObject:mDic];  
}

通過WebSocket發(fā)送消息绎速,根據(jù)WebSocket的狀態(tài)進(jìn)行不同的處理皮获,如果連接關(guān)閉進(jìn)行重連操作,連接成功后進(jìn)行消息的發(fā)送纹冤。

- (void)send:(id)data {
    __weak typeof(self)weakSelf = self;
    if (self.webSocket.readyState == SR_OPEN) {//open狀態(tài)可以發(fā)送數(shù)據(jù)
        [self.webSocket send:data];
    } else if (self.webSocket.readyState == SR_CONNECTING) {//正在連接 監(jiān)測狀態(tài)變?yōu)閛pen發(fā)送數(shù)據(jù)
        //通過定時器監(jiān)測狀態(tài)洒宝,如果變?yōu)檫B接狀態(tài)發(fā)送消息购公,超過次數(shù)發(fā)送失敗
        NSTimer * timer = [NSTimer scheduledTimerWithTimeInterval:timeout repeats:YES block:^(NSTimer * _Nonnull timer) {
            static NSInteger num = 0;
            num ++;
            if (num>reconnectCount) {
                [timer invalidate];
                num = 0;
            }
            if (weakSelf.webSocket.readyState == SR_OPEN) {
                [weakSelf.webSocket send:data];
                [timer invalidate];
                num = 0;
            }
        }];
        [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
    }else if (self.webSocket.readyState == SR_CLOSED||self.webSocket.readyState == SR_CLOSING) {//關(guān)閉狀態(tài) 重新連接
        [self reconnect:^{
            [weakSelf send:data];
        } fail:^{
            [weakSelf removeCallbackBlock:data];
        }];
    }
}

重連方法,可定義重連間隔時間和重連次數(shù)

static NSTimeInterval const timeout = 2; //重連間隔時間
static NSInteger const reconnectCount = 5; //重連次數(shù)
//重連方法
- (void)reconnect:(void(^)())complete fail:(void(^)())fail{
    NSTimer * timer = [NSTimer scheduledTimerWithTimeInterval:timeout repeats:YES block:^(NSTimer * _Nonnull timer) {
        static NSInteger num = 0;
        num ++;
        //超過重連次數(shù)雁歌,重連失敗
        if (num>reconnectCount) {
            if (fail) {
                fail();
            }
            [timer invalidate];
            num = 0;
        }
        //如果變?yōu)閛pen狀態(tài)宏浩,重連成功
        if (self.webSocket.readyState == SR_OPEN) {
            if (complete) {
                complete();
            }
            [timer invalidate];
            num = 0;
        }else {
        //其他狀態(tài)重新連接WebSocket服務(wù)器
            [self openForURLString:self.urlString];
        }
    }];
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}

通過接口對應(yīng)的apid,移除block回調(diào)方法将宪,成功接收消息以及發(fā)送失敗時都需要從數(shù)組中移除相應(yīng)的block回調(diào)绘闷。

//移除回調(diào)
- (void)removeCallbackBlock:(id)data {
    NSString * messageString = [NSString stringWithFormat:@"%@",data];
    NSData * messageData = [messageString dataUsingEncoding:NSUTF8StringEncoding];
    NSDictionary *jsonDic = [NSJSONSerialization JSONObjectWithData:messageData options:NSJSONReadingMutableLeaves error:nil];
    //根據(jù)apid進(jìn)行遍歷
    [self.callbackBlocks enumerateObjectsUsingBlock:^(NSDictionary *dic, NSUInteger idx, BOOL *stop) {
        if ([jsonDic[@"apid"]isEqualToString:dic[@"apid"]]) {
            [self.callbackBlocks removeObject:dic];
            *stop = YES;
        }
    }];
}

收到服務(wù)器消息的方法,利用SocketRocket提供的代理方法较坛,從block數(shù)組中找到對應(yīng)的apid進(jìn)行回調(diào)印蔗。

- (void)webSocket:(SRWebSocket *)webSocket didReceiveMessage:(id)message {
    //將接收到的消息進(jìn)行json解析
    NSString * messageString = [NSString stringWithFormat:@"%@",message];
    NSData * messageData = [messageString dataUsingEncoding:NSUTF8StringEncoding];
    NSDictionary *jsonDic = [NSJSONSerialization JSONObjectWithData:messageData options:NSJSONReadingMutableLeaves error:nil];
    jsonDic = [self byJSONObjectByRemovingKeysWithNullValues:jsonDic];
     //將消息通過apid進(jìn)行回調(diào),并將block從數(shù)組中移除 
    [self.callbackBlocks enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        NSDictionary * dic = [NSDictionary dictionaryWithDictionary:obj];
        if ([jsonDic[@"apid"]isEqualToString:dic[@"apid"]]) {
            *stop = YES;
            didReceiveMessageBlock block = dic[receiveCallbackKey];
            block(jsonDic);
            [self.callbackBlocks removeObject:dic];
        }
    }];   
}

監(jiān)測到連接斷開丑勤,進(jìn)行重連操作华嘹。

- (void)webSocket:(SRWebSocket *)webSocket didCloseWithCode:(NSInteger)code reason:(NSString *)reason wasClean:(BOOL)wasClean {
    NSLog(@"didCloseWithCode %ld %@ %d",(long)code,reason,wasClean);
    if (reason) {
        //重連 服務(wù)器關(guān)閉
        [self reconnect:nil fail:nil];
    }
    else {
        //主動關(guān)閉 不觸發(fā)重連
    }
}

模仿AFNetworking,寫了一個處理返回數(shù)據(jù)中存在NULL的方法

//遞歸去除NULL法竞,參考AFNetworking
- (id)byJSONObjectByRemovingKeysWithNullValues:(id)JSONObject {
    if ([JSONObject isKindOfClass:[NSArray class]]) {
        NSMutableArray *mutableArray = [NSMutableArray arrayWithCapacity:[(NSArray *)JSONObject count]];
        for (id value in (NSArray *)JSONObject) {
            [mutableArray addObject:[self byJSONObjectByRemovingKeysWithNullValues:value]];
        }
        return [NSArray arrayWithArray:mutableArray];
    } else if ([JSONObject isKindOfClass:[NSDictionary class]]) {
        NSMutableDictionary *mutableDictionary = [NSMutableDictionary dictionaryWithDictionary:JSONObject];
        for (id <NSCopying> key in [(NSDictionary *)JSONObject allKeys]) {
            id value = (NSDictionary *)JSONObject[key];
            if (!value || [value isEqual:[NSNull null]]) {
                [mutableDictionary removeObjectForKey:key];
            } else if ([value isKindOfClass:[NSArray class]] || [value isKindOfClass:[NSDictionary class]]) {
                mutableDictionary[key] = [self byJSONObjectByRemovingKeysWithNullValues:value];
            }
        }
        return [NSDictionary dictionaryWithDictionary:mutableDictionary];
    }
    return JSONObject;
}

總結(jié)

通過對SocketRocket的再次封裝耙厚,實現(xiàn)了項目需要,完成了在其他類不修改任何代碼的前提下岔霸,從http到WebSocket的過度薛躬。但是有些功能還需要完善,比如消息的超時機(jī)制呆细,對失敗的進(jìn)一步處理等等型宝。初次接觸WebSocket,會有不少疏漏絮爷,歡迎各路大神給我提出建議趴酣。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市坑夯,隨后出現(xiàn)的幾起案子岖寞,更是在濱河造成了極大的恐慌,老刑警劉巖柜蜈,帶你破解...
    沈念sama閱讀 217,734評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件仗谆,死亡現(xiàn)場離奇詭異,居然都是意外死亡淑履,警方通過查閱死者的電腦和手機(jī)胸私,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,931評論 3 394
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來鳖谈,“玉大人岁疼,你說我怎么就攤上這事。” “怎么了捷绒?”我有些...
    開封第一講書人閱讀 164,133評論 0 354
  • 文/不壞的土叔 我叫張陵瑰排,是天一觀的道長。 經(jīng)常有香客問我暖侨,道長椭住,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,532評論 1 293
  • 正文 為了忘掉前任字逗,我火速辦了婚禮京郑,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘葫掉。我一直安慰自己些举,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,585評論 6 392
  • 文/花漫 我一把揭開白布俭厚。 她就那樣靜靜地躺著户魏,像睡著了一般。 火紅的嫁衣襯著肌膚如雪挪挤。 梳的紋絲不亂的頭發(fā)上叼丑,一...
    開封第一講書人閱讀 51,462評論 1 302
  • 那天,我揣著相機(jī)與錄音扛门,去河邊找鬼鸠信。 笑死,一個胖子當(dāng)著我的面吹牛论寨,可吹牛的內(nèi)容都是我干的星立。 我是一名探鬼主播,決...
    沈念sama閱讀 40,262評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼政基,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了闹啦?” 一聲冷哼從身側(cè)響起沮明,我...
    開封第一講書人閱讀 39,153評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎窍奋,沒想到半個月后荐健,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,587評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡琳袄,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,792評論 3 336
  • 正文 我和宋清朗相戀三年江场,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片窖逗。...
    茶點(diǎn)故事閱讀 39,919評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡址否,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出碎紊,到底是詐尸還是另有隱情佑附,我是刑警寧澤樊诺,帶...
    沈念sama閱讀 35,635評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站音同,受9級特大地震影響词爬,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜权均,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,237評論 3 329
  • 文/蒙蒙 一顿膨、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧叽赊,春花似錦恋沃、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,855評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至取劫,卻和暖如春匆笤,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背谱邪。 一陣腳步聲響...
    開封第一講書人閱讀 32,983評論 1 269
  • 我被黑心中介騙來泰國打工炮捧, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人惦银。 一個月前我還...
    沈念sama閱讀 48,048評論 3 370
  • 正文 我出身青樓咆课,卻偏偏與公主長得像,于是被迫代替她去往敵國和親扯俱。 傳聞我的和親對象是個殘疾皇子书蚪,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,864評論 2 354

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

  • 花, 美的化身迅栅, 如流水一般殊校, 卻又那樣清晰。 她如仙子一般读存, 卻又那樣樸實...... 花为流。
    夢的妍閱讀 116評論 0 2
  • 我手中攥著那張僅剩的宣傳單尔当,踉踉蹌蹌地跑出會場莲祸,拼命招手?jǐn)r下一輛的士,差點(diǎn)被蹭倒在地。 “快開車虫给!隨便去哪里藤抡!” ...
    風(fēng)格里哦閱讀 576評論 4 36