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