OKHttp简介
# 1.OKHttp简介
# 1.1 简介
OKHttp是一款高效的HTTP客户端,支持连接同一地址的链接共享同一个Socket,通过连接池来减小响应延迟,还有透明的GZIP压缩,请求缓存等优势。其核心主要有路由、连接协议、拦截器、代理、安全性认证、连接池及网络适配。拦截器主要是指添加、溢出或者转移请求响应头部信息等操作。
OkHttp 是一个默认高效的 HTTP 客户端:
- HTTP/2 支持允许对同一主机的所有请求共享一个套接字。
- 连接池减少了请求延迟(如果 HTTP/2 不可用)。
- 透明 GZIP 可缩小下载大小。
- 响应缓存完全避免网络重复请求。
这个库也是square开源的一个网络请求库(OKHttp内部依赖okio)。现在已被Google使用在Android源码上了,可见其强大。关于网络请求库,现在应该还有很多人在使用android-async-http。他内部使用的是HttpClient,但是Google在6.0版本里面删除了HttpClient相关API,可见这个库现在有点过时了。
OKHttp主要功能
- 联网请求文本数据
- 大文件下载
- 大文件上传
- 请求图片
避免在主线程中更新UI
android.os.NetworkOnMainThreadException
# 1.2 同步请求和异步请求
同步请求的原理
当浏览器向服务器发送同步请求时,服务处理同步请求的过程中,浏览器会处于等待的状态,服务器处理完请求把数据响应给浏览器并覆盖浏览器内存中原有的数据,浏览器——重新加载页面并展示服务器响应的数据。
异步请求的原理
浏览器把请求交给代理对象—XMLHttpRequest(绝大多数浏览器都内置了这个对象),由代理对象向服务器发起请求,接收、解析服务器响应的数据,并把数据更新到浏览器指定的控件上。从而实现了页面数据的局部刷新。
# 2. Socket连接池复用
每次建立连接关闭都要三次握手四次挥手,显然会造成效率低下,Http协议中的KeepAlive机制在传输数据后仍然保持连接状态,当客户端需要再次传输数据时,直接使用空闲下来的连接而不需要重新建立连接。
OkHttp默认支持个并发KeepAlive,链路默认的存活时间为5分钟
# 3. 简单使用
# 3.1 使用步骤
1. 获取OKHttpClient对象
OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(8000, TimeUnit.MILLISECONDS)
.build();
2
3
2. 填写表单数据(只针对POST请求,GET请求略过)
// 普通表单
FormBody formBody = FormBody.Builder()
.add("page", "1")
.add("count", "2")
.add("type", "video")
.build()
// JSON方式请求
Map map = new HashMap();
map.put("mobile", "demoData");
map.put("password", "demoData");
JSONObject jsonObject = new JSONObject(map);
String jsonStr = jsonObject.toString();
RequestBody requestBodyJson = RequestBody.create(MediaType.parse("application/json;charset=utf-8"), jsonStr);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
3. 构建Request对象
Request request = new Request.Builder()
.url("http://192.168.31.32:8080/renren-fast/app/login") // 可以拼接参数
.addHeader("contentType", "application/json;charset=UTF-8")
.post(requestBodyJson)
.build();
2
3
4
5
4. 构建Callcall回调对象
final Call call = client.newCall(request);
5. 发起同步请求(同步/异步)
OkHttpClient 是连接对象,无论是什么请求,使用OKHttp都必须要创建这个对象。
- Request 是请求对象的参数,里面需要放置各种请求信息。
enqueue()
是使用异步请求方法。
// 第四步:异步get请求
call.enqueue(new Callback() {
/**
* 请求失败
* @param call
* @param e
*/
@Override
public void onFailure(Call call, IOException e) {
Log.i("TAG", e.getMessage());
}
/**
* 请求成功
* @param call
* @param response
* @throws IOException
*/
@Override
public void onResponse(Call call, Response response) throws IOException {
// 得到的子线程
String result = response.body().string();
Log.e("TAG", result);
tvRes.setText(result);
}
});
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 3.2 同步请求
同步请求在Android开发中不是很常用,在主线程中是不能进行网络请求的所以我们这个要开一个子线程进行同步请求。使用同步请求的是需要调用execute()
方法,Response接收返回的对象。
同步和异步请求只是最后一步的请求的方法不同而已。
使用及示例代码均为Kotlin
# GET同步请求
/**
* 同步请求
* 使用同步请求的是需要调用execute()方法,Response接收返回的对象。
* 同步和异步请求只是最后一步的请求的方法不同而已。
*/
private fun syncRequestByGet() {
val urlString = "http://apis.juhe.cn/fapig/euro2020/schedule" // 请求地址
val urlParameter = "type=2&key=828f26861263cc47c50c36a0985aff93"
// 第一步获取okHttpClient对象
val client: OkHttpClient = OkHttpClient.Builder()
.connectTimeout(8000, TimeUnit.MILLISECONDS)
.build()
// 第二步构建Request对象
val request: Request = Request.Builder()
.url("$urlString?$urlParameter") // 可以后加请求参数 url + str
.get() // 默认就是GET请求,可以不写
.build()
// 第三步构建Call对象
val call: Call = client.newCall(request)
// 发起同步请求(同步/异步)
Thread {
try {
val response = call.execute() // 提交并且接收返回数据
val resultData = response.body!!.string()
Log.i(TAG, "requestByGet: $resultData")
// 更新UI
runOnUiThread {
binding.mTextViewContext.text = resultData
}
} catch (e: IOException) {
e.printStackTrace()
}
}.start()
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
首先,创建了OkHttpClient实例,接着用Request.Builder构建了Request实例并传入了URL,然后httpClient.newCall
方法传入Request实例生成call,最后在子线程调用call.execute()执行请求获得结果response。
# POST同步请求
/**
* POST同步请求
*/
private fun syncRequestByPost() {
val client: OkHttpClient = OkHttpClient.Builder()
.connectTimeout(8000, TimeUnit.MILLISECONDS)
.build()
val formBody: RequestBody = FormBody.Builder()
.add("page", "1")
.add("count", "2")
.add("type", "video")
.build()
val request: Request = Request.Builder()
.url("https://api.apiopen.top/getJoke")
.post(formBody)
.build()
val call: Call = client.newCall(request)
Thread {
try {
val response = call.execute() // 提交并且接收返回数据
val resultData = response.body!!.string()
// 更新UI
runOnUiThread {
binding.mTextViewContext.text = resultData
}
} catch (e: Exception) {
Toast.makeText(this@MainActivity, "异常", Toast.LENGTH_SHORT).show()
e.printStackTrace()
}
}.start()
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
这是因为call.execute()
是同步方法。
想要在主线程直接使用而不用手动创建子线程可以嘛?当然可以,使用call.enqueue(callback)
即可
# 3.3 Get异步请求
其他请求方式像put、header、delete,主要在构建Request时把get()或post()换成put()、header()、delete()就可以了,但一般在Android端很少用到。
/**
* Get异步请求(不需要手动创建线程)
*/
private fun asyncRequestByGet() {
val client: OkHttpClient = OkHttpClient.Builder()
.connectTimeout(8000, TimeUnit.MILLISECONDS)
.build()
val request: Request = Request.Builder()
.url("https://api.apiopen.top/getJoke?page=1&count=2&type=video") // 可以后加请求参数 url + str
.get()
.build()
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: java.io.IOException) {
Log.i("TAG", e.message!!)
}
@Throws(java.io.IOException::class)
override fun onResponse(call: Call, response: Response) {
val resultData = response.body!!.string()
Log.i("TAG", resultData)
runOnUiThread {
binding.mTextViewContext.text = resultData
}
}
})
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
call.enqueue
会异步执行,需要注意的是,两个回调方法onFailure
、onResponse
是执行在子线程的,所以如果想要执行UI操作,需要使用Handler切换到UI线程。
# 3.4 Post异步请求
这里我们使用Post请求,和上面Get请求用的URL是一样的。不同的是用Post请求需要使用RequestBody这个对象,用add()方法,添加我们的请求参数。
/**
* Post异步请求
* @throws java
*/
private fun asyncRequestByPost() {
val urlStr = "https://api.apiopen.top/getJoke"
val client: OkHttpClient = OkHttpClient.Builder()
.connectTimeout(8000, TimeUnit.MILLISECONDS)
.build()
// 表单数据
val formBody = FormBody.Builder()
.add("page", "1")
.add("count", "2")
.add("type", "video")
.build()
val request: Request = Request.Builder()
.url(urlStr)
.post(formBody)
.build()
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: java.io.IOException) {
Log.i("TAG", e.message!!)
}
@Throws(java.io.IOException::class)
override fun onResponse(call: Call, response: Response) {
val resultData = response.body!!.string()
Log.e("TAG", resultData)
runOnUiThread {
binding.mTextViewContext.text = resultData
}
}
})
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
post请求提交表单
构建RequestBody除了上面的方式,还有它的子类FormBody,FormBody用于提交表单键值对,这种能满足平常开发大部分的需求。
//RequestBody:FormBody,表单键值对
RequestBody formBody = new FormBody.Builder()
.add("username", "iqqcode")
.add("password", "123456")
.build();
2
3
4
5
FormBody是通过FormBody.Builder用构建者模式创建,add键值对即可。它的contentType在内部已经指定了。
private static final MediaType CONTENT_TYPE = MediaType.get("application/x-www-form-urlencod
# 3.5 POST请求提交复杂请求体
RequestBody另一个子类MultipartBody,用于post请求提交复杂类型的请求体。复杂请求体可以同时包含多种类型的的请求体数据。
post请求 string、文件、表单,只有单一类型。考虑一种场景--注册场景,用户填写完姓名、电话,同时要上传头像图片,这时注册接口的请求体就需要 接受 表单键值对 以及文件了,那么前面的的post就无法满足了。那么就要用到MultipartBody了。 完整代码如下:
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());
}
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
可见,在构建RequestBody时是使用MultipartBody.Builder构建了MultipartBody实例,通过addFormDataPart方法传入了姓名、电话的键值对,也通过addFormDataPart("touxiang", "00.png", fileBody)传入了头像图片,其中"touxiang"是key值, "00.png"是文件名,fileBody是要以上传的图片创建的RequestBody。 因为所有数据都是以键值对的表单形式提交,所以要设置setType(MultipartBody.FORM)。
# 3.6 取消请求
每一个Call只能执行一次(原因会在下篇流程分析中说明)。如果想要取消正在执行的请求,可以使用call.cancel(),通常在离开页面时都要取消执行的请求的。
# 3.7 结果处理
请求回调的两个方法是指 传输层 的失败和成功。
- onFailure通常是connection连接失败或读写超时;
- onResponse是指,成功的从服务器获取到了结果,但是这个结果的响应码可能是404、500等,也可能就是200(response.code()的取值)。
- 如果response.code()是200,表示应用层请求成功了。
此时我们可以获取Response的ResponseBody,这是响应体。从面看到,可以从ResponseBody获取string、byte[]、InputStream,这样就可以对结果进行很多操作了,比如UI上展示string(要用Handler切换到UI线程)、通过InputStream写入文件等等。
# 4. 请求配置项
- 如何全局设置超时时长?
- 缓存位置、最大缓存大小 呢?
- 考虑有这样一个需求,我要监控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();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
这里通过OkHttpClient.Builder通过构建者模式设置了连接、读取、写入的超时时长,用cache()方法传入了由缓存目录、缓存大小构成的Cache实例,这样就解决了前两个问题。
还注意到,使用addInterceptor()方法添加了Interceptor实例,且重写了intercept方法。Interceptor意为拦截器,intercept()方法会在开始执行请求时调用。其中chain.proceed(request)内部是真正请求的过程,是阻塞操作,执行完后会就会得到请求结果ResponseBody,所以chain.proceed(request)的前后取当前时间,那么就知道整个请求所耗费的时间。上面chain.proceed(request)的前后分别打印的日志和时间,这样第三个问题也解决了。
另外,通常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();
2
3
4
5
6
这个Request实例:
- 使用addHeader()方法添加了请求头。
- 使用cacheControl(CacheControl.FORCE_NETWORK)设置此次请求是能使用网络,不用缓存。(还可以设置只用缓存FORCE_CACHE。)
# 5. 拦截器
拦截器的使用
拦截器创建:
class LogIntercept : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val response = chain.proceed(request)
val currentTime = System.currentTimeMillis()
Log.i("ICP", "intercept: ===> REQUEST = $request")
Log.i("ICP", "intercept: ===> RESPONSE = $response")
Log.i("ICP", "intercept: ===> 耗时${System.currentTimeMillis() - currentTime}ms")
return response
}
}
2
3
4
5
6
7
8
9
10
11
12
在创建实例时添加拦截:
val client: OkHttpClient = OkHttpClient.Builder()
.connectTimeout(8000, TimeUnit.MILLISECONDS)
.addInterceptor(LogIntercept()) // 添加拦截器
.build()
2
3
4