网络编程 -- OkHttp

一、原理

基于原生Http。

二、特点

  • 支持同步、异步。
  • 支持GZIP减少数据流量。
  • 缓存响应数据,减少重复的网络请求。
  • 自动重连,处理了代理服务器问题和SSL握手失败问题。
  • 支持SPDY,共享同个Socket来处理同一服务器的请求,若SPDY不可用则通过连接池来减少请求延时。
  • 请求、处理速度快,基于NIO(非阻塞式)、Okio(Square基于IO、NIO的一个高效处理数据流的开源库)。

三、使用场景

数据量大的重量级网络请求。

四、Call模型

  • Http客户端的任务是处理请求和响应。
  • Http请求:包含一个URL、请求方法、请求头,还可能包含请求体,请求体可以是数据流也可以是指定的内容类别。
  • Http响应:包含响应码、响应头和响应体。

1.请求的重写

为了保证正确性和传输效率,OkHttp会在发送请求之前重写它:

  • 添加原始请求中缺失的头信息,包括Content-Length、Transfer-Encoding、User-Agent、Host、Connection和Content-Type。
  • 为了实现透明的响应压缩,会增加Accept-Encoding头信息。
  • 如果收到了cookie,会增加cookie头信息。
  • 某些请求可能会对响应做缓存,如果被缓存的响应不是最新的,能做一个有条件的GET请求来下载更新后的响应,此功能需要添加If-Modified-Since和If-None-Match等头信息。

2.响应的重写

  • 如果使用了透明压缩,OkHttp会去掉对应响应的Content-Encoding和Content-Length头信息,因为它们不能应用于解压后的响应体。
  • 如果有条件的GET请求成功了,网络侧的响应和缓存的响应会被自动合并。

3.重定向

如果请求的URL被重定向了,服务器会返回类似于302这样的响应码,来指明新的URL,OkHttp能跟随新的URL,获取到最终的响应。

4.请求的重试

有时网络连接状况不好或者服务器不可达,会发生连接失败,OkHttp会自动使用不同的路由来重试请求。

5.Call模型

由于重写、重定向和重试等操作,一个简单请求可能会产生多个请求和响应,OkHttp使用了Call模型,为了满足请求任务,不论中间做了多少次请求和响应,都算作一个Call。

Call有两种方式来执行,同步方式和异步方式,由于OkHttp是一个Java库,不是Android库,它对Android主线程一无所知,所以异步回调方法是在后台线程执行的。

五、使用方法

添加依赖及网络权限

//build.gradle
implementation 'com.squareup.okhttp3:okhttp:3.2.0'

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

1.同步Get请求

//测试代码
public class MainActivity extends AppCompatActivity {
    private static final String TAG = "MainActivity";
    private final OkHttpClient client = new OkHttpClient();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Log.d(TAG, "zwm, onCreate");

        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    request();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

    private void request() throws IOException {
        Log.d(TAG, "zwm, request, thread: " + Thread.currentThread().getName());
        Request request = new Request.Builder()
                .url("https://www.baidu.com")
                .build();
        Response response = client.newCall(request).execute();
        if (!response.isSuccessful()) {
            throw new IOException("Unexpected code: " + response);
        }
        Headers responseHeaders = response.headers();
        for (int i = 0; i < responseHeaders.size(); i++) {
            Log.d(TAG, "zwm, response header, name: " + responseHeaders.name(i) + ", value: " + responseHeaders.value(i));
        }
        Log.d(TAG, "zwm, response body: " + response.body().string());
    }
}

//输出log
2019-12-26 11:23:47.872 zwm, onCreate
2019-12-26 11:23:47.874 zwm, request, thread: Thread-6
2019-12-26 11:23:48.428 zwm, response header, name: Cache-Control, value: private, no-cache, no-store, proxy-revalidate, no-transform
2019-12-26 11:23:48.428 zwm, response header, name: Connection, value: keep-alive
2019-12-26 11:23:48.428 zwm, response header, name: Content-Type, value: text/html
2019-12-26 11:23:48.428 zwm, response header, name: Date, value: Thu, 26 Dec 2019 03:23:36 GMT
2019-12-26 11:23:48.430 zwm, response header, name: Last-Modified, value: Mon, 23 Jan 2017 13:24:18 GMT
2019-12-26 11:23:48.430 zwm, response header, name: Pragma, value: no-cache
2019-12-26 11:23:48.430 zwm, response header, name: Server, value: bfe/1.0.8.18
2019-12-26 11:23:48.430 zwm, response header, name: Set-Cookie, value: BDORZ=27315; max-age=86400; domain=.baidu.com; path=/
2019-12-26 11:23:48.430 zwm, response header, name: Transfer-Encoding, value: chunked
2019-12-26 11:23:48.430 zwm, response header, name: OkHttp-Sent-Millis, value: 1577330628379
2019-12-26 11:23:48.430 zwm, response header, name: OkHttp-Received-Millis, value: 1577330628423
2019-12-26 11:23:48.433 zwm, response body: <!DOCTYPE html>
...

响应体的string()方法对于小文件来说非常方便和高效,但如果响应体较大(大于1M),要避免使用这一方法,因为它会将文件内容全部加载在内存上,在这种情况下,使用流来处理响应体。

2.异步Get请求

//测试代码
public class MainActivity extends AppCompatActivity {
    private static final String TAG = "MainActivity";
    private final OkHttpClient client = new OkHttpClient();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Log.d(TAG, "zwm, onCreate");

        new Thread(new Runnable() {
            @Override
            public void run() {
                request();
            }
        }).start();
    }

    private void request() {
        Log.d(TAG, "zwm, request, thread: " + Thread.currentThread().getName());
        Request request = new Request.Builder()
                .url("https://www.baidu.com")
                .build();
        client.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                Log.d(TAG, "zwm, onFailure, thread: " + Thread.currentThread().getName());
            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                Log.d(TAG, "zwm, onResponse, thread: " + Thread.currentThread().getName());
                Headers responseHeaders = response.headers();
                for (int i = 0; i < responseHeaders.size(); i++) {
                    Log.d(TAG, "zwm, response header, name: " + responseHeaders.name(i) + ", value: " + responseHeaders.value(i));
                }
                Log.d(TAG, "zwm, response body: " + response.body().string());
            }
        });
    }
}

//输出log
2019-12-26 11:35:11.880 zwm, onCreate
2019-12-26 11:35:11.885 zwm, request, thread: Thread-6
2019-12-26 11:35:12.676 zwm, onResponse, thread: OkHttp https://www.baidu.com/
2019-12-26 11:35:12.676 zwm, response header, name: Cache-Control, value: private, no-cache, no-store, proxy-revalidate, no-transform
2019-12-26 11:35:12.676 zwm, response header, name: Connection, value: keep-alive
2019-12-26 11:35:12.677 zwm, response header, name: Content-Type, value: text/html
2019-12-26 11:35:12.677 zwm, response header, name: Date, value: Thu, 26 Dec 2019 03:35:00 GMT
2019-12-26 11:35:12.677 zwm, response header, name: Last-Modified, value: Mon, 23 Jan 2017 13:24:18 GMT
2019-12-26 11:35:12.677 zwm, response header, name: Pragma, value: no-cache
2019-12-26 11:35:12.677 zwm, response header, name: Server, value: bfe/1.0.8.18
2019-12-26 11:35:12.677 zwm, response header, name: Set-Cookie, value: BDORZ=27315; max-age=86400; domain=.baidu.com; path=/
2019-12-26 11:35:12.677 zwm, response header, name: Transfer-Encoding, value: chunked
2019-12-26 11:35:12.677 zwm, response header, name: OkHttp-Sent-Millis, value: 1577331312637
2019-12-26 11:35:12.677 zwm, response header, name: OkHttp-Received-Millis, value: 1577331312672
2019-12-26 11:35:12.682 zwm, response body: <!DOCTYPE html>
...

3.Http头信息

//测试代码
public class MainActivity extends AppCompatActivity {
    private static final String TAG = "MainActivity";
    private final OkHttpClient client = new OkHttpClient();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Log.d(TAG, "zwm, onCreate");

        new Thread(new Runnable() {
            @Override
            public void run() {
                request();
            }
        }).start();
    }

    private void request() {
        Log.d(TAG, "zwm, request, thread: " + Thread.currentThread().getName());
        Request request = new Request.Builder()
                .url("https://www.baidu.com")
                .header("User-Agent", "OkHttp User-Agent")
                .addHeader("Accept", "text/html")
                .build();
        client.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                Log.d(TAG, "zwm, onFailure, thread: " + Thread.currentThread().getName());
            }

            @Override
            public void onResponse(Call call, Response response) {
                Log.d(TAG, "zwm, onResponse, thread: " + Thread.currentThread().getName());
                Log.d(TAG, "zwm, Server: " + response.header("Server"));
                Log.d(TAG, "zwm, Date: " + response.header("Date"));
                Log.d(TAG, "zwm, Vary: " + response.headers("Vary"));
            }
        });
    }
}

//输出log
2019-12-26 13:17:07.047 zwm, onCreate
2019-12-26 13:17:07.048 zwm, request, thread: Thread-6
2019-12-26 13:17:08.380 zwm, onResponse, thread: OkHttp https://www.baidu.com/
2019-12-26 13:17:08.380 zwm, Server: BWS/1.1
2019-12-26 13:17:08.381 zwm, Date: Thu, 26 Dec 2019 05:16:56 GMT
2019-12-26 13:17:08.381 zwm, Vary: []

4.Post字符串

//测试代码
public class MainActivity extends AppCompatActivity {
    private static final String TAG = "MainActivity";
    public static final MediaType MEDIA_TYPE_MARKDOWN = MediaType.parse("text/x-markdown; charset=utf-8");
    private final OkHttpClient client = new OkHttpClient();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Log.d(TAG, "zwm, onCreate");

        new Thread(new Runnable() {
            @Override
            public void run() {
                request();
            }
        }).start();
    }

    private void request() {
        Log.d(TAG, "zwm, request, thread: " + Thread.currentThread().getName());
        String postBody = ""
                + "Releases\n"
                + "--------\n"
                + "\n"
                + " * _1.0_ May 6, 2013\n"
                + " * _1.1_ June 15, 2013\n"
                + " * _1.2_ August 11, 2013\n";
        Request request = new Request.Builder()
                .url("https://api.github.com/markdown/raw")
                .post(RequestBody.create(MEDIA_TYPE_MARKDOWN, postBody))
                .build();
        client.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                Log.d(TAG, "zwm, onFailure, thread: " + Thread.currentThread().getName());
            }

            @Override
            public void onResponse(Call call, Response response) {
                Log.d(TAG, "zwm, onResponse, thread: " + Thread.currentThread().getName());
                try {
                    Log.d(TAG, "zwm, body: " + response.body().string());
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        });
    }
}

//输出log
2019-12-26 13:25:26.779 zwm, onCreate
2019-12-26 13:25:26.782 zwm, request, thread: Thread-6
2019-12-26 13:25:28.471 zwm, onResponse, thread: OkHttp https://api.github.com/markdown/raw
2019-12-26 13:25:28.497 zwm, body: <h2>
...

不能使用该方法传送大文件(大于1M)。

5.Post流

//测试代码
public class MainActivity extends AppCompatActivity {
    private static final String TAG = "MainActivity";
    public static final MediaType MEDIA_TYPE_MARKDOWN = MediaType.parse("text/x-markdown; charset=utf-8");
    private final OkHttpClient client = new OkHttpClient();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Log.d(TAG, "zwm, onCreate");

        new Thread(new Runnable() {
            @Override
            public void run() {
                request();
            }
        }).start();
    }

    private void request() {
        Log.d(TAG, "zwm, request, thread: " + Thread.currentThread().getName());
        RequestBody requestBody = new RequestBody() {
            @Override public MediaType contentType() {
                return MEDIA_TYPE_MARKDOWN;
            }

            @Override public void writeTo(BufferedSink sink) throws IOException { //使用Okio的buffered sink
                sink.writeUtf8("Numbers\n");
                sink.writeUtf8("-------\n");
                for (int i = 2; i <= 997; i++) {
                    sink.writeUtf8(String.format(" * %s = %s\n", i, factor(i)));
                }
            }

            private String factor(int n) {
                for (int i = 2; i < n; i++) {
                    int x = n / i;
                    if (x * i == n) return factor(x) + " × " + i;
                }
                return Integer.toString(n);
            }
        };
        Request request = new Request.Builder()
                .url("https://api.github.com/markdown/raw")
                .post(requestBody)
                .build();
        client.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                Log.d(TAG, "zwm, onFailure, thread: " + Thread.currentThread().getName());
            }

            @Override
            public void onResponse(Call call, Response response) {
                Log.d(TAG, "zwm, onResponse, thread: " + Thread.currentThread().getName());
                try {
                    Log.d(TAG, "zwm, body: " + response.body().string());
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        });
    }
}

//输出log
2019-12-26 13:30:49.632 zwm, onCreate
2019-12-26 13:30:49.648 zwm, request, thread: Thread-6
2019-12-26 13:30:53.491 zwm, onResponse, thread: OkHttp https://api.github.com/markdown/raw
2019-12-26 13:30:53.499 zwm, body: <h2>
...

6.Post文件

//测试代码
public class MainActivity extends AppCompatActivity {
    private static final String TAG = "MainActivity";
    public static final MediaType MEDIA_TYPE_MARKDOWN = MediaType.parse("text/x-markdown; charset=utf-8");
    private final OkHttpClient client = new OkHttpClient();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Log.d(TAG, "zwm, onCreate");

        new Thread(new Runnable() {
            @Override
            public void run() {
                request();
            }
        }).start();
    }

    private void request() {
        Log.d(TAG, "zwm, request, thread: " + Thread.currentThread().getName());
        File dir = getExternalCacheDir();
        if(!dir.exists()) {
            Log.d(TAG, "zwm, dir not exist, mkdir");
            dir.mkdir();
            return;
        }
        File file = new File(dir.getAbsolutePath(), "application123.json");
        if(!file.exists()) {
            Log.d(TAG, "zwm, file not exist");
            return;
        }
        Request request = new Request.Builder()
                .url("https://api.github.com/markdown/raw")
                .post(RequestBody.create(MEDIA_TYPE_MARKDOWN, file))
                .build();
        client.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                Log.d(TAG, "zwm, onFailure, thread: " + Thread.currentThread().getName());
            }

            @Override
            public void onResponse(Call call, Response response) {
                Log.d(TAG, "zwm, onResponse, thread: " + Thread.currentThread().getName());
                try {
                    Log.d(TAG, "zwm, body: " + response.body().string());
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        });
    }
}

//输出log
2019-12-26 13:43:04.978 zwm, onCreate
2019-12-26 13:43:04.979 zwm, request, thread: Thread-6
2019-12-26 13:43:10.758 zwm, onResponse, thread: OkHttp https://api.github.com/markdown/raw
2019-12-26 13:43:10.762 zwm, body: <p>{
...

7.Post form参数

//测试代码
public class MainActivity extends AppCompatActivity {
    private static final String TAG = "MainActivity";
    private final OkHttpClient client = new OkHttpClient();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Log.d(TAG, "zwm, onCreate");

        new Thread(new Runnable() {
            @Override
            public void run() {
                request();
            }
        }).start();
    }

    private void request() {
        Log.d(TAG, "zwm, request, thread: " + Thread.currentThread().getName());
        RequestBody formBody = new FormBody.Builder()
                .add("search", "Jurassic Park")
                .build();
        Request request = new Request.Builder()
                .url("https://en.wikipedia.org/w/index.php")
                .post(formBody)
                .build();
        client.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                Log.d(TAG, "zwm, onFailure, thread: " + Thread.currentThread().getName());
            }

            @Override
            public void onResponse(Call call, Response response) {
                Log.d(TAG, "zwm, onResponse, thread: " + Thread.currentThread().getName());
                try {
                    Log.d(TAG, "zwm, body: " + response.body().string());
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        });
    }
}

8.Post多部分请求

MultipartBody.Builder可以构建复杂的请求体,多部分请求体的每一个部分都是一个单一的请求体,可以定义它自身的请求头。

//测试代码
public class MainActivity extends AppCompatActivity {
    private static final String TAG = "MainActivity";
    private static final String IMGUR_CLIENT_ID = "...";
    private static final MediaType MEDIA_TYPE_PNG = MediaType.parse("image/png");

    private final OkHttpClient client = new OkHttpClient();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Log.d(TAG, "zwm, onCreate");

        new Thread(new Runnable() {
            @Override
            public void run() {
                request();
            }
        }).start();
    }

    private void request() {
        Log.d(TAG, "zwm, request, thread: " + Thread.currentThread().getName());
        // Use the imgur image upload API as documented at https://api.imgur.com/endpoints/image
        RequestBody requestBody = new MultipartBody.Builder()
                .setType(MultipartBody.FORM)
                .addFormDataPart("title", "Square Logo")
                .addFormDataPart("image", "logo-square.png",
                        RequestBody.create(MEDIA_TYPE_PNG, new File("website/static/logo-square.png")))
                .build();

        Request request = new Request.Builder()
                .header("Authorization", "Client-ID " + IMGUR_CLIENT_ID)
                .url("https://api.imgur.com/3/image")
                .post(requestBody)
                .build();
        client.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                Log.d(TAG, "zwm, onFailure, thread: " + Thread.currentThread().getName());
            }

            @Override
            public void onResponse(Call call, Response response) {
                Log.d(TAG, "zwm, onResponse, thread: " + Thread.currentThread().getName());
                try {
                    Log.d(TAG, "zwm, body: " + response.body().string());
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        });
    }
}

9.使用Gson来解析JSON响应

可以使用Gson库来解析服务器响应的JSON数据,其中ResponseBody.charStream()方法使用响应头中的Content-Type字段来选择使用何种字符集,如果没有指明,默认将使用UTF-8。

//添加Gson依赖:
implementation 'com.google.code.gson:gson:2.6.2'

//测试代码
public class MainActivity extends AppCompatActivity {
    private static final String TAG = "MainActivity";

    private final OkHttpClient client = new OkHttpClient();
    private final Gson gson = new Gson();
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Log.d(TAG, "zwm, onCreate");

        new Thread(new Runnable() {
            @Override
            public void run() {
                request();
            }
        }).start();
    }

    private void request() {
        Log.d(TAG, "zwm, request, thread: " + Thread.currentThread().getName());
        Request request = new Request.Builder()
                .url("https://api.github.com/gists/c2a7c39532239ff261be")
                .build();
        client.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                Log.d(TAG, "zwm, onFailure, thread: " + Thread.currentThread().getName());
            }

            @Override
            public void onResponse(Call call, Response response) {
                Log.d(TAG, "zwm, onResponse, thread: " + Thread.currentThread().getName());
                Gist gist = gson.fromJson(response.body().charStream(), Gist.class);
                for (Map.Entry<String, GistFile> entry : gist.files.entrySet()) {
                    Log.d(TAG, "zwm, key: " + entry.getKey());
                    Log.d(TAG, "zwm, value.content: " + entry.getValue().content);
                }
            }
        });
    }

    static class Gist {
        Map<String, GistFile> files;
    }

    static class GistFile {
        String content;
    }
}

//输出log
2019-12-26 14:00:08.963 zwm, onCreate
2019-12-26 14:00:08.966 zwm, request, thread: Thread-6
2019-12-26 14:00:12.509 zwm, onResponse, thread: OkHttp https://api.github.com/gists/c2a7c39532239ff261be
2019-12-26 14:00:12.560 zwm, key: OkHttp.txt
2019-12-26 14:00:12.560 zwm, value.content: 
...

10.Call的取消

使用Call.cancel()来停止一个执行中的请求,如果线程正在写请求或读响应,则会收到IOException。使用此方法,在一个Call已经不需要时取消它,可以节省网络流量。

11.使用缓存

基本使用

//测试代码
public class MainActivity extends AppCompatActivity {
    private static final String TAG = "MainActivity";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Log.d(TAG, "zwm, onCreate");

        new Thread(new Runnable() {
            @Override
            public void run() {
                request();
            }
        }).start();
    }

    private void request() {
        Log.d(TAG, "zwm, request, thread: " + Thread.currentThread().getName());
        //缓存文件夹
        File cacheFile = new File(getExternalCacheDir().toString(),"cache");
        //缓存大小为10M
        int cacheSize = 10 * 1024 * 1024;
        //创建缓存对象
        final Cache cache = new Cache(cacheFile,cacheSize);
        OkHttpClient client = new OkHttpClient.Builder()
                .cache(cache)
                .build();

        Request request = new Request.Builder()
                .url("http://publicobject.com/helloworld.txt")
                .build();
        client.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                Log.d(TAG, "zwm, onFailure, thread: " + Thread.currentThread().getName());
            }

            @Override
            public void onResponse(Call call, Response response) {
                Log.d(TAG, "zwm, onResponse, thread: " + Thread.currentThread().getName());
                try {
                    Log.d(TAG, "zwm, body: " + response.body().string());
                    Log.d(TAG, "zwm, cache: " + response.cacheResponse());
                    Log.d(TAG, "zwm, network: " + response.networkResponse());
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        });

        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        Log.d(TAG, "zwm, request2, thread: " + Thread.currentThread().getName());
        client.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                Log.d(TAG, "zwm, onFailure, thread2: " + Thread.currentThread().getName());
            }

            @Override
            public void onResponse(Call call, Response response) {
                Log.d(TAG, "zwm, onResponse, thread2: " + Thread.currentThread().getName());
                try {
                    Log.d(TAG, "zwm, body2: " + response.body().string());
                    Log.d(TAG, "zwm, cache2: " + response.cacheResponse());
                    Log.d(TAG, "zwm, network2: " + response.networkResponse());
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        });
    }
}

//输出log
2019-12-26 14:19:14.379 zwm, onCreate
2019-12-26 14:19:14.380 zwm, request, thread: Thread-6
2019-12-26 14:19:16.295 zwm, onResponse, thread: OkHttp http://publicobject.com/helloworld.txt
2019-12-26 14:19:16.347 zwm, body: 
...                     
2019-12-26 14:19:16.347 zwm, cache: null
2019-12-26 14:19:16.348 zwm, network: Response{protocol=http/1.1, code=200, message=OK, url=https://publicobject.com/helloworld.txt}
2019-12-26 14:19:19.803 zwm, request2, thread: Thread-6
2019-12-26 14:19:20.087 zwm, onResponse, thread2: OkHttp http://publicobject.com/helloworld.txt
2019-12-26 14:19:20.088 zwm, body2: 
...                     
2019-12-26 14:19:20.088 zwm, cache2: Response{protocol=http/1.1, code=200, message=OK, url=https://publicobject.com/helloworld.txt}
2019-12-26 14:19:20.088 zwm, network2: null

使用CacheControl(推荐)

CacheControl是针对Request的,可以针对每个请求设置不同的缓存策略。

CacheControl.Builder常用方法:

  • noCache() //不使用缓存,用网络请求
  • noStore() //不使用缓存,也不存储缓存
  • onlyIfCached() //只使用缓存
  • noTransform() //禁止转码
  • maxAge(10, TimeUnit.MILLISECONDS) //设置超时时间为10ms
  • maxStale(10, TimeUnit.SECONDS) //超时时间加上10s
  • minFresh(10, TimeUnit.SECONDS) //超时时间减去10s

设置一个10秒的超时时间:

//测试代码
public class MainActivity extends AppCompatActivity {
    private static final String TAG = "MainActivity";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Log.d(TAG, "zwm, onCreate");

        new Thread(new Runnable() {
            @Override
            public void run() {
                request();
            }
        }).start();
    }

    private void request() {
        Log.d(TAG, "zwm, request, thread: " + Thread.currentThread().getName());
        //缓存文件夹
        File cacheFile = new File(getExternalCacheDir().toString(),"cache");
        //缓存大小为10M
        int cacheSize = 10 * 1024 * 1024;
        //创建缓存对象
        final Cache cache = new Cache(cacheFile,cacheSize);
        OkHttpClient client = new OkHttpClient.Builder()
                .cache(cache)
                .build();
        //设置缓存时间为10秒
        CacheControl cacheControl = new CacheControl.Builder()
                .maxAge(10, TimeUnit.SECONDS)
                .build();
        Request request = new Request.Builder()
                .url("http://publicobject.com/helloworld.txt")
                .cacheControl(cacheControl)
                .build();
        client.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                Log.d(TAG, "zwm, onFailure, thread: " + Thread.currentThread().getName());
            }

            @Override
            public void onResponse(Call call, Response response) {
                Log.d(TAG, "zwm, onResponse, thread: " + Thread.currentThread().getName());
                try {
                    Log.d(TAG, "zwm, body: " + response.body().string());
                    Log.d(TAG, "zwm, cache: " + response.cacheResponse());
                    Log.d(TAG, "zwm, network: " + response.networkResponse());
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        });

        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        Log.d(TAG, "zwm, request2, thread: " + Thread.currentThread().getName());
        client.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                Log.d(TAG, "zwm, onFailure, thread2: " + Thread.currentThread().getName());
            }

            @Override
            public void onResponse(Call call, Response response) {
                Log.d(TAG, "zwm, onResponse, thread2: " + Thread.currentThread().getName());
                try {
                    Log.d(TAG, "zwm, body2: " + response.body().string());
                    Log.d(TAG, "zwm, cache2: " + response.cacheResponse());
                    Log.d(TAG, "zwm, network2: " + response.networkResponse());
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        });
    }
}

//输出log
2019-12-26 14:41:10.258 zwm, onCreate
2019-12-26 14:41:10.258 zwm, request, thread: Thread-6
2019-12-26 14:41:12.581 zwm, onResponse, thread: OkHttp http://publicobject.com/helloworld.txt
2019-12-26 14:41:12.593 zwm, body: 
...
2019-12-26 14:41:12.593 zwm, cache: null
2019-12-26 14:41:12.593 zwm, network: Response{protocol=http/1.1, code=200, message=OK, url=https://publicobject.com/helloworld.txt}
2019-12-26 14:41:30.599 zwm, request2, thread: Thread-6
2019-12-26 14:41:31.131 zwm, onResponse, thread2: OkHttp http://publicobject.com/helloworld.txt
2019-12-26 14:41:31.132 zwm, body2: 
...
2019-12-26 14:41:31.133 zwm, cache2: Response{protocol=http/1.1, code=200, message=OK, url=https://publicobject.com/helloworld.txt}
2019-12-26 14:41:31.133 zwm, network2: Response{protocol=http/1.1, code=304, message=Not Modified, url=https://publicobject.com/helloworld.txt}

强制使用缓存:

//测试代码
public class MainActivity extends AppCompatActivity {
    private static final String TAG = "MainActivity";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Log.d(TAG, "zwm, onCreate");

        new Thread(new Runnable() {
            @Override
            public void run() {
                request();
            }
        }).start();
    }

    private void request() {
        Log.d(TAG, "zwm, request, thread: " + Thread.currentThread().getName());
        //缓存文件夹
        File cacheFile = new File(getExternalCacheDir().toString(),"cache");
        //缓存大小为10M
        int cacheSize = 10 * 1024 * 1024;
        //创建缓存对象
        final Cache cache = new Cache(cacheFile,cacheSize);
        OkHttpClient client = new OkHttpClient.Builder()
                .cache(cache)
                .build();
        //设置缓存时间为10秒
        CacheControl cacheControl = new CacheControl.Builder()
                .maxAge(10, TimeUnit.SECONDS)
                .build();
        Request request = new Request.Builder()
                .url("http://publicobject.com/helloworld.txt")
                .cacheControl(cacheControl)
                .build();
        client.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                Log.d(TAG, "zwm, onFailure, thread: " + Thread.currentThread().getName());
            }

            @Override
            public void onResponse(Call call, Response response) {
                Log.d(TAG, "zwm, onResponse, thread: " + Thread.currentThread().getName());
                try {
                    Log.d(TAG, "zwm, body: " + response.body().string());
                    Log.d(TAG, "zwm, cache: " + response.cacheResponse());
                    Log.d(TAG, "zwm, network: " + response.networkResponse());
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        });

        try {
            Thread.sleep(20000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        Log.d(TAG, "zwm, request2, thread: " + Thread.currentThread().getName());
        request = new Request.Builder()
                .url("http://publicobject.com/helloworld.txt")
                .cacheControl(CacheControl.FORCE_CACHE)
                .build();
        client.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                Log.d(TAG, "zwm, onFailure, thread2: " + Thread.currentThread().getName());
            }

            @Override
            public void onResponse(Call call, Response response) {
                Log.d(TAG, "zwm, onResponse, thread2: " + Thread.currentThread().getName());
                try {
                    Log.d(TAG, "zwm, body2: " + response.body().string());
                    Log.d(TAG, "zwm, cache2: " + response.cacheResponse());
                    Log.d(TAG, "zwm, network2: " + response.networkResponse());
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        });
    }
}

//输出log
2019-12-26 14:50:50.155 zwm, onCreate
2019-12-26 14:50:50.156 zwm, request, thread: Thread-6
2019-12-26 14:50:53.710 zwm, onResponse, thread: OkHttp http://publicobject.com/helloworld.txt
2019-12-26 14:50:53.753 zwm, body: 
...                     
2019-12-26 14:50:53.754 zwm, cache: null
2019-12-26 14:50:53.755 zwm, network: Response{protocol=http/1.1, code=200, message=OK, url=https://publicobject.com/helloworld.txt}
2019-12-26 14:51:10.437 zwm, request2, thread: Thread-6
2019-12-26 14:51:10.467 zwm, onResponse, thread2: OkHttp http://publicobject.com/helloworld.txt
2019-12-26 14:51:10.468 zwm, body2: 
...                     
2019-12-26 14:51:10.468 zwm, cache2: Response{protocol=http/1.1, code=200, message=OK, url=https://publicobject.com/helloworld.txt}
2019-12-26 14:51:10.468 zwm, network2: null

不使用缓存:

//测试代码
public class MainActivity extends AppCompatActivity {
    private static final String TAG = "MainActivity";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Log.d(TAG, "zwm, onCreate");

        new Thread(new Runnable() {
            @Override
            public void run() {
                request();
            }
        }).start();
    }

    private void request() {
        Log.d(TAG, "zwm, request, thread: " + Thread.currentThread().getName());
        //缓存文件夹
        File cacheFile = new File(getExternalCacheDir().toString(),"cache");
        //缓存大小为10M
        int cacheSize = 10 * 1024 * 1024;
        //创建缓存对象
        final Cache cache = new Cache(cacheFile,cacheSize);
        OkHttpClient client = new OkHttpClient.Builder()
                .cache(cache)
                .build();
        //设置缓存时间为10秒
        CacheControl cacheControl = new CacheControl.Builder()
                .maxAge(10, TimeUnit.SECONDS)
                .build();
        Request request = new Request.Builder()
                .url("http://publicobject.com/helloworld.txt")
                .cacheControl(cacheControl)
                .build();
        client.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                Log.d(TAG, "zwm, onFailure, thread: " + Thread.currentThread().getName());
            }

            @Override
            public void onResponse(Call call, Response response) {
                Log.d(TAG, "zwm, onResponse, thread: " + Thread.currentThread().getName());
                try {
                    Log.d(TAG, "zwm, body: " + response.body().string());
                    Log.d(TAG, "zwm, cache: " + response.cacheResponse());
                    Log.d(TAG, "zwm, network: " + response.networkResponse());
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        });

        try {
            Thread.sleep(20000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        Log.d(TAG, "zwm, request2, thread: " + Thread.currentThread().getName());
        request = new Request.Builder()
                .url("http://publicobject.com/helloworld.txt")
                .cacheControl(CacheControl.FORCE_NETWORK)
                .build();
        client.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                Log.d(TAG, "zwm, onFailure, thread2: " + Thread.currentThread().getName());
            }

            @Override
            public void onResponse(Call call, Response response) {
                Log.d(TAG, "zwm, onResponse, thread2: " + Thread.currentThread().getName());
                try {
                    Log.d(TAG, "zwm, body2: " + response.body().string());
                    Log.d(TAG, "zwm, cache2: " + response.cacheResponse());
                    Log.d(TAG, "zwm, network2: " + response.networkResponse());
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        });
    }
}

//输出log
2019-12-26 14:52:53.590 zwm, onCreate
2019-12-26 14:52:53.590 zwm, request, thread: Thread-6
2019-12-26 14:52:56.634 zwm, onResponse, thread: OkHttp http://publicobject.com/helloworld.txt
2019-12-26 14:52:56.664 zwm, body: 
...                     
2019-12-26 14:52:56.665 zwm, cache: null
2019-12-26 14:52:56.665 zwm, network: Response{protocol=http/1.1, code=200, message=OK, url=https://publicobject.com/helloworld.txt}
2019-12-26 14:53:13.885 zwm, request2, thread: Thread-6
2019-12-26 14:53:14.831 zwm, onResponse, thread2: OkHttp http://publicobject.com/helloworld.txt
2019-12-26 14:53:14.837 zwm, body2: 
...                     
2019-12-26 14:53:14.838 zwm, cache2: null
2019-12-26 14:53:14.838 zwm, network2: Response{protocol=http/1.1, code=200, message=OK, url=https://publicobject.com/helloworld.txt}

maxAge设置为0,发起网络请求执行对比缓存:

//测试代码
public class MainActivity extends AppCompatActivity {
    private static final String TAG = "MainActivity";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Log.d(TAG, "zwm, onCreate");

        new Thread(new Runnable() {
            @Override
            public void run() {
                request();
            }
        }).start();
    }

    private void request() {
        Log.d(TAG, "zwm, request, thread: " + Thread.currentThread().getName());
        //缓存文件夹
        File cacheFile = new File(getExternalCacheDir().toString(),"cache");
        //缓存大小为10M
        int cacheSize = 10 * 1024 * 1024;
        //创建缓存对象
        final Cache cache = new Cache(cacheFile,cacheSize);
        OkHttpClient client = new OkHttpClient.Builder()
                .cache(cache)
                .build();
        //设置缓存时间为10秒
        CacheControl cacheControl = new CacheControl.Builder()
                .maxAge(10, TimeUnit.SECONDS)
                .build();
        Request request = new Request.Builder()
                .url("http://publicobject.com/helloworld.txt")
                .cacheControl(cacheControl)
                .build();
        client.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                Log.d(TAG, "zwm, onFailure, thread: " + Thread.currentThread().getName());
            }

            @Override
            public void onResponse(Call call, Response response) {
                Log.d(TAG, "zwm, onResponse, thread: " + Thread.currentThread().getName());
                try {
                    Log.d(TAG, "zwm, body: " + response.body().string());
                    Log.d(TAG, "zwm, cache: " + response.cacheResponse());
                    Log.d(TAG, "zwm, network: " + response.networkResponse());
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        });

        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        Log.d(TAG, "zwm, request2, thread: " + Thread.currentThread().getName());
        cacheControl = new CacheControl.Builder()
                .maxAge(0, TimeUnit.SECONDS)
                .build();
        request = new Request.Builder()
                .url("http://publicobject.com/helloworld.txt")
                .cacheControl(cacheControl)
                .build();
        client.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                Log.d(TAG, "zwm, onFailure, thread2: " + Thread.currentThread().getName());
            }

            @Override
            public void onResponse(Call call, Response response) {
                Log.d(TAG, "zwm, onResponse, thread2: " + Thread.currentThread().getName());
                try {
                    Log.d(TAG, "zwm, body2: " + response.body().string());
                    Log.d(TAG, "zwm, cache2: " + response.cacheResponse());
                    Log.d(TAG, "zwm, network2: " + response.networkResponse());
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        });
    }
}

使用缓存拦截器(不推荐)

如果服务器能在返回消息的时候添加好Cache-Control相关的消息头,那么客户端就能够正常使用缓存,但是如果服务器无法配合客户端添加Cache-Control相关的消息头的话,那么客户端要想使用缓存可以添加缓存拦截器,人为地添加响应消息中Cache-Control相关的消息头,然后再传递给用户。

//测试代码
public class MainActivity extends AppCompatActivity {
    private static final String TAG = "MainActivity";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Log.d(TAG, "zwm, onCreate");

        new Thread(new Runnable() {
            @Override
            public void run() {
                request();
            }
        }).start();
    }

    private void request() {
        Log.d(TAG, "zwm, request, thread: " + Thread.currentThread().getName());
        //缓存文件夹
        File cacheFile = new File(getExternalCacheDir().toString(),"cache");
        //缓存大小为10M
        int cacheSize = 10 * 1024 * 1024;
        //创建缓存对象
        final Cache cache = new Cache(cacheFile,cacheSize);
        OkHttpClient client = new OkHttpClient.Builder()
                .addNetworkInterceptor(new CacheInterceptor())
                .cache(cache)
                .build();
        //设置缓存时间为10秒
        CacheControl cacheControl = new CacheControl.Builder()
                .maxAge(10, TimeUnit.SECONDS)
                .build();
        Request request = new Request.Builder()
                .url("http://publicobject.com/helloworld.txt")
                .cacheControl(cacheControl)
                .build();
        client.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                Log.d(TAG, "zwm, onFailure, thread: " + Thread.currentThread().getName());
            }

            @Override
            public void onResponse(Call call, Response response) {
                Log.d(TAG, "zwm, onResponse, thread: " + Thread.currentThread().getName());
                try {
                    Log.d(TAG, "zwm, body: " + response.body().string());
                    Log.d(TAG, "zwm, cache: " + response.cacheResponse());
                    Log.d(TAG, "zwm, network: " + response.networkResponse());
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        });

        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        Log.d(TAG, "zwm, request2, thread: " + Thread.currentThread().getName());
        cacheControl = new CacheControl.Builder()
                .build();
        request = new Request.Builder()
                .url("http://publicobject.com/helloworld.txt")
                .cacheControl(cacheControl)
                .build();
        client.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                Log.d(TAG, "zwm, onFailure, thread2: " + Thread.currentThread().getName());
            }

            @Override
            public void onResponse(Call call, Response response) {
                Log.d(TAG, "zwm, onResponse, thread2: " + Thread.currentThread().getName());
                try {
                    Log.d(TAG, "zwm, body2: " + response.body().string());
                    Log.d(TAG, "zwm, cache2: " + response.cacheResponse());
                    Log.d(TAG, "zwm, network2: " + response.networkResponse());
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        });
    }

    static class CacheInterceptor implements Interceptor {
        private static final String TAG = "CacheInterceptor";
        @Override
        public Response intercept(Chain chain) throws IOException {
            Log.d(TAG, "zwm, intercept");
            Response originResponse = chain.proceed(chain.request());
            //设置缓存时间为60秒,并移除了pragma消息头,移除它的原因是因为pragma也是控制缓存的一个消息头属性
            return originResponse.newBuilder()
                    .removeHeader("pragma")
                    .header("Cache-Control","max-age=60")
                    .build();
        }
    }
}

//输出log
2019-12-26 15:13:26.326 zwm, onCreate
2019-12-26 15:13:26.327 zwm, request, thread: Thread-6
2019-12-26 15:13:26.628 zwm, intercept
2019-12-26 15:13:28.764 zwm, intercept
2019-12-26 15:13:29.011 zwm, onResponse, thread: OkHttp http://publicobject.com/helloworld.txt
2019-12-26 15:13:29.034 zwm, body: 
...                     
2019-12-26 15:13:29.034 zwm, cache: null
2019-12-26 15:13:29.034 zwm, network: Response{protocol=http/1.1, code=200, message=OK, url=https://publicobject.com/helloworld.txt}
2019-12-26 15:13:36.609 zwm, request2, thread: Thread-6
2019-12-26 15:13:36.645 zwm, onResponse, thread2: OkHttp http://publicobject.com/helloworld.txt
2019-12-26 15:13:36.646 zwm, body2: 
...                     
2019-12-26 15:13:36.646 zwm, cache2: Response{protocol=http/1.1, code=200, message=OK, url=https://publicobject.com/helloworld.txt}
2019-12-26 15:13:36.647 zwm, network2: null

12.设置超时

OkHttpClient client = new OkHttpClient.Builder()
    .connectTimeout(10, TimeUnit.SECONDS)  // 连接超时
    .writeTimeout(10, TimeUnit.SECONDS)    // Socket写超时
    .readTimeout(30, TimeUnit.SECONDS)     // Socket读超时
    .build();

在2.5.0版本之后,读、写、连接超时的默认值是10s。

六、源码解析

OkHttp源码解析系列

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

推荐阅读更多精彩内容