[计算机网络/HTTP/网络请求] Okhttp: 网络请求框架 [转]

目录

  • 准备改造一套数据源连接框架,需要借助OkHttp,故记录之。

本文主要是转载第1篇参考文献。

概述:Okhttp

  • Android中的网络请求框架,基本是okhttpRetrofit一统天下,而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.iojava.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&amp;tpl=mn&amp;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>&copy;2017&nbsp;Baidu&nbsp;<a href=http://www.baidu.com/duty/>使用百度前必读</a>&nbsp; <a href=http://jianyi.baidu.com/ class=cp-feedback>意见反馈</a>&nbsp;京ICP证030173号&nbsp; <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

  • 传入RequestBodyMediaType 还可以是其他类型,如客户端要给后台发送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

https://tools.ietf.org/html/rfc2045。

  • 查看各个文件类型所对应的Content-type字符串,可以访问:

https://www.runoob.com/http/http-content-type.html

请求配置项

  • 前置问题
  • 如何全局设置超时时长?
  • 缓存位置、最大缓存大小 呢?
  • 考虑有这样一个需求,我要监控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);
    }
}

实现了单例模式来确保 OkHttpUtilsOkHttpClient 实例的唯一性

案例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该安全协议,将其放入okhttpsslSocketFactory中。
  • 由于Retrofit只是对Okhttp网络接口的封装。因此实际使用中,该方法同样适用于Retrofit中。

原理与架构

okhttp 请求过程

FAQ for okhttp

面试可能会问到的问题:

  • 简单说一下okhttp
  • okhttp的核心类有哪些?
  • okhttp对于网络请求做了哪些优化,如何实现的?
  • okhttp架构中用到了哪些设计模式?
  • okhttp拦截器的执行顺序

Y 推荐资源

X 参考文献

posted @   千千寰宇  阅读(31)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 上周热点回顾(3.3-3.9)
历史上的今天:
2023-03-03 [汽车/制造] 汽车大数据应用探讨【待续】
点击右上角即可分享
微信分享提示