iOS 从HTTP到WebSocket的无缝过渡

什么是WebSocket

  • WebSocket协议是基于TCP的一种新的网络协议。它实现了浏览器与服务器全双工(full-duplex)通信——允许服务器主动发送信息给客户端。

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

需求分析

  • 在项目开发中,有一个需求,要用WebSocket替换全部的http请求。

  • 对于http请求,项目中一般使用AFNetworking,开发中也会对AFNetworking进行一些简单的封装。

  • 对于这个需求,我重新封装了网络工具类,在保证其它类不进行任何代码修改的前提下,完成了http到WebSocket的过渡。

功能实现

  • 对于网络工具类,对SocketRocket进行了封装,实现了基本的重连机制,block回调。因为目的是从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);//用于任务执行
    }
    return self;
}

初始化方法里创建了一个可变数组用于存放block回调,创建了一个队列用于任务的执行。

@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.发送消息

项目中采用apid作为接口标识,将apid和请求参数写入字典,生成json发送给服务器,并将block回调写入数组中,在消息接收成功后进行移除处理。

//声明成功和失败的block
typedef void(^didReceiveMessageBlock) (id message);
typedef void(^didFailWithErrorBlock) (NSError *error);
//block对应字典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作为接口标识
    [parameters setValue:url forKey:@"apid"];
    NSMutableDictionary * paramsDic = [NSMutableDictionary dictionaryWithDictionary:data];
    //请求参数写入字典
    [parameters setValue:paramsDic forKey:@"params"];
    //转换成json
    NSData * jsonData = [NSJSONSerialization dataWithJSONObject:parameters options:NSJSONWritingPrettyPrinted error:nil];
    NSString *jsonString = [[NSString alloc] initWithData:jsonData
                                                 encoding:NSUTF8StringEncoding];
    NSLog(@"jsonstring===%@",jsonString);
    //添加到任务队列中进行消息发送
    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,不会触发,为适配原先API
    didFailWithErrorBlock failWithError = ^(NSError *error) {
        postError(error);
    };
    //将block添加到数组中
    NSMutableDictionary * mDic = [[NSMutableDictionary alloc] init];
    mDic[receiveCallbackKey] = [receiveMesaage copy];
    mDic[failCallbackKey] = [failWithError copy];
    mDic[@"apid"] = url;
    [self.callbackBlocks addObject:mDic];  
}

通过WebSocket发送消息,根据WebSocket的状态进行不同的处理,如果连接关闭进行重连操作,连接成功后进行消息的发送。

- (void)send:(id)data {
    __weak typeof(self)weakSelf = self;
    if (self.webSocket.readyState == SR_OPEN) {//open状态可以发送数据
        [self.webSocket send:data];
    } else if (self.webSocket.readyState == SR_CONNECTING) {//正在连接 监测状态变为open发送数据
        //通过定时器监测状态,如果变为连接状态发送消息,超过次数发送失败
        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) {//关闭状态 重新连接
        [self reconnect:^{
            [weakSelf send:data];
        } fail:^{
            [weakSelf removeCallbackBlock:data];
        }];
    }
}

重连方法,可定义重连间隔时间和重连次数

static NSTimeInterval const timeout = 2; //重连间隔时间
static NSInteger const reconnectCount = 5; //重连次数
//重连方法
- (void)reconnect:(void(^)())complete fail:(void(^)())fail{
    NSTimer * timer = [NSTimer scheduledTimerWithTimeInterval:timeout repeats:YES block:^(NSTimer * _Nonnull timer) {
        static NSInteger num = 0;
        num ++;
        //超过重连次数,重连失败
        if (num>reconnectCount) {
            if (fail) {
                fail();
            }
            [timer invalidate];
            num = 0;
        }
        //如果变为open状态,重连成功
        if (self.webSocket.readyState == SR_OPEN) {
            if (complete) {
                complete();
            }
            [timer invalidate];
            num = 0;
        }else {
        //其他状态重新连接WebSocket服务器
            [self openForURLString:self.urlString];
        }
    }];
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}

通过接口对应的apid,移除block回调方法,成功接收消息以及发送失败时都需要从数组中移除相应的block回调。

//移除回调
- (void)removeCallbackBlock:(id)data {
    NSString * messageString = [NSString stringWithFormat:@"%@",data];
    NSData * messageData = [messageString dataUsingEncoding:NSUTF8StringEncoding];
    NSDictionary *jsonDic = [NSJSONSerialization JSONObjectWithData:messageData options:NSJSONReadingMutableLeaves error:nil];
    //根据apid进行遍历
    [self.callbackBlocks enumerateObjectsUsingBlock:^(NSDictionary *dic, NSUInteger idx, BOOL *stop) {
        if ([jsonDic[@"apid"]isEqualToString:dic[@"apid"]]) {
            [self.callbackBlocks removeObject:dic];
            *stop = YES;
        }
    }];
}

收到服务器消息的方法,利用SocketRocket提供的代理方法,从block数组中找到对应的apid进行回调。

- (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进行回调,并将block从数组中移除 
    [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];
        }
    }];   
}

监测到连接断开,进行重连操作。

- (void)webSocket:(SRWebSocket *)webSocket didCloseWithCode:(NSInteger)code reason:(NSString *)reason wasClean:(BOOL)wasClean {
    NSLog(@"didCloseWithCode %ld %@ %d",(long)code,reason,wasClean);
    if (reason) {
        //重连 服务器关闭
        [self reconnect:nil fail:nil];
    }
    else {
        //主动关闭 不触发重连
    }
}

模仿AFNetworking,写了一个处理返回数据中存在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;
}

总结

通过对SocketRocket的再次封装,实现了项目需要,完成了在其他类不修改任何代码的前提下,从http到WebSocket的过度。但是有些功能还需要完善,比如消息的超时机制,对失败的进一步处理等等。初次接触WebSocket,会有不少疏漏,欢迎各路大神给我提出建议。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容

  • 花, 美的化身, 如流水一般, 却又那样清晰。 她如仙子一般, 却又那样朴实...... 花。
    梦的妍阅读 113评论 0 2
  • 我手中攥着那张仅剩的宣传单,踉踉跄跄地跑出会场,拼命招手拦下一辆的士,差点被蹭倒在地。 “快开车!随便去哪里!” ...
    风格里哦阅读 570评论 4 36