从OkHttp的使用谈谈App网络层搭建的思维过程

前言

    在Android开发中,网络请求是每个开发者的必备技能。当前也有很多优秀、开源的网络请求库。例如:

其中Retrofit是对OkHttp的封装,Android-async-http是对HttpClient的封装,利用这些网络库开发者可以极大提升编码效率。即便这些优秀的网络库可以很方便进行网络请求,但大多数团队依旧要搭建App网络层,甚至把网络层单独封装成库使用,为什么呢?所以本文首先就要讨论:

  1 为什么要搭建App网络层呢?

    当知道搭建网络层的必要性之后,便摩拳擦掌准备去大干一番,但很快便面对一个问题:

  2 如何一步步搭建App网络层呢?

    终于搭建好了网络层,但使用时肯定能发现不少bug和可以优化的地方,那么:

  3 应该用什么样的思想来指导改进网络层呢?

在回答上述问题之前,先了解下网络请求的基本流程:

网络请求基本流程

    网络请求的实质是去查看、修改远程计算机(包括服务器)上的信息,仅从客户端来看基本流程如下:


网络请求流程.png

    如图示,网络请求的基本流程就是如此简单,和把大象放入冰箱一样,都是三步。接下来我们在代码级别来看看:

如何进行网络请求

  以使用OkHttp框架访问百度首页为例子

       //构造一个HttpClient 相当于设置个人邮箱。
       OkHttpClient client=new OkHttpClient();
       //创建Request 对象,相当于写信。
       Request request = new Request.Builder()
               .url("http://www.baidu.com")
               .build()
       //将Request封装为call,相当于把信放进邮箱,成为设置后待发送的信件
       Call call = client.newCall(request);
       // 放置到请求队列、开始发送并等待回复,相当于邮箱开始发动信件,并等待对方回复。
       call.enqueue(new Callback() {
           @Override
           public void onFailure(Call call, IOException e) {
              // 当请求被取消、连接中断、找不到服务器等问题会调用这个接口
           }

           @Override
           public void onResponse(Call call, final Response response) throws IOException {
               // 远程服务器成功返回调用
               final String res = response.body().string();
               runOnUiThread(new Runnable() {
                   @Override
                   public void run() {
                       Log.e("TAG"," "+res);
                   }
               });
           }
       });

   }

    由上可以看到用OkHttp框架进行网络请求逻辑清晰简单,跟我们用邮件跟朋友交流差不多。首先是设置邮箱(如果没有什么特殊需求就用默认设置、如上文)+写邮件内容,然后把写好邮件后放到设置好的邮箱里面,最后点击发送,等待朋友的回复,当朋友回复了就去查看处理。

为什么要搭建网络层

    知道如何用OkHttp后很兴奋,于是用这一套开始了网络请求之旅,so easy!!复制-粘贴-修改,复制-粘贴-修改,复制-粘贴-修改...终于做了七八个网络请求,一看任务量完成了六分之一,啊--累死宝宝了!!
    不行了不行了,要喝杯luckin coffee鼓舞下士气,说走就走,喝着咖啡想着回来就一口气加班搞定它。突然想起OOP重要原则-代码复用原则,一拍脑子我TM真是个憨货,把这些请求的共同部分提取出来,对外提供更简单的接口、这样用着方便不容易出错、以后有问题修改工作量也大大减少了,这就是我们搭建网络层第一个原因:

    一、近似业务模型的代码复用--方便使用和修改;

    说干就干,撸起袖子正准备上场,再次灵光一闪,不对不对,这次要好好思考下整个完美的,半途而废啥的最浪费时间了!
    这里要说明下,App网络层是我们抽离出来的一个单独模块,所以网络层搭建跟设计实现一个单独的网络请求库基本是一样的!
    做事第一步向优秀同行学习,去github上看了star比较多的一些OkHttp库封装,整理了下功能列表:

  • 一般的get请求
  • 一般的post请求
  • 基于Http Post的文件上传(类似表单)
  • 文件下载/加载图片
  • 上传下载的进度回调
  • 支持取消某个请求
  • 支持自定义Callback
  • 支持HEAD、DELETE、PATCH、PUT
  • 支持session的保持
  • 支持自签名网站https的访问,提供方法设置下证书就行
  • 支持RxJava
  • 支持自定义缓存策略

    这些网络库是很好参考借鉴,但是,它们跟我们的业务结合不紧密、直接用还是会造成:

    1 每个网络请求都要加上业务逻辑;
    2 有不少多余功能,导致网络层很大,可能影响效率 ;
    3 团队特殊要求达不到,比如利用三方实现DNS防劫持。

    所以需要仔细去分析业务、梳理网络请求类型。很快我们发现需要四种缓存策略:立即请求、缓存10s、缓存1h、缓存24h,所以搭建网络层第二个原因也是功能点:

    二、 全局,全团队统一的缓存策略;

    接下来需要去沉下心,去谷歌搜索下网络请求问题,针对业务场景去思考下一旦上线会遇到什么样的问题,很快确定了第二个问题,DNS劫持问题,所以我们搭建网路层第二个原因也是重要功能点:

    三、 全局、团队统一的 DNS反劫持;

    为了防止遗漏,又去找团队成员、上级老大聊天请教,看有什么特殊要求。这时候运营部门提了个需求,统计dns劫持率,老大说出现网络问题要能够快速定位。所以搭建App网络层第四个重点:

    四、 全局、团队统一的网络请求统计和关键log

    明确了什么要搭建App的网络层,以及必须具备哪些功能,接下便开始正面遭遇问题:

如何一步步搭建App网络层呢

    人类做事习惯上是顺序进行的,这就决定了人类的可靠性思维-逻辑思维是线性的,进而决定了人类的可靠性表达也是线性的!越顺滑的思路,越顺滑的表达,越容易被人理解与接受。
    所以当遭遇事件类相关任务,又不知道怎么做的时候,从事件整体业务流程进行分析是一个很好的切入点。
    从整体业务流程出发、使我们不至于迷失,但到了每一个环节该如何做,就需要一些指导与规范,那必须就是:
    SDK设计原则: A 简洁易用 B 功能完备 C 扩展性好。
    当然 “简单吗?优美吗?” 也是每个软件工程师须时时反问的。

一 、简洁易用-从用户使用与理解角度考虑模块划分与接口设计

    现在假设网络层已经搭建好了,用户网络层发起网络请求,所以遇到第一个问题节点就是网络层对外接口设计。接口设计原则是简单!简单!简单!不仅仅是代码看着的简单,而是在于用户易于理解和使用的简单!
    上文提到通过OkHttp网络请求大概分为四步:
    1 构建、设置OkHttpClient--相当于设置电子邮箱;
    2 构建Request请求内容--相当于写信;
    3 用OkHttpClient把Request转换成为待发送的Request-Call--相当于把信件放入邮箱,变为邮件;
    4 发送请求,等待回复,并处理。
    现在就设想最简单的网络请求是怎么样的:额、大概是这样的吧----客户端添加一个请求(由Url构建出来)- 发送出去-等待回复处理,比如如下

   new HttpClient.HttpClientBuilder().build()                        // 设置邮箱
                .addRequest(new GetRequest("http://www.baidu.com"))  //写信并添加到邮箱或者叫写邮件
                .sendRequest(new DefaultCallback(){          //发送邮件等待回复
                    @Override
                    public void onResponse(Call call, Response response) {
                        super.onResponse(call, response);
                        Log.e("JG","response="+response.toString());  //服务器成功返回
                    }
                    @Override
                    public void onFailure(Call call, IOException e) {
                        super.onFailure(call, e);
                    }
                });
 

    从这个简单流程出发、App网络层可抽象出如下模块:
    1 HttpClient 客户端模块 ;
    2 Request 请求模块 ;
    3 Callback 回复处理模。
    据此我们可以把App网络层划分为这三个大的模块。同理,在每一个大模块内部也要根据流程划分为更细的模块
    这一小节主要探讨模块划分与接口设计,核心思想是跳出代码逻辑,从整体业务流程出发,找到关键的处理节点,从而对网路层进行模块划分,从用户易于使用和理解角度进行接口设计。但这种设计是否行得通,还要从每一个具体业务实现进行重新审核。

二、功能完备-从业务需求实现出发审查模块划分的合理性

    接下来我们开始分析每一个业务需求、验证刚才的模块划分是否合理。

1 get/post请求

    最初我们从get/post请求开始思考业务逻辑,所以这个可以跟模块完美结合,get与post的区别在我们这里就是不同的Request封装。

2 DNS防劫持

    DNS-Domin Name System域名解析系统,将域名(例如:“http://www.baidu.com”)转换为IP地址(例如:220.181.112.244),这个解析过程涉及到本地缓存,运营商缓存,各级别域名服务器等,它是Http协议的一部分。
    DNS劫持劫持又称域名劫持,本质就是通过攻破DNS解析过程中某些环节与节点,来给用户返回假网址IP。
    既然DNS劫持结果是返回错误的IP,那是否直接用Ip来访问就可以防止DNS劫持了?这就是DNS反劫持的主要思想:拿到域名->通过Http请求访问权威三方(比如阿里的HTTPDNS)提供的DNS解析服务器->三方DNS解析服务器告诉你IP,便可通过该IP来进行网络请求了。
    OkHttp实现DNS反劫持由上文可知看到DNS反劫持关键在于,用三方的DNS解析系统代替系统默认的DNS解析系统!所以OkHttp提供了一个抽象的DNS类,用户只用继承这个类,便可以方便的接入自定义的DNS解析系统。

public class HttpDns implements Dns {
    private static final String TAG = HttpDns.class.getSimpleName();

    @Override
    public List<InetAddress> lookup(String hostname) throws UnknownHostException {
        Log.v(TAG, "lookup:" + hostname);
        //只需要在lookup方法中调用HttpDns的SDK去获取IP
        // 如果获取到了就返回一个List<InetAddress>的值
        // 如果购买了阿里的HttpDns服务就可以用
        //默认又返回系统的DNS解析,这就叫DNS降级
        return SYSTEM.lookup(hostname);
    }
}

然后通过OkHttClient设置此DNS,HttpClient模块主要就是封装OkHttClient,所以审核通过!可行再次+1。

3 缓存设置

    缓存设置指缓存的位置、大小、时间,OkHttp通过两种方式可以实现缓存设置:

//1  通过库cache接口
new OkHttpClient.Builder()
                .cache(new Cache(file, cacheSize)) // 配置缓存
// 2 通过拦截器
new OkHttpClient.Builder()
                .cache(new CacheInterceptor(){
            @Override
            public Response intercept(Chain chain) throws IOException {
                Request request = chain.request();
                Response response = chain.proceed(request);
                return response;
            }
        }); 

具体做法请参照:okhttp 缓存实践
它依旧可以通过OkHttpClient完成,我们依旧只需要把这部分封装在HttpClient模块就好。

4 全局log统计

    全局log统计依旧是使用拦截器完成,上代码

public class LogInterceptor implements Interceptor {
    private static final String TAG=LogInterceptor.class.getSimpleName();
    @Override
    public Response intercept(Chain chain) throws IOException {

       // 把请求request拦截下来
        Request request = chain.request();
       //可以打印请求内容
        Log.v(TAG,"request method="+request.method()+",request url="+request.url());
        //继续向下一个传递处理,并拦截到处理结果response
        Response response = chain.proceed(request);
        // 可以打印返回内容
        Log.v(TAG,"response="+response.toString());
        return response;
    }
}

依旧是封装在HttpClient模块。全部审核通过,不过从最初设计也可以知道,我们从OkHttp用法借鉴思想,肯定是可行的。

三、 扩展性好-从开闭原则进行模块间解耦操作

    现在各个模块分工明确、业务功能基本全部实现了,但随着业务的发展、我们可能会有新类型的请求、新种类的返回处理等,所以一开始我们就要考虑整个网路层的扩展性。
    扩展性好的关键在于模块间耦合度低,解耦的关键在于依赖抽象,也就是实体模块(比如一个实体类)之间没有直接调用关系,实体模块之间数据传递要通过中间层(抽象类或者接口)。
再次回顾下我们最初设计的调用接口:

   new HttpClient.HttpClientBuilder().build()                        // 设置邮箱
                .addRequest(new GetRequest("http://www.baidu.com"))  //写信并添加到邮箱或者叫写邮件
                .sendRequest(new DefaultCallback(){          //发送邮件等待回复
                    @Override
                    public void onResponse(Call call, Response response) {
                        super.onResponse(call, response);
                        Log.e("JG","response="+response.toString());  //服务器成功返回
                    }
                    @Override
                    public void onFailure(Call call, IOException e) {
                        super.onFailure(call, e);
                    }
                });
 

其中 addRequest

 public ReadyRequest addRequest(BaseRequest baseRequest){

        return new ReadyRequest(this,baseRequest);


    }

其中GetRequest是继承自BaseRequest,这样HttpClient这个实体类就没有直接和实体类GetRequest相关,这就是通过依赖抽象进行了解耦合。当我们需要一种新的Request,只需继承BaseRequest就可以方便的扩展使用。

如何不断优化App网络层

    现在我们做好了一个基本可以使用、并且具备一定扩展性的网络层,但是使用过程中肯定可以发现bug和可以优化地方,那么如何一步步把我们的网络层从普通变为卓越呢?
    那首先我还是会问一个问题,对一个具体APP来说怎么样才是一个顶级的网络层呢?
    1 安全性高;
    2 网络访问速度快、性能优越;
    3 用户使用方便。
看了一些优秀的网络层改进过程,暂时总结出来点如下:
1 统计网络请求常见bug与风险,增加预防处理;
2 统计业务流程想关性,进行预加载或者缓存;
3 统计用户使用习惯和思维、统一智能设置或者修改接口;
4 不断学习优秀软件的设计思维,进行部分重构。
这些都是大数据与AI思维的延伸,这部分还在继续思考中,如果各位有什么想法、欢迎在下面交流评论!!

总结

    本文从简单的网络请求开始,谈到了为了业务需求方便使用来搭建自己App的网络层,进而探讨了如何一步步实现一个App网络层,以一个怎么样的指导思想去完善和优化。其中重点是去重现了搭建、优化一个功能层或者说SDK的思维过程:

    1 跳出代码,从整体业务流程进行初步模块划分;

    2 从方便(用户)理解使用的角度设计调用接口;

    3 从业务具体实现出发,重新审视模块划分;

    4 用软件设计思维与模式再次审查当前结构设计,增加扩展性、方便用户灵活扩展。

    5 用大数据与AI进化思维进行不停优化;

    同时,也花了一两天时间,手动搭建一个App网络层来验证思维过程的可行性。gitHub地址:https://github.com/kingkong-li/networklib
欢迎各位大神前来交流、共同开发学习~

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

推荐阅读更多精彩内容