1.普通文件的上传下载采用的是retrofit+rxjava的形式;
1.1 依赖
implementation 'com.squareup.retrofit2:retrofit:2.4.0'
implementation 'com.squareup.retrofit2:adapter-rxjava:2.4.0'
implementation 'com.squareup.retrofit2:converter-gson:2.4.0'
implementation 'io.reactivex:rxandroid:1.2.1'
1.2 retrofit接口
@GET("{filePath}")
Observable<ResponseBody> getFile(@Path("filePath") String filePath,
@Query("token") String token);
@Multipart
@POST(ApiConstants.FILE_UPLOAD_URL)
Observable<ResponseBody> uploadFile(@Part("type") RequestBody type,
@PartMap Map<String, RequestBody> file);
@Multipart
@POST(ApiConstants.FILE_UPLOAD_URL)
Observable<ResponseBody> uploadFile2(@Part("type") RequestBody type,
@Part MultipartBody.Part file);
上传文件使用@Multiparty和@post方式,参数可以使用@PartMap(可以上传多个文件)或@Part的MultipartBody来传输
1.3 传输
1.3.1 上传多个文件
Map<String, RequestBody> fileMap = new HashMap<>();
RequestBody photoRequestBody = RequestBody.create(MediaType.parse("application/octet-stream"), file1);
fileMap.put("file\";filename=\"" + file1.getName(), photoRequestBody);
RequestBody photoRequestBody2 = RequestBody.create(MediaType.parse("application/octet-stream"), file2);
fileMap.put("file\";filename=\"" + file2.getName(), photoRequestBody2);
使用RequestBody的create方法创建对象,并放入map中;
ApiClient.initService(INoticeDetailBiz.class,
ApiConstants.FILE_SERVER, listener)
.uploadFile(RequestBody.create(MediaType.parse("text/plain"),"nstest",
fileMap)
.compose(RxSchedulerUtils.normalSchedulersTransformer())
.subscribe(new Observer<ResponseBody>() {
});
上面代码就是rxjava的调用方式了,不再多说;
1.3.2 上传单个文件MultiPart.Part
RequestBody requestBody1 = RequestBody.create(MediaType.parse("application/octet-stream"), file1);
MultipartBody.Part partFile = MultipartBody.Part.createFormData("file", file1.getName(), requestBody1);
使用MultipartBody.Part.createFormData方法创建接口所需的类型
ApiClient.initService(INoticeDetailBiz.class,
ApiConstants.FILE_SERVER, listener)
.uploadFile2(RequestBody.create(MediaType.parse("text/plain"),"nstest",
partFile)
.compose(RxSchedulerUtils.normalSchedulersTransformer())
.subscribe(new Observer<ResponseBody>() {
});
同样的rxjava调用
1.4 ApiClient类
private static Converter.Factory gsonConverterFactory = GsonConverterFactory.create(new Gson());
private static CallAdapter.Factory rxJavaCallAdapterFactory = RxJavaCallAdapterFactory.create();
public static <T> T initService(Class<T> clazz, String baseUrl,
ProgressListener listener) {
OkHttpClient.Builder builder = new OkHttpClient().newBuilder();
builder.connectTimeout(connectionTime, TimeUnit.SECONDS);
builder.readTimeout(readTime, TimeUnit.SECONDS);
builder.writeTimeout(writeTime, TimeUnit.SECONDS);
builder.addNetworkInterceptor(new StethoInterceptor());
builder.addInterceptor(new ErrorInterceptor());
HttpLoggingInterceptor httpLoggingInterceptor;
if (BuildConfig.DEBUG) {
httpLoggingInterceptor = new HttpLoggingInterceptor();
} else {
httpLoggingInterceptor = new HttpLoggingInterceptor(
message -> logger.debug(message));
}
httpLoggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
builder.addInterceptor(httpLoggingInterceptor);
if (null != listener) {
if (listener.isDownload()) {
builder.addInterceptor(new DownloadInterceptor(listener));
} else {
builder.addInterceptor(new UploadInterceptor(listener));
}
}
Retrofit retrofit = new Retrofit.Builder()
.client(builder.build())
.baseUrl(baseUrl)
.addConverterFactory(gsonConverterFactory)
.addCallAdapterFactory(rxJavaCallAdapterFactory)
.build();
return retrofit.create(clazz);
}
在这里,发现了三个新对象
ProgressListener //监听进度的接口
DownloadInterceptor //下载的拦截器
UploadInterceptor //上传的拦截器
2. 上传和下载进度监听的实现
2.1 进度监听接口
public interface ProgressListener {
void onProgress(int currentLength);//当前的进度,已经是百分数了
boolean isDownload();//返回true表示下载,false表示上传
}
2.2 上传进度监听
上传拦截器
package com.dtrt.preventpro.myhttp.download;
import java.io.IOException;
import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;
public class UploadInterceptor implements Interceptor {
private ProgressListener listener;
public UploadInterceptor(ProgressListener listener) {
this.listener = listener;
}
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
Request build = request.newBuilder().post(new ProgressRequestBody(request.body(), listener)).build();
return chain.proceed(build);
}
}
ProgressRequestBody.java
package com.dtrt.preventpro.myhttp.download;
import java.io.IOException;
import okhttp3.MediaType;
import okhttp3.RequestBody;
import okhttp3.ResponseBody;
import okio.Buffer;
import okio.BufferedSink;
import okio.BufferedSource;
import okio.ForwardingSink;
import okio.ForwardingSource;
import okio.Okio;
import okio.Sink;
import okio.Source;
public class ProgressRequestBody extends RequestBody {
private final RequestBody requestBody;
private final ProgressListener progressListener;
private BufferedSink bufferedSink;
public ProgressRequestBody(RequestBody requestBody, ProgressListener progressListener) {
this.requestBody = requestBody;
this.progressListener = progressListener;
}
@Override
public MediaType contentType() {
return requestBody.contentType();
}
@Override
public long contentLength() throws IOException {
return requestBody.contentLength();
}
@Override
public void writeTo(BufferedSink sink) throws IOException {
if (bufferedSink == null) {
//包装
Sink sk = sink(sink);
bufferedSink = Okio.buffer(sk );
}
//写入
requestBody.writeTo(bufferedSink);
//必须调用flush,否则最后一部分数据可能不会被写入
bufferedSink.flush();
}
private Sink sink(Sink sink) {
return new ForwardingSink(sink) {
//当前写入字节数
long bytesWritten = 0L;
//总字节长度,避免多次调用contentLength()方法
long contentLength = 0L;
@Override
public void write(Buffer source, long byteCount) throws IOException {
super.write(source, byteCount);
if (contentLength == 0) {
//获得contentLength的值,后续不再调用
contentLength = contentLength();
}
//增加当前写入的字节数
bytesWritten += byteCount;
//回调
if (progressListener != null) {
progressListener.onProgress((int)(bytesWritten*1.0f/requestBody.contentLength()*100));
}
}
};
}
}
其实就是继承RequestBody,把进度监听器传进去,在writeTo()方法中对buffer进行操作;其中会把写入的字节数累加下来,然后算出百分数,通过进度接口回调回去;所以在上面代码中的rxjava调用里面传入了一个ProgressListener对象。
private ProgressListener uploadListener = new ProgressListener() {
@Override
public void onProgress(int currentLength) {
runOnUiThread(new Runnable() {
@Override
public void run() {
tv_progress.setText("正在上传文件..."+currentLength+"%");
}
});
}
@Override
public boolean isDownload() {
return false;
}
};
这是页面写更新进度的地方,把这个对象传给ApiClient方法后;就会自动添加需要的拦截器;拦截器监听到进度变化后,又会回调回来更新ui;注意:这里进度是回调在子线程的,需要发到主线程更新ui。
2.3 同理,我们来看下载进度监听
下载拦截器
package com.dtrt.preventpro.myhttp.download;
import java.io.IOException;
import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;
public class DownloadInterceptor implements Interceptor {
private ProgressListener downloadListener;
public DownloadInterceptor(ProgressListener downloadListener) {
this.downloadListener = downloadListener;
}
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
Response response = chain.proceed(request);
return response.newBuilder()
.body(new ProgressResponseBody(response.body(), downloadListener))
.build();
}
}
ProgressResponseBody.java
package com.dtrt.preventpro.myhttp.download;
import java.io.IOException;
import okhttp3.MediaType;
import okhttp3.ResponseBody;
import okio.Buffer;
import okio.BufferedSource;
import okio.ForwardingSource;
import okio.Okio;
import okio.Source;
public 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() {
return responseBody.contentLength();
}
@Override
public BufferedSource source() {
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;
if (progressListener != null) {
progressListener.onProgress((int)(totalBytesRead*1.0f/responseBody.contentLength()*100));
}
return bytesRead;
}
};
}
}
同理,继承ResponseBody,把进度监听器传进去,在source()方法中对buffer进行操作;其中会把读到的字节数累加下来,然后算出百分数,通过进度接口回调回去;所以在上面代码中的rxjava调用里面传入了一个ProgressListener对象。
private ProgressListener downloadListener = new ProgressListener() {
@Override
public void onProgress(int currentLength) {
runOnUiThread(new Runnable() {
@Override
public void run() {
tv_progress.setText("正在下载文件..."+currentLength+"%");
}
});
}
@Override
public boolean isDownload() {
return true;
}
};
这是页面写更新进度的地方,把这个对象传给ApiClient方法后;就会自动添加需要的拦截器;拦截器监听到进度变化后,又会回调回来更新ui;注意:这里进度是回调在子线程的,需要发到主线程更新ui。里面的isDownload()方法:下载时返回true,上传时返回false。
至此,对于普通的文件上传,下载以及进度监听就写完了;但我们的项目中要支持200M以下的大文件上传;通过上面的代码测试发现,华为手机能上传70M的文件,vivo手机只能上传50M的文件;不同手机能支持的上传大小还不一样;但超过10M的都属于大文件了。下面介绍一种大文件上传方式。
3. 大文件上传
网上查到可以使用apache的httpclient来上传,就来试试吧;
3.1 添加依赖
//httpclient上传大文件功能
implementation group: 'org.apache.httpcomponents.client5' , name: 'httpclient5' , version: '5.0'
implementation 'org.slf4j:slf4j-android:1.7.22'
这里用的时httpclient5。
3.2 上传
private void upload(File file, ProgressListener listener){
HttpClient client = HttpClientBuilder.create().build();
HttpPost httpPost = new HttpPost(ApiConstants.FILE_SERVER+ApiConstants.FILE_UPLOAD_URL);
try {
MultipartEntityBuilder multipartEntityBuilder = MultipartEntityBuilder.create();
String filesKey = "file";
multipartEntityBuilder.addPart(filesKey, new MyFileBody(file, new WriteListener() {
@Override
public void registerWrite(long amountOfBytesWritten) {
System.out.println("===========>"+amountOfBytesWritten);
listener.onProgress((int)(amountOfBytesWritten * 1.0f / notice.getFile().length() * 100));
}
}));
// 第二个文件(多个文件的话,使用同一个key就行,后端用数组或集合进行接收即可)
//File file2 = new File("C:\\Users\\JustryDeng\\Desktop\\头像.jpg");
// 防止服务端收到的文件名乱码。 我们这里可以先将文件名URLEncode,然后服务端拿到文件名时在URLDecode。就能避免乱码问题。
// 文件名其实是放在请求头的Content-Disposition里面进行传输的,如其值为form-data; name="files"; filename="头像.jpg"
//multipartEntityBuilder.addBinaryBody(filesKey, file2, ContentType.DEFAULT_BINARY, URLEncoder.encode(file2.getName(), "utf-8"));
// 其它参数(注:自定义contentType,设置UTF-8是为了防止服务端拿到的参数出现乱码)
ContentType contentType = ContentType.create("text/plain", Charset.forName("UTF-8"));
multipartEntityBuilder.addTextBody("type", "nstest", contentType);
HttpEntity httpEntity = multipartEntityBuilder.build();
httpPost.setEntity(httpEntity);
//这里可以配置超时时间等其它参数
//RequestConfig requestConfig = RequestConfig
// .custom()
// .setConnectionRequestTimeout(10, TimeUnit.MINUTES)
// .setConnectTimeout(10, TimeUnit.MINUTES)
// .setResponseTimeout(10, TimeUnit.MINUTES)
// .build();
//httpPost.setConfig(requestConfig);
CloseableHttpResponse response = (CloseableHttpResponse) client.execute(httpPost);
System.out.println("HTTPS响应状态为:" + response.getCode());
if (response.getCode() == 200 && response.getEntity() != null) {
System.out.println("HTTPS响应内容长度为:" + response.getEntity().getContentLength());
// 主动设置编码,来防止响应乱码
String responseStr = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
System.out.println("HTTPS响应内容为:" + responseStr);
if (!TextUtils.isEmpty(responseStr)) {
notice.setCodes(responseStr);
commit(notice);
}else {
//上传失败
}
}else {
//上传失败
}
} catch (Exception e) {
e.printStackTrace();
//上传失败
}
}
其实和上面的上传文件类似;就是把文件包装成httpclient里的对象;然后根据返回码判断上传是否成功。这里有篇文章写的很详细:https://blog.csdn.net/justry_deng/article/details/81042379
重点看这里:
multipartEntityBuilder.addPart(filesKey, new MyFileBody(notice.getFile(), new WriteListener() { @Override public void registerWrite(long amountOfBytesWritten) { System.out.println("===========>"+amountOfBytesWritten); listener.onProgress((int)(amountOfBytesWritten * 1.0f / notice.getFile().length() * 100)); } }));
当向Entity对象中天加文件时,使用了MyFileBody:
package com.dtrt.preventpro.myhttp.download;
import org.apache.hc.client5.http.entity.mime.FileBody;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
public class MyFileBody extends FileBody {
private WriteListener listener;
public MyFileBody(File file, WriteListener listener) {
super(file);
this.listener = listener;
}
@Override
public void writeTo(OutputStream out) throws IOException {
OutputStreamProgress output = new OutputStreamProgress(out, listener);
super.writeTo(output);
}
public interface WriteListener {
void registerWrite(long amountOfBytesWritten);
}
public class OutputStreamProgress extends OutputStream {
private final OutputStream outstream;
private long bytesWritten=0;
private final WriteListener writeListener;
public OutputStreamProgress(OutputStream outstream, WriteListener writeListener) {
this.outstream = outstream;
this.writeListener = writeListener;
}
@Override
public void write(int b) throws IOException {
outstream.write(b);
bytesWritten++;
writeListener.registerWrite(bytesWritten);
}
@Override
public void write(byte[] b) throws IOException {
outstream.write(b);
bytesWritten += b.length;
writeListener.registerWrite(bytesWritten);
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
outstream.write(b, off, len);
bytesWritten += len;
writeListener.registerWrite(bytesWritten);
}
@Override
public void flush() throws IOException {
outstream.flush();
}
@Override
public void close() throws IOException {
outstream.close();
}
}
}
原理:MyFileBody继承了FileBody,在writeTo()方法中源码接受输出流的时候,我们把它换成安插了监听器的输出流;此时源码调用到写操作的时候,就可以触发监听器,监听写入的进度了。
OutputStreamProgress继承自输出流OutputStream,把监听器传进去,重写每个write方法,在写入操作的时候把写入的字节累加下来,并触发监听器;这样就实现了,大文件的上传。
页面的进度更新和上面的一样。使用这种方式传输,我测试的结果是可以上传500M以下的文件,大于500M的会报一个Socket管道断裂的错误。不过已经足够了,毕竟是手机,大文件传输用得地方很少。