iOS Socket重构设计

原文地址:iOS Socket重构设计

之前基于GCDAsyncSocket封装了一个Socket Manager类,但是由于业务复杂度的上升,之前设计的业务接口的数量逐渐增加,代理回调也随之增加,代理的使用也越来越麻烦,所以我们针对socket通信这块,进行了一次重构

这里有我们的新童鞋西兰花很大的功劳哈~

你可以直接使用我们封装好的Socket库,代码地址:GCDAsyncSocketManager


之前的设计方案可以看这里:socket重构前方案

针对老的设计,我们做出了以下几点修改方向:

0x00 拆分SocketManager


首先我们对SocketManager进行开刀,我们将socket相关的操作和业务相关的操作进行拆分,将业务相关的单独放到一个类里面完成,我们命名它为CommunicationManager
现在在SocketManager里面,我们只保留了服务器读写数据断开连接心跳重连GCDAsyncSocket回调设置

在CommunicationManager里面,我们做所有业务的操作

0x01 业务接口改为通用接口


由于业务请求类型的不断增加,业务接口的数量也在不断增加,这样使得头文件一眼望不到底…自己看起来都很头疼,更别说是使用方了...
首先我们将不同的业务请求以枚举的方式列出来,方便外部调用的时候查看,并且最好在枚举后面加上注释,例如:

/**

 * 业务类型
 */
typedef NS_ENUM(NSInteger, FIMRequestType) {
   FIMRequestType_Beat = 1,                       //心跳
   FIMRequestType_ConnectionAuthAppraisal,       //连接鉴权
  FIMRequestType_GetConversationsList,           //获取会话列表
   ...
};

这样我们就可以将业务接口用下面这一个通用的接口替换掉,只需要传type业务请求类型,body请求体和callback回调

/**

 * 向服务器发送数据
 *
 * @param type   请求类型
 * @param body   请求体
 */
- (void)socketWriteDataWithRequestType:(FIMRequestType)type
                           requestBody:(nonnull NSDictionary *)body
                           completion:(nullable SocketDidReadBlock)callback;

比如业务方可以如下使用:

NSDictionary *requestBody = @{ @"limit": @(10), @"offset": @(0) };

[[FIMCommunicationManager sharedInstance]
socketWriteDataWithRequestType:FIMRequestType_GetConversationsList
                   requestBody:requestBody
                   completion:^(NSError *error, id data) {
                       // do something

                   }];

0x02 告别Delegate,使用Blcok


前面也提到,之前会对不同的业务请求,设定相应的delegate回调,但是数量一多,使用起来那真的是槽糕,所以我们参考AFNetworking
的做法,发起请求时将block与一个唯一标识进行绑定,同时将这个唯一标识放到请求里面发给服务器(服务器对该标识不做任何处理),在等到GCDAsyncSocket回调回来的时候,我们通过服务器返回的这个标识,找到对应的block回调出去,这样对业务方来说,这个socket接口用起来其实和HTTP请求接口是一模一样的,将请求的上下文也关联起来了
如图:


具体实现:
发起请求时

- (void)socketWriteDataWithRequestType:(FIMRequestType)type

                           requestBody:(nonnull NSDictionary *)body
                           completion:(nullable SocketDidReadBlock)callback {
 // ...                           
   
 // 生成唯一标识
 NSString *blockRequestID = [self createRequestID];
 if (callback) {
     // 将block和标识进行绑定,存到一个全局变量里面
     [self.requestsMap setObject:callback forKey:blockRequestID];
 }
                             
 // ...
}

接收到GCDAsyncSocket回调时

- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {

 // ...
 
 // 根据服务器返回的标识得到相应的block
 SocketDidReadBlock didReadBlock = self.requestsMap[requestID];
 

 switch (requestType) {
       case FIMRequestType_ConnectionAuthAppraisal: {
           if (didReadBlock) {
               didReadBlock(nil, nil);
           }
       } break;
    // ...
    default: {
           // do something
       } break;
 }
 

 // ...

}

0x03 使用模拟服务器时间,来解决缓存消息保序问题


在socket模块里面,我们基于FMDB实现了一套缓存机制,�但是聊天页面对数据库读写操作的场景非常复杂,而且我们对发送失败的消息也进行了缓存,如果使用msgID对消息进行保序,你要考虑发送成功和失败消息的排序,以及重发消息之后的排序,等等场景,这样实现起来也会很让人头大

所以我们采用消息的创建时间来进行保序,这样不管消息是怎么操作的,从数据库里面读出来的数据,我们只需要根据创建时间来排下序返回给业务层,如果业务层对数据进行修改的时候,我们更新消息的创建时间,这样下次取出来的顺序和UI展示的顺序也还是一样的

那这个创建时间是由服务器生成的,而且消息发送成功之后,服务器也不会返回给我们这条消息的创建时间,而且失败的消息服务器那边是不会存的,所以就需要我们本地模拟服务器来生成这个时间

因为考虑到本地时间和服务器时间存在偏差,所以我们在socket建立连接成功之后,返回给我们服务器时间,我们拿到服务器时间之后和手机的本地时间做个比较,记录下这个偏差值,然后业务层在调用发送消息的接口时,socket内部模拟出服务器创建时间赋值给该消息,然后存到数据库里面,这样就可以基本保证数据库存储消息的顺序和服务器的顺序是一致的
�如图:


0x04 监听网络状态来改变socket连接状态


我们对socket连接状态也做了微调,我们通过测试微信的连接,发现以下两点:
1、网络断开后,socket直接断开,显示“未连接”
2、有网但是socket连接不上时,socket会一直重连,重连n次后,休眠几秒后,再重连,如此循环
所以我们也对socket连接做了调整,用AFNetWorking
库里面监测网络状态类AFNetworkReachabilityManager
AFNetworkReachabilityManager原理),在无网时,判断如果socket正在连接或者已连接时,我们主动调用disconnect
断开连接,如果有网,判断如果socket未连接,我们主动建立连接,建立连接不成功的情况时,我们走重连的流程,只是我们依旧保持了重连n次后,n次失败后不再重连了,这个是与微信不同的地方

0x05 使用FIMSocketModel


因为请求的数据结构基本一样,所以我们定义了FIMSocketModel类来方便对数据的转化,我们定义了几个必传的字段,以及可能请求不同所需的一些非必传字段,由于之前我们body体里面的内容是做了2次JSON转化处理的,所以业务层传入body内容时叫苦连天,FIMSocketModel也增加了- socketModelToJSONString
方法,方便Socket内部转化成JSON处理,这样业务层只需要传一个字典进来,Socket内部就会处理好一切,使用起来一下就方便了~

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

推荐阅读更多精彩内容

  • 第一部分、概念的理解1、什么是Socket?Socket又称之为“套接字”,是系统提供的用于网络通信的方法。它的实...
    Hevin_Chen阅读 2,446评论 0 5
  • 多线程、特别是NSOperation 和 GCD 的内部原理。运行时机制的原理和运用场景。SDWebImage的原...
    LZM轮回阅读 2,004评论 0 12
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,637评论 18 139
  • 有致 文/韦跃 清明的雨点细碎得像沉重的雾气。在高楼夹缝中,这雨会把人憋得缺氧,但走在乡间,风从开阔的田地上吹来,...
    大故事家阅读 389评论 0 1
  • 一篇文章带你领略一部电影,从此不用担心逼格不够。 今天小编为大家讲述一部被低估的电影——《投名状》。 10年前,一...
    小三爷2018阅读 415评论 0 1