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進行一些簡單的封裝。

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

功能實現(xiàn)

  • 對于網(wǎng)絡(luò)工具類,對SocketRocket進行了封裝,實現(xiàn)了基本的重連機制,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ù)組中,在消息接收成功后進行移除處理。

//聲明成功和失敗的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方法進行重寫。

- (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ù)隊列中進行消息發(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)進行不同的處理,如果連接關(guā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進行遍歷
    [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進行回調(diào)。

- (void)webSocket:(SRWebSocket *)webSocket didReceiveMessage:(id)message {
    //將接收到的消息進行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進行回調(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)測到連接斷開,進行重連操作。

- (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的過度。但是有些功能還需要完善,比如消息的超時機制,對失敗的進一步處理等等。初次接觸WebSocket,會有不少疏漏,歡迎各路大神給我提出建議。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

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

友情鏈接更多精彩內(nèi)容