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

96
张风捷特烈 Excellent
0.2 2018.11.26 22:36 字数 489

多文件上传是客户端与服务端两个的事,客户端负责发送,服务端负责接收
我们都知道客户端与服务器只是通过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.如何监听上传进度:

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

okhttp-post模拟表单上传文件到服务器.png
/**
 * 作者:张风捷特烈<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);
}
Android技术栈
Android技术栈
13.1万字 · 4.5万阅读 · 218人关注
决战安卓
Web note ad 1