美文网首页
网络层设计一-架构设计

网络层设计一-架构设计

作者: 王方帅 | 来源:发表于2023-10-07 10:54 被阅读0次

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

    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复用性与服务器压力之间的平衡 - 腾讯云开发者社区-腾讯云

    相关文章

      网友评论

          本文标题:网络层设计一-架构设计

          本文链接:https://www.haomeiwen.com/subject/tfzmbdtx.html