okHttp的文件的上传监听

2,519 阅读7分钟

首先http的请求报文是由:htpp首部+空行+请求数据。 我们的post就是往服务器中insert信息,所以我们必须要带上我们的请求数据-requestBody。 requestBody有两个继承类:FormBodyMultipartBody 数据类型大概:

  • 单纯的上传一个String、byte[]、File:可以使用Reqeust.create()方法。
  • 上传一些有键值对的数据:使用FormBody
  • 上传键值对和文件(.txt,png,mp3,ma4):使用MultipartBody

一:requestBody.create():

//上传String
public static RequestBody create(@Nullable MediaType contentType, String content) {
  ...
  //将String转换成二进制传输
  byte[] bytes = content.getBytes(charset);
  return create(contentType, bytes);
}

/** Returns a new request body that transmits {@code content}. */
public static RequestBody create(final @Nullable MediaType contentType, final byte[] content) {
  return create(contentType, content, 0, content.length);
}

/** Returns a new request body that transmits {@code content}. */
public static RequestBody create(final @Nullable MediaType contentType, final byte[] content,
    final int offset, final int byteCount) {
  if (content == null) throw new NullPointerException("content == null");
  Util.checkOffsetAndCount(content.length, offset, byteCount);
  return new RequestBody() {
    @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);
    }
  };
}

/** Returns a new request body that transmits the content of {@code file}. */
public static RequestBody create(final @Nullable MediaType contentType, final File file) {
  if (file == null) throw new NullPointerException("file == null");

  return new RequestBody() {
    @Override public @Nullable MediaType contentType() {
      return contentType;
    }
    @Override public long contentLength() {
      return file.length();
    }
    @Override public void writeTo(BufferedSink sink) throws IOException {
      try (Source source = Okio.source(file)) {
        sink.writeAll(source);
      }
    }
  };
}

例子不写了,就是往post(requestBody)就可以了。

二:FormBody提交键值对表单 键值对的信息可以用HashMap存储起来,也可以用javaBean存储进List<>。

//这里的大小最好是size*0.75+1,免得扩容
      HashMap<String,String> date= new HashMap<>(16);

      date.put("useName","daming");
      date.put("password","12345");

      FormBody.Builder builder = new FormBody.Builder();
      for (Map.Entry<String,String> map:date.entrySet()){
          builder.add(map.getKey(),map.getValue());
      }
      FormBody formBody = builder.build();
      
      Request request =new Request.Builder()
              .url(path)
              .post(formBody)
              .build();
      

三:MultipartBody上传文件 我们有时会把要上传的图片、视频的路径生成File上传给服务器。 先看看MultipartBody的添加方法addFormDataPart()

public Builder addFormDataPart(String name, String value) {
    return addPart(Part.createFormData(name, value));
  }

 //第三个参数我们用第一个方法创建
  public Builder addFormDataPart(String name, @Nullable String filename, RequestBody body) {
    return addPart(Part.createFormData(name, filename, body));
  }

模拟上传一个视频

File file = new File(Environment.getExternalStorageDirectory(),"text.png");
if(file.exists()) {
  OkHttpClient okHttpClient = new OkHttpClient();
  //构建请求body
  MultipartBody.Builder builder = new MultipartBody.Builder()
          .setType(MultipartBody.FORM)
          .addFormDataPart("platform", "android")
          .addFormDataPart("flie", file.getName(),
                  RequestBody.create(MediaType.parse(guessMimeType(file.getAbsolutePath())), 
  MultipartBody multipartBody = builder.build();
  //构建一个请求
  final Request request = new Request.Builder()
          .url("path")
          .post(multipartBody )
          .build();
  Call call = okHttpClient.newCall(request);
  call.enqueue(new Callback() {
      @Override
      public void onFailure(Call call, IOException e) {
      }
      @Override
      public void onResponse(Call call, Response response) throws IOException {
      }
  });
}


private String guessMimeType(String filePath) {
      FileNameMap fileNameMap = URLConnection.getFileNameMap();

      String mimType = fileNameMap.getContentTypeFor(filePath);

      if(TextUtils.isEmpty(mimType)){
          return "application/octet-stream";
      }
      return mimType;
  }
text/html:HTML格式
text/pain:纯文本格式
image/jpeg:jpg图片格式
application/json:JSON数据格式
application/octet-stream:二进制流数据(如常见的文件下载)
application/x-www-form-urlencoded:form表单encType属性的默认格式,表单数据将以key/value的形式发送到服务端
multipart/form-data:表单上传文件的格式

##文件上传/下载进度监听

显示文件下载进度:我们可以在接口的onResponse方法中拿到response的输入流,并拿到文件的总大小

//这是retry拦截器返回给我们response
@Override
public void onResponse(Call call, Response response) throws IOException {
   //拿到response的输入流
    InputStream is = response.body().byteStream();
    long sum = 0L;
    //文件总大小
    final long total = response.body().contentLength();
    int len = 0;
   //生成一个新文件
    File file  = new File(Environment.getExternalStorageDirectory(), "n.png");
    FileOutputStream fos = new FileOutputStream(file);
    byte[] buf = new byte[128];

    while ((len = is.read(buf)) != -1){
        fos.write(buf, 0, len);
        //每次递增
        sum += len;

        final long finalSum = sum;
        Log.d("pyh1", "onResponse: " + finalSum + "/" + total);
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                //将进度设置到TextView中
                contentTv.setText(finalSum + "/" + total);
            }
        });
    }
    fos.flush();
    fos.close();
    is.close();
}

文件上传监听 **问题分析:**如果我们要上传比较大的文件,我们就要监听它的下载进度。 首先,我们要获得File的总大小:我们在桥接拦截器的设置请求首部就会设置contentLength。我们看一下代码。

public final class BridgeInterceptor implements Interceptor {

@Override public Response intercept(Chain chain) throws IOException {
 Request userRequest = chain.request();
 Request.Builder requestBuilder = userRequest.newBuilder();

 RequestBody body = userRequest.body();
 if (body != null) {
   //调用RequestBody.contentType()拿到方法类型
   MediaType contentType = body.contentType();
   if (contentType != null) {
     requestBuilder.header("Content-Type", contentType.toString());
   }
    //调用RequestBody.contentLength()拿到文件大小
   long contentLength = body.contentLength();
   if (contentLength != -1) {
     requestBuilder.header("Content-Length", Long.toString(contentLength));
     requestBuilder.removeHeader("Transfer-Encoding");
   } else {
     requestBuilder.header("Transfer-Encoding", "chunked");
     requestBuilder.removeHeader("Content-Length");
   }
 }
}
}

所以,我们可以在MultipartBody的contentLength()方法中拿到文件的大小。那么怎么拿到文件上传的大小呢?在CallService拦截是专门进行数据通信的,我们看看它是如何传输数据,就可以在传输数据的方法中拿到我们传输的大小。

public final class CallServerInterceptor implements Interceptor {
@Override public Response intercept(Chain chain) throws IOException {
  RealInterceptorChain realChain = (RealInterceptorChain) chain;
  Exchange exchange = realChain.exchange();
  Request request = realChain.request();

  long sentRequestMillis = System.currentTimeMillis();

  exchange.writeRequestHeaders(request);

  boolean responseHeadersStarted = false;
  Response.Builder responseBuilder = null;
  if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {

    if ("100-continue".equalsIgnoreCase(request.header("Expect"))) {
      exchange.flushRequest();
      responseHeadersStarted = true;
      exchange.responseHeadersStart();
      responseBuilder = exchange.readResponseHeaders(true);
    }

    if (responseBuilder == null) {
      if (request.body().isDuplex()) {
        // Prepare a duplex body so that the application can send a request body later.
        exchange.flushRequest();
        BufferedSink bufferedRequestBody = Okio.buffer(
            exchange.createRequestBody(request, true));
       //看见没有,它是调用requestBody.writeTo方法中发送数据的。
        request.body().writeTo(bufferedRequestBody);
      } else {
        // Write the request body if the "Expect: 100-continue" expectation was met.
        BufferedSink bufferedRequestBody = Okio.buffer(
            exchange.createRequestBody(request, false));
        request.body().writeTo(bufferedRequestBody);
        bufferedRequestBody.close();
      }
}

所以,我们只要在MultipartBody的contentLength()方法和writeTo方法里面就可以拿到其总大小和上传大小。 可是MultipartBody修饰为final并不能继承类,重写方法。那么我们就用静态代理,通过构造参数把MultipartBody传进来。

public class ExMultipatyRequestBody extends RequestBody {

  private long total;  //总长度

  private long current; //当前进度

  private RequestBody requestBody; //MultipartBody

  private UpdateListener updateListener; //通过接口实例把参数传出去

  public ExMultipatyRequestBody(RequestBody requestBody){
      this.requestBody=requestBody;
  }

  public ExMultipatyRequestBody(RequestBody build, UpdateListener updateListener) {
      this.requestBody= build;
      this.updateListener=updateListener;
  }

  @Override
  public MediaType contentType() {
      return requestBody.contentType();
  }

  @Override
  public long contentLength() throws IOException {
      return requestBody.contentLength();
  }

  @Override
  public void writeTo(BufferedSink sink) throws IOException {

      total=contentLength();

      // writeTo方法里最终还是调用Sink.write方法传输数据,所以我们实现继承Sink的ForwardingSink
      ForwardingSink  forwardingSink = new ForwardingSink(sink) {
          @Override
          public void write(Buffer source, long byteCount) throws IOException {
              super.write(source, byteCount);
              concurrent+=byteCount;
              updateListener.update(total,concurrent);
          }
      };

      BufferedSink bufferedSink=(BufferedSink)forwardingSink;
      requestBody.writeTo(bufferedSink); //操作细节还是留给MultipartBody完成
      bufferedSink.flush();
  }
}

接口:

public interface UpdateListener {

  void update(long total,long current);
}

客户端:

private void initOkhttp() {

      File file = new File(Environment.getExternalStorageDirectory(),"text.png");

      if(file.exists()) {
          OkHttpClient okHttpClient = new OkHttpClient();
          //构建请求body
          MultipartBody.Builder builder = new MultipartBody.Builder()
                  .setType(MultipartBody.FORM)
                  .addFormDataPart("platform", "android")
                  .addFormDataPart("flie", file.getName(),
                          RequestBody.create(MediaType.parse(guessMimeType(file.getAbsolutePath())), file));

          MultipartBody multipartBody = builder.build();


          ExMultipatyRequestBody exMultipatyRequestBody = new ExMultipatyRequestBody(multipartBody, 
                  new UpdateListener() {
              @Override
              public void update(long total, long current) {
                  showDate(total, current);
              }
          });

          //构建一个请求
          final Request request = new Request.Builder()
                  .url("path")
                  .post(exMultipatyRequestBody)
                  .build();

          Call call = okHttpClient.newCall(request);
          call.enqueue(new Callback() {
              @Override
              public void onFailure(Call call, IOException e) {

              }

              @Override
              public void onResponse(Call call, Response response) throws IOException {

              }
          });
      }

  }

  private void showDate(final long total, final long current){
      runOnUiThread(new Runnable() {
          @Override
          public void run() {
              Toast.makeText(MainActivity7.this,"当前/总数="+current+"/"+total,Toast.LENGTH_LONG).show();
          }
      });

  }

###okHttp缓存 问题:对30s内同一个请求直接拿本地缓存。 分析:我们可以对第一次返回的response的缓存策略的cache-Control:max_age=30进行控制,所以我们要在CacheInterceptor得到response之前把reponse的缓存策略改了,okHttpClien添加拦截器有两个位置addInterceptor()在最前,addNetworkInterceptor()在Connect之后。 1.新增一个拦截器,在拿到后面传过来的response中设置缓存时间为30s,传给cache拦截器让它缓存数据

public class CacheResponseInterceptor  implements Interceptor {

  @Override
  public Response intercept(Chain chain) throws IOException {

      Response response=chain.proceed(chain.request());

      response=response.newBuilder()
              .removeHeader("cache-Control")
              .addHeader("cache-Control","max_age=30")
              .build();

      return response;
  }
}

第二加载新建的拦截器

File file = new File(Environment.getExternalStorageDirectory(),"cache");
Cache cache=new Cache(file,10*1024*1024);
  OkHttpClient okHttpClient = new OkHttpClient.Builder()
          .cache(cache)
          .addNetworkInterceptor(new CacheResponseInterceptor())
          .build();
//构建一个请求
final Request request = new Request.Builder()
      .url("path")
      .post(exMultipatyRequestBody)
      .build();
Call call = okHttpClient.newCall(request);
call.enqueue(new Callback() {
  @Override
  public void onFailure(Call call, IOException e) 
  }
  @Override
  public void onResponse(Call call, Response respo
     Log.d("cache=",""+response.cacheResponse());
     Log.d("response=",""+response.body().string()
  }
});

这就完成了在规定时间对同一个请求多次请求用本地缓存。

问题:在没网的情况下,直接使用本地缓存 分析:这个需要在我们构建request请求的时候,就判断有没有网络,如果没有网络,我们要设置只从本地拿取缓存,这个要在cache拦截器之前完成。

第一:新建一个拦截器判断是否有网络需要拿本地缓存

public class CacheRequestInterceptor implements Interceptor {
  private Context context;

  public CacheRequestInterceptor(Context context){
      this.context=context;
  }

  @Override
  public Response intercept(Chain chain) throws IOException {

      Request request =chain.request();
      //没有网络
      if(!isNetWork()){
          //只从本地拿缓存
          request=request.newBuilder()
                  .cacheControl(CacheControl.FORCE_CACHE)
                  .build();
      }
      return chain.proceed(request);
  }

  private boolean isNetWork() {
      //判断是否有网络
      return false;
  }
}

第二加载新建的拦截器

File file = new File(Environment.getExternalStorageDirectory(),"cache");
Cache cache=new Cache(file,10*1024*1024);
  OkHttpClient okHttpClient = new OkHttpClient.Builder()
          .cache(cache)
           .addInterceptor(new CacheRequestInterceptor(this))
          .addNetworkInterceptor(new CacheResponseInterceptor())
          .build();
//构建一个请求
final Request request = new Request.Builder()
      .url("path")
      .post(exMultipatyRequestBody)
      .build();
Call call = okHttpClient.newCall(request);
call.enqueue(new Callback() {
  @Override
  public void onFailure(Call call, IOException e) 
  }
  @Override
  public void onResponse(Call call, Response respo
     Log.d("cache=",""+response.cacheResponse());
     Log.d("response=",""+response.body().string()
  }
});

###OkHttpClient的单线程/多线程断点下载 区别:

区别

单线程下载 在上面已经有过单线程下载监听的代码,这里再写一遍:

OkHttpClient okHttpClient = new OkHttpClient();
Request request= new Request.Builder()
      .url("path")
      .build();
Call call=okHttpClient.newCall(request);
call.enqueue(new Callback() {
  @Override
  public void onFailure(Call call, IOException e) {
      e.printStackTrace();
  }
  @Override
  public void onResponse(Call call, Response response) throws IOException {
      InputStream inputStream= response.body().byteStream();
      final long total = response.body().contentLength();
      File file =new File(Environment.getDataDirectory(),"xx.apk");
      OutputStream outputStream = new FileOutputStream(file);
      byte[] buffer = new byte[1024*10];
      int len=0;
      long sum=0;
      while ((len=inputStream.read(buffer))!= -1){
          outputStream.write(buffer,0,len);
          sum+=len;
          
          final long finalSum= sum;
          runOnUiThread(new Runnable() {
              @Override
              public void run() {
                  Log.d("总比",""+finalSum+"/"+total);
              }
          });
      }
      outputStream.close();
      inputStream.close();
  }
});