Android開發(fā)——你應該要知道 OkHttp

android機器人.jpg

前言:現(xiàn)在做 Android 開發(fā)澳叉,應該絕大部分都是用 OkHttp 來做網(wǎng)絡請求的成洗∑垦辏可見 OkHttp 的強大遥椿。今天來總結下 OkHttp 的使用冠场,OkHttp 中用到了大量的 Builder 模式碴裙,建議看此文章前先閱讀 Android開發(fā)---Builder 模式必知必會

OkHttp 學習之前,應該對 Http 有一定的了解还棱,可以參考下這里:你需要了解的HTTP知識都在這里了诱贿!

開頭引用官網(wǎng)的自我介紹:

HTTP是現(xiàn)代應用網(wǎng)絡的方式。這是我們如何交換數(shù)據(jù)和媒體凭豪。有效地進行HTTP使您的東西加載更快嫂伞,并節(jié)省帶寬帖努。

OkHttp 有以下特點:

  • HTTP / 2支持允許同一主機的所有請求共享套接字拼余。
  • 連接池減少請求延遲(如果HTTP / 2不可用)匙监。
  • 透明GZIP縮小下載大小亭姥。
  • 響應緩存可以避免重復請求的網(wǎng)絡达罗。

ok粮揉,我們開始來探究一下 OkHttp 的使用滔蝉。

首先添加依賴

compile 'com.squareup.okhttp3:okhttp:3.8.0'

OkHttp 發(fā)起網(wǎng)絡請求非常簡單蝠引,具體步驟如下:

  • 構建 OkHttpClient 客戶端
  • 構建 Request 請求
  • 創(chuàng)建 Call 對象螃概,發(fā)起網(wǎng)絡請求
  • 處理 Response 響應結果

這么看來吊洼,只要搞懂這幾個關鍵類冒窍,跟調用請求的過程综液。對于 OkHttp 的基本使用就沒問題了谬莹。

一附帽、Request

Request 對應于 Http 的請求報文蕉扮,查看源碼可以看到慢显,Request 封裝了請求報文的一些字段荚藻。

發(fā)起網(wǎng)絡請求必然要配置這些請求報文字段应狱,Request 采用 Builder 模式除嘹,通過 Builder 內部類使我們靈活去配置 Request 對象尉咕,默認初始化Request request = new Request.Builder().build();是配置了請求頭,請求方法GET璃岳,也可以在調用build()方法之前調用對應方法進行配置年缎。如果需要對 其中的某個字段做特殊配置悔捶,可以Request.Builder builder = request.newBuilder() 拿到 Builder 對象進行 "返廠重建"。

  • Http 請求地址 url
  • 請求方法单芜,例如(GET蜕该,POST等等),配置請求方法時調用get()堂淡,post()等,內部都是調用 method()方法傳入請求方法與請求體扒腕,并對是否需要請求體進行判斷绢淀。
  • Headers:維護 Http 消息的頭字段,內部也是通過 Builder 模式構建字符串鍵值對來維護消息頭袜匿。
  • Request初始化時默認構造了 Headers的 Builder 實例更啄,我們初始化Request可以調用header()方法設置請求頭的name稚疹,value字段居灯。其實內部是調用了Headers的 Builder 實例的set()方法設置的,這種方法設置的 head表示name是唯一的内狗。因為設置之前內部調用了removeAll()方法先移除了相同的name對應的value怪嫌。
  • 如果想添加多個name相同的請求頭,可以調用addHeader()方法進行設置柳沙,其實內部是調用了Headers的 Builder 實例的add()方法設置的
  • 也可以調用 removeHeader()方法移除同一個name的所有請求頭
  • 可以通過 Requesthead()方法獲得指定請求頭岩灭,·其實內部也是通過 Headersget()方法通過name獲取的valueheaders()方法獲得同一個name的所有value值赂鲤,以list形式返回
  • 請求體 RequestBody(一般用來向服務器提交數(shù)據(jù)請求時用到)
  • tag噪径,設置請求標簽,用于取消網(wǎng)絡請求
  • CacheControl 用于控制緩存使用数初。

二找爱、構建 RequestBody

用于攜帶數(shù)據(jù)請求服務器,一般就是在特定的請求才會攜帶請求頭泡孩,例如 POST车摄,PUT等。查看 RequestBody源碼可以看到有五個 static 方法用于創(chuàng)建 RequestBody實例仑鸥,第一個參數(shù)都是 MediaType 表示數(shù)據(jù)的 MIME 類型吮播,第二個參數(shù)分別可選為:StringFile眼俊,byte[]或者ByteString意狠,也就是提交的數(shù)據(jù)內容。

  • MediaType類封裝了三個字段:type疮胖、subtype环戈、CharSet 誊役。分別對應 MIME 類型中的 數(shù)據(jù)類數(shù)據(jù)類型下的子類谷市,編碼類型蛔垢。例如 text/x-markdown; charset=utf-8text 代表文本大類(type)迫悠,x-markdown代表文本類下的子類(subtype)鹏漆,charset=utf-8則表示采用UTF-8編碼,內部封裝了一個解析方法parse()创泄,只要傳入這些信息就能獲得MediaType 實例艺玲。可以參考鏈接Media TypesMIME 參考手冊 平時比較常用的 MIME 類型如下:json :application/json鞠抑,xml:application/xml饭聚,png:image/pngjpg: image/jpeg搁拙,gif:image/gif

除了直接使用靜態(tài)方法create() 構建之外秒梳, RequestBody 還有兩個直接子類FormBodyMultipartBody箕速,分別用于提交 Form 表單中的鍵值對和構建分塊表單請求體(既可以添加表單,又可以也可以添加文件等二進制數(shù)據(jù))

FormBody表單創(chuàng)建

通過內部類 Builder 構建 FormBody 實例酪碘,同時調用 add() 傳入需要傳的鍵值對,例如:

RequestBody formBody = new FormBody.Builder()
        .add("username", "張少林")
        .build();

構建分塊表單請求體 MultipartBody

MultipartBody 也是通過內部類 MultipartBody.Builder 動態(tài)構建實例盐茎,構建實例之前有以下幾個方法:

  • setType():設置MultipartBodyMediaType類型兴垦,一般會設置為MultipartBody.FORM,也就是multipart/form-data.
  • addFormDataPart(String name, String value):添加字符串鍵值對
  • addFormDataPart(String name, @Nullable String filename, RequestBody body):添加表單文件
  • 其中三個重載 addPart() 方法字柠,分別添加請求體探越,其中的addPart(@Nullable Headers headers, RequestBody body)可以在添加RequestBody的時候,同時為其單獨設置請求頭

源碼淺析:查看MultipartBody.Builder源碼發(fā)現(xiàn)窑业,addFormDataPart(String name, String value)內部其實都是調用了addPart(Part part)钦幔,而addPart(Part part)內部是往全局實例化好的List<Part>中 add 實例化好的part。而MultipartBody.Part其實就是包含了RequestBody数冬,所以可以說 MultipartBody是一個RequestBody的集合节槐。

三、Response

Response對應于 Http 的響應報文拐纱,查看源碼可以看到铜异,Request 封裝了響應報文的一些字段。

封裝的字段有 對應的請求報文Request 秸架,響應碼 code揍庄,響應消息 message,響應體ResponseBody东抹,網(wǎng)絡請求響應結果以及緩存響應結果等字段蚂子。同樣的沃测,Response 的構建也是通過內部類 Response.Builder 進行動態(tài)靈活配置,對其中的一些常用方法做簡單介紹:

  • isSuccessful():用來判斷請求是否成功
  • body() :返回響應體ResponseBody
  • headers():獲取響應頭 Headers 對象

四食茎、ResponseBody

通過 Responsebody()方法可以得到響應體ResponseBody蒂破,響應體必須最終要被關閉,否則會導致資源泄露别渔、App運行變慢甚至崩潰附迷。

響應體中的數(shù)據(jù)有可能很大,應該只讀取一次響應體的數(shù)據(jù)哎媚。調用ResponseBodybytes()string()方法會將整個響應體數(shù)據(jù)寫入到內存中喇伯,可以通過source()byteStream()charStream()進行流式處理拨与。

五稻据、OkHttpClient

OkHttp 網(wǎng)絡請求客戶端,封裝了 一些客戶端請求的常用配置买喧,例如請求超時時間捻悯、連接超時時間、讀取超時時間岗喉、緩存目錄秋度、代理等等。

同樣钱床,OkHttpClient 也是通過內部類 Builder 模式,動態(tài)靈活的配置參數(shù)埠居,從而構建 OkHttpClient 實例查牌。默認初始化 OkHttpClient client = new OkHttpClient() 內部已經(jīng)幫我們配置了一系列參數(shù),我們需要自己配置參數(shù)的話滥壕,只需要通過內部類 Builder 實例纸颜,在調用 build()方法之前,調用對應方法動態(tài)配置即可绎橘。OkHttpClient client = new OkHttpClient.Builder().build();例如我們動態(tài)配置讀取時間為30秒(默認配置為10秒):

OkHttpClient client = new OkHttpClient.Builder()
        .readTimeout(30, TimeUnit.SECONDS)
        .build();

當配置完成之后胁孙,我們需要對特定的請求配置不同的參數(shù),也可以OkHttpClient.Builder builder = okHttpClient.newBuilder();
拿到 Builder 類實例進行"返廠重建"

當然了称鳞,還有很多參數(shù)可以配置涮较,具體可以查閱文檔 OkHttpClient參數(shù)配置

注意:在創(chuàng)建 OkHttpClient 時,應當保持全局單例冈止, 每一個 OkHttpClient 實例都保持著自己的連接池和線程池 狂票,重用連接池和線程池會節(jié)省一些內部資源,假如每次請求都去創(chuàng)建新的客戶端實例熙暴,就會造成一定的資源浪費

六闺属、創(chuàng)建 Call 對象慌盯,發(fā)起網(wǎng)絡請求

查閱 OkHttpClient 源碼,我們看到有個 newCall()方法掂器,傳入 Request 實例亚皂,返回一個 Call 對象。我們暫且不用考慮它的原理国瓮,只知道可以用它來發(fā)起網(wǎng)絡請求孕讳,返回 Response 響應報文。點擊進去查看 Call 接口巍膘,可以看到有兩個方法是用來請求網(wǎng)絡的厂财。

  • execute():同步的網(wǎng)絡請求,返回 Response 響應結果峡懈,由于同步請求會阻塞當前線程璃饱,我們使用過程還需要開啟子線程,所以一般較少使用肪康。
  • enqueue(Callback responseCallback):加入隊列荚恶,也就是異步網(wǎng)絡請求,傳入回調接口磷支,發(fā)起網(wǎng)絡請求時谒撼,請求成功會回掉Callback 對象的 onResponse 方法,同時返回 Response 響應報文雾狈,對結果進行處理廓潜。請求失敗則回調Callback對象的onFailure方法,對失敗進行處理善榛。由于異步請求不會阻塞當前線程辩蛋,所以一般使用異步請求,注意:不管是成功還是失敗的回調方法都是在子線程移盆,不能直接進行 ui 操作悼院。
  • isCanceled() 方法用于判斷網(wǎng)絡請求是否取消
  • isExecuted()方法用于判斷網(wǎng)絡請求是否已經(jīng)執(zhí)行
  • request()返回發(fā)起該請求的 Request 請求報文
  • clone()方法克隆一個 Call 對象

七、實際使用

至此咒循,已經(jīng)理清了 OkHttp 的關鍵類以及網(wǎng)絡請求步驟据途,現(xiàn)在對于 OkHttp 的基本使用已經(jīng)沒多大問題了增拥,下面舉例幾個實際場景使用切蟋,滴滴滴铝穷,開車~~~

別忘了添加網(wǎng)絡權限:

<uses-permission android:name="android.permission.INTERNET"/>

同步 GET 請求

  private void sync_get() throws IOException {
        //構建okHttp客戶端
        OkHttpClient okHttpClient = new OkHttpClient();
        //構建請求報文
        Request request = new Request.Builder()
                .url("http://www.baidu.com")//配置請求百度首頁地址
                .build();

        //發(fā)起同步請求劳翰,返回響應報文
        Response response = okHttpClient.newCall(request).execute();
        if (response.isSuccessful()) {
            //獲取Headers 請求頭對象
            Headers headers = response.headers();
            for (int i = 0; i < headers.size(); i++) {
                //分別獲取請求頭的name,value值
                String headName = headers.name(i);
                String headValue = headers.value(i);
            }
            //將響應體中的數(shù)據(jù)轉為 String ,讀取到內存
            final String string = response.body().string();
        } else {
            //失敗處理
        }
    }

注意: 同步請求必須開啟子線程齐莲,我這里省略部分代碼

異步 GET 請求

  private void async_get() {
        //構建okHttp客戶端
        OkHttpClient okHttpClient = new OkHttpClient();
        //構建請求報文
        Request request = new Request.Builder()
                .url("http://www.baidu.com")
                .build();
        //發(fā)起異步請求纤子,返回響應報文
        okHttpClient.newCall(request)
                .enqueue(new Callback() {
                    @Override
                    public void onFailure(Call call, IOException e) {
                        //失敗處理
                        Log.e(TAG, "onFailure: " + Thread.currentThread().getName());
                    }

                    @Override
                    public void onResponse(Call call, Response response) throws IOException {
                        Log.e(TAG, "onResponse: " + Thread.currentThread().getName());
                        //獲取Headers 請求頭對象
                        Headers headers = response.headers();
                        for (int i = 0; i < headers.size(); i++) {
                            //分別獲取請求頭的name,value值
                            String headName = headers.name(i);
                            String headValue = headers.value(i);
                        }
                        //將響應體中的數(shù)據(jù)轉為 String ,讀取到內存
                        final String string = response.body().string();
                    }
                });
    }

異步請求不需要自己開啟子線程胶台,打印回調方法的線程名發(fā)現(xiàn)沥阳,成功或者失敗回調默認都是在子線程

  //06-21 22:52:01.567 30162-30277/com.sunnada.okhttpdemo E/MainActivity: onResponse: OkHttp http://www.baidu.com/...

POST 上傳 json 字符串

public void async_post() {
        //解析數(shù)據(jù)類型為json
        MediaType MEDIA_TYPE_JSON
                = MediaType.parse("application/json; charset=utf-8");
        //構建OkHttp 客戶端
        OkHttpClient okHttpClient = new OkHttpClient();
        //構建json數(shù)據(jù)源
        User user = new User("張少林", "123456");
        String json = new Gson().toJson(user);
        //初始化請求體
        RequestBody requestBody = RequestBody.create(MEDIA_TYPE_JSON, json);
        //初始化請求報文
        Request request = new Request.Builder()
                .url("http://192.168.1.104:8080/users/uploadJson")//配置服務端請求地址
                .post(requestBody)//配置請求方法,傳入請求體
                .build();
        //發(fā)起異步請求哪痰,回調處理結果
        okHttpClient.newCall(request)
                .enqueue(new Callback() {
                    @Override
                    public void onFailure(Call call, IOException e) {
                        //失敗回調
                    }

                    @Override
                    public void onResponse(Call call, Response response) throws IOException {
                           if (!response.isSuccessful())
                            throw new IOException("Unexpected code " + response);
                        Log.e(TAG, "onResponse: " + response.body().string());
                    }
                });
    }

POST 上傳文件

    public void postFile() {
        //獲取手機中的圖片文件
        List<File> files = FileUtils.listFilesInDir(SDCardUtils.getSDCardPath() + "/Pictures/Screenshots", false);
        File file = files.get(1);
        String fileName = file.getName();
        //構建OkHttp 客戶端
        OkHttpClient okHttpClient = new OkHttpClient();
        if (!file.exists()) {
            ToastUtils.showLong("文件不存在");
        } else {
            //構建文件請求頭
            RequestBody requestBody = RequestBody.create(MediaType.parse("application/octet-stream"), file);
            Request request = new Request.Builder()
                    .url("http://192.168.1.104:8080/upload")
                    .post(requestBody)
                    .build();
            okHttpClient.newCall(request)
                    .enqueue(new Callback() {
                        @Override
                        public void onFailure(Call call, IOException e) {

                        }

                        @Override
                        public void onResponse(Call call, Response response) throws IOException {
                              if (!response.isSuccessful())
                            throw new IOException("Unexpected code " + response);
                        Log.e(TAG, "onResponse: " + response.body().string());
                        }
                    });
        }
    }

POST 提交表單

  public void post_form() {
        //構建okHttp客戶端
        OkHttpClient okHttpClient = new OkHttpClient();
        RequestBody formBody = new FormBody.Builder()
                .add("username", "張少林")
                .add("password", "123456")
                .build();
        Request request = new Request.Builder()
                .url("http://192.168.1.104:8080/users/register")
                .post(formBody)
                .build();
        okHttpClient.newCall(request)
                .enqueue(new Callback() {
                    @Override
                    public void onFailure(Call call, IOException e) {

                    }

                    @Override
                    public void onResponse(Call call, Response response) throws IOException {
                      if (!response.isSuccessful())
                            throw new IOException("Unexpected code " + response);
                        Log.e(TAG, "onResponse: " + response.body().string());
                    }
                });
    }

POST 圖文上傳

public void post_img_title() {
        //獲取手機中的圖片文件
        List<File> files = FileUtils.listFilesInDir(SDCardUtils.getSDCardPath() + "/Pictures/Screenshots", false);
        File file = files.get(1);
        MediaType MEDIA_TYPE_PNG = MediaType.parse("image/png");
        OkHttpClient client = new OkHttpClient();
        RequestBody requestBody = new MultipartBody.Builder()
                .setType(MultipartBody.FORM)//設置表單類型
                .addFormDataPart("title", "Logo")//添加表單鍵值對
                .addFormDataPart("image", "logo.png", RequestBody.create(MEDIA_TYPE_PNG, file))//添加表單文件
                .build();
        Request request = new Request.Builder()
                .url("xxxxxxx")
                .post(requestBody)
                .build();
        client.newCall(request)
                .enqueue(new Callback() {
                    @Override
                    public void onFailure(Call call, IOException e) {
                      //請求失敗處理
                    }

                    @Override
                    public void onResponse(Call call, Response response) throws IOException {
                        if (!response.isSuccessful())
                            throw new IOException("Unexpected code " + response);
                        Log.e(TAG, "onResponse: " + response.body().string());
                    }
                });
    }

文件下載遂赠,這里下載一張圖片

 public void downloadImg(){
        OkHttpClient client = new OkHttpClient();
        final Request request = new Request.Builder()
                .get()
                .url("https://www.baidu.com/img/bd_logo1.png")
                .build();
        Call call = client.newCall(request);
        call.enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                
            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                //拿到字節(jié)流
                InputStream is = response.body().byteStream();

                int len = 0;
                File file  = new File(Environment.getExternalStorageDirectory(), "image.png");
                FileOutputStream fos = new FileOutputStream(file);
                byte[] buf = new byte[128];

                while ((len = is.read(buf)) != -1){
                    fos.write(buf, 0, len);
                }

                fos.flush();
                //關閉流
                fos.close();
                is.close();
            }
        });
    }

滴滴,到站了晌杰,下車~~~

最后

結語:雖說正式開發(fā)中跷睦,一般會使用基于 OkHttp 封裝一層的網(wǎng)絡庫,例如 Retrofit 2肋演,OkGo抑诸,底層還是使用OkHttp 做網(wǎng)絡請求的,個人覺得絕對有必要理清 OkHttp 的基本使用爹殊,有時間甚至可以自己封裝一個網(wǎng)絡框架出來也是個好的磨練蜕乡。通過這次的總結,加上看 OkHttp 的部分源碼梗夸,收獲不小层玲,最大的感悟就是不應該只會調用 api ,而應該經(jīng)常去看看流行框架的源碼反症,好的框架源碼是值得一看的辛块。計劃下次對 OkHttp 的緩存,以及攔截器做一下總結~

一點點學習總結铅碍,一點點思考润绵,如有不足還請指出,如果喜歡胞谈,小手一點給個贊~~

如需轉載請注明出處尘盼,謝謝~
http://www.reibang.com/u/52ccd7428abe

最后的最后,感謝樂于分享的大神呜魄,參考鏈接:

更多原創(chuàng)文章會在公眾號第一時間推送,歡迎掃碼關注 張少林同學

張少林同學.jpg
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末悔叽,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子爵嗅,更是在濱河造成了極大的恐慌,老刑警劉巖笨蚁,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件睹晒,死亡現(xiàn)場離奇詭異,居然都是意外死亡括细,警方通過查閱死者的電腦和手機伪很,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來奋单,“玉大人锉试,你說我怎么就攤上這事±辣簦” “怎么了呆盖?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵拖云,是天一觀的道長。 經(jīng)常有香客問我应又,道長宙项,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任株扛,我火速辦了婚禮尤筐,結果婚禮上,老公的妹妹穿的比我還像新娘洞就。我一直安慰自己盆繁,他們只是感情好,可當我...
    茶點故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布旬蟋。 她就那樣靜靜地躺著油昂,像睡著了一般。 火紅的嫁衣襯著肌膚如雪咖为。 梳的紋絲不亂的頭發(fā)上秕狰,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天,我揣著相機與錄音躁染,去河邊找鬼鸣哀。 笑死,一個胖子當著我的面吹牛吞彤,可吹牛的內容都是我干的我衬。 我是一名探鬼主播,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼饰恕,長吁一口氣:“原來是場噩夢啊……” “哼挠羔!你這毒婦竟也來了?” 一聲冷哼從身側響起埋嵌,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤破加,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后雹嗦,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體范舀,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年了罪,在試婚紗的時候發(fā)現(xiàn)自己被綠了锭环。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡泊藕,死狀恐怖辅辩,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤玫锋,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布蛾茉,位于F島的核電站,受9級特大地震影響景醇,放射性物質發(fā)生泄漏臀稚。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一三痰、第九天 我趴在偏房一處隱蔽的房頂上張望吧寺。 院中可真熱鬧,春花似錦散劫、人聲如沸稚机。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽赖条。三九已至,卻和暖如春常熙,著一層夾襖步出監(jiān)牢的瞬間纬乍,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工裸卫, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留仿贬,地道東北人。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓墓贿,卻偏偏與公主長得像茧泪,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子聋袋,可洞房花燭夜當晚...
    茶點故事閱讀 42,916評論 2 344

推薦閱讀更多精彩內容