前言
在Android开发中,网络请求是每个开发者的必备技能。当前也有很多优秀、开源的网络请求库。例如:
其中Retrofit是对OkHttp的封装,Android-async-http是对HttpClient的封装,利用这些网络库开发者可以极大提升编码效率。即便这些优秀的网络库可以很方便进行网络请求,但大多数团队依旧要搭建App网络层,甚至把网络层单独封装成库使用,为什么呢?所以本文首先就要讨论:
1 为什么要搭建App网络层呢?
当知道搭建网络层的必要性之后,便摩拳擦掌准备去大干一番,但很快便面对一个问题:
2 如何一步步搭建App网络层呢?
终于搭建好了网络层,但使用时肯定能发现不少bug和可以优化的地方,那么:
3 应该用什么样的思想来指导改进网络层呢?
在回答上述问题之前,先了解下网络请求的基本流程:
网络请求基本流程
网络请求的实质是去查看、修改远程计算机(包括服务器)上的信息,仅从客户端来看基本流程如下:
如图示,网络请求的基本流程就是如此简单,和把大象放入冰箱一样,都是三步。接下来我们在代码级别来看看:
如何进行网络请求
以使用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
欢迎各位大神前来交流、共同开发学习~