目录
1.OkHttp简介
HTTP是一种现代常用的交换数据和媒体信息的网络方式。高效的使用HTTP将使数据加载更快,同时节省带宽。
OkHttp是一个HTTP客户端,拥有如下特性:
支持HTTP/2,并且允许对同一个主机的所有请求共享一个Socket连接;
连接池减少请求延迟(如果HTTP/2不支持);
透明的GZIP压缩减少响应数据的大小;
当网络出现问题时OkHttp将坚持自己的职责;它会自动恢复一般的连接问题。如果你的服务有多个IP地址,OkHttp将尝试切换地址在遇到第一个连接失败的情况下。这对于IPv4+IPv6和冗余数据中心中托管的服务是必要的。OkHttp启动了与现代TLS特性(SNI、ALPN)的新连接,如果握手失败,则返回到TLS 1.0。
使用OkHttp很容易。它的请求/响应API是用连贯的构建器和不变性来设计的。它支持同步阻塞调用和带有回调的异步调用。
OkHttp支持Android 2.3及以上版本。对于Java,最低要求是1.7。
2.使用
使用OkHttp很容易。它的请求/响应API是用连贯的构建器和不变性来设计的。它支持同步阻塞调用和带有回调的异步调用。
2.1Get同步阻塞调用
-创建OkHttpClient对象;
-创建Request请求;
-执行newCall创建Call;
-执行Call.execute();执行网络请求;
说明:当前执行是阻塞式调用,只有当前网络请求完成才会继续向下执行,Android3.0以后是不允许在主线程访问网络请求;
/**
* Get方式同步请求
*/
public void syncGetReqest() throws IOException {
//1.创建OkHttp客户端
OkHttpClient client = new OkHttpClient();
//2.创建网络请求
Request request = new Request.Builder()
.url("http://192.168.2.10:8080/ShiroTest/code/list")
.build();
//3.创建Call
Call call = client.newCall(request);
//执行网络请求
Response response= call.execute();
if(!response.isSuccessful()){
throw new IOException("Unexpected code " + response);
}
//获取响应头
Headers responseHeaders = response.headers();
for(int i=0; i < responseHeaders.size(); i++){
System.out.println(responseHeaders.name(i)+" : "+ responseHeaders.value(i));
}
//打印返回内容
System.out.println(response.body().string());
}
2.2Get异步调用
Get异步调用和Get同步调用方式类似,只是执行网络请求调用时Call.execute()方法调用换做Call.enqueue()通过接口回调的方式监听网络请求结果,
/**
* Get方式异步请求
*/
public void asyncGetReqest() throws IOException {
//1.创建OkHttp客户端
OkHttpClient client = new OkHttpClient();
//2.创建网络请求
Request request = new Request.Builder()
.url("http://publicobject.com/helloworld.txt")
.build();
//3.创建Call
Call call = client.newCall(request);
//4.执行网络请求
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
System.out.println("异步网络请求失败...");
}
@Override
public void onResponse(Call call, Response response) throws IOException {
if(!response.isSuccessful()){
throw new IOException("Unexpected code " + response);
}
Headers headers = response.headers();
for(int i=0;i<headers.size();i++){
System.out.println(headers.name(i)+" : ");
}
System.out.println("异步网络请求成功..."+response.body().string());
}
});
System.out.println("异步网络请求回调之后...");
Call.enqueue执行异步网络请求线程会加入到Dispatcher类中的runningAsyncCalls(维护正在运行的异步请求线程,方便取消当前执行的线程使用和维护异步请求线程状态)双端队列中,线程会加入到线程池中执行;
synchronized void enqueue(AsyncCall call) {
if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
runningAsyncCalls.add(call);
executorService().execute(call);
} else {
readyAsyncCalls.add(call);
}
}
2.3访问Header(请求头或者响应头)
通常HTTP报头的工作方式类似于Map<String, String>:每个字段都有一个值或一个都没有。但有些报头允许多个值,比如Guava的Multimap。例如,对于一个HTTP响应提供多个Vary报头是合规和常见的。OkHttp的api试图让这两种情况都变得有用。
在编写请求保头时,使用header(name、value)仅仅设置出现的名称的值。如果存在现有值,则在添加新值之前将删除已存在的值。使用addHeader(name, value)添加报头,而不删除已经出现的报头。
在读取一个响应报头时,使用header(name)返回最后一次出现的命名值。通常这也是唯一的发生!如果不存在值,header(name)将返回null。要将字段的所有值作为列表读取,请使headers(name)。
要访问所有headers,请使用支持按索引访问的Headers。
//访问Header
public void accessHeaders() throws IOException {
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url("https://api.github.com/repos/square/okhttp/issues")
.header("User-Agent", "OkHttp Headers.java")
.addHeader("Accept", "application/json; q=0.5")
.addHeader("Accept", "application/vnd.github.v3+json")
.build();
Call call = client.newCall(request);
Response response = call.execute();
if(!response.isSuccessful()){
throw new IOException("Unexpected code " + response);
}
Headers headers = response.headers();
System.out.println("Server: " + response.header("Server"));
System.out.println("Date: " + response.header("Date"));
System.out.println("Vary: " + response.headers("Vary"));
}
2.4post提交String
post方式提交数据和get方式提交的主要区别是Request需要添加RequestBody(需要提交给服务器的信息),创建RequestBody需要指定内容类型MediaType,MediaType主要用于描述请求/响应内容的类型,这个例子将post提交一个markdown文档给web服务去渲染markdown做为HTML;
RequestBody.create重载多个创建RequestBody的方法:
public static RequestBody create(MediaType contentType, String content)
public static RequestBody create(final MediaType contentType, final ByteString content)
public static RequestBody create(final MediaType contentType, final byte[] content)
public static RequestBody create(final MediaType contentType, final byte[] content,
final int offset, final int byteCount)
public static RequestBody create(final MediaType contentType, final File file)
注意事项:因为整个RequestBody(请求体)同时在内存中,所以避免使用此API发布较大(大于1 MiB)的文档。
//post请求字符串
public void postAString() throws IOException {
MediaType mediaType = MediaType.parse(
"text/x-markdown; charset=utf-8");
OkHttpClient client = new OkHttpClient();
String postBody = ""
+ "Releases\n"
+ "--------\n"
+ "\n"
+ " * _1.0_ May 6, 2013\n"
+ " * _1.1_ June 15, 2013\n"
+ " * _1.2_ August 11, 2013\n";
Request request = new Request.Builder()
.url("https://api.github.com/markdown/raw")
.post(RequestBody.create(mediaType, postBody))
.build();
Response response = client.newCall(request).execute();
if(!response.isSuccessful()){
throw new IOException("Unexpected code " + response);
}
System.out.println(response);
}
响应内容
Server : GitHub.com
Date : Wed, 12 Sep 2018 02:47:41 GMT
Content-Type : text/html;charset=utf-8
Transfer-Encoding : chunked
Status : 200 OK
X-RateLimit-Limit : 60
X-RateLimit-Remaining : 58
X-RateLimit-Reset : 1536723315
X-CommonMarker-Version : 0.17.11
Access-Control-Expose-Headers : ETag, Link, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval
Access-Control-Allow-Origin : *
Strict-Transport-Security : max-age=31536000; includeSubdomains; preload
X-Frame-Options : deny
X-Content-Type-Options : nosniff
X-XSS-Protection : 1; mode=block
Referrer-Policy : origin-when-cross-origin, strict-origin-when-cross-origin
Content-Security-Policy : default-src 'none'
X-Runtime-rack : 0.012857
X-GitHub-Request-Id : 5FA1:2DF0:524E91:C493B1:5B987E4C
OkHttp-Sent-Millis : 1536720461639
OkHttp-Received-Millis : 1536720461895
2.5Post方式提交流
在这里,我们将请求主体作为流进行POST提交。这个请求主体的内容在编写时生成。这个示例直接放到Okio缓冲接收器中。您的程序可能更喜欢OutputStream,您可以从BufferedSink.outputStream()中获得它。
Okio是一个使java.io和java.nio访问、存储和处理数据变得更加容易组件;
public void postStream() throws IOException {
MediaType mediaType = MediaType.parse("text/x-markdown; charset=utf-8");
OkHttpClient client = new OkHttpClient();
RequestBody requestBody = new RequestBody() {
@Override
public MediaType contentType() {
return mediaType;
}
@Override
public void writeTo(BufferedSink sink) throws IOException {
sink.writeUtf8("Numbers\n");
sink.writeUtf8("-------\n");
for(int i=2; i<= 997; i++){
sink.writeUtf8(String.format(" * %s = %s\n", i, factor(i)));
}
}
private String factor(int n) {
for (int i = 2; i < n; i++) {
int x = n / i;
if (x * i == n) return factor(x) + " × " + i;
}
return Integer.toString(n);
}
};
Request request = new Request.Builder()
.url("https://api.github.com/markdown/raw")
.post(requestBody)
.build();
Response response = client.newCall(request).execute();
if(!response.isSuccessful()){
throw new IOException("Unexpected code " + response);
}
Headers headers = response.headers();
for(int i=0; i<headers.size(); i++){
System.out.println(headers.name(i)+" : "+headers.value(i));
}
System.out.println(response.body().string());
}
2.6Post方式提交一个文件
将文件封装到RequestBody中,提交给服务器
public void postAFile() throws IOException {
MediaType mediaType = MediaType.parse("text/x-markdown; charset=utf-8"); //MediaType.parse("image/png");
OkHttpClient client = new OkHttpClient();
File file = new File("/Users/fandong/Desktop/normal.jpg");
Request request = new Request.Builder()
.url("https://api.github.com/markdown/raw")
.post(RequestBody.create(mediaType, file))
.build();
Response response = client.newCall(request).execute();
if(!response.isSuccessful()){
throw new IOException("Unexpected code " + response);
}
Headers headers = response.headers();
for(int i=0; i<headers.size(); i++){
System.out.println(headers.name(i)+" : "+headers.value(i));
}
System.out.println(response.body().string());
}
2.7Post方式提交Form 表单
使用FormBody.Builder去构建一个类似HTML中<form>标签一样工作的请求体。名称和值将使用HTML中编译form的URL编码方式编码;
public void postFormParams() throws IOException {
OkHttpClient client = new OkHttpClient();
RequestBody formBody = new FormBody.Builder()
.add("search", "Jurassic Park")
.build();
Request request = new Request.Builder()
.url("https://en.wikipedia.org/w/index.php")
.post(formBody)
.build();
Response response = client.newCall(request).execute();
Headers headers = response.headers();
for(int i=0; i<headers.size(); i++){
System.out.println(headers.name(i)+" : "+headers.value(i));
}
System.out.println(response.body().string());
}
FormBody中存放key和value定义;
private final List<String> encodedNames;
private final List<String> encodedValues;
2.8Post方式提交分块请求
MultipartBody.Builder 能构建与HTML文件上传表单兼容的复杂的请求体。一个multipart request body的每一部分都是一个她自己的请求体,并且能够定义它自己的headers。这些请求头可以描述部分body,例如Content-Disposition。这个Content-Length和Content-Type头如果合适的话将自动被添加。
public void postMultipartBody() throws IOException {
OkHttpClient client = new OkHttpClient();
String IMGUR_CLIENT_ID = "...";
MediaType mediaType = MediaType.parse("image/png");
RequestBody requestBody = new MultipartBody.Builder()
//设置form表单为multipart/form-data类型,multipart/form-data是将文件以二进制的形式上传,这样可以实现多种类型的文件上传
.setType(MultipartBody.FORM)
//说明每一部分都是一个请求体,并且定义自己的Headers
.addPart(Headers.of("Content-Disposition", "form-data; name=\"title\""),
RequestBody.create(null, "Square Logo"))
//form表单数据
.addFormDataPart("title", "Square Logo")
//form表单文件数据
.addFormDataPart("image", "logo-square.png",
RequestBody.create(mediaType, new File("/Users/fandong/Desktop/normal.jpg")))
.build();
Request request = new Request.Builder()
.header("Authorization", "Client-ID " + IMGUR_CLIENT_ID)
.url("https://api.imgur.com/3/image")
.post(requestBody)
.build();
// Call call = client.newCall(request);
// call.enqueue(new Callback() {
// @Override
// public void onFailure(Call call, IOException e) {
//
// }
//
// @Override
// public void onResponse(Call call, Response response) throws IOException {
// System.out.println(response.body().string());
// }
// });
Response response = client.newCall(request).execute();
System.out.println(response.body().string());
}
简单的MultipartBody源码分析:
a.MultipartBody继承自RequestBody,是一个复杂的请求体类,他可以包含多个请求体和对应的头;
b.MultipartBody定义了Part内部类,Part用于存放添加的表单信息,Part定义的请求体和请求体对应的Headers;
private Part(Headers headers, RequestBody body) {
this.headers = headers;
this.body = body;
}
c.MultipartBody定义了private final List<Part> parts;变量存放添加的表单数据,说明可以添加多个请求体和对应的Headers;
d.MultipartBody调用所有的add...方法最终都会调用如下方法最终添加到parts中;
/** Add a part to the body. */
public Builder addPart(Part part) {
if (part == null) throw new NullPointerException("part == null");
parts.add(part);
return this;
}
3.OkHttp定义的拦截器Interceptors
拦截器的作用:拦截器是一种强大的机制,可以监视、重写和重试调用。
拦截器分为(Application Interceptors)全局拦截器和Network Interceptors网络拦截器;
3.1全局拦截器:
a.定义拦截器
class LoggingInterceptor implements Interceptor {
@Override public Response intercept(Interceptor.Chain chain) throws IOException {
Request request = chain.request();
//1.请求前--打印请求信息
long t1 = System.nanoTime();
logger.info(String.format("Sending request %s on %s%n%s",
request.url(), chain.connection(), request.headers()));
//2.执行网络请求
Response response = chain.proceed(request);
//3.网络响应后--打印响应信息
long t2 = System.nanoTime();
logger.info(String.format("Received response for %s in %.1fms%n%s",
response.request().url(), (t2 - t1) / 1e6d, response.headers()));
return response;
}
}
b.全局拦截器添加,使用addInterceptor()方法添加拦截器
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(new LoggingInterceptor())
.build();
Request request = new Request.Builder()
.url("http://www.publicobject.com/helloworld.txt")
.header("User-Agent", "OkHttp Example")
.build();
Response response = client.newCall(request).execute();
response.body().close();
这个URL地址http://www.publicobject.com/helloworld.txt重定向到https://publicobject.com/helloworld.txt地址, 并且OkHttp将跟随自动重定向。我们(application interceptor)全局拦截器只被调用一次并且chain.proceed()返回重定向响应:
INFO: Sending request http://www.publicobject.com/helloworld.txt on null
User-Agent: OkHttp Example
INFO: Received response for https://publicobject.com/helloworld.txt in 1179.7ms
Server: nginx/1.4.6 (Ubuntu)
Content-Type: text/plain
Content-Length: 1759
Connection: keep-alive
我们可以看到我们被重定向了,因为response.request().url()与request.url()不同。两个日志语句记录两个不同的url。
3.2网络拦截器:
a.添加拦截器方法addNetworkInterceptor()
OkHttpClient client = new OkHttpClient.Builder()
.addNetworkInterceptor(new LoggingInterceptor())
.build();
Request request = new Request.Builder()
.url("http://www.publicobject.com/helloworld.txt")
.header("User-Agent", "OkHttp Example")
.build();
Response response = client.newCall(request).execute();
response.body().close();
当我们运行这段代码时,拦截器会运行两次。第一次是请求http://www.publicobject.com/helloworld.txt,第二次是重定向到https://publicobject.com/helloworld.txt。
INFO: Sending request http://www.publicobject.com/helloworld.txt on Connection{www.publicobject.com:80, proxy=DIRECT hostAddress=54.187.32.157 cipherSuite=none protocol=http/1.1}
User-Agent: OkHttp Example
Host: www.publicobject.com
Connection: Keep-Alive
Accept-Encoding: gzip
INFO: Received response for http://www.publicobject.com/helloworld.txt in 115.6ms
Server: nginx/1.4.6 (Ubuntu)
Content-Type: text/html
Content-Length: 193
Connection: keep-alive
Location: https://publicobject.com/helloworld.txt
INFO: Sending request https://publicobject.com/helloworld.txt on Connection{publicobject.com:443, proxy=DIRECT hostAddress=54.187.32.157 cipherSuite=TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA protocol=http/1.1}
User-Agent: OkHttp Example
Host: publicobject.com
Connection: Keep-Alive
Accept-Encoding: gzip
INFO: Received response for https://publicobject.com/helloworld.txt in 80.9ms
Server: nginx/1.4.6 (Ubuntu)
Content-Type: text/plain
Content-Length: 1759
Connection: keep-alive
网络请求还包含更多的数据,比如Accept-Encoding:由OkHttp添加的gzip头来声明对响应压缩的支持。网络拦截器的链具有非空连接,可用于查询用于连接到web服务器的IP地址和TLS配置。
拦截器更多介绍:https://github.com/square/okhttp/wiki/Interceptors
4.其他
4.1推荐使用单例模式维护OkHttpClient对象,所有请求使用单例中创建的OkHttpClient发送网络请求,OkHttpClient自己会维护一套请求网络的线程池,不需要我们每次网络请求都创建OkHttpClient,每次都创建OkHttpClient会造成资源的浪费;
当然,也可以使用如下的方式来创建一个新的 OkHttpClient 实例,它们共享连接池、线程池和配置信息。
OkHttpClient eagerClient = client.newBuilder()
.readTimeout(500, TimeUnit.MILLISECONDS)
.build();
Response response = eagerClient.newCall(request).execute();
4.2每一个Call(其实现是RealCall)只能执行一次,否则会报异常,具体参见 RealCall#execute()
参考:
https://github.com/square/okhttp/wiki/Recipes
https://www.jianshu.com/p/da4a806e599b