okHttp系列(二):高级功能:下载,上传以及拦截器
2018-02-07 15:55
811 查看
1.下载文件
在ResponseBody中有如下接口:
byte()
string()
bytesStream()
charStream()
其中byte()和string()是一次读取,用来获取体积比较小的内容。但如果遇到大文件的话,就应该用流的方式。
所谓下载也就是将服务器返回的数据存储在本地。
当体积体积较小时,用byte()或者string()获取内容。
当体积很大时(超过1m),就应该用流的方式,用byteStream()或者charStream().
这里我用流的方式演示从网络上下载一张图片,然后保存在本地,然后显示出来。我是用bytesStream()方法。
2.上传文件
action定义到表单发送的位置,这里是upload_file.php,说明表单将会发送到主机上的upload_file.php上。
method 中的方法是post。这个一定要写对,文件上传的内容必须放在实体中,不能添加在header中,所以不能用get,要用post.
enctype,这个定义内容是 multipart/form-data.
enctype有两个值的范围。
* 一个是application/x-www-form-urlencoded(默认值),传输文本信息
* 一个是multipart/form-data.传输二进制信息
标签的 type=”file” 属性规定了应该把输入作为文件来处理。举例来说,当在浏览器中预览时,会看到输入框旁边有一个浏览按钮。
文件上传过程中,用fiddler抓取刚才的包可以得到下面消息:
提取我们关心的内容则是:
Content-Type:在Content-Disposition:后面是为了说明表单中某一项传输的内容格式。比如,我此次上传的是一个图片文件test.png。所以它的值是image/png.如果我上传的是一个文本文件text.txt.则它的值是text/plain。
常见的Content-type值如下:
* .*(二进制文件,不知道格式的文件) application/octet-stream
* .txt -> text/plain
* .png -> image/png
* .jpg -> image/jpeg
* .html -> text/html
* .mp4 -> video/mpeg
* .apk -> application/vnd.android.package-archive
2.2 okhttp上传文件
清楚了上传的原理与流程,我们就可以用okhttp来模拟表单发送消息,从而达到上传文件的目的。
我们再把思路捋一捋。
1. 用http协议。
2. 添加相应的header.这里指Content-type:multipart/form-data
3. 在表单项的实体中添加对应的内容描述。Content-Disposition:form-data; name=”file”; filename=”test.png”和Content-Type: image/png在这里Content-Type:application/octet-stream的话可以传输任何文件。
4. 添加具体的实体数据。
这里运用了一个知识点MultiPartBody.
MuiltipartBody.Builder可以构建一个html的文件上传表单这样的复杂的网络请求消息实体(request body).
注意它能够构造复杂的消息实体。复杂在于它包含的内容也可以由RequestBody构成,在Okhttp中称为Part.
如我可以同时发送一段文本、一张图片、一个Mp4文件给服务器,它们被MultipartBody封装在同一个表单,然后进行post请求。
每一个part,也就是每一个实体都可以定义自己的headers。目前,这些被包含在form-data中的消息实体应该有描述了Content-Disosition的header。当然,Okhttp会自动添加它的Content-Length属性。
下面是精简的MultipartBody源码:
代码不多。主要使用步骤。
1. new MultiPartBody.Builder()创建Builder对象。
2. addPart()或者addFormDataPart()添加文件或者是表单数据。
3. 然后调用build()方法生成MultiPartBody对象。
4. 调用Requst对象的post()方法,访问远程服务。
3.拦截器(Interceptors)
拦截器是一个强大的机制,它能对Call进行监测、改写、重试连接。它能够对请求和回复进行二次加工。
3.1 Log拦截器
下面是官网的一个例子。
这段代码的主要功能是打印Request和Response的相关信息,比如url地址,比如路由,比如headers.
下面我们来编写代码测试一下。
在编写代码之前,先介绍两个概念就是应用拦截器(Application Interceptors)和网络拦截器(Network Interceptors).
Interceptors要么被当成Application Interceptors注册,要么被当成Network Interceptors注册。
在请求中人为中添加了一个头信息,结果Log信息如下:
可以看到它详细打印了Request和Response的的一些信息。
而log信息,也是完全一样的。说到这里大家可能有些迷惑。其实是这样的,NetworkInterceptor比Application打印更详尽的信息。我举的例子中http://blog.csdn.net/briblue没有进行重定向。如果我把上面例子中的url换成是http:www.github.com来进行测试的话。情况大有不同。
可以看到我们原本是访问http://www.github.com,但是它内部重定向到https://www.github.com.
但是添加NetworkInterceptor追踪到了它的状态。并且NetworkInterceptror能够打印的信息更多。比如Content-Encoding:gzip。
Application interceptor特点
不必关心url的重定向和重连。
只执行一次,即使Resopnse是来自于缓存。
只关心request的原始意图,而不用关心额外添加的Header信息如If-None-Match
NetworkInterceptor的特点
能够详尽地追踪访问链接的重定向。
短时间内的网络访问,它将不执行缓存过来的回应。
监测整个网络访问过程中的数据流向。
实际开发中,大家可以根据自己的需求添加相应的Interceptor.
拦截器作用之下载进度
通过OkHttp Interceptor可以监听下载文件的进度
public class ProgressDownLoadFile {
private static final OkHttpClient client = new OkHttpClient();
public static void main(String[] args) throws IOException {
//监听下载进度
final ProgressListener progressListener = new ProgressListener() {
@Override public void update(long bytesRead, long contentLength, boolean done) {
System.out.format("%d%% done\n", (100 * bytesRead) / contentLength);
}
};
//添加拦截器,自定义ResponseBody,添加下载进度
client.networkInterceptors().add(new Interceptor() {
@Override public Response intercept(Chain chain) throws IOException {
Response originalResponse = chain.proceed(chain.request());
return originalResponse.newBuilder().body(
new ProgressResponseBody(originalResponse.body(), progressListener))
.build();
}
});
//封装请求
Request request = new Request.Builder()
//下载地址
.url("http://img.pconline.com.cn/images/upload/upc/tx/wallpaper/1308/15/c2/24494083_1376530583817.jpg")
.build();
//发送异步请求
client.newCall(request).enqueue(new Callback() {
@Override public void onFailure(Request request, IOException e) {
System.out.println("failure");
}
@Override public void onResponse(Response response) throws IOException {
//将返回结果转化为流,并写入文件
int len;
byte[] buf = new byte[2048];
InputStream inputStream = response.body().byteStream();
//可以在这里自定义路径
File file1 = new File("d:\\file.jpg");
FileOutputStream fileOutputStream = new FileOutputStream(file1);
while ((len = inputStream.read(buf)) != -1) {
fileOutputStream.write(buf, 0, len);
}
fileOutputStream.flush();
fileOutputStream.close();
inputStream.close();
}
});
}
/**
* 添加进度监听的ResponseBody
*/
private static class ProgressResponseBody extends ResponseBody {
private final ResponseBody responseBody;
private final ProgressListener progressListener;
private BufferedSource bufferedSource;
public ProgressResponseBody(ResponseBody responseBody, ProgressListener progressListener) {
this.responseBody = responseBody;
this.progressListener = progressListener;
}
@Override public MediaType contentType() {
return responseBody.contentType();
}
@Override public long contentLength() throws IOException {
return responseBody.contentLength();
}
@Override public BufferedSource source() throws IOException {
if (bufferedSource == null) {
bufferedSource = Okio.buffer(source(responseBody.source()));
}
return bufferedSource;
}
private Source source(Source source) {
return new ForwardingSource(source) {
long totalBytesRead = 0L;
@Override public long read(Buffer sink, long byteCount) throws IOException {
long bytesRead = super.read(sink, byteCount);
totalBytesRead += bytesRead != -1 ? bytesRead : 0;
progressListener.update(totalBytesRead, responseBody.contentLength(), bytesRead == -1);
return bytesRead;
}
};
}
}
interface ProgressListener {
/**
* @param bytesRead 已下载字节数
* @param contentLength 总字节数
* @param done 是否下载完成
*/
void update(long bytesRead, long contentLength, boolean done);
}
}4.上传进度
重写RequestBody中的contentLength方法获取文件的长度,重写writeTo方法获取写入的字节数来实现
import com.squareup.okhttp.*;
import okio.Buffer;
import okio.BufferedSink;
import okio.Okio;
import okio.Source;
import java.io.File;
import java.io.IOException;
public class ProgressUploadFile {
private static final OkHttpClient okHttpClient = new OkHttpClient();
private void run() {
MultipartBuilder builder = new MultipartBuilder().type(MultipartBuilder.FORM);
File file = new File("D:\\file.jpg");
builder.addFormDataPart("file", file.getName(), createCustomRequestBody(MultipartBuilder.FORM, file, new ProgressListener() {
@Override public void onProgress(long totalBytes, long remainingBytes, boolean done) {
System.out.print((totalBytes - remainingBytes) * 100 / totalBytes + "%");
}
}));
RequestBody requestBody = builder.build();
Request request = new Request.Builder()
.url("http://localhost:8080/upload") //地址
.post(requestBody)
.build();
okHttpClient.newCall(request).enqueue(new Callback() {
@Override public void onFailure(Request request, IOException e) {
}
@Override public void onResponse(Response response) throws IOException {
System.out.println("response.body().string() = " + response.body().string());
}
});
}
public static RequestBody createCustomRequestBody(final MediaType contentType, final File file, final ProgressListener listener) {
return new RequestBody() {
@Override public MediaType contentType() {
return contentType;
}
@Override public long contentLength() {
return file.length();
}
@Override public void writeTo(BufferedSink sink) throws IOException {
Source source;
try {
source = Okio.source(file);
//sink.writeAll(source);
Buffer buf = new Buffer();
Long remaining = contentLength();
for (long readCount; (readCount = source.read(buf, 2048)) != -1; ) {
sink.write(buf, readCount);
listener.onProgress(contentLength(), remaining -= readCount, remaining == 0);
}
} catch (Exception e) {
e.printStackTrace();
}
}
};
}
interface ProgressListener {
void onProgress(long totalBytes, long remainingBytes, boolean done);
}
public static void main(String[] args) {
new ProgressUploadFile().run();
}
}
在ResponseBody中有如下接口:
byte()
string()
bytesStream()
charStream()
其中byte()和string()是一次读取,用来获取体积比较小的内容。但如果遇到大文件的话,就应该用流的方式。
所谓下载也就是将服务器返回的数据存储在本地。
当体积体积较小时,用byte()或者string()获取内容。
当体积很大时(超过1m),就应该用流的方式,用byteStream()或者charStream().
这里我用流的方式演示从网络上下载一张图片,然后保存在本地,然后显示出来。我是用bytesStream()方法。
private void testDownload(){ //网络上的一张图 String url = "http://img4.cache.netease.com/photo/0026/2015-05-19/APVC513454A40026.jpg"; //图片下载时保存的地址 final File filePath = new File(getExternalCacheDir().toString(),"tmp.jpg"); OkHttpClient client = new OkHttpClient(); Request request = new Request.Builder().url(url).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 { if(response.isSuccessful()){ //获取inputstream对象 InputStream is = response.body().byteStream(); FileOutputStream fos = new FileOutputStream(filePath); int b = 0; while((b = is.read()) != -1){ fos.write(b); } fos.close(); //下载成功后加载图片 final Bitmap bitmap = BitmapFactory.decodeFile(filePath.getAbsolutePath()); runOnUiThread(new Runnable() { @Override public void run() { mImg.setImageBitmap(bitmap); } }); } //关掉response.body response.body().close(); } }); }
2.上传文件
html代码
<html> <head> <title>test upload</title> </head> <body> <form action="upload_file.php" method="post" enctype="multipart/form-data"> <label>filename:</label> <input type="file" name="file" id="file" /> <br/> <input type="submit" name="submit" value="submit"> </form> </body> </html>
action定义到表单发送的位置,这里是upload_file.php,说明表单将会发送到主机上的upload_file.php上。
method 中的方法是post。这个一定要写对,文件上传的内容必须放在实体中,不能添加在header中,所以不能用get,要用post.
enctype,这个定义内容是 multipart/form-data.
enctype有两个值的范围。
* 一个是application/x-www-form-urlencoded(默认值),传输文本信息
* 一个是multipart/form-data.传输二进制信息
标签的 type=”file” 属性规定了应该把输入作为文件来处理。举例来说,当在浏览器中预览时,会看到输入框旁边有一个浏览按钮。
文件上传过程中,用fiddler抓取刚才的包可以得到下面消息:
POST http://localhost/upload_file.php HTTP/1.1 Host: localhost User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:27.0) Gecko/20100101 Firefox/27.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: zh-cn,zh;q=0.8,en-us;q=0.5,en;q=0.3 Accept-Encoding: gzip, deflate Referer: http://localhost/upload.html Connection: keep-alive Content-Type: multipart/form-data; boundary=---------------------------178612565028255 Content-Length: 11185 -----------------------------178612565028255 Content-Disposition: form-data; name="file"; filename="test.png" Content-Type: image/png PNG IHDR ) sRGB gAMA a pHYs o d *"IDATx^ a'p kf? Y#AJT 8 P V nd # Gl p D B^ +Z W $.- * B AG KI C; ~ [ G 7 t `z `z `z `z `z `z `z `z w E t s P IEND B` -----------------------------178612565028255 Content-Disposition: form-data; name="submit" submit -----------------------------178612565028255--
提取我们关心的内容则是:
Content-Type: multipart/form-data; boundary=---------------------------178612565028255 Content-Length: 11185 -----------------------------178612565028255 Content-Disposition: form-data; name="file"; filename="test.png" Content-Type: image/png PNG IHDR ) sRGB gAMA a pHYs o d *"IDATx^ a'p kf? Y#AJT 8 P V nd # Gl p D B^ +Z W $.- * B AG KI C; ~ [ G 7 t `z `z `z `z `z `z `z `z w E t s P IEND B`
Content-Type: multipart/form-data;
在这里可以看到Content-Type果真是multipart/form-data,而后面的boundary=—————————178612565028255,boundary是分界线的意思,后面的数字是随机数,用来分割实体。比如我上传了一个文本一张图片,它们之间就用这个来分割区别开来。Content-Disposition
disposition的英文单词是配置的意思,在这里用来区分表单的内容,因为一个表单中有许多项,这里为了说明这一段属于哪一项。name=”file”是因为之前我的标签中定义了name=”file”。filename=”test.png”代表我此次上传的文件名字为test.png.Content-Type:在Content-Disposition:后面是为了说明表单中某一项传输的内容格式。比如,我此次上传的是一个图片文件test.png。所以它的值是image/png.如果我上传的是一个文本文件text.txt.则它的值是text/plain。
常见的Content-type值如下:
* .*(二进制文件,不知道格式的文件) application/octet-stream
* .txt -> text/plain
* .png -> image/png
* .jpg -> image/jpeg
* .html -> text/html
* .mp4 -> video/mpeg
* .apk -> application/vnd.android.package-archive
2.2 okhttp上传文件
清楚了上传的原理与流程,我们就可以用okhttp来模拟表单发送消息,从而达到上传文件的目的。
我们再把思路捋一捋。
1. 用http协议。
2. 添加相应的header.这里指Content-type:multipart/form-data
3. 在表单项的实体中添加对应的内容描述。Content-Disposition:form-data; name=”file”; filename=”test.png”和Content-Type: image/png在这里Content-Type:application/octet-stream的话可以传输任何文件。
4. 添加具体的实体数据。
private void testUpload(){ //这个地址是我PC机上的IP地址,我用的是genimotion模拟器,如果用手机要保证手机和pc在同一个局域网内 String url = "http://172.26.133.50//upload_file.php"; File file = new File(Environment.getExternalStorageDirectory().toString(),"test.png"); //创建Okhttp客户端 OkHttpClient client = new OkHttpClient(); //定义MIME类型 MediaType mediaType = MediaType.parse("application/octet-stream"); //创建代表文件的实体 RequestBody fileBody = RequestBody.create(mediaType,file); //创建表单实体,并把文件实体添加到表单实体当中 RequestBody requestBody = new MultipartBody.Builder() .setType(MultipartBody.FORM) .addPart(Headers.of("Content-Disposition","form-data;name=\"file\";" + "filename=\"test.png\""),fileBody) 4000 .build(); //创建request对象,并把实体以Post方式发送给服务器 Request request = new Request.Builder() .url(url) .post(requestBody) .build(); //创建Call对象。 final Call call = client.newCall(request); new Thread(new Runnable() { @Override public void run() { try { Response response = call.execute(); if(response.isSuccessful()){ Log.d(TAG, "run: upload is successed."); }else{ Log.d(TAG, "run: upload is failed "); } response.close(); } catch (IOException e) { e.printStackTrace(); Log.e(TAG, "run: "+e.getLocalizedMessage() ); } } }).start(); }
这里运用了一个知识点MultiPartBody.
MuiltipartBody.Builder可以构建一个html的文件上传表单这样的复杂的网络请求消息实体(request body).
注意它能够构造复杂的消息实体。复杂在于它包含的内容也可以由RequestBody构成,在Okhttp中称为Part.
如我可以同时发送一段文本、一张图片、一个Mp4文件给服务器,它们被MultipartBody封装在同一个表单,然后进行post请求。
每一个part,也就是每一个实体都可以定义自己的headers。目前,这些被包含在form-data中的消息实体应该有描述了Content-Disosition的header。当然,Okhttp会自动添加它的Content-Length属性。
下面是精简的MultipartBody源码:
public final class MultipartBody extends RequestBody { public static final MediaType MIXED = MediaType.parse("multipart/mixed"); public static final MediaType ALTERNATIVE = MediaType.parse("multipart/alternative"); public static final MediaType DIGEST = MediaType.parse("multipart/digest"); public static final MediaType PARALLEL = MediaType.parse("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.parse("multipart/form-data"); private final ByteString boundary; private final MediaType originalType; private final MediaType contentType; private final List<Part> parts; private long contentLength = -1L; MultipartBody(ByteString boundary, MediaType type, List<Part> parts) { this.boundary = boundary; this.originalType = type; this.contentType = MediaType.parse(type + "; boundary=" + boundary.utf8()); this.parts = Util.immutableList(parts); } public List<Part> parts() { return parts; } public Part part(int index) { return parts.get(index); } /** A combination of {@link #type()} and {@link #boundary()}. */ @Override public MediaType contentType() { return contentType; } @Override public long contentLength() throws IOException { long result = contentLength; if (result != -1L) return result; return contentLength = writeOrCountBytes(null, true); } //Part是它的内部静态类 public static final class Part { public static Part create(RequestBody body) { return create(null, body); } private final Headers headers; private final RequestBody body; private Part(Headers headers, RequestBody body) { this.headers = headers; this.body = body; } } //MultiPartBody通过内部的Builder对象构建 public static final class Builder { private final ByteString boundary; private MediaType type = MIXED; private final List<Part> parts = new ArrayList<>(); public Builder() { this(UUID.randomUUID().toString()); } public Builder(String boundary) { this.boundary = ByteString.encodeUtf8(boundary); } /** * Set the MIME type. Expected values for {@code type} are {@link #MIXED} (the default), {@link * #ALTERNATIVE}, {@link #DIGEST}, {@link #PARALLEL} and {@link #FORM}. */ public Builder setType(MediaType type) { if (type == null) { throw new NullPointerException("type == null"); } if (!type.type().equals("multipart")) { throw new IllegalArgumentException("multipart != " + type); } this.type = type; return this; } /** Add a part to the body. */ public Builder addPart(RequestBody body) { return addPart(Part.create(body)); } /** Add a part to the body. */ public Builder addPart(Headers headers, RequestBody body) { return addPart(Part.create(headers, body)); } /** Add a form data part to the body. */ public Builder addFormDataPart(String name, String value) { return addPart(Part.createFormData(name, value)); } /** Add a form data part to the body. */ public Builder addFormDataPart(String name, String filename, RequestBody body) { return addPart(Part.createFormData(name, filename, body)); } /** Add a part to the body. */ public Builder addPart(Part part) { if (part == null) throw new NullPointerException("part == null"); parts.add(part); return this; } /** Assemble the specified parts into a request body. */ public MultipartBody build() { if (parts.isEmpty()) { throw new IllegalStateException("Multipart body must have at least one part."); } return new MultipartBody(boundary, type, parts); } } }
代码不多。主要使用步骤。
1. new MultiPartBody.Builder()创建Builder对象。
2. addPart()或者addFormDataPart()添加文件或者是表单数据。
3. 然后调用build()方法生成MultiPartBody对象。
4. 调用Requst对象的post()方法,访问远程服务。
3.拦截器(Interceptors)
拦截器是一个强大的机制,它能对Call进行监测、改写、重试连接。它能够对请求和回复进行二次加工。
3.1 Log拦截器
下面是官网的一个例子。
class LoggingInterceptor implements Interceptor { @Override public Response intercept(Interceptor.Chain chain) throws IOException { Request request = chain.request(); long t1 = System.nanoTime(); Log.i(TAG, "intercept: "+String.format("Sending request %s on %s%n%s", request.url(), chain.connection(), request.headers())); Response response = chain.proceed(request); long t2 = System.nanoTime(); Log.i(TAG, "intercept: "+String.format("Received response for %s in %.1fms%n%s", response.request().url(), (t2 - t1) / 1e6d, response.headers())); return response; } }
这段代码的主要功能是打印Request和Response的相关信息,比如url地址,比如路由,比如headers.
下面我们来编写代码测试一下。
在编写代码之前,先介绍两个概念就是应用拦截器(Application Interceptors)和网络拦截器(Network Interceptors).
Interceptors要么被当成Application Interceptors注册,要么被当成Network Interceptors注册。
应用拦截器(Application Interceptors)
如果当成应用拦截器添加的话,那么要在OkHttpClient.Builder中的addInterceptors()方法中添加。private void testInterceptors(){ OkHttpClient client = new OkHttpClient.Builder() .addInterceptor(new LoggingInterceptor()) .build(); String url = "http://blog.csdn.net/briblue"; Request request = new Request.Builder() .url(url) .addHeader("name","frank") .build(); final Call call = client.newCall(request); new Thread(new Runnable() { @Override public void run() { try { Response response = call.execute(); response.body().close(); } catch (IOException e) { e.printStackTrace(); } } }).start(); }
在请求中人为中添加了一个头信息,结果Log信息如下:
I/SeniorActivity: intercept: Sending request http://blog.csdn.net/briblue on null name: frank I/SeniorActivity: intercept: Received response for http://blog.csdn.net/briblue in 164.0ms Server: openresty Date: Mon, 24 Oct 2016 04:03:57 GMT Content-Type: text/html; charset=utf-8 Transfer-Encoding: chunked Connection: keep-alive Keep-Alive: timeout=20 Vary: Accept-Encoding Cache-Control: private Set-Cookie: uuid=1ffd7e7e-6bed-41c3-b48c-f61f915f02f8; expires=Tue, 25-Oct-2016 04:03:57 GMT; path=/ X-Powered-By: PHP 5.4.28
可以看到它详细打印了Request和Response的的一些信息。
网络拦截器(Network Interceptors)
上面讲了一个拦截器被当成Application Interceptors注册到了Okhttpclient.同时,一个拦截器也可以当成Netowork Interceptors注册到Okhttpclient.调用的是它的Builder对象的addNetworkInterceptor.代码区别不大。private void testNetworkInterceptors(){ OkHttpClient client = new OkHttpClient.Builder() //不同于Application Interceptor中addInterceptor() .addNetworkInterceptor(new LoggingInterceptor()) .build(); String url = "http://blog.csdn.net/briblue"; Request request = new Request.Builder() .url(url) .addHeader("name","frank") .build(); final Call call = client.newCall(request); new Thread(new Runnable() { @Override public void run() { try { Response response = call.execute(); response.body().close(); } catch (IOException e) { e.printStackTrace(); } } }).start(); }
而log信息,也是完全一样的。说到这里大家可能有些迷惑。其实是这样的,NetworkInterceptor比Application打印更详尽的信息。我举的例子中http://blog.csdn.net/briblue没有进行重定向。如果我把上面例子中的url换成是http:www.github.com来进行测试的话。情况大有不同。
I/SeniorActivity: intercept: Sending request http://www.github.com/ on Connection{www.github.com:80, proxy=DIRECT hostAddress=www.github.com/192.30.253.113:80 cipherSuite=none protocol=http/1.1} name: frank host: www.github.com Connection: Keep-Alive Accept-Encoding: gzip User-Agent: okhttp/3.4.1 I/SeniorActivity: intercept: Received response for http://www.github.com/ in 1040.4ms Content-length: 0 Location: https://www.github.com/ Connection: close I/SeniorActivity: intercept: Sending request https://www.github.com/ on Connection{www.github.com:443, proxy=DIRECT hostAddress=www.github.com/192.30.253.113:443 cipherSuite=TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 protocol=http/1.1} name: frank Host: www.github.com Connection: Keep-Alive Accept-Encoding: gzip User-Agent: okhttp/3.4.1 I/SeniorActivity: intercept: Received response for https://www.github.com/ in 703.6ms Content-length: 0 Location: https://github.com/ Connection: close I/SeniorActivity: intercept: Sending request https://github.com/ on Connection{github.com:443, proxy=DIRECT hostAddress=github.com/192.30.253.113:443 cipherSuite=TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 protocol=http/1.1} name: frank Host: github.com Connection: Keep-Alive Accept-Encoding: gzip User-Agent: okhttp/3.4.1 I/SeniorActivity: intercept: Received response for https://github.com/ in 524.6ms Server: GitHub.com Date: Mon, 24 Oct 2016 06:17:32 GMT Content-Type: text/html; charset=utf-8 Transfer-Encoding: chunked Status: 200 OK Cache-Control: no-cache Vary: X-PJAX X-UA-Compatible: IE=Edge,chrome=1 Set-Cookie: logged_in=no; domain=.github.com; path=/; expires=Fri, 24 Oct 2036 06:17:32 -0000; secure; HttpOnly Set-Cookie: _gh_sess=eyJzZXNzaW9uX2lkIjoiNWJhMWZkYTUyYjFhMmVmZWM1OTc5ZTUzNGE0ZTQ4OTMiLCJfY3NyZl90b2tlbiI6IlZXNGVTTVhSRHhiOVlWL2E5anEwSHFxYXBOcFd3OHFvcHJtZjI5c05FOVk9In0%3D--64b03a16a3f1bf8b15208b101702a02a0419d9ec; path=/; secure; HttpOnly X-Request-Id: 9eaae01ca7d4e264de21da3d1a1c36e7 X-Runtime: 0.010049 Content-Security-Policy: default-src 'none'; base-uri 'self'; block-all-mixed-content; child-src render.githubusercontent.com; connect-src 'self' uploads.github.com status.github.com api.github.com www.google-analytics.com github-cloud.s3.amazonaws.com wss://live.github.com; font-src assets-cdn.github.com; form-action 'self' github.com gist.github.com; frame-ancestors 'none'; frame-src render.githubusercontent.com; img-src 'self' data: assets-cdn.github.com identicons.github.com collector.githubapp.com github-cloud.s3.amazonaws.com *.githubusercontent.com; media-src 'none'; script-src assets-cdn.github.com; style-src 'unsafe-inline' assets-cdn.github.com Strict-Transport-Security: max-age=31536000; includeSubdomains; preload Public-Key-Pins: max-age=5184000; X-Content-Type-Options: nosniff X-Served-By: a22dbcbd09a98eacdd14ac7804a635dd Content-Encoding: gzip X-GitHub-Request-Id: 0E17AF3E:6C7B:67BF0A4:580DA77B
可以看到我们原本是访问http://www.github.com,但是它内部重定向到https://www.github.com.
但是添加NetworkInterceptor追踪到了它的状态。并且NetworkInterceptror能够打印的信息更多。比如Content-Encoding:gzip。
Application interceptor特点
不必关心url的重定向和重连。
只执行一次,即使Resopnse是来自于缓存。
只关心request的原始意图,而不用关心额外添加的Header信息如If-None-Match
NetworkInterceptor的特点
能够详尽地追踪访问链接的重定向。
短时间内的网络访问,它将不执行缓存过来的回应。
监测整个网络访问过程中的数据流向。
实际开发中,大家可以根据自己的需求添加相应的Interceptor.
拦截器作用之压缩数据
因为拦截器可以拿到请求的数据,和回应的数据,所以基本上它能做任何事。比如我们可以在这里拦截一些不符合特定场景的请求。比如我们可以在回应中校验数据的完整性。比如为了节省带宽,我们可以将数据进行gzip压缩进行数据发送,然后在Response中解压,一切都神不知鬼不觉的。下面的例子来自官网,讲得是一个如何定义一个压缩数据功能的拦截器。/** This interceptor compresses the HTTP request body. Many webservers can't handle this! */ final class GzipRequestInterceptor implements Interceptor { @Override public Response intercept(Interceptor.Chain chain) throws IOException { Request originalRequest = chain.request(); if (originalRequest.body() == null || originalRequest.header("Content-Encoding") != null) { return chain.proceed(originalRequest); } Request compressedRequest = originalRequest.newBuilder() .header("Content-Encoding", "gzip") .method(originalReq ce05 uest.method(), gzip(originalRequest.body())) .build(); return chain.proceed(compressedRequest); } private RequestBody gzip(final RequestBody body) { return new RequestBody() { @Override public MediaType contentType() { return body.contentType(); } @Override public long contentLength() { return -1; // We don't know the compressed length in advance! } @Override public void writeTo(BufferedSink sink) throws IOException { BufferedSink gzipSink = Okio.buffer(new GzipSink(sink)); body.writeTo(gzipSink); gzipSink.close(); } }; } }
拦截器作用之下载进度
通过OkHttp Interceptor可以监听下载文件的进度
public class ProgressDownLoadFile {
private static final OkHttpClient client = new OkHttpClient();
public static void main(String[] args) throws IOException {
//监听下载进度
final ProgressListener progressListener = new ProgressListener() {
@Override public void update(long bytesRead, long contentLength, boolean done) {
System.out.format("%d%% done\n", (100 * bytesRead) / contentLength);
}
};
//添加拦截器,自定义ResponseBody,添加下载进度
client.networkInterceptors().add(new Interceptor() {
@Override public Response intercept(Chain chain) throws IOException {
Response originalResponse = chain.proceed(chain.request());
return originalResponse.newBuilder().body(
new ProgressResponseBody(originalResponse.body(), progressListener))
.build();
}
});
//封装请求
Request request = new Request.Builder()
//下载地址
.url("http://img.pconline.com.cn/images/upload/upc/tx/wallpaper/1308/15/c2/24494083_1376530583817.jpg")
.build();
//发送异步请求
client.newCall(request).enqueue(new Callback() {
@Override public void onFailure(Request request, IOException e) {
System.out.println("failure");
}
@Override public void onResponse(Response response) throws IOException {
//将返回结果转化为流,并写入文件
int len;
byte[] buf = new byte[2048];
InputStream inputStream = response.body().byteStream();
//可以在这里自定义路径
File file1 = new File("d:\\file.jpg");
FileOutputStream fileOutputStream = new FileOutputStream(file1);
while ((len = inputStream.read(buf)) != -1) {
fileOutputStream.write(buf, 0, len);
}
fileOutputStream.flush();
fileOutputStream.close();
inputStream.close();
}
});
}
/**
* 添加进度监听的ResponseBody
*/
private static class ProgressResponseBody extends ResponseBody {
private final ResponseBody responseBody;
private final ProgressListener progressListener;
private BufferedSource bufferedSource;
public ProgressResponseBody(ResponseBody responseBody, ProgressListener progressListener) {
this.responseBody = responseBody;
this.progressListener = progressListener;
}
@Override public MediaType contentType() {
return responseBody.contentType();
}
@Override public long contentLength() throws IOException {
return responseBody.contentLength();
}
@Override public BufferedSource source() throws IOException {
if (bufferedSource == null) {
bufferedSource = Okio.buffer(source(responseBody.source()));
}
return bufferedSource;
}
private Source source(Source source) {
return new ForwardingSource(source) {
long totalBytesRead = 0L;
@Override public long read(Buffer sink, long byteCount) throws IOException {
long bytesRead = super.read(sink, byteCount);
totalBytesRead += bytesRead != -1 ? bytesRead : 0;
progressListener.update(totalBytesRead, responseBody.contentLength(), bytesRead == -1);
return bytesRead;
}
};
}
}
interface ProgressListener {
/**
* @param bytesRead 已下载字节数
* @param contentLength 总字节数
* @param done 是否下载完成
*/
void update(long bytesRead, long contentLength, boolean done);
}
}4.上传进度
重写RequestBody中的contentLength方法获取文件的长度,重写writeTo方法获取写入的字节数来实现
import com.squareup.okhttp.*;
import okio.Buffer;
import okio.BufferedSink;
import okio.Okio;
import okio.Source;
import java.io.File;
import java.io.IOException;
public class ProgressUploadFile {
private static final OkHttpClient okHttpClient = new OkHttpClient();
private void run() {
MultipartBuilder builder = new MultipartBuilder().type(MultipartBuilder.FORM);
File file = new File("D:\\file.jpg");
builder.addFormDataPart("file", file.getName(), createCustomRequestBody(MultipartBuilder.FORM, file, new ProgressListener() {
@Override public void onProgress(long totalBytes, long remainingBytes, boolean done) {
System.out.print((totalBytes - remainingBytes) * 100 / totalBytes + "%");
}
}));
RequestBody requestBody = builder.build();
Request request = new Request.Builder()
.url("http://localhost:8080/upload") //地址
.post(requestBody)
.build();
okHttpClient.newCall(request).enqueue(new Callback() {
@Override public void onFailure(Request request, IOException e) {
}
@Override public void onResponse(Response response) throws IOException {
System.out.println("response.body().string() = " + response.body().string());
}
});
}
public static RequestBody createCustomRequestBody(final MediaType contentType, final File file, final ProgressListener listener) {
return new RequestBody() {
@Override public MediaType contentType() {
return contentType;
}
@Override public long contentLength() {
return file.length();
}
@Override public void writeTo(BufferedSink sink) throws IOException {
Source source;
try {
source = Okio.source(file);
//sink.writeAll(source);
Buffer buf = new Buffer();
Long remaining = contentLength();
for (long readCount; (readCount = source.read(buf, 2048)) != -1; ) {
sink.write(buf, readCount);
listener.onProgress(contentLength(), remaining -= readCount, remaining == 0);
}
} catch (Exception e) {
e.printStackTrace();
}
}
};
}
interface ProgressListener {
void onProgress(long totalBytes, long remainingBytes, boolean done);
}
public static void main(String[] args) {
new ProgressUploadFile().run();
}
}
相关文章推荐
- springMVC高级部分(数据校验,数据错误回显(自定义格式错误显示),拦截器,异常处理,文件上传,文件下载,springmvc运行流程以及springmvc和struts2对比)
- OkHttp(get,post,拦截器,上传,下载)
- Struts2.0实现的文件上传(单附件和多附件)以及附件下载功能
- Struts2.0实现的文件上传(单附件和多附件)以及附件下载功能
- 使用linux的Proftpd创建FTP服务器时的一些高级设置(开放续上传,续下载功能)
- Struts2.0实现的文件上传(单附件和多附件)以及附件下载功能
- IPFS + 区块链 系列】 入门篇 - IPFS + Ethereum (中篇)-js-ipfs-api - 图片上传到IPFS以及下载
- nodejs ssh2基本功能封装,实现上传、下载文件以及文件夹
- 微信JS图片上传与下载功能--微信JS系列文章(三)
- RxJava+RxAndroid+OKHTTP实现get post 以及下载图片功能
- 用Struts2更好的实现文件的上传、下载功能以及解决中文名称问题
- mybatis高级应用系列一:拦截器实现分页功能
- OKHttp3的工具类(拦截器,下载,上传)
- Spring MVC的文件上传和下载以及拦截器的使用实例
- Android高效率编码-第三方SDK详解系列(二)——Bmob后端云开发,实现登录注册,更改资料,修改密码,邮箱验证,上传,下载,推送消息,缩略图加载等功能
- struts2实现图片的上传以及下载功能
- Struts2.0实现的文件上传(单附件和多附件)以及附件下载功能
- OkHttp基本使用(三)上传下载功能实现
- JavaWeb实现文件上传下载功能实例解析以及项目工程示例下载
- Android高效率编码-第三方SDK详解系列(二)——Bmob后端云开发,实现登录注册,更改资料,修改密码,邮箱验证,上传,下载,推送消息,缩略图加载等功能