[番外]理一理Android多文件上传那点事
2018-11-26 本文已影响16人
e4e52c116681
多文件上传是客户端与服务端两个的事,客户端负责发送,服务端负责接收
我们都知道客户端与服务器只是通过http协议进行交流,那么http协议应该会对上传文件有所规范
你可以根据这些规范来自己拼凑请求头,可以用使用已经封装好的框架,如Okhttp3
一、先理一理表单点提交点的时候发生了什么?
1.客户端的请求(requst)
请求头会有:
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary5sGoxdCHIEYZKCMC
其中boundary=----WebKitFormBoundary5sGoxdCHIEYZKCMC
可看做是分界线
表单中的数据会和请求体对应,比如只有一个<input/>标签,里面是字符串
//===================描述String:<input type="text"/>==============
------WebKitFormBoundary5sGoxdCHIEYZKCMC
Content-Disposition:form-data;name="KeyName"
Content-Type: text/plain;charset="utf-8"
[String数据XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX]
------WebKitFormBoundary5sGoxdCHIEYZKCMC
比如只有一个<input type="file"/>标签,里面是二进制文件流:file stream
//===================描述file:<input type="file"/>================
------WebKitFormBoundary5sGoxdCHIEYZKCMC
Content-Disposition:form-data;name="KeyName";filename="xxx.xxx"
Content-Type: 对应MimeTypeMap
[file stream]
------WebKitFormBoundary5sGoxdCHIEYZKCMC
这便是客户端的请求
2.客户端的接收和处理
服务端会受到客户端的请求,然后根据指定格式对请求体进行解析
然后是文件你就可以在服务端保存,保存成功便是成功上传成功,下面是SpringBoot对上传的处理:
/**
* 多文件上传(包括一个)
*
* @param files 上传的文件
* @return 上传反馈信息
*/
@PostMapping(value = "/upload")
public @ResponseBody
ResultBean uploadImg(@RequestParam("file") List<MultipartFile> files) {
StringBuilder result = new StringBuilder();
for (MultipartFile file : files) {
if (file.isEmpty()) {
return ResultHandler.error("Upload Error");
}
String fileName = file.getOriginalFilename();//获取名字
String path = "F:/SpringBootFiles/imgs/";
File dest = new File(path + "/" + fileName);
if (!dest.getParentFile().exists()) { //判断文件父目录是否存在
dest.getParentFile().mkdir();
}
try {
file.transferTo(dest); //保存文件
result.append(fileName).append("上传成功!\n");
} catch (IllegalStateException | IOException e) {
e.printStackTrace();
result.append(fileName).append("上传失败!\n");
}
}
return ResultHandler.ok(result.toString());
}
所以文件上传,需要服务端和客户端的配合,缺一不可
二、okhttp模拟表单文件上传文件
1.单文件上传
单文件上传.png /**
* 模拟表单上传文件:通过MultipartBody
*/
private void doUpload() {
File file = new File(Environment.getExternalStorageDirectory(), "toly/ds4Android.apk");
RequestBody fileBody = RequestBody.create(MediaType.parse("application/octet-stream"), file);
//1.获取OkHttpClient对象
OkHttpClient okHttpClient = new OkHttpClient();
//2.获取Request对象
RequestBody requestBody = new MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("file", file.getName(), fileBody)
.build();
Request request = new Request.Builder()
.url(Cons.BASE_URL + "upload")
.post(requestBody).build();
//3.将Request封装为Call对象
Call call = okHttpClient.newCall(request);
//4.执行Call
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
Log.e(TAG, "onFailure: " + e);
}
@Override
public void onResponse(Call call, Response response) throws IOException {
String result = response.body().string();
Log.e(TAG, "onResponse: " + result);
runOnUiThread(() -> Toast.makeText(MainActivity.this, result, Toast.LENGTH_SHORT).show());
}
});
}
addFormDataPart源码跟踪
可见底层也是根据
Content-Disposition:form-data;name=XXX
来拼凑的请求体
/** Add a form data part to the body. */
public Builder addFormDataPart(String name, @Nullable String filename, RequestBody body) {
return addPart(Part.createFormData(name, filename, body));
}
public static Part createFormData(String name, @Nullable String filename, RequestBody body) {
if (name == null) {
throw new NullPointerException("name == null");
}
StringBuilder disposition = new StringBuilder("form-data; name=");
appendQuotedString(disposition, name);
if (filename != null) {
disposition.append("; filename=");
appendQuotedString(disposition, filename);
}
return create(Headers.of("Content-Disposition", disposition.toString()), body);
}
2.如何监听上传进度:
okhttp-post模拟表单上传文件到服务器.png该类是网上流传的方案之一,思路是每次服务端write的时候对写出的进度值进行累加
/**
* 作者:张风捷特烈<br/>
* 时间:2018/10/16 0016:13:44<br/>
* 邮箱:1981462002@qq.com<br/>
* 说明:监听上传进度的请求体
*/
public class CountingRequestBody extends RequestBody {
protected RequestBody delegate;//请求体的代理
private Listener mListener;//进度监听
public CountingRequestBody(RequestBody delegate, Listener listener) {
this.delegate = delegate;
mListener = listener;
}
protected final class CountingSink extends ForwardingSink {
private long byteWritten;//已经写入的大小
private CountingSink(Sink delegate) {
super(delegate);
}
@Override
public void write(@NonNull Buffer source, long byteCount) throws IOException {
super.write(source, byteCount);
byteWritten += byteCount;
mListener.onReqProgress(byteWritten, contentLength());//每次写入触发回调函数
}
}
@Nullable
@Override
public MediaType contentType() {
return delegate.contentType();
}
@Override
public long contentLength() {
try {
return delegate.contentLength();
} catch (IOException e) {
e.printStackTrace();
return -1;
}
}
@Override
public void writeTo(@NonNull BufferedSink sink) throws IOException {
CountingSink countingSink = new CountingSink(sink);
BufferedSink buffer = Okio.buffer(countingSink);
delegate.writeTo(buffer);
buffer.flush();
}
/////////////----------进度监听接口
public interface Listener {
void onReqProgress(long byteWritten, long contentLength);
}
}
使用:
//对请求体进行包装成CountingRequestBody
CountingRequestBody countingRequestBody = new CountingRequestBody(
requestBody, (byteWritten, contentLength) -> {
Log.e(TAG, "doUpload: " + byteWritten + "/" + contentLength);
if (byteWritten == contentLength) {
runOnUiThread(()->{
mIdBtnUploadPic.setText("UpLoad OK");
});
} else {
runOnUiThread(()->{
mIdBtnUploadPic.setText(byteWritten + "/" + contentLength);
});
}
});
Request request = new Request.Builder()
.url(Cons.BASE_URL + "upload")
.post(countingRequestBody).build();//使用CountingRequestBody进行请求
捕捉上传进度
3.多文件的上传
也就是多加几个文件到请求体
/**
* 模拟表单上传文件:通过MultipartBody
*/
private void doUpload() {
File file = new File(Environment.getExternalStorageDirectory(), "toly/ds4Android.apk");
File file2 = new File(Environment.getExternalStorageDirectory(), "DCIM/Screenshots/Screenshot_2018-1
RequestBody fileBody = RequestBody.create(MediaType.parse("application/octet-stream"), file);
RequestBody fileBody2 = RequestBody.create(MediaType.parse("application/octet-stream"), file2);
//1.获取OkHttpClient对象
OkHttpClient okHttpClient = new OkHttpClient();
//2.获取Request对象
RequestBody requestBody = new MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("file", file.getName(), fileBody)
.addFormDataPart("file", file2.getName(), fileBody2)
.build();
CountingRequestBody countingRequestBody = new CountingRequestBody(
requestBody, (byteWritten, contentLength) -> {
Log.e(TAG, "doUpload: " + byteWritten + "/" + contentLength);
if (byteWritten == contentLength) {
runOnUiThread(()->{
mIdBtnUploadPic.setText("UpLoad OK");
});
} else {
runOnUiThread(()->{
mIdBtnUploadPic.setText(byteWritten + "/" + contentLength);
});
}
});
Request request = new Request.Builder()
.url(Cons.BASE_URL + "upload")
.post(countingRequestBody).build();
//3.将Request封装为Call对象
Call call = okHttpClient.newCall(request);
//4.执行Call
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
Log.e(TAG, "onFailure: " + e);
}
@Override
public void onResponse(Call call, Response response) throws IOException {
String result = response.body().string();
Log.e(TAG, "onResponse: " + result);
runOnUiThread(() -> Toast.makeText(MainActivity.this, result, Toast.LENGTH_SHORT).show());
}
});
}
三、直接传输二进制流:
也就是直接把流post在请求里,在服务端对request获取输入流is,再写到服务器上
1.Android端:
private void doPostFile() {//向服务器传入二进制流
File file = new File(Environment.getExternalStorageDirectory(), "toly/ds4Android.apk");
//1.获取OkHttpClient对象
OkHttpClient okHttpClient = new OkHttpClient();
//2.构造Request--任意二进制流:application/octet-stream
Request request = new Request.Builder()
.url(Cons.BASE_URL + "postfile")
.post(RequestBody.create(MediaType.parse("application/octet-stream"), file))
.post(new FormBody.Builder().add("name", file.getName()).build())
.build();
//3.将Request封装为Call对象
Call call = okHttpClient.newCall(request);
//4.执行Call
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
Log.e(TAG, "onFailure: " + e);
}
@Override
public void onResponse(Call call, Response response) throws IOException {
String result = response.body().string();
Log.e(TAG, "onResponse: " + result);
runOnUiThread(() -> Toast.makeText(MainActivity.this, result, Toast.LENGTH_SHORT).show());
}
});
}
SpringBoot端:
@PostMapping(value = "/postfile")
public @ResponseBody
ResultBean postFile(@RequestParam(value = "name") String name, HttpServletRequest request) {
String result = "";
ServletInputStream is = null;
FileOutputStream fos = null;
try {
File file = new File("F:/SpringBootFiles/imgs", name);
fos = new FileOutputStream(file);
is = request.getInputStream();
byte[] buf = new byte[1024];
int len = 0;
while ((len = is.read(buf)) != -1) {
fos.write(buf, 0, len);
}
result = "SUCCESS";
} catch (IOException e) {
e.printStackTrace();
result = "ERROR";
} finally {
try {
if (is != null) {
is.close();
}
if (fos != null) {
fos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return ResultHandler.ok(result);
}