OkHttp3簡單使用教程(一):請求和響應

weixin_34138377發表於2018-01-13

一,HTTP請求、響應報文格式

要弄明白網路框架,首先需要先掌握Http請求的,響應的報文格式。

HTTP請求報文格式:

HTTP請求報文主要由請求行、請求頭部、請求正文3部分組成.

9984264-ba41ac3ebe82326f.png
request.png

  1. 請求行:由請求方法,URL,協議版本三部分構成,之間用空格隔開
    請求方法包括:POST、GET、HEAD、PUT、POST、TRACE、OPTIONS、DELETE
    協議版本:HTTP/主版本號.次版本號,常用的有HTTP/1.0和HTTP/1.1
    9984264-51b34ce4a677fc11.png
    請求方法.png
  2. 請求頭部:
    請求頭部為請求報文新增了一些附加資訊,由“名/值”對組成,每行一對,名和值之間使用冒號分隔
    常見請求頭如下:
    Host ----接受請求的伺服器地址,可以是IP:埠號,也可以是域名
    User-Agent ----傳送請求的應用程式名稱
    Connection ---- 指定與連線相關的屬性,如Connection:Keep-Alive
    Accept-Charset ---- 通知服務端可以傳送的編碼格式
    Accept-Encoding ---- 通知服務端可以傳送的資料壓縮格式
    Accept-Language ---- 通知服務端可以傳送的語言
  3. 請求正文
    可選部分,比如GET請求就沒有請求正文
  4. 請求示例
    9984264-a6497b3fa08339bf.png
    image.png
HTTP響應報文格式:

HTTP響應報文主要由狀態行、響應頭部、響應正文3部分組成

9984264-265b8fdfdf361fce.png
響應報文.png

  1. 狀態行
    由3部分組成,分別為:協議版本,狀態碼,狀態碼描述,之間由空格分隔
    狀態碼:為3位數字,200-299的狀態碼錶示成功,300-399的狀態碼指資源重定向,400-499的狀態碼指客戶端請求出錯,500-599的狀態碼指服務端出錯(HTTP/1.1向協議中引入了資訊性狀態碼,範圍為100-199)
    常見的:
    200:響應成功
    302:重定向跳轉,跳轉地址通過響應頭中的Location屬性指定
    400:客戶端請求有語法錯誤,引數錯誤,不能被伺服器識別
    403:伺服器接收到請求,但是拒絕提供服務(認證失敗)
    404:請求資源不存在
    500:伺服器內部錯誤

    9984264-f61ff41047341152.png
    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. 響應示例
    9984264-ecfd3747b6b7acaa.png
    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方法如下:

9984264-6974018e791a1e9d.png
image.png

9984264-71eecb9fd8bed880.png
filebody.png

可以看出,基本都是重寫了抽象類的RequestBody的三種方法,所以我們也可以繼承實現自己的Body體:
9984264-bc847fae711a4d24.png
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點

相關文章