什么是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,會有不少疏漏絮爷,歡迎各路大神給我提出建議趴酣。