前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >[番外]理一理Android多文件上传那点事

[番外]理一理Android多文件上传那点事

作者头像
张风捷特烈
发布2018-12-18 16:55:00
1.5K0
发布2018-12-18 16:55:00
举报
文章被收录于专栏:Android知识点总结

多文件上传是客户端与服务端两个的事,客户端负责发送,服务端负责接收 我们都知道客户端与服务器只是通过http协议进行交流,那么http协议应该会对上传文件有所规范 你可以根据这些规范来自己拼凑请求头,可以用使用已经封装好的框架,如Okhttp3


一、先理一理表单点提交点的时候发生了什么?
1.客户端的请求(requst)

请求头会有:Content-Type: multipart/form-data; boundary=----WebKitFormBoundary5sGoxdCHIEYZKCMC 其中boundary=----WebKitFormBoundary5sGoxdCHIEYZKCMC可看做是分界线

表单中的数据会和请求体对应,比如只有一个<input/>标签,里面是字符串

代码语言:javascript
复制
//===================描述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

代码语言:javascript
复制
//===================描述file:<input type="file"/>================
------WebKitFormBoundary5sGoxdCHIEYZKCMC
Content-Disposition:form-data;name="KeyName";filename="xxx.xxx"
Content-Type: 对应MimeTypeMap

[file stream]
------WebKitFormBoundary5sGoxdCHIEYZKCMC

这便是客户端的请求


2.客户端的接收和处理

服务端会受到客户端的请求,然后根据指定格式对请求体进行解析 然后是文件你就可以在服务端保存,保存成功便是成功上传成功,下面是SpringBoot对上传的处理:

代码语言:javascript
复制
/**
 * 多文件上传(包括一个)
 *
 * @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

代码语言:javascript
复制
 /**
  * 模拟表单上传文件:通过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来拼凑的请求体

代码语言:javascript
复制
/** 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.如何监听上传进度:

该类是网上流传的方案之一,思路是每次服务端write的时候对写出的进度值进行累加

okhttp-post模拟表单上传文件到服务器.png

代码语言:javascript
复制
/**
 * 作者:张风捷特烈<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);
    }
}

使用:

代码语言:javascript
复制
//对请求体进行包装成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.多文件的上传

也就是多加几个文件到请求体

代码语言:javascript
复制
 /**
  * 模拟表单上传文件:通过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端:
代码语言:javascript
复制
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端:
代码语言:javascript
复制
@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);
}
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2018.11.26 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、先理一理表单点提交点的时候发生了什么?
    • 1.客户端的请求(requst)
      • 2.客户端的接收和处理
      • 二、okhttp模拟表单文件上传文件
        • 1.单文件上传
          • addFormDataPart源码跟踪
        • 2.如何监听上传进度:
          • 3.多文件的上传
          • 三、直接传输二进制流:
            • 1.Android端:
              • SpringBoot端:
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档