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  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,会有不少疏漏,欢迎各路大神给我提出建议。

你可能感兴趣的:(iOS 从HTTP到WebSocket的无缝过渡)