OkHttp3简单使用教程(一):请求和响应

一,HTTP请求、响应报文格式

要弄明白网络框架,首先需要先掌握Http请求的,响应的报文格式。

HTTP请求报文格式:

HTTP请求报文主要由请求行、请求头部、请求正文3部分组成.

request.png

  1. 请求行:由请求方法,URL,协议版本三部分构成,之间用空格隔开
    请求方法包括:POST、GET、HEAD、PUT、POST、TRACE、OPTIONS、DELETE
    协议版本:HTTP/主版本号.次版本号,常用的有HTTP/1.0和HTTP/1.1
    请求方法.png
  2. 请求头部:
    请求头部为请求报文添加了一些附加信息,由“名/值”对组成,每行一对,名和值之间使用冒号分隔
    常见请求头如下:
    Host ----接受请求的服务器地址,可以是IP:端口号,也可以是域名
    User-Agent ----发送请求的应用程序名称
    Connection ---- 指定与连接相关的属性,如Connection:Keep-Alive
    Accept-Charset ---- 通知服务端可以发送的编码格式
    Accept-Encoding ---- 通知服务端可以发送的数据压缩格式
    Accept-Language ---- 通知服务端可以发送的语言
  3. 请求正文
    可选部分,比如GET请求就没有请求正文
  4. 请求示例
    image.png
HTTP响应报文格式:

HTTP响应报文主要由状态行、响应头部、响应正文3部分组成

响应报文.png

  1. 状态行
    由3部分组成,分别为:协议版本,状态码,状态码描述,之间由空格分隔
    状态码:为3位数字,200-299的状态码表示成功,300-399的状态码指资源重定向,400-499的状态码指客户端请求出错,500-599的状态码指服务端出错(HTTP/1.1向协议中引入了信息性状态码,范围为100-199)
    常见的:
    200:响应成功
    302:重定向跳转,跳转地址通过响应头中的Location属性指定
    400:客户端请求有语法错误,参数错误,不能被服务器识别
    403:服务器接收到请求,但是拒绝提供服务(认证失败)
    404:请求资源不存在
    500:服务器内部错误

    image.png

  2. 响应头部 :
    与请求头部类似,为响应报文添加了一些附加信息
    Server - 服务器应用程序软件的名称和版本
    Content-Type - 响应正文的类型(是图片还是二进制字符串)
    Content-Length - 响应正文长度
    Content-Charset - 响应正文使用的编码
    Content-Encoding - 响应正文使用的数据压缩格式
    Content-Language - 响应正文使用的语言

Server: bfe/1.0.8.1 
Date: Sat, 04 Apr 2015 02:49:41 GMT 
Content-Type: text/html; charset=utf-8 
Vary: Accept-Encoding 
Cache-Control: private 
cxy_all: baidu+8ee3da625d74d1aa1ac9a7c34a2191dc 
Expires: Sat, 04 Apr 2015 02:49:38 GMT 
X-Powered-By: HPHP 
bdpagetype: 1 
bdqid: 0xb4eababa0002db6e 
bduserid: 0 
Set-Cookie: BDSVRTM=0; path=/ 
BD_HOME=0; path=/ 
H_PS_PSSID=13165_12942_1430_13075_12867_13322_12691_13348_12723_12797_13309_13325_13203_13161_13256_8498; path=/; domain=.baidu.com 
__bsi=18221750326646863206_31_0_I_R_2_0303_C02F_N_I_I; expires=Sat, 04-Apr-15 02:49:46 GMT; domain=www.baidu.com; path=/ 
Content-Encoding: gzip 
X-Firefox-Spdy: 3.1
  1. 响应正文
    是请求响应的最终结果,都在响应体里。
    报文可以承载很多类型的数字数据:图片、视频、HTML文档、软件应用程序等
  2. 响应示例
    image.png

二,HTTP请求和响应的基本使用

主要包含:

  • 一般的get请求
  • 一般的post请求
  • 基于Http的文件上传
  • 文件下载
  • 加载图片
  • 支持请求回调,直接返回对象、对象集合
  • 支持session的保持
  1. 添加网络访问权限并添加库依赖
  <uses-permission android:name="android.permission.INTERNET" />
api 'com.squareup.okhttp3:okhttp:3.9.0'
  1. HTTP的GET请求
//1,创建okHttpClient对象
OkHttpClient mOkHttpClient = new OkHttpClient();
//2,创建一个Request
final Request request = new Request.Builder()
                .url("https://www.baidu.com")
                .build();
//3,新建一个call对象
Call call = mOkHttpClient.newCall(request); 
//4,请求加入调度,这里是异步Get请求回调
call.enqueue(new Callback()
        {
            @Override
            public void onFailure(Request request, IOException e)
            {
            }

            @Override
            public void onResponse(final Response response) throws IOException
            {
                    //String htmlStr =  response.body().string();
            }
        });             

对以上的简单请求的构成:

  • 发送一个GET请求的步骤,首先构造一个Request对象,参数最起码有个URL,当然也可以通过Request.Builder设置更多的参数比如:header、method等
//URL带的参数
HashMap<String,String> params = new HashMap<>();
//GET 请求带的Header
HashMap<String,String> headers= new HashMap<>();
//HttpUrl.Builder构造带参数url
 HttpUrl.Builder urlBuilder = HttpUrl.parse(url).newBuilder();
        if (params != null) {

            for (String key : params.keySet()) {
                urlBuilder.setQueryParameter(key, params.get(key));
            }
        }
        Request request = new Request.Builder()
                .url(urlBuilder.build())
                .headers(headers == null ? new Headers.Builder().build() : Headers.of(headers))
                .get()
                .build();
  • 通过Request的对象去构造得到一个Call对象,类似于将你的请求封装成了任务,既然是任务,就会有execute(),enqueue()和cancel()等方法。
    execute():同步GET请求
 //同步
Response response = call.execute()
if(response.isSuccessful()){
     //响应成功
}

enqueue():异步GET请求,将call加入调度队列,然后等待任务执行完成,我们在Callback中即可得到结果。
cancel():Call请求的取消,okHttp支持请求取消功能,当调用请求的cancel()时,请求就会被取消,抛出异常。又是需要监控许多Http请求的执行情况,可以把这些请求的Call搜集起来,执行完毕自动剔除,如果在请求执行过程中(如下载),想取消执行,可使用call.cancel()取消。

  • 请求的响应Response
    对于同步GET请求,Response对象是直接返回的。异步GET请求,通过onResponse回调方法传参数,需要注意的是这个onResponse回调方法不是在主线程回调,可以使用runInUIThread(new Runnable(){})
    我们希望获得返回的字符串,可以通过response.body().string()获取;
    如果希望获得返回的二进制字节数组,则调用response.body().bytes()
    如果你想拿到返回的inputStream,则调用response.body().byteStream()

3. HTTP的POST请求
看来上面的简单的get请求,基本上整个的用法也就掌握了,比如post携带参数,也仅仅是Request的构造的不同。

   //POST参数构造MultipartBody.Builder,表单提交
   HashMap<String,String> params = new HashMap<>();
     MultipartBody.Builder urlBuilder = new MultipartBody.Builder()
                            .setType(MultipartBody.FORM);
                    if (params != null) {
                        for (String key : params.keySet()) {
                            if (params.get(key)!=null){
                                urlBuilder.addFormDataPart(key, params.get(key));
                            }
                            //urlBuilder.addFormDataPart(key, params.get(key));

                        }
                    }
// 构造Request->call->执行
 Request request = new Request.Builder()
                            .headers(extraHeaders == null ? new Headers.Builder().build() : Headers.of(extraHeaders))//extraHeaders 是用户添加头
                            .url(url)
                            .post(urlBuilder.build())//参数放在body体里
                            .build();
Call call = httpClient.newCall(request);
 try (Response response = call.execute()) {
            if (response.isSuccessful()){
             //响应成功
             }
  }

Post的时候,参数是包含在请求体中的,所以我们通过MultipartBody.Builder 添加多个String键值对,然后去构造RequestBody,最后完成我们Request的构造。
4. OKHTTP的上传文件
上传文件本身也是一个POST请求。在上面的POST请求中可以知道,POST请求的所有参数都是在BODY体中的,我们看看请求体的源码RequestBody:请求体=contentType + BufferedSink
RequestBody

//抽象类请求体,**请求体=contentType + BufferedSink**
public abstract class RequestBody {
  /** Returns the Content-Type header for this body. */
 //返回Body体的内容类型
  public abstract @Nullable MediaType contentType();

  /**
   * Returns the number of bytes that will be written to {@code sink} in a call to {@link #writeTo},
   * or -1 if that count is unknown.
   */
  //返回写入sink的字节长度
  public long contentLength() throws IOException {
    return -1;
  }

  /** Writes the content of this request to {@code sink}. */
  //写入缓存sink
  public abstract void writeTo(BufferedSink sink) throws IOException;

  /**
   * Returns a new request body that transmits {@code content}. If {@code contentType} is non-null
   * and lacks a charset, this will use UTF-8.
   */
   //创建一个请求体,如果contentType不等于null且缺少字符集,将使用UTF-8
  public static RequestBody create(@Nullable MediaType contentType, String content) {
    Charset charset = Util.UTF_8;
    if (contentType != null) {
      //contentType里面的字符集
      charset = contentType.charset();
      if (charset == null) {
        charset = Util.UTF_8;
        //contentType 里面加入字符集
        contentType = MediaType.parse(contentType + "; charset=utf-8");
      }
    }
    //按字符集变成字节
    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 ByteString content) {
    return new RequestBody() {
      @Override public @Nullable MediaType contentType() {
        //请求体需要的内容类型
        return contentType;
      }

      @Override public long contentLength() throws IOException {
       //写入BufferedSink 的长度
        return content.size();
      }

      @Override public void writeTo(BufferedSink sink) throws IOException {
       //将需要传输的字节,写入缓存BufferedSink 中
        sink.write(content);
      }
    };
  }

  /** 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}. */
  //创建一个请求体,传输文件file内容,其实就是file写入bufferedSink
  public static RequestBody create(final @Nullable MediaType contentType, final File file) {
    if (file == null) throw new NullPointerException("content == 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 {
        Source source = null;
        try {
         //文件写入BufferedSink 
          source = Okio.source(file);
          sink.writeAll(source);
        } finally {
          Util.closeQuietly(source);
        }
      }
    };
  }
}

Http请求中Content-Type
客户端在进行http请求服务器的时候,需要告诉服务器请求的类型,服务器在返回给客户端的数据的时候,也需要告诉客户端返回数据的类型
默认的ContentType为 text/html 也就是网页格式. 常用的内容类型

  • text/plain :纯文本格式 .txt
  • text/xml : XML格式 .xml
  • image/gif :gif图片格式 .gif
  • image/jpeg :jpg图片格式 .jpg
  • image/png:png图片格式 .png
  • audio/mp3 : 音频mp3格式 .mp3
  • audio/rn-mpeg :音频mpga格式 .mpga
  • video/mpeg4 : 视频mp4格式 .mp4
  • video/x-mpg : 视频mpa格式 .mpg
  • video/x-mpeg :视频mpeg格式 .mpeg
  • video/mpg : 视频mpg格式 .mpg
    以application开头的媒体格式类型:
  • application/xhtml+xml :XHTML格式
  • application/xml : XML数据格式
  • application/atom+xml :Atom XML聚合格式
  • application/json : JSON数据格式
  • application/pdf :pdf格式
  • application/msword : Word文档格式
  • application/octet-stream : 二进制流数据(如常见的文件下载)
    MultipartBody.Builder 添加多个String键值对
//MultipartBody源码,MultipartBody其实也是RequestBody ,需要在此RequestBody 体内,添加多个Part
/** An <a href="http://www.ietf.org/rfc/rfc2387.txt">RFC 2387</a>-compliant request body. */
public final class MultipartBody extends RequestBody {
  /**
   * The "mixed" subtype of "multipart" is intended for use when the body parts are independent and
   * need to be bundled in a particular order. Any "multipart" subtypes that an implementation does
   * not recognize must be treated as being of subtype "mixed".
   */
  //混合的内容类型
  public static final MediaType MIXED = MediaType.parse("multipart/mixed");

  /**
   * The "multipart/alternative" type is syntactically identical to "multipart/mixed", but the
   * semantics are different. In particular, each of the body parts is an "alternative" version of
   * the same information.
   */
  public static final MediaType ALTERNATIVE = MediaType.parse("multipart/alternative");

  /**
   * This type is syntactically identical to "multipart/mixed", but the semantics are different. In
   * particular, in a digest, the default {@code Content-Type} value for a body part is changed from
   * "text/plain" to "message/rfc822".
   */
  public static final MediaType DIGEST = MediaType.parse("multipart/digest");

  /**
   * This type is syntactically identical to "multipart/mixed", but the semantics are different. In
   * particular, in a parallel entity, the order of body parts is not significant.
   */
  public static final MediaType PARALLEL = MediaType.parse("multipart/parallel");

  /**
   * The media-type multipart/form-data follows the rules of all multipart MIME data streams as
   * outlined in RFC 2046. In forms, there are a series of fields to be supplied by the user who
   * fills out the form. Each field has a name. Within a given form, the names are unique.
   */
  public static final MediaType FORM = MediaType.parse("multipart/form-data");

  private static final byte[] COLONSPACE = {':', ' '};
  private static final byte[] CRLF = {'\r', '\n'};
  private static final byte[] DASHDASH = {'-', '-'};

  private final ByteString boundary;
  private final MediaType originalType;

 //请求体的内容类型
  private final MediaType contentType;
  //MultiPartBody需要添加多个Part对象,一起请求
  private final List<Part> parts;
  private long contentLength = -1L;
  //构造函数
  MultipartBody(ByteString boundary, MediaType type, List<Part> parts) {
    this.boundary = boundary;
    this.originalType = type;
    this.contentType = MediaType.parse(type + "; boundary=" + boundary.utf8());
    this.parts = Util.immutableList(parts);
  }

  public MediaType type() {
    return originalType;
  }

  public String boundary() {
    return boundary.utf8();
  }

  /** The number of parts in this multipart body. */
  //multipart 的数量
  public int size() {
    return parts.size();
  }
 //多个parts
  public List<Part> parts() {
    return parts;
  }
 
  public Part part(int index) {
    return parts.get(index);
  }

  /** A combination of {@link #type()} and {@link #boundary()}. */
  //MultiPart的内容类型
  @Override public MediaType contentType() {
    return contentType;
  }

  @Override public long contentLength() throws IOException {
    long result = contentLength;
    if (result != -1L) return result;
    return contentLength = writeOrCountBytes(null, true);
  }
  //将每个part写入BufferedSink中,传输
  @Override public void writeTo(BufferedSink sink) throws IOException {
    writeOrCountBytes(sink, false);
  }

  /**
   * Either writes this request to {@code sink} or measures its content length. We have one method
   * do double-duty to make sure the counting and content are consistent, particularly when it comes
   * to awkward operations like measuring the encoded length of header strings, or the
   * length-in-digits of an encoded integer.
   */
  //将每个Part的内容都写入,MultiPartBody的BufferedSink 中
  private long writeOrCountBytes(@Nullable BufferedSink sink, boolean countBytes) throws IOException {
    long byteCount = 0L;

    Buffer byteCountBuffer = null;
    if (countBytes) {
      sink = byteCountBuffer = new Buffer();
    }
   //写每个part
    for (int p = 0, partCount = parts.size(); p < partCount; p++) {
      Part part = parts.get(p);
     //Part的Headers和RequestBody 
      Headers headers = part.headers;
      RequestBody body = part.body;

      sink.write(DASHDASH);
      sink.write(boundary);
      sink.write(CRLF);

      //Part的Headers写入sink
      if (headers != null) {
     
        for (int h = 0, headerCount = headers.size(); h < headerCount; h++) {
          sink.writeUtf8(headers.name(h))
              .write(COLONSPACE)
              .writeUtf8(headers.value(h))
              .write(CRLF);
        }
      }
      //Part的RequestBody写入Part
     //1,写contentType 
      MediaType contentType = body.contentType();
      if (contentType != null) {
        sink.writeUtf8("Content-Type: ")
            .writeUtf8(contentType.toString())
            .write(CRLF);
      }
     //2,写contentLength 
      long contentLength = body.contentLength();
      if (contentLength != -1) {
        sink.writeUtf8("Content-Length: ")
            .writeDecimalLong(contentLength)
            .write(CRLF);
      } else if (countBytes) {
        // We can't measure the body's size without the sizes of its components.
        byteCountBuffer.clear();
        return -1L;
      }

      sink.write(CRLF);
      //3,写body体
      if (countBytes) {
        byteCount += contentLength;
      } else {
        body.writeTo(sink);
      }

      sink.write(CRLF);
    }

    sink.write(DASHDASH);
    sink.write(boundary);
    sink.write(DASHDASH);
    sink.write(CRLF);

    if (countBytes) {
      byteCount += byteCountBuffer.size();
      byteCountBuffer.clear();
    }

    return byteCount;
  }

  /**
   * Appends a quoted-string to a StringBuilder.
   *
   * <p>RFC 2388 is rather vague about how one should escape special characters in form-data
   * parameters, and as it turns out Firefox and Chrome actually do rather different things, and
   * both say in their comments that they're not really sure what the right approach is. We go with
   * Chrome's behavior (which also experimentally seems to match what IE does), but if you actually
   * want to have a good chance of things working, please avoid double-quotes, newlines, percent
   * signs, and the like in your field names.
   */
    //装换换行符,tab符号,引号
  static StringBuilder appendQuotedString(StringBuilder target, String key) {
    target.append('"');
    for (int i = 0, len = key.length(); i < len; i++) {
      char ch = key.charAt(i);
      switch (ch) {
        case '\n':
          target.append("%0A");
          break;
        case '\r':
          target.append("%0D");
          break;
        case '"':
          target.append("%22");
          break;
        default:
          target.append(ch);
          break;
      }
    }
    target.append('"');
    return target;
  }
 //Part 的定义,Part 是由Headers+RequestBody组成
  public static final class Part {
    public static Part create(RequestBody body) {
      return create(null, body);
    }

    public static Part create(@Nullable Headers headers, RequestBody body) {
      if (body == null) {
        throw new NullPointerException("body == null");
      }
      //Part的headers不能存在Content-Type和Content-Length字段
      if (headers != null && headers.get("Content-Type") != null) {
        throw new IllegalArgumentException("Unexpected header: Content-Type");
      }
      if (headers != null && headers.get("Content-Length") != null) {
        throw new IllegalArgumentException("Unexpected header: Content-Length");
      }
      return new Part(headers, body);
    }
    //创建key-value的Part,name其实就是key
    public static Part createFormData(String name, String value) {
      return createFormData(name, null, RequestBody.create(null, value));
    }
    //创建key-value的Part
    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=");
      // disposition =  form-data; name=name;
      appendQuotedString(disposition, name);//对name中的特殊符号转换

      if (filename != null) {
        disposition.append("; filename=");
        // disposition =  form-data; name=name; filename=filename;
        appendQuotedString(disposition, filename);//对filename中的特殊符号转换
      }
     //创建Part 体,Headers(Content-Disposition- form-data; name=name; filename=filename)+body
      return create(Headers.of("Content-Disposition", disposition.toString()), body);
    }
    //headers
    final @Nullable Headers headers;
    //body
    final RequestBody body;

    private Part(@Nullable Headers headers, RequestBody body) {
      this.headers = headers;
      this.body = body;
    }
   //Part的headers
    public @Nullable Headers headers() {
      return headers;
    }
    //Part的body体
    public RequestBody body() {
      return body;
    }
  }

  public static final class Builder {
    private final ByteString boundary;
    private MediaType type = MIXED;
    private final List<Part> parts = new ArrayList<>();

    public Builder() {
      this(UUID.randomUUID().toString());
    }

    public Builder(String boundary) {
      this.boundary = ByteString.encodeUtf8(boundary);
    }

    /**
     * Set the MIME type. Expected values for {@code type} are {@link #MIXED} (the default), {@link
     * #ALTERNATIVE}, {@link #DIGEST}, {@link #PARALLEL} and {@link #FORM}.
     */
    public Builder setType(MediaType type) {
      if (type == null) {
        throw new NullPointerException("type == null");
      }
      if (!type.type().equals("multipart")) {
        throw new IllegalArgumentException("multipart != " + type);
      }
      this.type = type;
      return this;
    }

    /** Add a part to the body. */
   //添加Part
    public Builder addPart(RequestBody body) {
      return addPart(Part.create(body));
    }

    /** Add a part to the body. */
    //添加Part
    public Builder addPart(@Nullable Headers headers, RequestBody body) {
      return addPart(Part.create(headers, body));
    }

    /** Add a form data part to the body. */
   //添加表单数据Part
    public Builder addFormDataPart(String name, String value) {
      return addPart(Part.createFormData(name, value));
    }

    /** Add a form data part to the body. */
    //添加表单数据Part
    public Builder addFormDataPart(String name, @Nullable String filename, RequestBody body) {
      return addPart(Part.createFormData(name, filename, body));
    }

    /** Add a part to the body. */
    public Builder addPart(Part part) {
      if (part == null) throw new NullPointerException("part == null");
      parts.add(part);
      return this;
    }

    /** Assemble the specified parts into a request body. */
    public MultipartBody build() {
      if (parts.isEmpty()) {
        throw new IllegalStateException("Multipart body must have at least one part.");
      }
     //构建MultipartBody对象
      return new MultipartBody(boundary, type, parts);
    }
  }
}

总结一下MultipartBody:

  1. MultipartBody本质一个是一个RequestBody,具有自己的contentType+BufferedSink,是POST请求的最外层封装,需要添加多个Part
  2. Part对象组成:Headers+RequestBody。是MultipartBody的成员变量,需要写入MultipartBody的BufferedSink中。

HTTP真正的上传文件

  1. 最基本的上传文件:

重点:RequestBody create(MediaType contentType, final File file)构造文件请求体RequestBody ,并且添加到MultiPartBody中

OkHttpClient client = new OkHttpClient();
        // form 表单形式上传,MultipartBody的内容类型是表单格式,multipart/form-data
        MultipartBody.Builder urlBuilder= new MultipartBody.Builder().setType(MultipartBody.FORM);
      
      //参数
       HashMap<String,String> params = new HashMap<>();
       if (params != null) {
                for (String key : params.keySet()) {
                    if (params.get(key)!=null){
                        urlBuilder.addFormDataPart(key, params.get(key));
                    }
               }
            }
        //需要上传的文件,需要携带上传的文件(小型文件 不建议超过500K)
         HashMap<String,String> files= new HashMap<>();
         if (files != null) {
             for (String key : files.keySet()) {
                //重点:RequestBody create(MediaType contentType, final File file)构造文件请求体RequestBody 
                 urlBuilder.addFormDataPart(key, files.get(key).getName(), RequestBody.create(MediaType.parse("multipart/form-data"), files.get(key)));
               }
             }
           //构造请求request 
            Request request = new Request.Builder()
                            .headers(extraHeaders == null ? new Headers.Builder().build() : Headers.of(extraHeaders))
                            .url(url)
                            .post(urlBuilder.build())
                            .build();
         //异步执行请求
          newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                Log.i("lfq" ,"onFailure");
            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                //非主线程
                if (response.isSuccessful()) {
                    String str = response.body().string();
                    Log.i("tk", response.message() + " , body " + str);

                } else {
                    Log.i("tk" ,response.message() + " error : body " + response.body().string());
                }
            }
        });

2. 大文件分块异步上传
我们知道Post上传文件,简单的说就是将文件file封装成RequestBody体,然后添加到MultiPartBody的addPart中构造MultiPartBody所需要的Part对象(Headers+body),RequestBody是个抽象类,里面的所有create方法如下:

image.png

filebody.png

可以看出,基本都是重写了抽象类的RequestBody的三种方法,所以我们也可以继承实现自己的Body体:
image.png

EG:已上传相机图片(5M)为例,分块多线程异步同时上传,但是这种方法需要服务端接口才行。

//文件路径
String path = "xxx.jpg";

1,文件块对象

public static final int FILE_BLOCK_SIZE = 500 * 1024;//500k
 /*文件块描述*/
    public static class FileBlock {
        public long start;//起始字节位置
        public long end;//结束字节位置
        public int index;//文件分块索引
    }

2,文件切块

 //计算切块,存储在数组
 final SparseArray<FileBlock> blockArray = splitFile(path, FILE_BLOCK_SIZE);
 /**
     * 文件分块
     *
     * @param filePath  文件路径
     * @param blockSize 块大小
     *
     * @return 分块描述集合 文件不存在时返回空
     */
    public static SparseArray<FileBlock> splitFile(String filePath, long blockSize) {
        File file = new File(filePath);
        if (!file.exists()) {
            return null;
        }
        SparseArray<FileBlock> blockArray = new SparseArray<>();
        int i = 0;
        int start = 0;
        while (start < file.length()) {
            i++;
            FileBlock fileBlock = new FileBlock();
            fileBlock.index = i;
            fileBlock.start = start;
            start += blockSize;
            fileBlock.end = start;
            blockArray.put(i, fileBlock);
        }
        blockArray.get(i).end = file.length();
        return blockArray;
    }

3,对文件块分块多线程异步上传
服务端的接口:

url:domain/sync/img/upload
method: POST
//请求参数
data = {
        'img_md5': 'dddddsds',
        'total': 10, #总的分片数
        'index': 5, #该分片所在的位置, start by 1
    }
请求返回值json:
 {
        'status': 206/205/400/409/500,
        'msg': '分片上传成功/上传图片成功/参数错误/上传数据重复/上传失败'
        'data': {  # 205时有此字段
            'img_url': 'https://foo.jpg',
        }
    }

只需要图片的md5,总的分片数,该分片的位置,当一块传输成功时返回206,当全部块传完成是返回206,并返回该图片在服务器的url
服务端接口返回解析类:

/**
     * 分片上传部分的接口返回
     *
     * @link {http://10.16.69.11:5000/iSync/iSync%E6%9C%8D%E5%8A%A1%E7%AB%AFv4%E6%96%87%E6%A1%A3/index.html#4_1}
     */
    public static class ChuckUploadData  implements Serializable {
        public ChuckUploadBean data;
        public static class ChuckUploadBean implements Serializable{
            public String img_url;
        }
        /** 此块是否上传成功 */
        public boolean isPicSuccess() {
            return status == 206 || status == 409;
        }

        /** 全部原图是否上传成功 */
        public boolean isAllPicSuccess() {
            return status == 205;
        }

        public boolean isRepitition(){
            return status == 409;
        }

    }
   //上传图片的线程池
   ExcutorService threadPool =  Executors.newCachedThreadPool();
   //上传函数
 /**
     * 上传原图,异步上传
     *
     * @param httpCallback 回调接口
     * @param md5         文件md5
     * @param path         图片路径
     * @param total        总块数
     * @param index        分块索引
     * @param start        分块开始位置
     * @param end          分块结束位置
     */
    public static void uploadBigImage(String userId, final HttpListenerAdapter<ChuckUploadData> httpCallback, String md5, String path, int total, int index, long start, long end) {
        HashMap<String, String> params = new HashMap<String, String>();
        params.put("img_uuid", uuid);//完整文件的md5
        params.put("total", String.valueOf(total));//总的分片数
        params.put("index", String.valueOf(index));//当前分片位置,从1开始
        //全局单例OKHttpClient
        OkHttpClient httpClient = DataProvider.getInstance().inkApi.getLongWaitHttpClient();

        Runnable httpUploadRunnable = HttpRunnableFactory.newPostFileBlockRunnable(
                httpClient,
                upload_url,//上传url,自定义
                null,
                params,//上传参数
                "image",
                new File(path),//图片文件
                start,//index块开始的位置
                end,//index块结束的位置
                ChuckUploadData.class,
                httpCallback);//回调函数
        threadManager.submit httpUploadRunnable );
    }
 /**
     * 异步post请求 表单方式拆块上传大型文件用,构造Runnable 
     *
     * @param httpClient  okhttp客户端
     * @param url         请求地址
     * @param headers     额外添加的header(通用header由中断器统一添加)
     * @param params      请求参数
     * @param fileKey     文件的接收用key
     * @param file        大型文件对象
     * @param seekStart   起始字节
     * @param seekEnd     结束字节
     * @param cls         返回结果需要序列化的类型
     * @param listener    异步回调
     * @param <T>         返回结果需要序列化的类型声明
     *
     * @return 异步post请求用的默认Runnable
     */
    public static <T> Runnable newPostFileBlockRunnable(final OkHttpClient httpClient,  final String url, final Map<String, String> headers, final Map<String, String> params, final String fileKey, final File file, final long seekStart, final long seekEnd, final Class<T> cls, final HttpListenerAdapter<T> listener) {
        return new Runnable () {
            @Override
            public void run() {
                Log.e("http", "---postfile---");
                Log.e("http", "url: " + url);
                Log.e("http", "extraHeaders: " + headers);
                Log.e("http", "params: " + params);
                Log.e("http", "filepath: " + file.getPath());
                Log.e("http", "seekStart: " + seekStart);
                Log.e("http", "seekEnd: " + seekEnd);

                Call call = null;
                if (listener != null) {
                    listener.onStart(call);
                }
                try {
                    if (TextUtils.isEmpty(url)) {
                        throw new InterruptedException("url is null exception");
                    }
                     //构造path文件的index块的seekStart到seekEnd的请求体requestBody ,添加到MultiPartBody中
                    RequestBody requestBody = new RequestBody() {
                        @Override
                        public MediaType contentType() {
                           //请求体的内容类型
                            return MediaType.parse("multipart/form-data");
                        }

                        @Override
                        public void writeTo(BufferedSink sink) throws IOException {
                            //切块上传
                            long nowSeek = seekStart;
                            long seekEndWrite = seekEnd;
                            if (seekEndWrite == 0) {
                                seekEndWrite = file.length();
                            }
                            //跳到开始位置
                            FileInputStream in = new FileInputStream(file);
                            if (seekStart > 0) {
                                long amt = in.skip(seekStart);
                                if (amt == -1) {
                                    nowSeek = 0;
                                }
                            }
                            //将该块的字节内容写入body的BufferedSink 中
                            int len;
                            byte[] buf = new byte[BUFFER_SIZE_DEFAULT];
                            while ((len = in.read(buf)) >= 0 && nowSeek < seekEndWrite) {
                                sink.write(buf, 0, len);
                                nowSeek += len;
                                if (nowSeek + BUFFER_SIZE_DEFAULT > seekEndWrite) {
                                    buf = new byte[Integer.valueOf((seekEndWrite - nowSeek) + "")];
                                }
                            }
                            closeStream(in);
                        }

                    };
                    //组装其它参数
                    MultipartBody.Builder urlBuilder = new MultipartBody.Builder()
                            .setType(MultipartBody.FORM);
                    if (params != null) {
                        for (String key : params.keySet()) {
                            //urlBuilder.addFormDataPart(key, params.get(key));
                            if (params.get(key)!=null){
                                urlBuilder.addFormDataPart(key, params.get(key));
                            }
                        }
                    }
                    //把文件块的请求体添加到MultiPartBody中
                    urlBuilder.addFormDataPart(fileKey, file.getName(), requestBody);
                    Request request = new Request.Builder()
                            .headers(headers == null ? new Headers.Builder().build() : Headers.of(headers))
                            .url(url)
                            .post(urlBuilder.build())
                            .build();

                    call = httpClient.newCall(request);
                    //虽说是同步调用call.execute(),但是此Http请求过程是在线程池中的,相当于异步调用
                    try (Response response = call.execute()) {
                        if (!response.isSuccessful()){
                            throw new IOException("Unexpected code " + response.code());
                        }
                         /*打印json串,json样式的*/
                        String json = response.body().string();
                        //解析返回的响应json
                        T result = JsonUtils.getObjFromStr(cls, json);
                        if (listener != null) {
                            //防止回调内的业务逻辑引起二次onFailure回调
                            try {
                                listener.onResponse(call, result);
                            } catch (Exception e) {
                                e.printStackTrace();
                            }
                        }
                    } finally {
                       
                    }
                } catch (Exception e) {
                    if (listener != null) {
                        //中途取消导致的中断
                        if (call != null && call.isCanceled()) {
                            listener.onCancel(call);
                        } else {
                            //其它意义上的请求失败
                            listener.onFailure(call, e);
                        }
                    }
                } finally {
                    if (listener != null) {
                        listener.onEnd(call);
                    }
                }
            }
        };
    }
//循环遍历所有的文章块,多线程上传
 for (int i = 0; i < blockArray.size(); i++) {
             //异步分块上传
              final FileUtil.FileBlock block = blockArray.get(i + 1);
              //提交线程池,异步上传单块
            uploadBigImage(userId, new HttpListenerAdapter<ChuckUploadData>() {
                         @Override
                         public void onResponse(Call call, SyncBeans.ChuckUploadData bean) {
                              try {
                                   //单块上传
                                    if (bean != null ) {
                                          if (bean.isPicSuccess()) {
                                                //205,单块成功不做处理
                                           } else if (bean.isAllPicSuccess()) {
                                               //206,全部成功
                                           }
                                      }
                                }catch(Exception e){}
                            },uuid, mediaBean.imageNativeUrl, blockArray.size(), block.index, block.start, block.end);
     }           

5. OKHttp下载文件,并通知进度

下载文件的原理其实很简单,下载过程其实就是一个GET过程(上传文件是POST过程相对应),下载文件需要在异步线程中执行(方法有二,1,使用okhttp的call.enquene()方法异步执行,2,使用call.excute()同步方法,但是在线程次中执行整个请求过程),在成功响应之后,获得网络文件输入流InputStream,然后循环读取输入流上的文件,写入文件输出流。

/**
     * @param url 下载连接
     * @param saveDir 储存下载文件的SDCard目录
     * @param params url携带参数
     * @param extraHeaders 请求携带其他的要求的headers
     * @param listener 下载监听
     */
    public void download(final String url, final String saveDir,HashMap<String,String> params, HashMap<String,String> extraHeaders,final OnDownloadListener listener) {
       //构造请求Url
       HttpUrl.Builder urlBuilder = HttpUrl.parse(url).newBuilder();
           if (params != null) {
               for (String key : params.keySet()) {
                   if (params.get(key)!=null){
                      urlBuilder.setQueryParameter(key, params.get(key));//非必须
                     } 
              }
            }
        //构造请求request
         Request request = new Request.Builder()
                            .url(urlBuilder.build())
                            .headers(extraHeaders == null ? new Headers.Builder().build() : Headers.of(extraHeaders))//headers非必须
                            .get()
                            .build();
       //异步执行请求
        okHttpClient.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                // 下载失败
                listener.onDownloadFailed();
            }
            @Override
            public void onResponse(Call call, Response response) throws IOException {
               //非主线程
                InputStream is = null;
                byte[] buf = new byte[2048];
                int len = 0;
                FileOutputStream fos = null;
                // 储存下载文件的目录
                String savePath = isExistDir(saveDir);
                try {
                    //获取响应的字节流
                    is = response.body().byteStream();
                    //文件的总大小
                    long total = response.body().contentLength();
                    File file = new File(savePath);
                    fos = new FileOutputStream(file);
                    long sum = 0;
                   //循环读取输入流
                    while ((len = is.read(buf)) != -1) {
                        fos.write(buf, 0, len);
                        sum += len;
                        int progress = (int) (sum * 1.0f / total * 100);
                        // 下载中
                        if(listener != null){
                            listener.onDownloading(progress);
                         }
                       
                    }
                    fos.flush();
                    // 下载完成
                    if(listener != null){
                          listener.onDownloadSuccess();
                     }
                
                } catch (Exception e) {
                     if(listener != null){
                          listener.onDownloadFailed();
                     }
                   
                } finally {
                    try {
                        if (is != null)
                            is.close();
                    } catch (IOException e) {
                    }
                    try {
                        if (fos != null)
                            fos.close();
                    } catch (IOException e) {
                    }
                }
            }
        });
    }

至此,OKHTTP3的基本网络请求访问,发送GET请求,发送POST请求,基本上传文件,切块多线程异步上传文件,下载文件就到这里了,其实下载文件还可以做成断点续传,获取每次的seek点

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,529评论 5 475
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,015评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,409评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,385评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,387评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,466评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,880评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,528评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,727评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,528评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,602评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,302评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,873评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,890评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,132评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,777评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,310评论 2 342

推荐阅读更多精彩内容