OkHttp3簡單使用教程(一):請(qǐng)求和響應(yīng)

一楼肪,HTTP請(qǐng)求、響應(yīng)報(bào)文格式

要弄明白網(wǎng)絡(luò)框架汉柒,首先需要先掌握Http請(qǐng)求的误褪,響應(yīng)的報(bào)文格式。

HTTP請(qǐng)求報(bào)文格式:

HTTP請(qǐng)求報(bào)文主要由請(qǐng)求行碾褂、請(qǐng)求頭部兽间、請(qǐng)求正文3部分組成.

request.png

  1. 請(qǐng)求行:由請(qǐng)求方法,URL正塌,協(xié)議版本三部分構(gòu)成嘀略,之間用空格隔開
    請(qǐng)求方法包括:POST、GET乓诽、HEAD帜羊、PUT、POST鸠天、TRACE讼育、OPTIONS、DELETE
    協(xié)議版本:HTTP/主版本號(hào).次版本號(hào)稠集,常用的有HTTP/1.0和HTTP/1.1
    請(qǐng)求方法.png
  2. 請(qǐng)求頭部:
    請(qǐng)求頭部為請(qǐng)求報(bào)文添加了一些附加信息奶段,由“名/值”對(duì)組成,每行一對(duì)剥纷,名和值之間使用冒號(hào)分隔
    常見請(qǐng)求頭如下:
    Host ----接受請(qǐng)求的服務(wù)器地址痹籍,可以是IP:端口號(hào),也可以是域名
    User-Agent ----發(fā)送請(qǐng)求的應(yīng)用程序名稱
    Connection ---- 指定與連接相關(guān)的屬性晦鞋,如Connection:Keep-Alive
    Accept-Charset ---- 通知服務(wù)端可以發(fā)送的編碼格式
    Accept-Encoding ---- 通知服務(wù)端可以發(fā)送的數(shù)據(jù)壓縮格式
    Accept-Language ---- 通知服務(wù)端可以發(fā)送的語言
  3. 請(qǐng)求正文
    可選部分词裤,比如GET請(qǐng)求就沒有請(qǐng)求正文
  4. 請(qǐng)求示例
    image.png
HTTP響應(yīng)報(bào)文格式:

HTTP響應(yīng)報(bào)文主要由狀態(tài)行刺洒、響應(yīng)頭部、響應(yīng)正文3部分組成

響應(yīng)報(bào)文.png

  1. 狀態(tài)行
    由3部分組成吼砂,分別為:協(xié)議版本逆航,狀態(tài)碼,狀態(tài)碼描述渔肩,之間由空格分隔
    狀態(tài)碼:為3位數(shù)字因俐,200-299的狀態(tài)碼表示成功,300-399的狀態(tài)碼指資源重定向周偎,400-499的狀態(tài)碼指客戶端請(qǐng)求出錯(cuò)抹剩,500-599的狀態(tài)碼指服務(wù)端出錯(cuò)(HTTP/1.1向協(xié)議中引入了信息性狀態(tài)碼,范圍為100-199)
    常見的:
    200:響應(yīng)成功
    302:重定向跳轉(zhuǎn)蓉坎,跳轉(zhuǎn)地址通過響應(yīng)頭中的Location屬性指定
    400:客戶端請(qǐng)求有語法錯(cuò)誤澳眷,參數(shù)錯(cuò)誤,不能被服務(wù)器識(shí)別
    403:服務(wù)器接收到請(qǐng)求蛉艾,但是拒絕提供服務(wù)(認(rèn)證失斍弧)
    404:請(qǐng)求資源不存在
    500:服務(wù)器內(nèi)部錯(cuò)誤

    image.png

  2. 響應(yīng)頭部 :
    與請(qǐng)求頭部類似,為響應(yīng)報(bào)文添加了一些附加信息
    Server - 服務(wù)器應(yīng)用程序軟件的名稱和版本
    Content-Type - 響應(yīng)正文的類型(是圖片還是二進(jìn)制字符串)
    Content-Length - 響應(yīng)正文長度
    Content-Charset - 響應(yīng)正文使用的編碼
    Content-Encoding - 響應(yīng)正文使用的數(shù)據(jù)壓縮格式
    Content-Language - 響應(yīng)正文使用的語言

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. 響應(yīng)正文
    是請(qǐng)求響應(yīng)的最終結(jié)果勿侯,都在響應(yīng)體里拓瞪。
    報(bào)文可以承載很多類型的數(shù)字?jǐn)?shù)據(jù):圖片、視頻助琐、HTML文檔祭埂、軟件應(yīng)用程序等
  2. 響應(yīng)示例
    image.png

二,HTTP請(qǐng)求和響應(yīng)的基本使用

主要包含:

  • 一般的get請(qǐng)求
  • 一般的post請(qǐng)求
  • 基于Http的文件上傳
  • 文件下載
  • 加載圖片
  • 支持請(qǐng)求回調(diào)兵钮,直接返回對(duì)象蛆橡、對(duì)象集合
  • 支持session的保持
  1. 添加網(wǎng)絡(luò)訪問權(quán)限并添加庫依賴
  <uses-permission android:name="android.permission.INTERNET" />
api 'com.squareup.okhttp3:okhttp:3.9.0'
  1. HTTP的GET請(qǐng)求
//1,創(chuàng)建okHttpClient對(duì)象
OkHttpClient mOkHttpClient = new OkHttpClient();
//2,創(chuàng)建一個(gè)Request
final Request request = new Request.Builder()
                .url("https://www.baidu.com")
                .build();
//3,新建一個(gè)call對(duì)象
Call call = mOkHttpClient.newCall(request); 
//4,請(qǐng)求加入調(diào)度掘譬,這里是異步Get請(qǐng)求回調(diào)
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();
            }
        });             

對(duì)以上的簡單請(qǐng)求的構(gòu)成:

  • 發(fā)送一個(gè)GET請(qǐng)求的步驟泰演,首先構(gòu)造一個(gè)Request對(duì)象,參數(shù)最起碼有個(gè)URL屁药,當(dāng)然也可以通過Request.Builder設(shè)置更多的參數(shù)比如:header粥血、method等柏锄。
//URL帶的參數(shù)
HashMap<String,String> params = new HashMap<>();
//GET 請(qǐng)求帶的Header
HashMap<String,String> headers= new HashMap<>();
//HttpUrl.Builder構(gòu)造帶參數(shù)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的對(duì)象去構(gòu)造得到一個(gè)Call對(duì)象酿箭,類似于將你的請(qǐng)求封裝成了任務(wù),既然是任務(wù)趾娃,就會(huì)有execute(),enqueue()和cancel()等方法缭嫡。
    execute():同步GET請(qǐng)求
 //同步
Response response = call.execute()
if(response.isSuccessful()){
     //響應(yīng)成功
}

enqueue():異步GET請(qǐng)求,將call加入調(diào)度隊(duì)列抬闷,然后等待任務(wù)執(zhí)行完成妇蛀,我們?cè)贑allback中即可得到結(jié)果耕突。
cancel():Call請(qǐng)求的取消,okHttp支持請(qǐng)求取消功能评架,當(dāng)調(diào)用請(qǐng)求的cancel()時(shí)眷茁,請(qǐng)求就會(huì)被取消,拋出異常纵诞。又是需要監(jiān)控許多Http請(qǐng)求的執(zhí)行情況上祈,可以把這些請(qǐng)求的Call搜集起來,執(zhí)行完畢自動(dòng)剔除浙芙,如果在請(qǐng)求執(zhí)行過程中(如下載)登刺,想取消執(zhí)行,可使用call.cancel()取消嗡呼。

  • 請(qǐng)求的響應(yīng)Response
    對(duì)于同步GET請(qǐng)求纸俭,Response對(duì)象是直接返回的。異步GET請(qǐng)求南窗,通過onResponse回調(diào)方法傳參數(shù)揍很,需要注意的是這個(gè)onResponse回調(diào)方法不是在主線程回調(diào),可以使用runInUIThread(new Runnable(){})矾瘾。
    我們希望獲得返回的字符串女轿,可以通過response.body().string()獲取壕翩;
    如果希望獲得返回的二進(jìn)制字節(jié)數(shù)組蛉迹,則調(diào)用response.body().bytes()
    如果你想拿到返回的inputStream放妈,則調(diào)用response.body().byteStream()

3. HTTP的POST請(qǐng)求
看來上面的簡單的get請(qǐng)求北救,基本上整個(gè)的用法也就掌握了,比如post攜帶參數(shù)芜抒,也僅僅是Request的構(gòu)造的不同珍策。

   //POST參數(shù)構(gòu)造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));

                        }
                    }
// 構(gòu)造Request->call->執(zhí)行
 Request request = new Request.Builder()
                            .headers(extraHeaders == null ? new Headers.Builder().build() : Headers.of(extraHeaders))//extraHeaders 是用戶添加頭
                            .url(url)
                            .post(urlBuilder.build())//參數(shù)放在body體里
                            .build();
Call call = httpClient.newCall(request);
 try (Response response = call.execute()) {
            if (response.isSuccessful()){
             //響應(yīng)成功
             }
  }

Post的時(shí)候宅倒,參數(shù)是包含在請(qǐng)求體中的攘宙,所以我們通過MultipartBody.Builder 添加多個(gè)String鍵值對(duì),然后去構(gòu)造RequestBody拐迁,最后完成我們Request的構(gòu)造蹭劈。
4. OKHTTP的上傳文件
上傳文件本身也是一個(gè)POST請(qǐng)求。在上面的POST請(qǐng)求中可以知道线召,POST請(qǐng)求的所有參數(shù)都是在BODY體中的铺韧,我們看看請(qǐng)求體的源碼RequestBody:請(qǐng)求體=contentType + BufferedSink
RequestBody

//抽象類請(qǐng)求體,**請(qǐng)求體=contentType + BufferedSink**
public abstract class RequestBody {
  /** Returns the Content-Type header for this body. */
 //返回Body體的內(nèi)容類型
  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的字節(jié)長度
  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.
   */
   //創(chuàng)建一個(gè)請(qǐng)求體缓淹,如果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");
      }
    }
    //按字符集變成字節(jié)
    byte[] bytes = content.getBytes(charset);
    return create(contentType, bytes);
  }

  /** Returns a new request body that transmits {@code content}. */
 //創(chuàng)建新的請(qǐng)求體塔逃,傳輸字節(jié)
  public static RequestBody create(
      final @Nullable MediaType contentType, final ByteString content) {
    return new RequestBody() {
      @Override public @Nullable MediaType contentType() {
        //請(qǐng)求體需要的內(nèi)容類型
        return contentType;
      }

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

      @Override public void writeTo(BufferedSink sink) throws IOException {
       //將需要傳輸?shù)淖止?jié),寫入緩存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}. */
  //創(chuàng)建一個(gè)請(qǐng)求體料仗,傳輸文件file內(nèi)容湾盗,其實(shí)就是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請(qǐng)求中Content-Type
客戶端在進(jìn)行http請(qǐng)求服務(wù)器的時(shí)候,需要告訴服務(wù)器請(qǐng)求的類型立轧,服務(wù)器在返回給客戶端的數(shù)據(jù)的時(shí)候淹仑,也需要告訴客戶端返回?cái)?shù)據(jù)的類型
默認(rèn)的ContentType為 text/html 也就是網(wǎng)頁格式. 常用的內(nèi)容類型

  • 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數(shù)據(jù)格式
  • application/atom+xml :Atom XML聚合格式
  • application/json : JSON數(shù)據(jù)格式
  • application/pdf :pdf格式
  • application/msword : Word文檔格式
  • application/octet-stream : 二進(jìn)制流數(shù)據(jù)(如常見的文件下載)
    MultipartBody.Builder 添加多個(gè)String鍵值對(duì)
//MultipartBody源碼,MultipartBody其實(shí)也是RequestBody 肺孵,需要在此RequestBody 體內(nèi)匀借,添加多個(gè)Part
/** An <a >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".
   */
  //混合的內(nèi)容類型
  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;

 //請(qǐng)求體的內(nèi)容類型
  private final MediaType contentType;
  //MultiPartBody需要添加多個(gè)Part對(duì)象,一起請(qǐng)求
  private final List<Part> parts;
  private long contentLength = -1L;
  //構(gòu)造函數(shù)
  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 的數(shù)量
  public int size() {
    return parts.size();
  }
 //多個(gè)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的內(nèi)容類型
  @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);
  }
  //將每個(gè)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.
   */
  //將每個(gè)Part的內(nèi)容都寫入,MultiPartBody的BufferedSink 中
  private long writeOrCountBytes(@Nullable BufferedSink sink, boolean countBytes) throws IOException {
    long byteCount = 0L;

    Buffer byteCountBuffer = null;
    if (countBytes) {
      sink = byteCountBuffer = new Buffer();
    }
   //寫每個(gè)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符號(hào)是鬼,引號(hào)
  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);
    }
    //創(chuàng)建key-value的Part紫新,name其實(shí)就是key
    public static Part createFormData(String name, String value) {
      return createFormData(name, null, RequestBody.create(null, value));
    }
    //創(chuàng)建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);//對(duì)name中的特殊符號(hào)轉(zhuǎn)換

      if (filename != null) {
        disposition.append("; filename=");
        // disposition =  form-data; name=name; filename=filename;
        appendQuotedString(disposition, filename);//對(duì)filename中的特殊符號(hào)轉(zhuǎn)換
      }
     //創(chuàng)建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. */
   //添加表單數(shù)據(jù)Part
    public Builder addFormDataPart(String name, String value) {
      return addPart(Part.createFormData(name, value));
    }

    /** Add a form data part to the body. */
    //添加表單數(shù)據(jù)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.");
      }
     //構(gòu)建MultipartBody對(duì)象
      return new MultipartBody(boundary, type, parts);
    }
  }
}

總結(jié)一下MultipartBody:

  1. MultipartBody本質(zhì)一個(gè)是一個(gè)RequestBody,具有自己的contentType+BufferedSink芒率,是POST請(qǐng)求的最外層封裝囤耳,需要添加多個(gè)Part
  2. Part對(duì)象組成:Headers+RequestBody。是MultipartBody的成員變量偶芍,需要寫入MultipartBody的BufferedSink中充择。

HTTP真正的上傳文件

  1. 最基本的上傳文件:

重點(diǎn):RequestBody create(MediaType contentType, final File file)構(gòu)造文件請(qǐng)求體RequestBody ,并且添加到MultiPartBody中

OkHttpClient client = new OkHttpClient();
        // form 表單形式上傳,MultipartBody的內(nèi)容類型是表單格式,multipart/form-data
        MultipartBody.Builder urlBuilder= new MultipartBody.Builder().setType(MultipartBody.FORM);
      
      //參數(shù)
       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()) {
                //重點(diǎn):RequestBody create(MediaType contentType, final File file)構(gòu)造文件請(qǐng)求體RequestBody 
                 urlBuilder.addFormDataPart(key, files.get(key).getName(), RequestBody.create(MediaType.parse("multipart/form-data"), files.get(key)));
               }
             }
           //構(gòu)造請(qǐng)求request 
            Request request = new Request.Builder()
                            .headers(extraHeaders == null ? new Headers.Builder().build() : Headers.of(extraHeaders))
                            .url(url)
                            .post(urlBuilder.build())
                            .build();
         //異步執(zhí)行請(qǐng)求
          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中構(gòu)造MultiPartBody所需要的Part對(duì)象(Headers+body),RequestBody是個(gè)抽象類材彪,里面的所有create方法如下:

image.png

filebody.png

可以看出观挎,基本都是重寫了抽象類的RequestBody的三種方法,所以我們也可以繼承實(shí)現(xiàn)自己的Body體:
image.png

EG:已上傳相機(jī)圖片(5M)為例段化,分塊多線程異步同時(shí)上傳嘁捷,但是這種方法需要服務(wù)端接口才行。

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

1,文件塊對(duì)象

public static final int FILE_BLOCK_SIZE = 500 * 1024;//500k
 /*文件塊描述*/
    public static class FileBlock {
        public long start;//起始字節(jié)位置
        public long end;//結(jié)束字節(jié)位置
        public int index;//文件分塊索引
    }

2,文件切塊

 //計(jì)算切塊,存儲(chǔ)在數(shù)組
 final SparseArray<FileBlock> blockArray = splitFile(path, FILE_BLOCK_SIZE);
 /**
     * 文件分塊
     *
     * @param filePath  文件路徑
     * @param blockSize 塊大小
     *
     * @return 分塊描述集合 文件不存在時(shí)返回空
     */
    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,對(duì)文件塊分塊多線程異步上傳
服務(wù)端的接口:

url:domain/sync/img/upload
method: POST
//請(qǐng)求參數(shù)
data = {
        'img_md5': 'dddddsds',
        'total': 10, #總的分片數(shù)
        'index': 5, #該分片所在的位置, start by 1
    }
請(qǐng)求返回值json:
 {
        'status': 206/205/400/409/500,
        'msg': '分片上傳成功/上傳圖片成功/參數(shù)錯(cuò)誤/上傳數(shù)據(jù)重復(fù)/上傳失敗'
        'data': {  # 205時(shí)有此字段
            'img_url': 'https://foo.jpg',
        }
    }

只需要圖片的md5,總的分片數(shù)穗泵,該分片的位置普气,當(dāng)一塊傳輸成功時(shí)返回206谜疤,當(dāng)全部塊傳完成是返回206佃延,并返回該圖片在服務(wù)器的url
服務(wù)端接口返回解析類:

/**
     * 分片上傳部分的接口返回
     *
     * @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();
   //上傳函數(shù)
 /**
     * 上傳原圖,異步上傳
     *
     * @param httpCallback 回調(diào)接口
     * @param md5         文件md5
     * @param path         圖片路徑
     * @param total        總塊數(shù)
     * @param index        分塊索引
     * @param start        分塊開始位置
     * @param end          分塊結(jié)束位置
     */
    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));//總的分片數(shù)
        params.put("index", String.valueOf(index));//當(dāng)前分片位置现诀,從1開始
        //全局單例OKHttpClient
        OkHttpClient httpClient = DataProvider.getInstance().inkApi.getLongWaitHttpClient();

        Runnable httpUploadRunnable = HttpRunnableFactory.newPostFileBlockRunnable(
                httpClient,
                upload_url,//上傳url,自定義
                null,
                params,//上傳參數(shù)
                "image",
                new File(path),//圖片文件
                start,//index塊開始的位置
                end,//index塊結(jié)束的位置
                ChuckUploadData.class,
                httpCallback);//回調(diào)函數(shù)
        threadManager.submit httpUploadRunnable );
    }
 /**
     * 異步post請(qǐng)求 表單方式拆塊上傳大型文件用履肃,構(gòu)造Runnable 
     *
     * @param httpClient  okhttp客戶端
     * @param url         請(qǐng)求地址
     * @param headers     額外添加的header(通用header由中斷器統(tǒng)一添加)
     * @param params      請(qǐng)求參數(shù)
     * @param fileKey     文件的接收用key
     * @param file        大型文件對(duì)象
     * @param seekStart   起始字節(jié)
     * @param seekEnd     結(jié)束字節(jié)
     * @param cls         返回結(jié)果需要序列化的類型
     * @param listener    異步回調(diào)
     * @param <T>         返回結(jié)果需要序列化的類型聲明
     *
     * @return 異步post請(qǐng)求用的默認(rèn)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");
                    }
                     //構(gòu)造path文件的index塊的seekStart到seekEnd的請(qǐng)求體requestBody 仔沿,添加到MultiPartBody中
                    RequestBody requestBody = new RequestBody() {
                        @Override
                        public MediaType contentType() {
                           //請(qǐng)求體的內(nèi)容類型
                            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;
                                }
                            }
                            //將該塊的字節(jié)內(nèi)容寫入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);
                        }

                    };
                    //組裝其它參數(shù)
                    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));
                            }
                        }
                    }
                    //把文件塊的請(qǐng)求體添加到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);
                    //雖說是同步調(diào)用call.execute(),但是此Http請(qǐng)求過程是在線程池中的尺棋,相當(dāng)于異步調(diào)用
                    try (Response response = call.execute()) {
                        if (!response.isSuccessful()){
                            throw new IOException("Unexpected code " + response.code());
                        }
                         /*打印json串封锉,json樣式的*/
                        String json = response.body().string();
                        //解析返回的響應(yīng)json
                        T result = JsonUtils.getObjFromStr(cls, json);
                        if (listener != null) {
                            //防止回調(diào)內(nèi)的業(yè)務(wù)邏輯引起二次onFailure回調(diào)
                            try {
                                listener.onResponse(call, result);
                            } catch (Exception e) {
                                e.printStackTrace();
                            }
                        }
                    } finally {
                       
                    }
                } catch (Exception e) {
                    if (listener != null) {
                        //中途取消導(dǎo)致的中斷
                        if (call != null && call.isCanceled()) {
                            listener.onCancel(call);
                        } else {
                            //其它意義上的請(qǐng)求失敗
                            listener.onFailure(call, e);
                        }
                    }
                } finally {
                    if (listener != null) {
                        listener.onEnd(call);
                    }
                }
            }
        };
    }
//循環(huán)遍歷所有的文章塊,多線程上傳
 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下載文件成福,并通知進(jìn)度

下載文件的原理其實(shí)很簡單,下載過程其實(shí)就是一個(gè)GET過程(上傳文件是POST過程相對(duì)應(yīng))荆残,下載文件需要在異步線程中執(zhí)行(方法有二奴艾,1,使用okhttp的call.enquene()方法異步執(zhí)行内斯,2蕴潦,使用call.excute()同步方法,但是在線程次中執(zhí)行整個(gè)請(qǐng)求過程)俘闯,在成功響應(yīng)之后潭苞,獲得網(wǎng)絡(luò)文件輸入流InputStream,然后循環(huán)讀取輸入流上的文件真朗,寫入文件輸出流此疹。

/**
     * @param url 下載連接
     * @param saveDir 儲(chǔ)存下載文件的SDCard目錄
     * @param params url攜帶參數(shù)
     * @param extraHeaders 請(qǐng)求攜帶其他的要求的headers
     * @param listener 下載監(jiān)聽
     */
    public void download(final String url, final String saveDir,HashMap<String,String> params, HashMap<String,String> extraHeaders,final OnDownloadListener listener) {
       //構(gòu)造請(qǐng)求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));//非必須
                     } 
              }
            }
        //構(gòu)造請(qǐng)求request
         Request request = new Request.Builder()
                            .url(urlBuilder.build())
                            .headers(extraHeaders == null ? new Headers.Builder().build() : Headers.of(extraHeaders))//headers非必須
                            .get()
                            .build();
       //異步執(zhí)行請(qǐng)求
        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;
                // 儲(chǔ)存下載文件的目錄
                String savePath = isExistDir(saveDir);
                try {
                    //獲取響應(yīng)的字節(jié)流
                    is = response.body().byteStream();
                    //文件的總大小
                    long total = response.body().contentLength();
                    File file = new File(savePath);
                    fos = new FileOutputStream(file);
                    long sum = 0;
                   //循環(huán)讀取輸入流
                    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的基本網(wǎng)絡(luò)請(qǐng)求訪問秀菱,發(fā)送GET請(qǐng)求,發(fā)送POST請(qǐng)求蹭睡,基本上傳文件衍菱,切塊多線程異步上傳文件,下載文件就到這里了肩豁,其實(shí)下載文件還可以做成斷點(diǎn)續(xù)傳脊串,獲取每次的seek點(diǎn)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市清钥,隨后出現(xiàn)的幾起案子琼锋,更是在濱河造成了極大的恐慌,老刑警劉巖祟昭,帶你破解...
    沈念sama閱讀 218,858評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件缕坎,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡篡悟,警方通過查閱死者的電腦和手機(jī)谜叹,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門匾寝,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人荷腊,你說我怎么就攤上這事艳悔。” “怎么了女仰?”我有些...
    開封第一講書人閱讀 165,282評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵猜年,是天一觀的道長。 經(jīng)常有香客問我疾忍,道長乔外,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,842評(píng)論 1 295
  • 正文 為了忘掉前任一罩,我火速辦了婚禮袁稽,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘擒抛。我一直安慰自己推汽,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,857評(píng)論 6 392
  • 文/花漫 我一把揭開白布歧沪。 她就那樣靜靜地躺著歹撒,像睡著了一般。 火紅的嫁衣襯著肌膚如雪诊胞。 梳的紋絲不亂的頭發(fā)上暖夭,一...
    開封第一講書人閱讀 51,679評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音撵孤,去河邊找鬼迈着。 笑死,一個(gè)胖子當(dāng)著我的面吹牛邪码,可吹牛的內(nèi)容都是我干的裕菠。 我是一名探鬼主播,決...
    沈念sama閱讀 40,406評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼闭专,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼奴潘!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起影钉,我...
    開封第一講書人閱讀 39,311評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤画髓,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后平委,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體奈虾,經(jīng)...
    沈念sama閱讀 45,767評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了肉微。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片匾鸥。...
    茶點(diǎn)故事閱讀 40,090評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖浪册,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情岗照,我是刑警寧澤村象,帶...
    沈念sama閱讀 35,785評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站攒至,受9級(jí)特大地震影響厚者,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜迫吐,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,420評(píng)論 3 331
  • 文/蒙蒙 一库菲、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧志膀,春花似錦熙宇、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至戳稽,卻和暖如春馆蠕,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背惊奇。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評(píng)論 1 271
  • 我被黑心中介騙來泰國打工互躬, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人颂郎。 一個(gè)月前我還...
    沈念sama閱讀 48,298評(píng)論 3 372
  • 正文 我出身青樓吼渡,卻偏偏與公主長得像,于是被迫代替她去往敵國和親乓序。 傳聞我的和親對(duì)象是個(gè)殘疾皇子诞吱,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,033評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容