Retrofit 实际上就是高度封装的 OkHttp,每次用到都是到处搜搜就写上,后面发现从协议出发才能真正地弄懂这些 api
1. OkHttp发出请求概览
使用okhttp发起一次请求并获取请求结果,一般而言我们需要使用到五个类
-
OkHttpClient
Call 的工厂类
-
request
http 请求的实例
-
Call
(OkHttp3)Call 是代表已经准备好执行并且可能正在执行的请求
可以使用 cancel 关闭这个请求。实际上Http请求由于链路的不可靠性,OkHttp协助你实现的请求封装并不是机械地添加上必须的
Content-Length
等 header 直接发出后等待。而是会协助进行一系列的操作-
重写请求
添加上必须的header,例如
Content-Length
,Encoding
,原来的请求如果没有添加Accept-Encoding
类型时,OkHttp也会自动添加Accept-Encoding:Gzip
,以此希望服务器能返回压缩数据 -
重写响应
服务器返回压缩数据时,OkHttp将自动解压并去掉解压前留下的
Content-Length
header。 -
跟踪请求
请求返回 302 Found 并携带有 资源移动后的 url 时,OkHttp会自动按照新的url进行一次请求
-
-
Callback
异步请求中的回调接口
-
Response
http 请求返回信息的实例,主要包含 http 的 header 和 body
private final OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url("baseUrl")
.build();
// 同步请求
Response response = client.newCall(request).execute();
if(!respnse.isSuccessful()){
// To handle the request error
}
// 异步请求
client.newCall(request).enqueue(new Callback(){
// 重写onFailure()和onResponse()
// 异步请求的request在onResponse()中可以获取
});
2. http 首部
下面是一个服务器返回的首部示例
304 Not Modified
Access-Control-Allow-Origin: *
Age: 2318192
Cache-Control: public, max-age=315360000
Connection: keep-alive
Date: Mon, 18 Jul 2016 16:06:00 GMT
Server: Apache
Vary: Accept-Encoding
Via: 1.1 3dc30c7222755f86e824b93feb8b5b8c.cloudfront.net (CloudFront)
X-Amz-Cf-Id: TOl0FEm6uI4fgLdrKJx0Vao5hpkKGZULYN2TWD2gAWLtr7vlNjTvZw==
X-Backend-Server: developer6.webapp.scl3.mozilla.com
X-Cache: Hit from cloudfront
X-Cache-Info: cached
大部分的http首部包含的键值对 - name:value 都不会有键的重复,一般而言可以用 HashMap 进行存储,但是http协议允许首部中存在相同的 name,所以OkHttp采用的策略是在需要时使用 Multimap 存储首部,Multimap 是一种允许包含重复的键的键值表。
2.1 操作首部
-
添加
对 Request 添加首部
Request request = new Request.Builder().url("baseUrl") // 添加首部 .addHeader("Uer-Agent", "OkHttp Headers.java") // 添加首部 // 与 addHeader 不同的是,该方法会覆盖掉相同键的首部 .header("Accept", "application/json; q=0.5") .build();
-
获取
从 Response 获取首部
client.newCall(request).enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { e.printStackTrace(); } @Override public void onResponse(Call call, Response response) throws IOException { if (!response.isSuccessful()) throw new IOException("Unexpected code " + response); // 获取首部。也可以使用 header() 方法但 header() 只会返回一个首部 Headers responseHeaders = response.headers(); for (int i = 0, size = responseHeaders.size(); i < size; i++) { System.out.println(responseHeaders.name(i) + ": " + responseHeaders.value(i)); } System.out.println(response.body().string()); } });
3. Post 请求
下面是一个客户端发送的 Post 请求示例
POST https://127.0.0.1:8080 HTTP/1.1
cache-control: no-cache
Postman-Token: 305a878a-5fb7-4ba708476-384fbf15cb47
Content-Type: application/x-www-form-urlencoded
User-Agent: PostmanRuntime/6.4.2
Accept: */*
Host: 127.0.0.1:8080
accept-encoding: gzip, deflate
content-length: 28
Connection: keep-alive
hereIsBody=true
Post 请求包含 header 和 body 两部分,与 get 请求直接在url中传递数据不同的地方在于 post 的数据一般是放在 body 中
3.1 添加 Body
如果 Post 需要携带 Body,需要操作 Request 类,添加可以添加一个 RequestBody 来自定义 body
public static final MediaType MEDIA_TYPE = MediaType.parse("text/plain; charset=utf-8");
Request request = new Request.Builder()
.url("TargetUrl")
.post(RequestBody.create(MEDIA_TYPE,"MyBody"))
.build();
值得注意的是,虽然 http 是超文本协议并且也允许传输文件,但是文件如果直接转换成 string 载入到内存中,不仅会造成卡顿还可能造成OOM。这里 RequestBody.create() 方法有多个重载,支持的body有 String,ByteString,byte[],File
3.2 提交表单
表单内容虽然可以由我们自己生成url,再于RequestBody.create()
时设置MediaType为"application/x-www-form-urlencoded"
来提交,但 OkHttp 也提供了基本的url生成方法,下面是一个示例
RequestBody formBody = new FormBody.Builder()
.add("userName","admin")
.add("userLevel","admin")
.build();
Response response = client.newCall(request).execute();
3.3 提交 multipart/form-data 类型
multipart/form-data
也是前后端交互中常见的类型。下面是一个携带有该类型数据的POST请求示例,其中boundary的值没有固定要求,这里设置的是 ----myBoundary----
POST http://127.0.0.1:8080 HTTP/1.1
Content-Type:multipart/form-data; boundary=----myBoundary----
----myBoundary----
Content-Disposition: form-data; name="file"; filename="tmp.png"
Content-Type: image/png
...content of tmp.png...
----myBoundary----
Content-Disposition: form-data; name="city_id"
1
----myBoundary----
如果要提交上述内容,可以模仿下面这个示例
static final MediaType MEDIA_TYPE_PNG = MediaType.parse("image/png");
RequestBody requestBody = new MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("title", "传输测试")
.addFormDataPart("image", "logo.png",
RequestBody.create(MEDIA_TYPE_PNG, new File("pngurl/logo.png")))
.build();
Request request = new Request.Builder()
.url("https://127.0.0.1:8080/")
.post(requestBody)
.build();
4. 使用 Cache
之前展示的html首部中都包含有 cache-control:
这个响应头,这个响应头允许多种指令,下面是最常见的几种
-
cache-control: no-cache
资源会被缓存,但是使用前需要询问服务器是否有效,有效服务器返回 HTTP/304 Not Modified,无效服务器返回 HTTP/200 OK
-
cache-control: no-store
资源不允许被缓存,每次使用都需要请求进行该资源的重新下载
-
cache-control: max-age
使用秒为单位定义响应资源的有效时间
-
cache-control: public / private
public 标注的资源可以被 CDN 以及 代理服务器 缓存,而 private 标注的资源只允许 浏览器端缓存
具体的使用可以参考 Prevent unnecessary network requests with the HTTP Cache 这篇文章
OkHttp 开启缓存需要定义一个私有的缓存目录,以及允许缓存文件的大小,下面是一个简单的开启缓存的示例
private val client: OkHttpClient = OkHttpClient.Builder()
.cache(Cache(
directory = File(application.cacheDir, "http_cache"),
maxSize = 50L * 1024L * 1024L // 50 MiB
))
.build()
5. Https 支持
TLS (Transport Layer Security Protocol)
SSL (Secure Sockets Layer)
OkHttp实际上协助开发者兼容了 boringssl 和 OpenSSL 等多种以及多版本的 SSL/TLS 实现库,以便开发者在不了解底层协议的情况下与多种使用不同 SSL/TLS 实现库的服务器进行通信,如果需要配置可以参考以下官方示例
// ConnectionSpec 即连接规格,该类中存储的字段用来大概地确定OkHttp使用的SSL/TLS的版本
// 使用这个类来指定可以让代码不需要跟随协议的更新而更新,而交由OkHttp来实现
OkHttpClient client = new OkHttpClient.Builder()
.connectionSpec(Arrays.asList(ConnectionSpec.MODERN_TLS, ConnectionSpec.COMPATIBLE_TLS))
.build();
// 如果需要自定义 ConnectionSpec,参考下面的示例
ConnectionSpec spec = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
.tlsVersions(TlsVersion.TLS_1_2)
.cipherSuites(
CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
CipherSuite.TLS_DHE_RSA_WITH_AES_128_GCM_SHA256)
.build();
OkHttpClient client = new OkHttpClient.Builder()
.connectionSpecs(Collections.singletonList(spec))
.build();
实际上 SSL/TLS 版本多年来经历了多次更新
SSL3 (1995) -> TLS1.0 (1999) -> TLS1.1(2006) -> TLS1.2 (2008) -> TLS1.3 (2018)
下面两个握手示例,分别为 TLS1.2 和 TLS1.3。TLS1.2的示例来自 RFC 5246 第35面,TLS1.3的示例来自 RFC 8446 第10面
Client Server
ClientHello -------->
ServerHello
Certificate*
ServerKeyExchange*
CertificateRequest*
<-------- ServerHelloDone
Certificate*
ClientKeyExchange
CertificateVerify*
[ChangeCipherSpec]
Finished -------->
[ChangeCipherSpec]
<-------- Finished
Application Data <-------> Application Data
Client Server
Key ^ ClientHello
Exch | + key_share*
| + signature_algorithms*
| + psk_key_exchange_modes*
v + pre_shared_key* -------->
ServerHello ^ Key
+ key_share* | Exch
+ pre_shared_key* v
{EncryptedExtensions} ^ Server
{CertificateRequest*} v Params
{Certificate*} ^
{CertificateVerify*} | Auth
{Finished} v
<-------- [Application Data*]
^ {Certificate*}
Auth | {CertificateVerify*}
v {Finished} -------->
[Application Data] <-------> [Application Data]
+ Indicates noteworthy extensions sent in the
previously noted message.
* Indicates optional or situation-dependent
messages/extensions that are not always sent.
{} Indicates messages protected using keys
derived from a [sender]_handshake_traffic_secret.
[] Indicates messages protected using keys
derived from [sender]_application_traffic_secret_N.
可以看到使用 TLS1.3 时,可以减少客户端和服务器之间的一次 Hello 握手,网上搜索到的大部分关于 HTTPS 的解释都是对 TLS1.2 进行的。
实际上 Android 4.4.2 (2014.6) 开始才支持 TLS1.2 协议,Android 8.1 (2017.12) 开始支持 TLS 1.3,彼时 TLS1.3 的 RFC 尚处于草案阶段,可见近年来 Android 越来越注重于用户的信息安全。如果需要详细的协议支持表,可以访问 SSL Labs
6. OkHttp 拦截器
拦截器是 OkHttp 非常重要的一个功能拓展,开发者可以用它监听,改写和手动重试请求。我们定义完拦截器后将它添加到对应的 Client 中即可
val myClient = okHttpBuilder
.addInterceptor(MyAppInterceptor())
.addNetworkInterceptor(MyNetInterceptor())
.build()
代码中添加的两种拦截器分别是应用拦截器和网络拦截器,官方文档中这张视图非常好地展示了两种拦截器生效的位置
图中的灰块代表拦截器,和视图一致的是,拦截器是链式调用的,OkHttp会按照拦截器添加的顺序,依次分发内容给对应的拦截器。这意味着如果有用于压缩和加密的两个拦截器,就可以控制压缩和加密的先后顺序。
下面是一个简单的拦截器,记录了所有使用了该拦截器的 Client 收发的信息,该代码样例来自于官方文档
private class MyInterceptor:Interceptor{
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val timeStart = System.nanoTime()
Log.i(TAG, "intercept: sending request ${request.url()} on ${chain.connection()}\nheader=${request.headers()}")
val response = chain.proceed(request)
val timeEnd = System.nanoTime()
Log.i(TAG, "intercept: received response for ${response.request().url()} ${(timeEnd-timeStart)/0x1e6d}\nheader=${response.headers()}")
return response
}
}
拦截器中有个关键的方法调用,即chain.proceed(request)
,该方法调用时会将拦截到的请求传递到 OkHttp core,从而产生实际的 http 请求
6.1 应用拦截器和网络拦截器的不同之处
之前提到的OkHttp代替我们进行的操作,例如请求压缩文件,自动重定向,自动添加必要首部都在上图中的 OkHttp core 部分,用户拦截器可以只关注于应用表面上发送和收到的数据。而如果要获取到实际的首部和重定向的次数,则需要使用网络拦截器。
我们可以试着将这个日志用拦截器分别添加为应用拦截器和网络拦截器,执行一次请求
OkHttpClient client = new OkHttpClient.Builder()
.addNetworkInterceptor(new LoggingInterceptor())
.addInterceptor(new LoggingInterceptor())
.build();
Request request = new Request.Builder()
// 这里测试的url来自于wanandroid提供的http url api
// 如果想本地测试,名称和密码请自行注册
.url("https://www.wanandroid.com/user/login/?username=myName&password=myPwd")
.build();
// 请在非 UI 线程中执行下面的阻塞代码
Response response = client.newCall(request).execute();
response.body().close();
得到的内容为
-
网络拦截器
2021-07-21 17:32:36.860 9513-9598/com.example.retrofitjava I/RetrofitManager: intercept: sending request https://www.wanandroid.com/user/login/?username=myName&password=myPwd on Connection{www.wanandroid.com:443, proxy=DIRECT hostAddress=www.wanandroid.com/47.104.74.169:443 cipherSuite=TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA protocol=http/1.1} header=Content-Length: 0 Host: www.wanandroid.com Connection: Keep-Alive Accept-Encoding: gzip User-Agent: okhttp/3.14.9 2021-07-21 17:32:37.234 9513-9598/com.example.retrofitjava I/RetrofitManager: intercept: received response for https://www.wanandroid.com/user/login/?username=myName&password=myPwd 48069 header=Server: Apache-Coyote/1.1 Set-Cookie: JSESSIONID=69FCD58ECA50172EC63ECB24F0395132; Path=/; Secure; HttpOnly Set-Cookie: loginUserName=2642981383; Expires=Fri, 20-Aug-2021 09:32:36 GMT; Path=/ Set-Cookie: token_pass=630dd3ec7b0ec6692be5c9d8b90dc6a6; Expires=Fri, 20-Aug-2021 09:32:36 GMT; Path=/ Set-Cookie: loginUserName_wanandroid_com=myName; Domain=wanandroid.com; Expires=Fri, 20-Aug-2021 09:32:36 GMT; Path=/ Set-Cookie: token_pass_wanandroid_com=630dd3ec7b0ec6692be5c9d8b90dc6a6; Domain=wanandroid.com; Expires=Fri, 20-Aug-2021 09:32:36 GMT; Path=/ Content-Type: application/json;charset=UTF-8 Transfer-Encoding: chunked Date: Wed, 21 Jul 2021 09:32:35 GMT 2021-07-21 17:32:41.076 9513-9556/com.example.retrofitjava I/le.retrofitjav: ProcessProfilingInfo new_methods=1869 is saved saved_to_disk=1 resolve_classes_delay=5000
-
应用拦截器
可以看到必要的首部实际上都是由 OkHttp core 代替我们添加的
2021-07-21 17:42:40.339 10721-10805/com.example.retrofitjava I/RetrofitManager: intercept: sending request https://www.wanandroid.com/user/login/?username=myName&password=myPwd on null header= 2021-07-21 17:42:41.074 10721-10805/com.example.retrofitjava I/RetrofitManager: intercept: received response for https://www.wanandroid.com/user/login/?username=myName&password=myPwd 94317 header=Server: Apache-Coyote/1.1 Set-Cookie: JSESSIONID=495F705ABEFFFB94DDEA209BA60C6D62; Path=/; Secure; HttpOnly Set-Cookie: loginUserName=2642981383; Expires=Fri, 20-Aug-2021 09:42:40 GMT; Path=/ Set-Cookie: token_pass=630dd3ec7b0ec6692be5c9d8b90dc6a6; Expires=Fri, 20-Aug-2021 09:42:40 GMT; Path=/ Set-Cookie: loginUserName_wanandroid_com=myName; Domain=wanandroid.com; Expires=Fri, 20-Aug-2021 09:42:40 GMT; Path=/ Set-Cookie: token_pass_wanandroid_com=630dd3ec7b0ec6692be5c9d8b90dc6a6; Domain=wanandroid.com; Expires=Fri, 20-Aug-2021 09:42:40 GMT; Path=/ Content-Type: application/json;charset=UTF-8 Transfer-Encoding: chunked Date: Wed, 21 Jul 2021 09:42:39 GMT
6.2 通过拦截器更改请求
用户发起的请求封装在 Request 对象中,由于拦截器重写的 intercept()
方法中可以获得到 Chain 从而获得到用户或者上一个拦截器发出的 Request。我们可以在拦截器中重新封装一次 Request 再使用 chain.procceed()
发出。所以拦截器可以添加,移除甚至替换掉 header,也可以更改掉 body。
下面展示的例子中,在原有body存在,并且header中没有标注内容已加密时,对原有请求的body进行压缩
private class ProcessInterceptor:Interceptor{
override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request()
if (originalRequest.body()==null ||
originalRequest.header("Content-Encoding")!=null){
return chain.proceed(originalRequest)
}
val compressedRequest = originalRequest.newBuilder()
.header("Content-Encoding","gzip")
//保持原有request的get或者post请求
.method(originalRequest.method(), gzip(originalRequest.body()))
.build()
return chain.proceed(compressedRequest)
}
}
下面展示的例子中更改了响应的header,强制响应为缓存一分钟,实际上应该避免修改响应,这种操作应该由服务端完成
private static final Interceptor REWRITE_CACHE_CONTROL_INTERCEPTOR = new Interceptor() {
@Override public Response intercept(Interceptor.Chain chain) throws IOException {
Response originalResponse = chain.proceed(chain.request());
return originalResponse.newBuilder()
.header("Cache-Control", "max-age=60")
.build();
}
};