您当前的位置: 首页 >  http

郭梧悠

暂无认证

  • 2浏览

    0关注

    402博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文

OkHttp文件上传下载

郭梧悠 发布时间:2017-08-09 23:08:52 ,浏览量:2

在之前的博客中从源码角度简单分析了Okhttp的执行流程,本篇就整体的发送请求流程做一个简单的梳理,目的主要是为自己封装的一套Okhttp请求工具做前提说明,毕竟研究这么长时间的东西,写一套自己可以用的组建还是挺令人愉快的。闲言少叙,Let’s Go.

我们知道http请求包括三个部分即:状态行,请求头、请求体。所以一些网络请求框架对数据的组织基本上都围绕着这三个部分进行展开,可谓万变不离其宗。Okhttp当然也不例外,在Okttp中用Request对象和ReqeustBody对象来分表一个请求及其请求所携带的请求体。

对应一个http请求来说,是否需要请求体(下文用ReqeustBody表示)是根求的方法来判定的,比如Get请求就不需要请求体,而post请求则需要请求体。在《http权威指南》这本书上对此简单列了一个表格: 这里写图片描述

当然上面只是列了一些常用的方法,那么OKhttp是怎么判定一个请求是否需要请求体的呢?本系列博客中初步接触到Okhttp判定是否需要RequestBody是在对CallServerInterceptor这个拦截器进行分析的时候:通过HttpMethod.permitsRequestBody(request.method())来判定是否需要RequestBody.需要注意的是Okhttp的delete是允许客户端传请求体的:

//该Builder为构建Requesnt对象的构造方法
 public Builder delete(@Nullable RequestBody body) {
      return method("DELETE", body);
    }
 public Builder delete() {
      return delete(Util.EMPTY_REQUEST);
    }

正如上面nullable的注解,delete请求在okhttp中对请求体也是支持的。

RequestBody简单说明:

上文提到在Okhttp中以ReqeustBody来作为一个请求体,该类也很简单,就是一个抽象类:

public abstract class RequestBody {
  /** Returns the Content-Type header for this body. */
  public abstract @Nullable MediaType contentType();
  //所发送请求体的长度或者大小
  public long contentLength() throws IOException {
    return -1;
  }

  public abstract void writeTo(BufferedSink sink) throws IOException;
  }

ReqeustBody主要包含如下两方面内容: 1、ReqeustBody包含一个MediaType表示请求体的Content-Type首部对应的值,具体用到该值得地方只在BridgeInterceptor拦截器中(注:Content-Type首部表示请求体所承载对象的类型,它可以告诉我们如何去解释数据,比如是图像还是文本等):

  //构建Content-type首部
  MediaType contentType = body.contentType();
      if (contentType != null) {
        requestBuilder.header("Content-Type", contentType.toString());
      }

2、writeTo该方法为抽象方法,表示客户端如何向服务器发送请求体的数据,因为请求体的不同,所以具体发送的方式也不同,所以该方法交给子类去实现。该方法具体的调用时机是在CallServerInterceptor这个拦截器中:

  if (responseBuilder == null) {
        Sink requestBodyOut = httpCodec.createRequestBody(request, request.body().contentLength());
        BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);
        //调用writeTo方法来送请求体
        request.body().writeTo(bufferedRequestBody);
        bufferedRequestBody.close();
      }  

为了使用方便,OKhttp提供了两个ReqeustBody的实现类:FormBody和MultipartBody.这两个可以完成我们业务中的绝大部分逻辑,当然如果不能满足需求的话,也可以构建自己的RequestBody。在ReqeustBody这个类里面提供了如下几个static方法来方便你构建自己的RequestBody: 这里写图片描述 图中第一个create和第三个create方法最终会调用第四个create的方法来生成一个RequestBody:

  public static RequestBody create(final @Nullable MediaType contentType, final byte[] content,
      final int offset, final int byteCount) {
   //省略部分代码
      @Override public @Nullable MediaType contentType() {
        return contentType;
      }

      @Override public long contentLength() {
        return byteCount;
      }

      @Override public void writeTo(BufferedSink sink) throws IOException {
        sink.write(content, offset, byteCount);
      }
    };
  }

比如如果我们想post一个json字符串,那么我们就可以通过如下一个的代码来创建一个post请求:

  //创建jsonType
        MediaType jsonType =  MediaType.parse("application/json; charset=utf-8");
        
        //创建json请求体
        RequestBody jsonBody = RequestBody.create(jsonType, new JSONObject().toString());

        Request.Builder builder = new Request.Builder();
      
        //post提交jsonBody
        builder.post(jsonBody);

也就是说使用okhttp创建自己的ReqeustBody很简单,也就是两个步骤: 1、传content-type(可为空)构建MediaType 2、传入ReqeustBody所包含的内容。

FormBody简单说明:

可以说在Okhttp中content-type(或者MediaType)决定了你的ReqeustBody对象的具体类型,那么OkHttp提供的FormBody的Content-Type是什么呢?如下所示

//FormBody
private static final MediaType CONTENT_TYPE =
      MediaType.parse("application/x-www-form-urlencoded");

熟悉html的应该知道,html中有一个form表单标签,该标签有一个enctype属性,默认的就是x-www-form-urlencoded.该类型是指表单的提交,并且将提交的数据进行urlencode。该类型会将数据拼接成键值对的方式,比如name=Java&age = 23.这正对应这FormBody的两个属性:


  private final List encodedNames;
  private final List encodedValues;

正如上文所说,okhttp本身提供的FormBody就可以满足绝大部分的网络请求,我们可以用FormBody来拼接请求参数来完成post,put,或者delete的请求。通常来说我们通过一个map来保存参数的键值对 和headers的键值队。所以下面的代码能完成post,put,或者delete,patch的基本请求,其实现思路可以在代码中复用:

  //params 是一个Map
  FormBody.Builder builder= new FormBody.Builder();
  //添加headers
  for (Map.Entry entry : headers.entrySet()) {     
        builder.addHeader(entry.getKey(), entry.getValue());
   }

   //添加params 非get请求
   for (Map.Entry entry :params.entrySet()) {
      if (entry.getValue() != null) {
                    builder.add(entry.getKey(), entry.getValue());
                }
            }
   body= builder.build();
   
   Request.Builder requestBuilder = new Request.Builder();
   //type表示的是请求方法类型,为enum类型
  switch (type) {
   case POST://post请求
          requestBuilder.post(body);
          break;
      case PUT://put请求
          requestBuilder.put(body);
          break;
      case DELETE://delete请求
          requestBuilder.delete(body);
          break;
  }

上文也说到okhttp通过ReqeustBody的writeTo方法来发送数据,那么在此就简单看下FormBody方法的writeTo方法实现:

public void writeTo(BufferedSink sink) throws IOException {
    writeOrCountBytes(sink, false);
  }
      
  private long writeOrCountBytes(@Nullable BufferedSink sink, boolean countBytes) {
    //省略部分代码
    for (int i = 0, size = encodedNames.size(); i < size; i++) {
      if (i > 0) buffer.writeByte('&');
      buffer.writeUtf8(encodedNames.get(i));
      buffer.writeByte('=');
      buffer.writeUtf8(encodedValues.get(i));
    }
 //省略部分代码
    return byteCount;
  }

通过writeOrCountBytes里的for循环可以发现FormBody也是拼接成形如name=Java&age = 23键值对来进行数据发送的。

ResponseBody的简单说明:

当然发送过后就等着Response对象的返回了。http的数据返回也是有Body的,在Okhttp中用ResponseBody(抽象类)来表示服务器数据返回的实体。okhttp是在CallServerInterceptor拦截器中完成对Response的构建的:

    response = response.newBuilder()
          .body(httpCodec.openResponseBody(response))//组建ResponseBody
          .build();

抛开http2不谈,我们先看看http1所代表的http1Codec是怎么构建响应的:

public ResponseBody openResponseBody(Response response) {
    Source source = getTransferStream(response);
    return new RealResponseBody(response.headers(), Okio.buffer(source));
  }

实际上我们更关心的是ResponseBody的所持有的数据,在RealResponseBody的父类ResponseBody 中提供了三个方法:

//获取socket链接的输入流
 public final InputStream byteStream() {
    return source().inputStream();
  }
  //返回字符串,比如json串等:该方法只能调用一次
 public final String string() throws IOException {
    BufferedSource source = source();
    try {
      Charset charset = Util.bomAwareCharset(source, charset());
      return source.readString(charset);
    } finally {
     //关闭流,所以该方法只能调用一次
      Util.closeQuietly(source);
    }
  }
//将服务器响应的数据转换成byte数组返回:该方法只能调用一次
public final byte[] bytes() throws IOException {
    long contentLength = contentLength();
  
    BufferedSource source = source();
    byte[] bytes;
    try {
      bytes = source.readByteArray();
    } finally {
     //关闭流,所以该方法只能调用一次
      Util.closeQuietly(source);
    }
 
    return bytes;
  }

Okhttp实现下载功能

既然我们能从ResponseBody中获取InputStream输入流,那我们就可以把服务器返回的数据通过输入流写入到文件中,从而实现文件下载的功能了。下面功能的核心代码如下所示:

          InputStream  is = response.byteStream();
            File dir = new File(filePath);
            if (!dir.exists()) {
                dir.mkdirs();
            }
            File file = new File(dir, fileName);
            fos = new FileOutputStream(file);
            int len = 0;
            byte[] buf = new byte[2048];
            while ((len = is.read(buf)) != -1) {
                fos.write(buf, 0, len);
            }
            fos.flush();

MultipartBody简单分析

既然文件下载功能简单实现了,那么文件上传很容易,没有想象的那么复杂,换句话说文件上传反过来看就是服务器实现了从客户端下载文件的功能。当然文件下载需要的content-type跟FormBody不一样,文件上传所需要的content-type为multipart/form-data(http协议采纳了多部对象集合(multipart),发送一份报文主体内可含有多个类型实体,通常在图片或者文本上传时使用,其中邮件上传各种附件的机制也是用了MIME的Mulipart方法)。事实上文图中RequestBody的最后一个create方法就是用来处理文件上传的RequestBody的。

public static RequestBody create(final @Nullable MediaType contentType, final File file) {
     
    return new RequestBody() {
       //省略部分代码
      @Override public void writeTo(BufferedSink sink) throws IOException {
        Source source = null;
        try {
        //打开文件的输入流
          source = Okio.source(file);
          //通过socket的输入流来向服务器发送文件数据
          sink.writeAll(source);
        } finally {
          Util.closeQuietly(source);
        }
      }
    };
  }

上面create方法创造的ReqeustBody,其Body体就是一个File对象,通过ReqeustBody的writeTo方法来实现文件的上传: 1、调用Okio.soure方法打开目标文件的输入流,读取文件数据 2、通过sink这个TCP链接的OutputStream对象向服务器发送文件数据。

那么这个构建出来用于文件上传的ReqeustBody是怎么用的呢?这个就是MultipartBody实现的功能了。 MulitpartBody的builder对象有如下方法:

public MultipartBody.Builder addFormDataPart(String name, String filename, RequestBody body) {
            return addPart(MultipartBody.Part.createFormData(name, filename, body));
        }
 ///
 public static Part createFormData(String name, @Nullable String filename, RequestBody body) {
       //创建part对象
      return create(Headers.of("Content-Disposition", disposition.toString()), body);
    }
    //
     public static Part create(@Nullable Headers headers, RequestBody body) {
      //将RequestBody的headers,body构成part
      return new Part(headers, body);
    }

addFormDataPart方法就是通过文件名和create(final @Nullable MediaType contentType, final File file)创建的一个requestBody来完成。 从上面的代码也可以发现,对于MulitpartBody来说该对象可以添加一个或者多个ReqeustBody,只不过这些body连同他们各自的headers组成一个Part对象交给了MultipartBody.所以我们也可以在文件上传的同时也可以一并发送其余的ReqeustBody来发送一些附加信息。

所以对于文件上传功能来说,有如下几个基本步骤

1、所以首先要先创建一个MultipartBody:

//MediaType FORM = MediaType.parse("multipart/form-data");
MultipartBody.Builder multipartBuilder= new MultipartBody.Builder()
                .setType(MultipartBody.FORM);

2、构建fileBody:

        /*okhttp文件上传三要素*/
        String name = fileRequest.getName();
        String fileName = fileRequest.getFileName();
        File file = fileRequest.getFile();

        RequestBody fileBody = RequestBody.create(null, file);

注意第一个name相当于html表单表单中的name属性。 3、将fileBody最为一个part交给MulitpartBody的builder:

   multipartBuilder.addFormDataPart(name, fileName, fileBody);
   //构建multipartBody对象
  MultipartBody mulitpartBody = multipartBuilder.build();

4、构建request对象发起post请求:

Reqeust.Builder requestBuilder = new RequestBuilder();
requestBuilder.setUrl(yoururl)
requestBuilder.post(mulitpartBody);

如果你有其他请求参数要发送的话,可以如下操作:

 
  for (Map.Entry entry : paramsMap.entrySet()) { 
         multipartBuilder.addPart(null, RequestBody.create(null, entry.getValue()));
             
      }

如果还需要添加header的话,则可以用requestBuilder的addHeader方法:

for (Map.Entry entry : headers.entrySet()) {
    requestBuilder.addHeader(entry.getKey(), entry.getValue());
 }

完整的代码结构如下:

 private Request createOkRequest(UpLoadFileRequest fileRequest) {
        /*用来创建request对象*/
        okhttp3.Request.Builder requestBuilder = new okhttp3.Request.Builder();
        
        /*初始化MultipartBody对象*/
        MultipartBody.Builder bodyBuilder = new MultipartBody.Builder()
                .setType(MultipartBody.FORM);

        /*okhttp文件请求三要素*/
        String name = fileRequest.getName();
        String fileName = fileRequest.getFileName();
        File file = fileRequest.getFile();
        /*创建文件请求体*/
        RequestBody fileBody = RequestBody.create(null, file);
    
        bodyBuilder.addFormDataPart(name, fileName, fileBody);
        /*添加其他请求参数 */     
        if (fileRequest.hasParams()) {
            for (Map.Entry entry : fileRequest.getParams().entrySet()) {
                if (entry.getValue() != null) {
                    bodyBuilder.addPart(null, RequestBody.create(null, entry.getValue()));
                }
            }
        }
 
 /*添加headers信息 */      
for (Map.Entry entry : headers.entrySet()) {
    requestBuilder.addHeader(entry.getKey(), entry.getValue());
 }

        /*添加body*/
        requestBuilder.post(bodyBuilder.build());
        /*创建request*/
        return requestBuilder.build();
    }

既然知道文件上传的操作,那么批量上传也很简单,无非就是一个文件集合,然后循环调用MultipartBody的addFormData方法,实例代码如下:

/*文件上传的集合*/
List fileInfos = fileUploadRequest.getFiles();
/*几何遍历*/
 for (int i = 0, size = fileInfos.size(); i < size; i++) {
       FileUploadRequest.FileInfo fileInfo = fileInfos.get(i);
       
    /*为每一个文件创建一个body*/
   RequestBody fileBody = RequestBody.create(MediaType.parse(FileUploadRequest.guessMimeType(fileInfo.fileName)), fileInfo.file);

 /*将body添加到form中*/                 bodyBuilder.addFormDataPart(fileInfo.name, fileInfo.fileName, fileBody);
 }

到此为止Okhttp发起网络请求的原理基本讲解完毕,为此博主对OKhttp做了简单的封装,包含了文件上传,下载等功能,应用到自己的项目中,源码已经添加到github上,如有不当之处欢迎批评指正。

-------补充更新2021-07-21------ 事实上,如果想要知道文件上传的百分比进度,只需要将上文的 sink.writeAll(source);方法给成sink.write(buf, readCount)即可,完整的代码如下:

  @Override
            public void writeTo(BufferedSink bufferedSink) {
                Source source = Okio.source(file);
                Buffer buf = new Buffer();
                Long remaining = contentLength();
                for (long readCount; (readCount = source.read(buf, 2048)) != -1; ) {
                    bufferedSink.write(buf, readCount);
                    final long remainingBytes = remaining -= readCount;
                    final Long finalRemaining = remaining;
                    sHandler.post(new Runnable() {
                        @Override
                        public void run() {
                            listener.onProgress(contentLength(), remainingBytes, finalRemaining == 0);
                        }
                    });
                }
            }
关注
打赏
1663674776
查看更多评论
立即登录/注册

微信扫码登录

0.0438s