[计算机网络/HTTP/网络请求] Okhttp: 网络请求框架 [转]
目录
- 序
- 概述:Okhttp
- 安装指南
- 使用指南
- 原理与架构
- FAQ for okhttp
- Y 推荐资源
- X 参考文献
序
- 准备改造一套数据源连接框架,需要借助
OkHttp
,故记录之。
本文主要是转载第1篇参考文献。
概述:Okhttp
Android
中的网络请求框架,基本是okhttp
和Retrofit
一统天下,而Retrofit
又是以okhttp
为基础。
所以,系统学习okhttp的使用和原理就很有必要了。
-
okhttp
是由square
公司开发,Android中公认最好用的网络请求框架,在接口封装上做的简单易用, -
它有以下默认特性:
- 支持HTTP/2,允许所有同一个主机地址的请求共享同一个socket连接
- 使用连接池减少请求延时
- 透明的GZIP压缩减少响应数据的大小
- 缓存响应内容,避免一些完全重复的请求
- 当网络出现问题的时候OkHttp 会自动恢复一般的连接问题,如果你的服务有多个IP地址,当第一个IP请求失败时,OkHttp会交替尝试你配置的其他IP。
安装指南
Maven依赖
implementation 'com.squareup.okhttp3:okhttp:3.14.9'
implementation 'com.squareup.okio:okio:1.17.5'
其中
Okio
库是对Java.io
和java.nio
的补充,以便能够更加方便,快速的访问、存储和处理你的数据。
OkHttp
的底层使用该库作为支持。
另外,如果是在Android上运行,别忘了申请网络请求权限,如果还使用网络请求的缓存功能,那么还要申请读写外存的权限:
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
使用指南
get请求
-
get
方式中又可以分为2种情况:同步请求和异步请求; -
同步请求在进行请求的时候,当前线程会被阻塞住,直到得到服务器的响应后,后面的代码才会执行;
-
而异步请求不会阻塞当前线程,它采用了回调的方式,请求是在另一个线程中执行的,不会影响当前的线程。
-
以百度主页为例,进行Get请求:
同步请求
import okhttp3.Request;
import okhttp3.Call;
import okhttp3.OkHttpClient;
public void getSync() {
//同步请求
OkHttpClient httpClient = new OkHttpClient();
String url = "https://www.baidu.com/";
Request getRequest = new Request.Builder()
.url(url)
.get()
.build();
//准备好请求的Call对象
Call call = httpClient.newCall(getRequest);//只是配置请求,不会触发网络操作 (需确保每个 Call 对象只执行一次,避免重复使用)
try {
Response response = call.execute();//必须调用 execute() 或 enqueue() 才会真正发起请求
Log.i(TAG, "okHttpGet run: response:"+ response.body().string());
} catch (IOException e) {
e.printStackTrace();
}
}
- 首先,创建了
OkHttpClient
实例,接着用Request.Builder
构建了Request
实例、并传入了百度主页的url- 然后,
httpClient.newCall
方法传入Request
实例生成call,最后在子线程调用call.execute()
执行请求获得结果response
。
使用
OkHttp
进行get请求,是比较简单的,只要在构建Request实例时更换url就可以了。
异步请求
call.execute()
是同步方法。- 想要在主线程直接使用可以嘛?当然可以,使用
call.enqueue(callback)
即可
public void getAsync() {
//异步请求
OkHttpClient httpClient = new OkHttpClient();
String url = "https://www.baidu.com/";
Request getRequest = new Request.Builder()
.url(url)
.get()
.build();
//准备好请求的Call对象
Call call = httpClient.newCall(getRequest);
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
}
@Override
public void onResponse(Call call, Response response) throws IOException {
Log.i(TAG, "okHttpGet enqueue: onResponse:" + response.body().string());
ResponseBody body = response.body();
String string = body.string();
byte[] bytes = body.bytes();
InputStream inputStream = body.byteStream();
}
});
}
call.enqueue
会异步执行,需要注意的是,2个回调方法onFailure、onResponse是执行在子线程的
所以,如果想要执行UI操作,需要使用Handler或其他方式切换到UI线程。
取消请求
- 每一个
Call
对象只能执行一次。
如果想要取消正在执行的请求,可以使用
call.cancel()
,通常在离开页面时都要取消执行的请求的。
结果处理
- 请求回调的两个方法是指 传输层 的失败和成功。
onFailure
通常是connection连接失败或读写超时;onResponse
是指成功得从服务器获取到了结果,但是这个结果的响应码可能是404、500等,也可能就是200(response.code()的取值)。- 如果
response.code()
是200
,表示应用层请求成功了。此时我们可以获取Response的
ResponseBody
,这是响应体。
从面看到,可以从ResponseBody获取string、byte[ ]、InputStream,这样就可以对结果进行很多操作了。
比如UI上展示string(要用Handler切换到UI线程)、通过InputStream写入文件等等。
上面异步请求执行后 结果打印如下:
okHttpGet run: response:<!DOCTYPE html> <!--STATUS OK--><html> <head><meta http-equiv=content-type content=text/html;charset=utf-8><meta http-equiv=X-UA-Compatible content=IE=Edge><meta content=always name=referrer><link rel=stylesheet type=text/css
href=https://ss1.bdstatic.com/5eN1bjq8AAUYm2zgoY3K/r/www/cache/bdorz/baidu.min.css><title>百度一下,你就知道</title></head> <body link=#0000cc> <div id=wrapper> <div id=head> <div class=head_wrapper> <div class=s_form> <div class=s_form_wrapper> <div id=lg>
<img hidefocus=true src=//www.baidu.com/img/bd_logo1.png width=270 height=129> </div> <form id=form name=f action=//www.baidu.com/s class=fm> <input type=hidden name=bdorz_come value=1> <input type=hidden name=ie value=utf-8> <input type=hidden name=f value=8> <input type=hidden name=rsv_bp value=1> <input type=hidden name=rsv_idx value=1> <input type=hidden name=tn value=baidu><span class="bg s_ipt_wr"><input id=kw name=wd class=s_ipt value maxlength=255 autocomplete=off autofocus=autofocus></span><span class="bg s_btn_wr"><input type=submit id=su value=百度一下 class="bg s_btn" autofocus></span> </form> </div> </div> <div id=u1> <a href=http://news.baidu.com name=tj_trnews class=mnav>新闻</a> <a href=https://www.hao123.com name=tj_trhao123 class=mnav>hao123</a> <a href=http://map.baidu.com name=tj_trmap class=mnav>地图</a> <a href=http://v.baidu.com name=tj_trvideo class=mnav>视频</a> <a href=http://tieba.baidu.com name=tj_trtieba class=mnav>贴吧</a> <noscript> <a href=http://www.baidu.com/bdorz/login.gif?login&tpl=mn&u=http%3A%2F%2Fwww.baidu.com%2f%3fbdorz_come%3d1 name=tj_login class=lb>登录</a> </noscript> <script>document.write('<a href="http://www.baidu.com/bdorz/login.gif?login&tpl=mn&u='+ encodeURIComponent(window.location.href+ (window.location.search === "" ? "?" : "&")+ "bdorz_come=1")+ '" name="tj_login" class="lb">登录</a>'); </script> <a href=//www.baidu.com/more/ name=tj_briicon class=bri style="display: block;">更多产品</a> </div> </div> </div> <div id=ftCon> <div id=ftConw> <p id=lh> <a href=http://home.baidu.com>关于百度</a> <a href=http://ir.baidu.com>About Baidu</a> </p> <p id=cp>©2017 Baidu <a href=http://www.baidu.com/duty/>使用百度前必读</a> <a href=http://jianyi.baidu.com/ class=cp-feedback>意见反馈</a> 京ICP证030173号 <img src=//www.baidu.com/img/gs.gif> </p> </div> </div> </div> </body> </html>
post请求
POST
请求将参数放在请求的主体中,不会直接显示在URL中。Post请求也分为同步和异步方式,和get方式用法相同
同步POST
public void postSync(){//同步请求
new Thread(new Runnable() {
@Override
public void run() {
OkHttpClient okHttpClient = new OkHttpClient();
FormBody formBody = new FormBody.Builder()
.add("a","1")
.add("b","2")
.build();
Request request=new Request.Builder()
.post(formBody)
.url("https://www.httpbin.org/post")
.build();
//准备好请求的Call对象
Call call = okHttpClient.newCall(request);
try {
Response response = call.execute();
Log.i("postSync",response.body().string());
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
异步POST
public void postAsync(){//异步请求
OkHttpClient okHttpClient=new OkHttpClient();
FormBody formBody=new FormBody.Builder() //okhttp3.FormBody extends RequestBody
.add("a","1")
.add("b","2")
.build();
Request request=new Request.Builder()
.post(formBody)
.url("https://www.httpbin.org/post")
.build();
//准备好请求的Call对象
Call call = okHttpClient.newCall(request);
call.enqueue(new Callback() {
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e) {
}
@Override
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
if(response.isSuccessful()){
Log.i("postAsync",response.body().string());
}
}
});
}
post请求提交多种 MediaType 格式
text/x-markdown
: String、文件
post
请求与get
请求的区别 是在构造Request对象时,需要多构造一个RequestBody
对象,用它来携带我们要提交的数据,其他都是一样的。
示例如下:
import okhttp3.MediaType;
import okhttp3.RequestBody;
OkHttpClient httpClient = new OkHttpClient();
MediaType contentType = MediaType.parse("text/x-markdown; charset=utf-8");
String content = "hello!";
RequestBody body = RequestBody.create(contentType, content);
Request getRequest = new Request.Builder()
.url("https://api.github.com/markdown/raw")
.post(body)
.build();
Call call = httpClient.newCall(getRequest);
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
}
@Override
public void onResponse(Call call, Response response) throws IOException {
Log.i(TAG, "okHttpPost enqueue: \n onResponse:"+ response.toString() +"\n body:" +response.body().string());
}
});
- 对比
get
请求,把构建Request时的get()
改成post(body)
,并传入RequestBody
实例。
RequestBody实例是通过
create
方法创建,需要指定请求体内容类型、请求体内容。
这里是传入了一个指定为markdown格式的文本。
application/json
: json
- 传入
RequestBody
的MediaType
还可以是其他类型,如客户端要给后台发送json字符串、发送一张图片,那么可以定义为:
- demo1
// RequestBody:jsonBody,json字符串
String json = "jsonString";
RequestBody jsonBody = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), json);
//RequestBody:fileBody, 上传文件
File file = new File(Environment.getExternalStorageDirectory(), "1.png");
RequestBody fileBody = RequestBody.create(MediaType.parse("image/png"), file);
- demo2
if(esUrl.endsWith("/_search")){//dsl语法
JSONObject jsonObject = JSON.parseObject(sql);
jsonStr = JSON.toJSONString(jsonObject);
} else {//es opendistro 插件的SQL语法
json.put("query", sql);
jsonStr = JSON.toJSONString(json);
}
OkHttpClient okHttpClient = SpringContextUtil.getBean(OkHttpClient.class);
String credential = Credentials.basic( username, password);// base64({user}:{password})
Request request = new Request.Builder()
.header("Authorization", credential)
.method("POST", RequestBody.create(MediaType.get("application/json"), jsonStr))
.url(esUrl)
.build();
MediaType
Call newCall = okHttpClient.newCall(request);//只是配置请求,不会触发网络操作 (需确保每个 Call 对象只执行一次,避免重复使用)
Response response = newCall.execute();//必须调用 execute() 或 enqueue() 才会真正发起请求
if (!response.isSuccessful()) {
ObjectMapper mapper = new ObjectMapper();
String body = new String(response.body().bytes());
logger.error("code:{},message:{},body:{}", response.code(), response.message(), body);
ErrorMessage errorMessage = mapper.readValue(body, ErrorMessage.class);
if (null != errorMessage && errorMessage.getError() != null && errorMessage.getError().getType() != null) {
throw new RuntimeException(ErrorCodeEnum.QUERY_ES_FAIL.getCode(), ErrorCodeEnum.QUERY_ES_FAIL.getMsg() + ", cause that : " + errorMessage.getError().getType());
}
}
application/x-www-form-urlencoded
: 提交表单
- 构建RequestBody除了上面的方式,还有它的子类FormBody,FormBody用于提交表单键值对,这种能满足平常开发大部分的需求。
//RequestBody:FormBody,表单键值对
RequestBody formBody = new FormBody.Builder()
.add("username", "henry")
.add("password", "666")
.build();
FormBody
是通过FormBody.Builder
用构建者模式创建,add
键值对即可。它的contentType
在内部已经指定了。
public final class FormBody extends RequestBody {
private static final MediaType CONTENT_TYPE = MediaType.get("application/x-www-form-urlencoded");
...
}
multipart/*
: 提交复杂请求体
RequestBody
另一个子类MultipartBody
,用于post
请求提交复杂类型的请求体。
- 复杂请求体可以同时包含多种类型的的请求体数据。
上面介绍的 post请求 string、文件、表单,只有单一类型。
考虑一种场景–注册场景,用户填写完姓名、电话,同时要上传头像图片,这时注册接口的请求体就需要 接受 表单键值对 以及文件了,那么前面讲的的post就无法满足了。
那么就要用到MultipartBody了。
- 源码
/** An <a href="http://www.ietf.org/rfc/rfc2387.txt">RFC 2387</a>-compliant request body. */
public final class MultipartBody extends RequestBody {
/**
* The "mixed" subtype of "multipart" is intended for use when the body parts are independent and
* need to be bundled in a particular order. Any "multipart" subtypes that an implementation does
* not recognize must be treated as being of subtype "mixed".
*/
public static final MediaType MIXED = MediaType.get("multipart/mixed");
/**
* The "multipart/alternative" type is syntactically identical to "multipart/mixed", but the
* semantics are different. In particular, each of the body parts is an "alternative" version of
* the same information.
*/
public static final MediaType ALTERNATIVE = MediaType.get("multipart/alternative");
/**
* This type is syntactically identical to "multipart/mixed", but the semantics are different. In
* particular, in a digest, the default {@code Content-Type} value for a body part is changed from
* "text/plain" to "message/rfc822".
*/
public static final MediaType DIGEST = MediaType.get("multipart/digest");
/**
* This type is syntactically identical to "multipart/mixed", but the semantics are different. In
* particular, in a parallel entity, the order of body parts is not significant.
*/
public static final MediaType PARALLEL = MediaType.get("multipart/parallel");
/**
* The media-type multipart/form-data follows the rules of all multipart MIME data streams as
* outlined in RFC 2046. In forms, there are a series of fields to be supplied by the user who
* fills out the form. Each field has a name. Within a given form, the names are unique.
*/
public static final MediaType FORM = MediaType.get("multipart/form-data");
...
- demo
OkHttpClient httpClient = new OkHttpClient();
// MediaType contentType = MediaType.parse("text/x-markdown; charset=utf-8");
// String content = "hello!";
// RequestBody body = RequestBody.create(contentType, content);
//RequestBody:fileBody,上传文件
File file = drawableToFile(this, R.mipmap.bigpic, new File("00.jpg"));
RequestBody fileBody = RequestBody.create(MediaType.parse("image/jpg"), file);
//RequestBody:multipartBody, 多类型 (用户名、密码、头像)
MultipartBody multipartBody = new MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("username", "hufeiyang")
.addFormDataPart("phone", "123456")
.addFormDataPart("touxiang", "00.png", fileBody)
.build();
Request getRequest = new Request.Builder()
.url("http://yun918.cn/study/public/file_upload.php")
.post(multipartBody)
.build();
Call call = httpClient.newCall(getRequest);
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
Log.i(TAG, "okHttpPost enqueue: \n onFailure:"+ call.request().toString() +"\n body:" +call.request().body().contentType()
+"\n IOException:"+e.getMessage());
}
@Override
public void onResponse(Call call, Response response) throws IOException {
Log.i(TAG, "okHttpPost enqueue: \n onResponse:"+ response.toString() +"\n body:" +response.body().string());
}
});
更多 MedisType
MediaType
更多类型信息可以查看RFC 2045
- 查看各个文件类型所对应的Content-type字符串,可以访问:
请求配置项
- 前置问题
- 如何全局设置超时时长?
- 缓存位置、最大缓存大小 呢?
- 考虑有这样一个需求,我要监控App通过 OkHttp 发出的 所有 原始请求,以及整个请求所耗费的时间,如何做?
这些问题,在OkHttp这里很简单。把OkHttpClient实例的创建,换成以下方式即可:
OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(15, TimeUnit.SECONDS)
.readTimeout(10, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS)
.cache(new Cache(getExternalCacheDir(),500 * 1024 * 1024))
.addInterceptor(new Interceptor() {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
String url = request.url().toString();
Log.i(TAG, "intercept: proceed start: url"+ url+ ", at " + System.currentTimeMillis());
Response response = chain.proceed(request);
ResponseBody body = response.body();
Log.i(TAG, "intercept: proceed end: url"+ url+ ", at " + System.currentTimeMillis());
return response;
}
})
.build();
这里通过
OkHttpClient.Builder
通过构建者模式设置了连接、读取、写入的超时时长,用cache()
方法传入了由缓存目录、缓存大小构成的Cache
实例,这样就解决了前两个问题。
- 使用
addInterceptor()
方法添加了Interceptor
实例,且重写了intercept
方法。Interceptor
意为拦截器,intercept()
方法会在开始执行请求时调用。
其中chain.proceed(request)
内部是真正请求的过程,是阻塞操作,执行完后会就会得到请求结果ResponseBody
所以chain.proceed(request)
的前后取当前时间,那么就知道整个请求所耗费的时间。上面chain.proceed(request)的前后分别打印的日志和时间,这样第三个问题也解决了。
- 具体
Interceptor
是如何工作,后面介绍。另外,通常
OkHttpClient
实例是全局唯一的,这样这些基本配置就是统一,且内部维护的连接池也可以有效复用(后面介绍)。
全局配置的有了,单个请求的也可以有一些单独的配置。
Request getRequest = new Request.Builder()
.url("http://yun918.cn/study/public/file_upload.php")
.post(multipartBody)
.addHeader("key","value")
.cacheControl(CacheControl.FORCE_NETWORK)
.build();
- 使用
addHeader()
方法添加了请求头。- 使用
cacheControl(CacheControl.FORCE_NETWORK)
设置此次请求是能使用网络,不用缓存。(还可以设置只用缓存FORCE_CACHE
)
最佳实践: MyOkHttpClientConfig
- pom.xml
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.9.1 or 3.14.9[推荐]</version>
</dependency>
- MyOkHttpClientConfig
import lombok.extern.slf4j.Slf4j;
import okhttp3.ConnectionPool;
import okhttp3.OkHttpClient;
import okhttp3.internal.connection.Transmitter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.annotation.Nullable;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@Configuration
@Slf4j
public class OkHttpClientConfig {
//Socket.setSoTimeout(int timeout)
public final static String OKHTTP_CONNECTTIMEOUT_PARAM = "OKHTTP_CONNECTTIMEOUT";
public final static String OKHTTP_CONNECTTIMEOUT_DEFAULT = "1000";
public final static String OKHTTP_CALLTIMEOUT_PARAM = "OKHTTP_CALLTIMEOUT";
public final static String OKHTTP_CALLTIMEOUT_DEFAULT = "3000";
public final static String OKHTTP_READTIMEOUT_PARAM = "OKHTTP_READTIMEOUT";
public final static String OKHTTP_READTIMEOUT_DEFAULT = "1000";
public final static String OKHTTP_CONNECTIONPOOL_MAXIDLE_PARAM = "OKHTTP_CONNECTIONPOOL_MAXIDLE";
public final static String OKHTTP_CONNECTIONPOOL_MAXIDLE_DEFAULT = "100";
public final static String OKHTTP_CONNECTIONPOOL_KEPPALIVCEDURATION_PARAM = "OKHTTP_CONNECTIONPOOL_KEPPALIVCEDURATION";
public final static String OKHTTP_CONNECTIONPOOL_KEPPALIVCEDURATION_DEFAULT = "30000";
@Bean(name = "okHttpClient")
public OkHttpClient okHttpClient() {
OkHttpClient okHttpClient = buildOkHttpClient( OkHttpClient.class.getSimpleName() + "SpringBean(okHttpClient)");
return okHttpClient;
}
/**
* 构建 OkHttpClient 连接池
* @param caller 可选参数,标明调用方
* @return
*/
public static OkHttpClient buildOkHttpClient(@Nullable String caller){
// MyOkHttpRetryInterceptor.Builder builder = new MyOkHttpRetryInterceptor.Builder()
// .retryInterval(500)
// .executionCount(3);
Map<String, String> env = System.getenv();
long connectTimeout = Long.valueOf( env.getOrDefault(OKHTTP_CONNECTTIMEOUT_PARAM, OKHTTP_CONNECTTIMEOUT_DEFAULT) );
long callTimeout = Long.valueOf( env.getOrDefault(OKHTTP_CALLTIMEOUT_PARAM, OKHTTP_CALLTIMEOUT_DEFAULT) );
long readTimeout = Long.valueOf( env.getOrDefault(OKHTTP_READTIMEOUT_PARAM, OKHTTP_READTIMEOUT_DEFAULT) );
int maxIdleConnections = Integer.valueOf( env.getOrDefault( OKHTTP_CONNECTIONPOOL_MAXIDLE_PARAM, OKHTTP_CONNECTIONPOOL_MAXIDLE_DEFAULT ) );
long keepAliveDuration = Long.valueOf( env.getOrDefault( OKHTTP_CONNECTIONPOOL_KEPPALIVCEDURATION_PARAM, OKHTTP_CONNECTIONPOOL_KEPPALIVCEDURATION_DEFAULT ) );
log.info("config({}) | connectTimeout:{}, callTimeout:{}, readTimeout:{}, maxIdleConnections:{}, keepAliveDuration:{}"
, caller==null?"":caller
, connectTimeout, callTimeout, readTimeout, maxIdleConnections, keepAliveDuration
);
return new OkHttpClient().newBuilder()
.connectTimeout(connectTimeout, TimeUnit.MILLISECONDS)
.callTimeout(callTimeout, TimeUnit.MILLISECONDS)
.readTimeout(readTimeout, TimeUnit.MILLISECONDS)
.retryOnConnectionFailure(true) //开启 okhttp 自带的 重试阻拦器
.followRedirects(true)//重定向
//.addInterceptor( new MyOkHttpRetryInterceptor(builder) )//本重置拦截器,尚不可靠
.connectionPool(new ConnectionPool(maxIdleConnections, keepAliveDuration, TimeUnit.MILLISECONDS))
.build();
}
public static OkHttpClient buildOkHttpClient(){
return buildOkHttpClient(null);
}
}
拦截器的使用
OkHttp
的拦截器(Interceptors
)提供了强大的自定义和修改HTTP请求和响应的能力。- 拦截器允许在发送请求前、收到响应后以及其他阶段对HTTP流量进行拦截和处理。
例如:拦截器可以修改请求的URL、请求方法、请求头部、请求体等。
这对于添加身份验证头、设置缓存控制头等场景很有用。
用法如下:
public void interceptor(){
OkHttpClient okHttpClient=new OkHttpClient.Builder()//添加拦截器的使用OkHttpClient的内部类Builder
.addInterceptor(new Interceptor() {//使用拦截器可以对所有的请求进行统一处理,而不必每个request单独去处理
@NonNull
@Override
public Response intercept(@NonNull Chain chain) throws IOException {
//前置处理,以proceed方法为分割线:提交请求前
Request request = chain.request().newBuilder()
.addHeader("id", "first request")
.build();
Response response = chain.proceed(request);
//后置处理:收到响应后
return response;
}
})
.addNetworkInterceptor(new Interceptor() {//这个在Interceptor的后面执行,无论添加顺序如何
@NonNull
@Override
public Response intercept(@NonNull Chain chain) throws IOException {
Log.i("id",chain.request().header("id"));
return chain.proceed(chain.request());
}
})
.cache(new Cache(new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).getAbsolutePath()+"/cache"),1024*1024))//添加缓存
.build();
Request request=new Request.Builder()
.url("https://www.httpbin.org/get?a=1&b=2")
.build();
//准备好请求的Call对象
Call call = okHttpClient.newCall(request);
//异步请求
call.enqueue(new Callback() {
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e) {
}
@Override
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
if(response.isSuccessful()){
Log.i("interceptor",response.body().string());
}
}
});
}
Cookie的使用
- 大家应该有这样的经历,就是有些网站的好多功能都需要用户登录之后才能访问,而这个功能可以用
cookie
实现:
- 首先,在客户端登录之后,服务器给客户端发送一个cookie,由客户端保存;
- 然后,客户端在访问需要登录之后才能访问的功能时,只要携带这个cookie,服务器就可以识别该用户是否登录。
用法如下
public void cookie(){
Map<String,List<Cookie>> cookies = new HashMap<>();
new Thread(new Runnable() {
@Override
public void run() {
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.cookieJar(new CookieJar() {
@Override
public void saveFromResponse(@NonNull HttpUrl httpUrl, @NonNull List<Cookie> list) {//保存服务器发送过来的cookie
cookies.put("cookies",list);
}
@NonNull
@Override
public List<Cookie> loadForRequest(@NonNull HttpUrl httpUrl) {//请求的时候携带cookie
if(httpUrl.equals("www.wanandroid.com")){
return cookies.get("cookies");
}
return new ArrayList<>();
}
})
.build();
FormBody formBody = new FormBody.Builder()
.add("username","ibiubiubiu")
.add("password","Lhh823924.")
.build();
Request request = new Request.Builder() //模拟登录
.url("https://wanandroid.com/user/lg")
.post(formBody)
.build();
//准备好请求的Call对象
Call call = okHttpClient.newCall(request);
try {
Response response = call.execute();
Log.i("login",response.body().string());
} catch (IOException e) {
e.printStackTrace();
}
//请求收藏页面,必须登录之后才能访问到
request=new Request.Builder()
.url("https://wanandroid.com/lg/collect")
.build();
//准备好请求的Call对象
call = okHttpClient.newCall(request);
try {
Response response = call.execute();
Log.i("collect",response.body().string());
} catch (IOException e) {
e.printStackTrace();
}
}
})
.start();
}
连接池 : OkHttp ConnectionPool
基础概念
-
OkHttp ConnectionPool 是 OkHttp 库中的一个关键组件,用于管理 HTTP 连接的复用。
-
OkHttp ConnectionPool 是一个连接池,用于存储和管理 HTTP 连接。
它允许在多个请求之间复用连接,从而提高网络请求的效率。
连接池中的连接可以被多个线程共享,并且会根据配置的策略(如最大空闲连接数、连接超时时间等)来管理连接的生命周期。
优势
- 提高性能:通过复用连接,减少了每次请求时的握手和连接建立时间。
- 减少资源消耗:避免频繁地创建和销毁连接,节省了系统资源。
- 支持并发:允许多个线程同时使用同一个连接,提高了并发处理能力。
参数项
OkHttp ConnectionPool 主要有以下几种配置参数:
- maxIdleConnections:池中最大空闲连接数。
- keepAliveDuration:连接保持活跃的时间。
- timeUnit:keepAliveDuration 的时间单位。
应用场景
- Web 服务器和客户端之间的通信:在高并发场景下,使用连接池可以显著提高响应速度。
- 移动应用网络请求:在移动应用中,频繁的网络请求可以通过连接池优化性能。
- 微服务架构中的服务间调用:在微服务架构中,服务之间的调用可以通过连接池提高效率。
常用方法
//连接数 : Returns total number of connections in the pool.
int : okHttpClient.connectionPool().connectionCount();
//空闲连接数 : Returns the number of idle connections in the pool.
int : okHttpClient.connectionPool().idleConnectionCount()
//关闭所有连接 : Close and remove all idle connections in the pool.
void : okHttpClient.connectionPool().evictAll();
okhttp3.ConnectionPool
的 底层实现类:okhttp3.internal.connection.RealConnectionPool
借助连接池可解决的常见问题
Q: 连接池中的连接超时 / 配置连接池
-
原因:可能是由于网络不稳定或服务器响应慢导致的。
-
解决方法
import okhttp3.ConnectionPool;
ConnectionPool pool = new ConnectionPool(5, 5, TimeUnit.MINUTES);
OkHttpClient client = new OkHttpClient.Builder()
.connectionPool(pool)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.connectTimeout(30, TimeUnit.SECONDS)
.build();
Q: 连接池的默认配置
- 连接池默认配置:
- 每个地址的空闲连接数为 5个,每个空闲连接的存活时间为 5分钟
package okhttp3;
public ConnectionPool() {
this(5, 5, TimeUnit.MINUTES);
}
public ConnectionPool(int maxIdleConnections, long keepAliveDuration, TimeUnit timeUnit) {
this.maxIdleConnections = maxIdleConnections;
this.keepAliveDurationNs = timeUnit.toNanos(keepAliveDuration);
if (keepAliveDuration <= 0) {
throw new IllegalArgumentException("keepAliveDuration <= 0: " + keepAliveDuration);
}
}
Q: 过期连接的处理策略?
- 连接池每次添加一个新的连接时,都会先清理当前连接池中过期的连接,通过清理线程池
executor
执行清理任务cleanupRunnable
。
Q: 连接池中的连接数过多
-
原因:可能是由于客户端请求过于频繁,导致连接池中的连接数超过了设定的最大值。
-
解决方案:
ConnectionPool pool = new ConnectionPool(100, 5, TimeUnit.MINUTES);
OkHttpClient client = new OkHttpClient.Builder()
.connectionPool(pool)
.build();
Q: 连接池中的连接无法释放
-
原因:可能是由于某些请求未正确关闭连接导致的。
-
解决方案:
确保每次请求后都正确关闭响应体:
Response response = client.newCall(request).execute();
try {
// 处理响应
} finally {
response.close();
}
- 综合案例
import okhttp3.ConnectionPool;
import okhttp3.OkHttpClient;
import java.util.concurrent.TimeUnit;
public class OkHttpExample {
public static void main(String[] args) {
ConnectionPool pool = new ConnectionPool(100, 5, TimeUnit.MINUTES);
OkHttpClient client = new OkHttpClient.Builder()
.connectionPool(pool)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.connectTimeout(30, TimeUnit.SECONDS)
.build();
// 使用 client 进行网络请求
}
}
通过合理配置和使用 OkHttp ConnectionPool,可以有效提升网络请求的性能和稳定性。
参考文献
综合应用
案例1:封装OkHttp单例模式 : OkHttpUtils(自定义)
public class OkHttpUtils {
/**
* 单例模式
*/
private static OkHttpUtils okHttpUtils = null;
private OkHttpUtils() {
}
public static OkHttpUtils getInstance() {
//双层判断,同步锁
if (okHttpUtils == null) {
synchronized (OkHttpUtils.class) {
if(okHttpUtils == null){
okHttpUtils = new OkHttpUtils();
}
}
}
return okHttpUtils;
}
/**
* 单例模式
* 封装OkhHttp
* synchronized同步方法
*/
private static OkHttpClient okHttpClient = null;
private static synchronized OkHttpClient getOkHttpClient() {
if (okHttpClient == null) {
//拦截器
HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor(new HttpLoggingInterceptor.Logger() {
@Override
public void log(String message) {
//拦截日志消息
Log.i("henry", "log: " + message);
}
});
//设置日志拦截器模式
interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
okHttpClient = new OkHttpClient.Builder()
//日志拦截器
.addInterceptor(interceptor)
//应用拦截器
.addInterceptor(new Interceptor() {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request()
.newBuilder()
.addHeader("source", "android")
.build();
return chain.proceed(request);
}
})
.build();
}
return okHttpClient;
}
/**
* doGet
*/
public void doGet(String url, Callback callback) {
//创建okhttp
OkHttpClient okHttpClient = getOkHttpClient();
Request request = new Request.Builder()
.url(url)
.build();
okHttpClient.newCall(request).enqueue(callback);
}
/**
* doPost
*/
public void doPost(String url, Map<String, String> params, Callback callback) {
OkHttpClient okHttpClient = getOkHttpClient();
//请求体
FormBody.Builder formBody = new FormBody.Builder();
for (String key : params.keySet()) {
//遍历map集合
formBody.add(key, params.get(key));
}
Request request = new Request.Builder()
.url(url)
.post(formBody.build())
.build();
okHttpClient.newCall(request).enqueue(callback);
}
}
实现了单例模式来确保
OkHttpUtils
和OkHttpClient
实例的唯一性
案例2:配置https的自签证书和信任所有证书
HTTPS / SSL 证书
HTTPS
协议是由SSL
+HTTP
协议构建的可进行加密传输、身份认证的网络协议,要比http协议安全。
一般支持
https
的网站,都是CA
(Certificate Authority
)机构颁发的证书,但是一般该机构颁发的证书需要提供费用且有使用时间的限制,到期需要续费。
否则,默认该链接是不信任的,通过okHttp无法直接访问。
但是我们可以使用自签的方式,通过
JDK
自带的keytool.exe
生成一个自己的证书,然后使用该证书内容。
虽然也是会出现提示“不安全”,但是我们可以通过okhttp
访问链接。
使用自签证书
- 将证书文件放置在
assets
目录(也可以放置在其他目录下,只要能正确读取到该文件),在创建OkhttpClient
对象时sslSocketFactory()
将该证书信息添加。
private SSLContext getSLLContext() {
SSLContext sslContext = null;
try {
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
InputStream certificate = mContext.getAssets().open("gdroot-g2.crt");
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(null);
String certificateAlias = Integer.toString(0);
keyStore.setCertificateEntry(certificateAlias, certificateFactory.generateCertificate(certificate));
sslContext = SSLContext.getInstance("TLS");
final TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(keyStore);
sslContext.init(null, trustManagerFactory.getTrustManagers(), new SecureRandom());
} catch (CertificateException e) {
e.printStackTrace();
} catch (KeyStoreException e) {
e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (KeyManagementException e) {
e.printStackTrace();
}
return sslContext;
}
信任所有证书
- 通过添加证书的形式,可以实现客户端访问Https服务端的功能,但是如果服务端更换证书内容,那么客户端需要相应的更换https证书,否则无法正常交互获取不到数据,我们可以通过自定义
X509TrustManager
的形式实现来规避所有的证书检测,实现信任所有证书的目的。
private OkHttpClient getHttpsClient() {
OkHttpClient.Builder okhttpClient = new OkHttpClient().newBuilder();
//信任所有服务器地址
okhttpClient.hostnameVerifier(new HostnameVerifier() {
@Override
public boolean verify(String s, SSLSession sslSession) {
//设置为true
return true;
}
});
//创建管理器
TrustManager[] trustAllCerts = new TrustManager[] { new X509TrustManager() {
@Override
public void checkClientTrusted(
java.security.cert.X509Certificate[] x509Certificates,
String s) throws java.security.cert.CertificateException {
}
@Override
public void checkServerTrusted(
java.security.cert.X509Certificate[] x509Certificates,
String s) throws java.security.cert.CertificateException {
}
@Override
public java.security.cert.X509Certificate[] getAcceptedIssuers() {
return new java.security.cert.X509Certificate[] {};
}
} };
try {
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, trustAllCerts, new java.security.SecureRandom());
//为OkHttpClient设置sslSocketFactory
okhttpClient.sslSocketFactory(sslContext.getSocketFactory());
} catch (Exception e) {
e.printStackTrace();
}
return okhttpClient.build();
}
- 创建
X509TrustManager
对象,并实现其中的方法。由于X509TrustManager
是通用证书格式,只需要拿到该格式就行。最后init该安全协议,将其放入okhttp
的sslSocketFactory
中。- 由于
Retrofit
只是对Okhttp
网络接口的封装。因此实际使用中,该方法同样适用于Retrofit中。
原理与架构
okhttp 请求过程
FAQ for okhttp
面试可能会问到的问题:
- 简单说一下okhttp
- okhttp的核心类有哪些?
- okhttp对于网络请求做了哪些优化,如何实现的?
- okhttp架构中用到了哪些设计模式?
- okhttp拦截器的执行顺序
Y 推荐资源
- [网络/HTTPS/Java] PKI公钥基础设施体系:数字证书(X.509)、CA机构 | 含:证书管理工具(jdk keytool / openssl) - 博客园/千千寰宇
- [Linux/Bash/Shell] curl & wget - 博客园/千千寰宇
X 参考文献

本文链接: https://www.cnblogs.com/johnnyzen/p/18749434
关于博文:评论和私信会在第一时间回复,或直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
日常交流:大数据与软件开发-QQ交流群: 774386015 【入群二维码】参见左下角。您的支持、鼓励是博主技术写作的重要动力!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 上周热点回顾(3.3-3.9)
2023-03-03 [汽车/制造] 汽车大数据应用探讨【待续】