网络层设计一-架构设计

一、我认为,一个设计合理的网络层应支持以下方面:

1. 应该支持缓存(三方框架:hyperoslo/Cache)

 1. 使用缓存有以下优点:使用缓存可以降低服务器的压力,更快速的响应用户的请求,提升用户体验。

  1. 对于很长时间都不会有变化的接口来说,可以设置一个时效性,在时效性之内使用缓存,当然这个时效性要通知到客服及Cloud人员,以便支持用户需求。localOrRemote

  2. 对于一些场景可以支持先显示缓存再加载远端,远端返回后覆盖缓存,比如点击微信朋友圈就是先显示的缓存然后去发起请求。localThenRemote

  3. 对于一些场景可以支持先加载远端,远端失败才显示缓存。(总体来说返回用户失败不如返回缓存用户更友好)remoteThenLocal

  4. 对于一些有很高时效性的场景可以不使用缓存,直接加载远端数据。remote

 2. 对于服务器修改了一个字段,客户端有缓存一直显示的缓存没有加载服务器修改后的数据,这种怎么处理呢?

  1. 客户端可以设置一个网络层统一的缓存过期时长,并告诉Cloud,这是我们设置的时长,让Cloud明白客户端只有在这个时长后才会生效。

  2. 如果Cloud不想等这个时长可以在APP设置页面手动APP清理缓存数据,并且这是合理的,服务器手动修改了字段想要打破客户端的缓存机制直接加载最新的那就是需要手动清理缓存。

  3. 客户端可以只做App生命周期内的缓存,比如只做内存缓存、或也做磁盘缓存但在启动时进行清理。

 3. 缓存应该怎么做?

  1. 缓存的枚举值定义为:

public enum VKNetworkCache {
    case localThenRemote//先用缓存同时请求远程数据
    case localOrRemote//如果有缓存 只使用缓存
    case remote//忽略缓存
    case local
    case remoteThenLocal//先请求远程,远程失败时返回local
}

  2. 通常所说的三级缓存是指内存缓存、磁盘缓存、云端缓存,而云端缓存需要依赖云端开发人员去做,客户端能独立做的有内存缓存、磁盘缓存。

let diskConfig = DiskConfig(name: "WZCache", 
expiry: .seconds(60 * 60 * 24 * 3), maxSize: 1024 * 1024 * 64)

let memoryConfig = MemoryConfig(expiry: .never, 
countLimit: 100, totalCostLimit: 1024 * 1024 * 16)

  3. 缓存的Key使用encoded requestURL、method、parameters拼成一个字符串去MD5为key来存缓存。

2. 应该支持Json转Model(三方框架:alibaba/HandyJSON )

 1. Json转Model是一个通用的需求,网络层直接做了转Model的操作,业务调用就可以使用更少的代码来实现网络请求,所以有必要在网络层做转Model的操作。

 2. 苹果系统提供的Decodable方式有很多缺点:

  1. 当需要自定义变量名称时,需要写出所有的CodingKeys,例如下面例子只是userID的key不同却需要把nonce、id、state、name、location全部列出来。

enum CodingKeys: String, CodingKey {
    case nonce
    case id, state, name, location
    case userID = "userId"
}

  2. 当服务端某个字段没有返回,客户端又没有声明为可选类型时,会导致解析Model失败。

let id: String
//or
let id: String?

  3. 给struct里的let常量设置默认值将会很麻烦,其中一种做法是重写init(from decoder: Decoder)方法。

init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    name = try container.decode(String.self, forKey: .name)
    age = try container.decodeIfPresent(Int.self, forKey: .age) ?? -1
}

 3. HandyJSON对比苹果系统具有以下优点:

  1. 当需要自定义变量名称时,只需要写需要自定义的变量,其他变量不需要写。

mutating func mapping(mapper: HelpingMapper) {
    mapper <<< userID <-- "userId"
}

  2. 继承自HandyJSON协议后,其属性强制要求声明为可选类型或赋初值。不存在解析不声明为可选类型解析失败的问题。

struct Cat: HandyJSON {
    var id: Int = 1
    var color: String?
    var name: String?
}

  3. 给struct里的let常量设置默认值只需要初始化设置即可。

struct People: HandyJSON {
    let id: Int?
    var name: String?
    let age: String = 18
}

  4. HandyJSON demo

3. 应该有清晰易用的Log打印

 应该支持DEBUG模式下的Log打印,打印以一个完整的请求为单位,request等待response返回再统一打印,打印以火箭🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀图标开始,以火焰🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥图标结束,中间是请求url、method、requestBody、responseBody,requestBody、responseBody均为格式化无换行、无斜杠、无空格的JSON格式,可以作为唯一信息来源跟服务器端进行联调。

[WZNetwork]: begin 🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀
[WZNetwork]: url->https://beta-platform-service.wyzecam.com/api/v1/hms/v2/profile, method->get
[WZNetwork]: requestBody->{"hms_id":"11111"}
[WZNetwork]: responseBody->{"message":"string","response":[{"location_name":
"string","phone_number":"string","space_id":"string"}],"status":0}
[WZNetwork]: end 🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥

4. 回调建议统一采用Delegate+少量Notification来实现

 1. Delegate具有以下优点:

  1. 不会因为忘记[weak self]引起循环引用,导致内存泄漏。

  2. 统一回调方法,便于维护和调试。

 2. Block具有以下缺点:

  1. 容易引起循环引用,导致内存泄漏,需要在回调加固定制式代码。

[weak self] in guard let self = self else { return }

  2. 回调散落在各个调用的地方,不能统一打断点调试所有请求。

  3. block在离散型场景下不符合使用规范

   当回调之后要做的任务在每次回调时都是一致的情况下用delegate,当回调之后要做的任务在每次回调都无法确保一致的情况下用block,在离散调用场景下每次回调都能保证任务一致,所以用delegate。

  4. block所包含的回调代码跟调用代码在一个地方,会导致那部分代码变得很长,因为同时包含调用逻辑和回调逻辑,一定程度上违背了single function, single task的原则。在调用的地方就只要写调用逻辑,在回调的时候只写回调逻辑。

   有的业务工程师意识到这个问题,会写一个一句话的方法去做转发。比如这样:

[API callApiWithParam:param successed:^(Response *response){
    [self successedWithResponse:response];
} failed:^(Request *request, NSError *error){
    [self failedWithRequest:request error:error];
}];

   这时候网络层架构设计时采用delegate的方式的话业务工程师实现就不用这么绕了。

 3. 仅有少数场景需要使用Notification,比如网络状态变化:从蜂窝网络变为Wifi、从有网变为无网。

 4. DelegateTest demo

5. 底层采用集约型API发起请求,给业务层使用时封装离散型API调用方式

 1. 集约型调用方式是指所有Api的调用只有一个类,然后这个类接收subURL、method、params以及callback,然后通过调用send方法,这个类就会根据这些参数去发起请求,获得response都通过callback返回给调用方。比如这样:

//集约式Api调用方式
WZRequest.send(subURL: "v1/hms/v2/profile", method: "post", params: ["hms_id": "1111"]) { response in
    print(response)
} failure: { error in
    print(error)
}

 2. 离散型调用方式是指一个Api对应一个request,这个request是只要提供params对应的Model和delegate,subURL、method已经集成到了request中。比如这样:

//离散型Api调用方式
let request = WZCPPProfileRequest(body: .init(hms_id: "1111"), delegate: self)
request.send()

 3. 单看下层大家都是集约型调用方式,因为除了业务相关的部分(subURL、method、params),剩下的都是要统一处理的,比如加解密、URL拼接、请求的发起和回调。然而对于业务层使用来说,我倾向于离散型调用方式,这样在request内部可以针对不同的请求来设置不同的请求策略,而调用的时候只需要提供必要的参数即可。

  1. 比如用户多次下拉刷新请求的场景,可以在某个request中写判断逻辑,当前有请求则不重复发起请求。

  2. 比如用户在筛选的场景下,用户变更了筛选条件,可以在对应的request种写判断逻辑,取消之前的请求,发起新请求。

  3. 比如要针对某个请求做AOP,离散型调用方式就很容易实现,而集约型调用方式实现就很复杂。

  4. 离散型调用方式能够最大程度的给业务方提供灵活性,比如loadNextPage,比如对请求参数进行验证。

 4. 集约型和离散型 demo

6. 可以支持批量请求

 1. 服务器是否需要一个接口返回指定客户端业务的所有数据呢?

  在服务器设计接口时,通常需要做分层结构,在最底层,提供颗粒度非常小的、灵活性高的Api接口;往上的层,粒度逐步变粗。业务足够复杂时,本身Api可以视为基础数据Api。如果某个业务所需要的接口调用数据太多,应该在基础数据Api层上建立业务层,在业务层内部调用基础数据Api层的相关接口,把其封装成统一的、适合于当前访问的业务,依次返回给客户端。

 2. 当服务端没有做这一层或没有精力做这一层时,客户端如何维护好一个页面的多个接口的请求呢?

  这就需要客户端的网络层提供支持批量请求的能力。比如有一个Profile的更新需求,需要同时更新CPP和MMS两个的Profile,则批量请求调用示例如下:

let profile = WZCPPProfile(hms_id: "111", devices: ["222"])
let request1 = WZPutCPPProfileRequest(body: profile)
let request2 = WZPutMMSProfileRequest(body: profile)
let batchRequest = WZBatchRequest(requestArray: [request1, request2], delegate: self)
batchRequest.send()

extension ViewController: WZBatchRequestDelegate {
    func batchRequestDidSuccess(batchRequest: WZBatchRequest) {
        //...
    }

    func batchRequestDidFailure(error: Error) {
        //...
    }
}

 3. 批量请求 demo

7. 可以支持链式请求

 1. 当需要批量发起多个请求,但是多个请求之间又具有依赖关系时,此时需要发起链式请求,比如有这样一个需求,UpdateMMSProfile依赖于GetMMSProfileRequest去获取到最新的MMSProfile才能更新某个字段,而GetMMSProfileRequest又依赖于GetCPPProfileRequest拿到hms_id才能去请求,这时候使用链式请求就可以写成以下形式:

let request1 = WZGetCPPProfileRequest(body: .init())
let chainRequest = WZChainRequest(requestArray: [request1], delegate: self)
chainRequest.send()

extension ViewController: WZChainRequestDelegate {
    func baseRequestDidSuccess(chainRequest: WZChainRequest, baseRequest: some WZRequestable) {
        if let request = baseRequest as? WZGetCPPProfileRequest, let cppProfile = request.responseBody {
            let request2 = WZGetMMSProfileRequest(body: .init(hms_id: cppProfile.hms_id))
            chainRequest.add(request2)
        } else if let request = baseRequest as? WZGetMMSProfileRequest, let mmsProfile = request.responseBody {
            mmsProfile.devices = []
            let request3 = WZUpdateMMSProfileRequest(body: mmsProfile)
            chainRequest.add(request3)
        }
    }
    
    func chainRequestDidSuccess(chainRequest: WZChainRequest) {
        let requests = chainRequest.requestArray
        //可以统一处理所有链式请求的结果,也可以在每个request的回调中分别处理。
    }

    func chainRequestDidFailure(error: Error) {
        print(error)
    }
}

 2. 链式请求 demo

二、网络层优化可以做以下事情:

1,DNS优化

2,HTTP/2支持:HTTP/2基于SPDY协议实现多路复用,使用HPACK算法进行头部压缩。

三、有任何问题欢迎评论区留言进行探讨。

参考文章:

YTKNetwork源码解析 - 掘金

iOS应用架构谈 网络层设计方案

开源一个封装AFNetworking的网络框架 - SJNetwork - 掘金

接口粒度:API复用性与服务器压力之间的平衡 - 腾讯云开发者社区-腾讯云

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

推荐阅读更多精彩内容