2.Android okhttp源码教学十年老司机带你飞 面试官都得对你刮目相看(极度针对面试)

面试官:为什么用Okhttp,而不选择其它网络框架?

支持HTTP2/SPDY,允许所有同一个主机地址的请求共享同一个Socket连接(SPDY是Google开发的基于TCP的传输层协议,用以最小化网络延迟,提升网络速度,优化用户的网络使用体验。)

   关于HTTP2的优点,主要有:

多路复用:就是针对同个域名的请求,都可以在同一条连接中并行进行,而且头部和数据都进行了二进制封装。

二进制分帧:传输都是基于字节流进行的,而不是文本,二进制分帧层处于应用层和传输层之间。

头部压缩:HTTP1.x每次请求都会携带完整的头部字段,所以可能会出现重复传输,因此HTTP2采用HPACK对其进行压缩优化,可以节省不少的传输流量。

 OkHttp由于基于Http协议,所以http协议都支持

 在早期的版本中,OkHttp支持Http1.0,1.1,SPDY协议,但是Http2协议的问世,导致OkHttp也做出了改变,OkHttp鼓励开发者使用HTTP2,不再对SPDY协议给予支持。另外,新版本的OkHttp还有一个新的亮点就是支持WebScoket,这样我   们就可以非常方便的建立长连接了

 2.连接池减少请求延时(socket自动选择最好路线,并支持自动重连,拥有自动维护的socket连接池,减少握手次数,减少了请求延迟,共享Socket,减少对服务器的请求次数。)

透明的GZIP压缩减少响应数据的大小(基于Headers的缓存策略减少重复的网络请求)

透明的GZIP压缩减少响应数据的大小{{{拥有Interceptors轻松处理请求与响应(自动处理GZip压缩)}}},使用Gzip来压缩request和response, 减少传输数据量, 从而减少流量消耗.重试及重定向

面试官:简述一下OkHttp

OkHttp是一个非常优秀的网络请求框架,已被谷歌加入到Android的源码中。

支持http2,对一台机器的所有请求共享同一个socket   。1.2是否支持呢????

内置连接池,支持连接复用,减少延迟

支持透明的gzip压缩响应体

通过缓存避免重复的请求

请求失败时自动重试主机的其他ip,自动重定向

很多设计模式,好用的API。链式调用

面试官:okhttp支持的协议是什么?

android支持http2.0,Okhttp如何开启的Http2.0

https://blog.csdn.net/weixin_42522669/article/details/117576189

有非常好的握手过程

自动的一个过程:ALPN协议

当后端支持的协议内包含Http2.0时,则就会把请求升级到Http2.0阶段。

面试官:看过OkHttp的源码吗,简单说一下

三大点:

第一。分发器(处理高并发请求)

1.用法解析 同步和异步 怎么实现的

2.okhttp线程池工作原理

3.同步队列里面,运行队列和准备怎么工作的

4.如何实现并发的?并发控制

第二。拦截器(每个拦截器的作用)

1.责任链模式

2.拦截器 缓存机制  缓存基于DiskLruCache

3.自定义拦截器

4.多域名如何封装?测试和正式如何封装

第三。网络拦截器Connection原理

1.拦截器 socker连接池复用机制详解

缓存机制(拦截器)

2.在无网的时候直接使用缓存,这该怎么做呢?很简单,我们只要自定义一个拦截器,在我们的请求头中判断没有网络可用时,缓存策略为强制使用缓存。

3.缓存机制是怎么样的?网络请求缓存处理,okhttp如何处理网络缓存的?

重试机制(拦截器)

1.请求失败了怎么做的

2.网络重试是怎么实现的

比如:请求失败之后,会在结束之后,添加新的?还是继续进行重试?重试多少次?

面试官:怎么设计一个自己的网络访问框架,为什么这么设计?

同上:并发,处理请求,处理响应,复用

先参考现有的框架,找一个比较合适的框架作为启动点,比如说,基于上面讲到的okhttp的优点,选择okhttp的源码进行阅读,并且将主线的流程抽取出,为什么这么做,因为okhttp里面虽然涉及到了很多的内容,但是我们用到的内容并不是特别多;保证先能运行起来一个基本的框架;

考虑拓展,有了基本框架之后,我会按照我目前在项目中遇到的一些需求或者网路方面的问题,看看能不能基于我这个框架进行优化,比如服务器它设置的缓存策略,

我应该如何去编写客户端的缓存策略去对应服务器的,还比如说,可能刚刚去建立基本的框架时,不会考虑HTTPS的问题,那么也会基于后来都要求https,进行拓展;

面试官:你是怎么封装okhttp的?

同一的请求头,然后封装请求体参数。处理响应,通过泛型,然后回调里面解析数据

面试官:okhttp如何处理并发的?

.同步和异步

.3队列  64链接  5 host      同步1个队列,异步2个队列

异步请求处理:队列是否满,否则假如到准备队列。然后线程池执行,通过5大拦截器返回响应

. 处理完成,移除 。然后判断准备队列大小。添加到正在执行的队列里面

其中Dispatcher有一个线程池,用于执行异步的请求.并且内部还维护了3个双向任务队列,

分别是:准备异步执行的任务队列、正在异步执行的任务队列、正在同步执行的任务队列.

/** Executes calls. Created lazily. */

//这个线程池是需要的时候才会被初始化

    private @Nullable

    ExecutorServiceexecutorService;

    /** Ready async calls in the order they'll be run. */

    private final DequereadyAsyncCalls =new ArrayDeque<>();

    /** Running asynchronous calls. Includes canceled calls that haven't finished yet. */

    private final DequerunningAsyncCalls =new ArrayDeque<>();

    /** Running synchronous calls. Includes canceled calls that haven't finished yet. */

    private final DequerunningSyncCalls =new ArrayDeque<>();

    public synchronized ExecutorServiceexecutorService() {

if (executorService ==null) {

//注意,该线程池没有核心线程,线程数量可以是Integer.MAX_VALUE个(相当于没有限制),超过60秒没干事就要被回收

            executorService =new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,

                    new SynchronousQueue<>(), Util.threadFactory("OkHttp Dispatcher", false));

        }

return executorService;

    }

synchronized void enqueue(AsyncCall call) {

if (runningAsyncCalls.size()

runningAsyncCalls.add(call);

    executorService().execute(call);

  }else {

readyAsyncCalls.add(call);

  }

}

2个队列:重点一:

首先加上同步锁,然后判断实际的运行请求数是否小于允许的最大的请求数量(64) 并且共享主机的正在运行的调用的数量小于同时最大的相同Host的请求数(5)

同一个服务器地址不超过5个

public final class Dispatcher {  private int maxRequests = 64;  private int maxRequestsPerHost = 5;

如果这个AsyncCall请求符合条件(判断实际的运行请求数是否小于允许的最大的请求数量(64) 并且共享主机的正在运行的调用的数量小于同时最大的相同Host的请求数(5)) 才会添加到执行异步请求队列,然后通过线程池进行异步请求否则就把这个AsyncCall请求添加到就绪(等待)异步请求队列当中

如果都符合就把请求添加到正在执行的异步请求队列当中,然后通过线程池去执行这个请求call,否则的话在就绪(等待)异步请求队列当中添加

面试官:为什么是arrayDeque,这个队列,有什么好处

说到这LinkedList表示不服,我们知道LinkedList同样也实现了Deque接口,内部是用链表实现的双端队列,那为什么不用LinkedList呢?

实际上这与readyAsyncCalls向runningAsyncCalls转换有关,当执行完一个请求或调用enqueue方法入队新的请求时,会对readyAsyncCalls进行一次遍历,将那些符合条件的等待请求转移到runningAsyncCalls队列中并交给线程池执行。尽管二者都能完成这项任务,但是由于链表的数据结构致使元素离散的分布在内存的各个位置,CPU缓存无法带来太多的便利,另外在垃圾回收时,使用数组结构的效率要优于链表。

面试官:为什么要异步用两个队列呢?

因为Dispatcher默认支持最大的并发请求是64个,单个Host最多执行5个并发请求,

如果超过,则Call会先被放入到readyAsyncCall中,当出现空闲的线程时,再将readyAsyncCall中的线程移入到runningAsynCalls中,执行请求。先看Dispatcher的流程,跟着流程读源码

面试官:大于64链接之后,准备都队列是如何处理的?准备队列是如何加入到运行队列里面?

处理完成一个请求之后,会在finlly调用,finish方法

private <T> void finished(Deque<T> calls, T call, boolean promoteCalls) {

    int runningCallsCount;

    Runnable idleCallback;

    synchronized (this) {

      if (!calls.remove(call)) throw new AssertionError("Call wasn't in-flight!");//将请求移除集合

      if (promoteCalls) promoteCalls();

     ...

    }

private void promoteCalls() {

    if (runningAsyncCalls.size() >= maxRequests) return; // Already running max capacity.

    if (readyAsyncCalls.isEmpty()) return; // No ready calls to promote.


    for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {

      AsyncCall call = i.next();


      if (runningCallsForHost(call) < maxRequestsPerHost) {

        i.remove();

        runningAsyncCalls.add(call);

        executorService().execute(call);

      }

      if (runningAsyncCalls.size() >= maxRequests) return; // Reached max capacity.

    }

通过结束后再来一次

通过拦截器链得到Response,然后通过重定向拦截器判断是否取消,取消调用callBack的失败方法,没有取消就直接返回结果

最后无论是否取消,都会调用dispatcher的finish方法,后面会讲到

client.dispatcher().finished(this);

同步源代码分析:

@Override public Responseexecute()throws IOException {

synchronized (this) {

if (executed)throw new IllegalStateException("Already Executed");

    executed =true;

  }

captureCallStackTrace();

  try {

client.dispatcher().executed(this);

    Response result = getResponseWithInterceptorChain();

    if (result ==null)throw new IOException("Canceled");

    return result;

  }finally {

client.dispatcher().finished(this);

  }

}

调用了dispatercher,添加到队列client.dispatcher().executed(this);

使用案例:

//1.创建OkHttpClient对象

    OkHttpClient okHttpClient =new OkHttpClient();

    //2.创建Request对象,设置一个url地址(百度地址),设置请求方式。

    Request request =new Request.Builder()

.url("http://www.baidu.com")

.get()

.build();

    //3.创建一个call对象,参数就是Request请求对象

    final Call call = okHttpClient.newCall(request);

    //4.同步调用会阻塞主线程,这边在子线程进行

    new Thread(new Runnable() {

@Override

        public void run() {

try {

//同步调用,返回Response,会抛出IO异常

                Response response =call.execute();

            }catch (IOException e) {

e.printStackTrace();

            }

}

}).start();

}

同步请求总结:在哪个线程回调,就在哪个线程处理。

面试官:什么场景用同步请求?

举例:分片上传!

异步请求:

executorService().execute(call);

public synchronized ExecutorServiceexecutorService() {

if (executorService ==null) {

executorService =new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,

        new SynchronousQueue(), Util.threadFactory("OkHttp Dispatcher", false));

  }

return executorService;

}

面试官:okhttp线程池工作原理是怎样的?

在Okhttp中,构建了一个核心为[0, Integer.MAX_VALUE]的线程池,它不保留任何最小线程数,随时创建更多的线程数,当线程空闲时只能活60秒

线程池execute,其实就是要执行线程的run方法有封装了一层,最终看

AsyncCall的excute方法:

@Override protected void execute() {

boolean signalledCallback =false;

  try {

Response response = getResponseWithInterceptorChain();

    if (retryAndFollowUpInterceptor.isCanceled()) {

signalledCallback =true;

      responseCallback.onFailure(RealCall.this, new IOException("Canceled"));

    }else {

signalledCallback =true;

      responseCallback.onResponse(RealCall.this, response);

    }

}catch (IOException e) {

if (signalledCallback) {

// Do not signal the callback twice!

      Platform.get().log(INFO, "Callback failure for " + toLoggableString(), e);

    }else {

responseCallback.onFailure(RealCall.this, e);

    }

}finally {

client.dispatcher().finished(this);

  }

}

同步和异步总结:

对于同步和异步请求,唯一的区别就是异步请求会放在线程池(ThreadPoolExecutor)中去执行,而同步请求则会在当前线程中执行,注意:同步请求会阻塞当前线程。

同步请求可以得到respones,异步请求通过回调得到。

面试官:okhttp的拦截器是怎么理解的?

1.拦截器主要处理2个东西,request和respondse.可以看看源码的拦截器,拦截器主要用来观察,修改以及可能短路的请求输出和响应的回来。

在依次介绍各个拦截器之前,先介绍一个比较重要的类:RealInterceptorChain,直译就是拦截器链类;

Response result = getResponseWithInterceptorChain();

没错,在getResponseWithInterceptorChain();方法中我们就用到了这个RealInterceptorChain类。

ResponsegetResponseWithInterceptorChain()throws IOException {

// Build a full stack of interceptors.

  List interceptors =new ArrayList<>();

  interceptors.addAll(client.interceptors());

  interceptors.add(retryAndFollowUpInterceptor);

  interceptors.add(new BridgeInterceptor(client.cookieJar()));

  interceptors.add(new CacheInterceptor(client.internalCache()));

  interceptors.add(new ConnectInterceptor(client));

  if (!forWebSocket) {

interceptors.addAll(client.networkInterceptors());

  }

interceptors.add(new CallServerInterceptor(forWebSocket));

  Interceptor.Chain chain =new RealInterceptorChain(

interceptors, null, null, null, 0, originalRequest);

  return chain.proceed(originalRequest);

}

2.责任链模式:(递归调用)循环调用,通过不停的调用自己。然后最后退出

public Response proceed(Request request)throws IOException {

return this.proceed(request, this.streamAllocation, this.httpCodec, this.connection);

面试官:责任链模式是怎么样的?

责任链模式的类似情况如下:

publicResponse proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,

    RealConnection connection)throws IOException {

if (index >=interceptors.size())throw new AssertionError();

  calls++;

  // If we already have a stream, confirm that the incoming request will use it.

  if (this.httpCodec !=null && !this.connection.supportsUrl(request.url())) {

throw new IllegalStateException("network interceptor " +interceptors.get(index -1)

+" must retain the same host and port");

  }

// If we already have a stream, confirm that this is the only call to chain.proceed().

  if (this.httpCodec !=null &&calls >1) {

throw new IllegalStateException("network interceptor " +interceptors.get(index -1)

+" must call proceed() exactly once");

  }

// Call the next interceptor in the chain.

  RealInterceptorChain next =new RealInterceptorChain(

interceptors, streamAllocation, httpCodec, connection, index +1, request);

  Interceptor interceptor =interceptors.get(index);

  Response response = interceptor.intercept(next);

  // Confirm that the next interceptor made its required call to chain.proceed().

  if (httpCodec !=null &&index +1

throw new IllegalStateException("network interceptor " + interceptor

+" must call proceed() exactly once");

  }

// Confirm that the intercepted response isn't null.

  if (response ==null) {

throw new NullPointerException("interceptor " + interceptor +" returned null");

  }

图解:

每个拦截器负责一个特殊的职责.最后那个拦截器负责请求服务器,然后服务器返回了数据再根据这个拦截器的顺序逆序返回回去,最终就得到了网络数据.

demo:https://www.jianshu.com/p/ecf55b01ec19

OkHttp的这种拦截器链采用的是责任链模式,这样的好处是将请求的发送和处理分开,并且可以动态添加中间的处理方实现对请求的处理、短路等操作。

责任链模式:一个对象持有下个对象的引用;

请求:通过5个拦截器,把一个一个请求拼接起来

结果:最后一个先响应,先进后出的原理。

举一个列子:出现弹框,然后一个个关闭。

拦截器得到响应的结果可以看到这里的拦截链使用的非常巧妙,有点像栈的数据结构。依次将各个拦截器的方法入栈,最后得到response,再依次弹栈。如果是我来写的话,可能就直接一个for循环依次调用每个拦截器的拦截方法。但是这样的话我还得再来一遍反循环,再来依次处理加工response。很明显这里栈的结构更符合我们的业务场景。

 demo:Android 手写okhttp责任链:

面试官:okhttp有哪些拦截器?它们的作用分别是什么?

五种拦截器

看源码就知道执行顺序是

retryAndFollowUpInterceptor-》BridgeInterceptor-》CacheInterceptor-》ConnectInterceptor-》CallServerInterceptor

但是看上面这段源码发现好像并不是只有这五种对吧!有两句代码,分别是addAll方法,添加了两个集合,集合存储的是啥?

这里其实是自定义拦截器,可以看到,自定义拦截器添加的顺序分别又有两种

根据顺序分别叫做:Application Interceptors和Network Interceptors

CallServerInterceptor是拦截器链中最后一个拦截器,负责将网络请求提交给服务器

拦截器的执行顺序:先执行自定义的拦截器

interceptors.addAll(client.interceptors());

interceptors.add(retryAndFollowUpInterceptor);

interceptors.add(new BridgeInterceptor(client.cookieJar()));

interceptors.add(new CacheInterceptor(client.internalCache()));

interceptors.add(new ConnectInterceptor(client));

if (!forWebSocket) {

interceptors.addAll(client.networkInterceptors());

}

interceptors.add(new CallServerInterceptor(forWebSocket));

第一拦截器:RetryAndFollowUpInterceptor(重试,重定向拦截器,code:301,302)

常用的重定向方式有:301 redirect、302 redirect与meta fresh。

用来实现连接失败的重试和重定向

重试机制:

他处于责任链的顶端,负责网络请求的开始工作,也负责收尾的工作。

一开始,创建了StreamAllocation对象,他封装了网络请求相关的信息:连接池,地址信息,网络请求,事件回调,负责网络连接的连接、关闭,释放等操作

followUpCount是用来记录我们发起网络请求的次数的,为什么我们发起一个网络请求,可能okhttp会发起多次呢?

例如https的证书验证,我们需要经过:发起 -> 验证 -> 响应,三个步骤需要发起至少两次的请求,或者我们的网络请求被重定向,在我们第一次请求得到了新的地址后,再向新的地址发起网络请求。(发现迁移到新地址,访问新的服务器地址!)

在网络请求中,不同的异常,重试的次数也不同,okhttp捕获了两种异常:RouteException和IOException。

RouteException:所有网络连接失败的异常,包括IOException中的连接失败异常;

IOException:除去连接异常的其他的IO异常。

这个时候我们需要判断是否需要重试:

其中的路由地址我们先忽略,这个之后我们还会讨论。假定没有其他路由地址的情况下:

1、连接失败,并不会重试;

2、如果连接成功,因为特定的IO异常(例如认证失败),也不会重试

其实这两种情况是可以理解的,如果连接异常,例如无网络状态,重试也只是毫秒级的任务,不会有特别明显的效果,如果是网络很慢,到了超时时间,应该让用户及时了解失败的原因,如果一味重试,用户就会等待多倍的超时时间,用户体验并不好。认证失败的情况就更不用多说了。

3. 默认重连3次??????

RetryAndFollowUpInterceptor拦截设置最大重定向次数为20次;

private static final int MAX_FOLLOW_UPS =20;

如果我们非要重试多次怎么办?

自定义Interceptor,增加计数器,重试到你满意就可以了:

通过 recover 方法检测该 RouteException 是否能重新连接;

第二拦截器:BridgeInterceptor(桥接拦截器)

用来修改请求和响应的 header 信息

负责设置编码方式,添加头部,Keep-Alive 连接以及应用层和网络层请求和响应类型之间的相互转换

public final class BridgeInterceptorimplements Interceptor {

private final CookieJar cookieJar;

    public BridgeInterceptor(CookieJar cookieJar) {

this.cookieJar = cookieJar;

    }

public Response intercept(Chain chain)throws IOException {

Request userRequest = chain.request();

        Builder requestBuilder = userRequest.newBuilder();

        RequestBody body = userRequest.body();

        if (body !=null) {

MediaType contentType = body.contentType();

            if (contentType !=null) {

requestBuilder.header("Content-Type", contentType.toString());

            }

long contentLength = body.contentLength();

            if (contentLength != -1L) {

requestBuilder.header("Content-Length", Long.toString(contentLength));

                requestBuilder.removeHeader("Transfer-Encoding");

            }else {

requestBuilder.header("Transfer-Encoding", "chunked");

                requestBuilder.removeHeader("Content-Length");

            }

}

第三拦截器:CacheInterceptor(缓存拦截器)

用来实现响应缓存。比如获取到的 Response 带有 Date,Expires,Last-Modified,Etag 等 header,表示该 Response 可以缓存一定的时间,下次请求就可以不需要发往服务端,直接拿缓存的

面试官:okhttp的缓存是怎么样的?

缓存策略

1、如果网络不可用并且无可用的有效缓存,则返回504错误;

2、继续,如果不需要网络请求,则直接使用缓存;

3、继续,如果需要网络可用,则进行网络请求;

4、继续,如果有缓存,并且网络请求返回HTTP_NOT_MODIFIED,说明缓存还是有效的,则合并网络响应和缓存结果。同时更新缓存;

5、继续,如果没有缓存,则写入新的缓存;

对于okhttp的缓存解决方案,我的需求是:

1、有网的时候也可以读取缓存,并且可以控制缓存的过期时间,这样可以减轻服务器压力

2、有网的时候不读取缓存,比如一些及时性较高的接口请求

3、无网的时候读取缓存,并且可以控制缓存过期的时间

okhttp&http缓存策略

强制缓存:当用户端第一次请求数据是,服务端返回了缓存的过期时间(Expires与Cache-Control),没有过期即可以继续使用缓存,否则则不适用,无需再向服务端讯问。

比照缓存:当用户端第一次请求数据时,服务端会将缓存标识(Etag/If-None-Match与Last-Modified/If-Modified-Since)与数据一起返回给用户端,用户端将两者都备份到缓存中 ,再次请求数据时,用户端将上次备份的缓存

标识发送给服务端,服务端根据缓存标识进行判断,假如返回304,则表示缓存可用,假如返回200,标识缓存不可用,使用最新返回的数据。

ETag是用资源标识码标识资源能否被修改,Last-Modified是用时间戳标识资源能否被修改。ETag优先级高于Last-Modified。

解决办法:添加拦截器:

/**

    * 有网时候的缓存

    */finalInterceptorNetCacheInterceptor=newInterceptor(){@OverridepublicResponseintercept(Chainchain)throwsIOException{Requestrequest=chain.request();Responseresponse=chain.proceed(request);intonlineCacheTime=30;//在线的时候的缓存过期时间,如果想要不缓存,直接时间设置为0returnresponse.newBuilder().header("Cache-Control","public, max-age="+onlineCacheTime).removeHeader("Pragma").build();}};/**

    * 没有网时候的缓存

    */finalInterceptorOfflineCacheInterceptor=newInterceptor(){@Override

缓存策略:

那么服务器是如何对比呢?有两种情况,一种是ETag。服务器会对资源做一个ETag算法,生成一个字符串。这个字符串实际上代表了资源在服务器的版本。服务器在响应头中会加入这个ETag给客户端。客户端缓存后下次继续请求的时候把缓存中的ETag取出来加入到请求头。服务器取出请求头中的ETag,与当前服务器该资源实时的ETag作对比。如果不一致则表明服务器有更新资源,则返回200响应码,并返回资源。如果客户端的ETag和服务端的相同,代表服务端没有更新数据,则返回304响应码,并不返回资源。这样就达到了使用缓存去节省时间和流量的目的。另一种是Last-Modified,它代表的是资源在服务器更新的时间,和ETag类似客户端从缓存中取出这个时间加入到请求头,服务器根据最近的一次更新时间与之对比,决定是返回资源还是返回304。

总结下,如果服务器通过Cache-Control头规定了过期时间,没过期的话可以直接使用缓存。否则,使用Etag或者Last-Modified加入请求体,服务器接收后与最新的数据进行比对,决定直接返回最新数据还是304响应码。是不是其实一点都不复杂呢?

我们的客户端也就是OkHttpClient在缓存这块要做哪些事呢?

1.首先,需要取缓存,并确定缓存有无过期。

2.如果过期,看看有没有Etag或Last-Modified可以加到请求体。

3.最后如果服务器返回304,我们要直接使用缓存,

4.如果返回200,我们需要更新缓存。按照这个顺序,我们来在源码中找一找,这些操作是如何实现的。

1)CacheControl( HTTP 中的Cache-Control 和Pragma 缓存控制):指定缓存规则

2)Cache(缓存类)

3)DiskLruCache(文件化的LRU 缓存类)

(1)读取缓存:先获限OkHttpClient 的Cache 缓存对象,就是上面创建OkHttpClient 设置的

Cahce; 传Request 请求到Cache 的get 方法查找缓存响应数据Response;构造一个缓存策略,

再调用它的get 去决策使用网络请求还是缓存响应。若使用缓存,它的cacheResponse 不为

空,networkRequest 为空,用缓存构造响应直接返回。若使用请求,则cacheResponse 为

空,networkRequest 不为空,开始网络请求流程。

Cache 的get 获取缓存方法,计算request 的key 值(请求url 进行md5 加密),根据key 值

去DisLruCache 查找是否存在缓存内容,存则则创建绘存Entry 实体。ENTRY_METADATA 代表

响应头信息,ENTRY_BODY 代表响应体信息。如果缓存存在,在指定目录下会有两个文件

****.0 *****.1 分别存储某个请求缓存响应头和响应体信息。

CacheStrategy 的get 方法:1)若缓存响应为空或2)请求是https 但缓存响应没有握手信息;

3)请求和缓存响应都是不可缓存的;4)请求是onCache,并且又包含if-Modified-Since 或

If-None-Match 则不使用缓存; 再计算请求有效时间是否符合响应的过期时间,若响应在有

效范围内,则缓存策略使用缓存,否则创建一个新的有条件的请求,返回有条件的缓存策略。

( 2 ) 存储缓存流程: 从HttpEngine 的readResponse() 发送请求开始, 判断

hasBody(userResponse), 如果缓存的话, maybeCache() 缓存响应头信息,

unzip(cacheWritingResponse(storeRequest, userResponse))缓存响应体

3.网络请求缓存处理,okhttp 如何处理网络缓存的;

(1)网络缓存优先考虑强制缓存,再考虑对比缓存

--首先判断强制缓存中的数据的是否在有效期内。如果在有效期,则直接使用缓存。如

果过了有效期,则进入对比缓存。

--在对比缓存过程中,判断ETag 是否有变动,如果服务端返回没有变动,说明资源未改

变,使用缓存。如果有变动,判断Last-Modified。

--判断Last-Modified,如果服务端对比资源的上次修改时间没有变化,则使用缓存,否

则重新请求服务端的数据,并作缓存工作

CacheStrategy是其中的缓存策略,Cache类

除了缓存策略类CacheStrategy,还有一个重要的类Cache没讲。这里简单讲下这个类就是负责存取缓存的。它里面存储缓存的对象是DiskLruCache,而DiskLruCache内部又是LinkedHashMap<String, Entry>,关于LinkedHashMap大家有可以查阅一下相关文章,它底层是一个哈希表和链表的组合,并且由于同时维护了一个可以给存储对象排序的双向链表,因此可以实现Lru算法。

private static boolean validate(Response cached, Response network) {

if (network.code() ==304) {

return true;

    }else {

Date lastModified = cached.headers().getDate("Last-Modified");

        if (lastModified !=null) {

Date networkLastModified = network.headers().getDate("Last-Modified");

            if (networkLastModified !=null && networkLastModified.getTime() < lastModified.getTime()) {

return true;

            }

}

return false;

    }

}

if

(networkRequest ==null && cacheResponse ==null) {

return (new Builder()).request(chain.request()).protocol(Protocol.HTTP_1_1).code(504).message("Unsatisfiable Request (only-if-cached)").body(EMPTY_BODY).sentRequestAtMillis(-1L).receivedResponseAtMillis(System.currentTimeMillis()).build();

}else if (networkRequest ==null) {

return cacheResponse.newBuilder().cacheResponse(stripBody(cacheResponse)).build();

}else {

分析:

1.cacheResponse也为null说明我们没有有效的缓存response,而我们又不会进行网络请求,因此给上层构建了一个响应码味504的response

2.如果cacheResponse不为null,说明我们有可用缓存,而此次请求又不会再请求网络,因此直接将缓存response返回。

3.重点分析:

1).此时如果服务器返回的响应码为HTTP_NOT_MODIFIED,也就是我们常见的304,代表服务器的资源没有变化,客户端去取本地缓存即可,此时服务器不会返回响应体

private static boolean

validate(Response cached, Response network) {

if (network.code() ==304) {

return true;

    }else {

Date lastModified = cached.headers().getDate("Last-Modified");

        if (lastModified !=null) {

Date networkLastModified = network.headers().getDate("Last-Modified");

            if (networkLastModified !=null && networkLastModified.getTime() < lastModified.getTime()) {

return true;

            }

}

return false;

    }

}

2).直接使用networkResponse构建response并返回。此时我们还需要做一件事,就是更新我们的缓存,将最终response写入到cache对象中去

private Response cacheWritingResponse(final CacheRequest cacheRequest, Response response)throws IOException {

if (cacheRequest ==null) {

return response;

    }else {

Sink cacheBodyUnbuffered = cacheRequest.body();

        if (cacheBodyUnbuffered ==null) {

return response;

        }else {

final BufferedSource source = response.body().source();

            final BufferedSink cacheBody = Okio.buffer(cacheBodyUnbuffered);

            Source cacheWritingSource =new Source() {

boolean cacheRequestClosed;

                public long read(Buffer sink, long byteCount)throws IOException {

long bytesRead;

                    try {

bytesRead = source.read(sink, byteCount);

                    }catch (IOException var7) {

if (!this.cacheRequestClosed) {

this.cacheRequestClosed =true;

                            cacheRequest.abort();

                        }

throw var7;

                    }

if (bytesRead == -1L) {

if (!this.cacheRequestClosed) {

this.cacheRequestClosed =true;

                            cacheBody.close();

                        }

return -1L;

                    }else {

sink.copyTo(cacheBody.buffer(), sink.size() - bytesRead, bytesRead);

                        cacheBody.emitCompleteSegments();

                        return bytesRead;

                    }

}

public Timeout timeout() {

return source.timeout();

                }

public void close()throws IOException {

if (!this.cacheRequestClosed && !Util.discard(this, 100, TimeUnit.MILLISECONDS)) {

this.cacheRequestClosed =true;

                        cacheRequest.abort();

                    }

source.close();

                }

};

            return response.newBuilder().body(new RealResponseBody(response.headers(), Okio.buffer(cacheWritingSource))).build();

        }

}

}

先了解下HTTP协议的缓存机制:

首先缓存分为三种:过期时间缓存、第一差异缓存和第二差异缓存,而且在优先级上,过期时间缓存 > 第一差异缓存 > 第二差异缓存。

过期时间缓存,就是通过HTTP响应头部的字段控制:

expires:响应字段,绝对过期时间,HTTP1.0。

Cache-Control:响应字段,相对过期时间,HTTP1.1。注意如果值为no-cache,表示跳过过期时间缓存逻辑,值为no-store表示跳过过期时间缓存逻辑和差异缓存逻辑,也就是不使用缓存数据。

当客户端请求时,发现缓存未过期,就直接返回缓存数据了,不请求网络,否则,执行第一差异缓存逻辑:

If-None-Match:请求字段,值为ETag。

ETag:响应字段,服务端会根据内容生成唯一的字符串。

如果服务端发现If-None-Match的值和当前ETag一样,就说明数据内容没有变化,就返回304,否则,执行第二差异缓存逻辑:

If-Modified-Since:请求字段,客户端告诉服务端本地缓存的资源的上次修改时间。

Last-Modified:响应字段,服务端告诉客户端资源的最后修改时间。

如果服务端发现If-Modified-Since的值就是资源的最后修改时间,就说明数据内容没有变化,就返回304,否则,返回所有资源数据给客户端,响应码为200。

回到OkHttp,CacheInterceptor拦截器处理的逻辑,其实就是上面所说的HTTP缓存逻辑,注意到OkHttp提供了一个现成的缓存类Cache,它采用DiskLruCache实现缓存策略,至于缓存的位置和大小,需要你自己指定。

这里其实会有个问题,上面的缓存都是依赖HTTP协议本身的缓存机制的,如果我们请求的服务器不支持这套缓存机制,或者需要实现更灵活的缓存管理,直接使用上面这套缓存机制就可能不太可行了,这时我们可以自己新增拦截器,自行实现缓存的管理。

第四拦截器:

ConnectInterceptor负责与服务器建立链接:很重要(是最后一个拦截器)

和服务器通信

我们发现目前为止我们还没有进行真正的请求。别急,ConnectInterceptor就是一个负责建立http连接的拦截器

封装了socket连接和TLS握手等逻辑

用来打开到服务端的连接。其实是调用了 StreamAllocation 的newStream 方法来打开连接的。建联的 TCP 握手,TLS 握手都发生该阶段。过了这个阶段,和服务端的 socket 连接打通

public final class RealConnectionextends Listenerimplements Connection {

private final Route route;

    private Socket rawSocket;

    public Socket socket;

    private Handshake handshake;

    private Protocol protocol;

    public volatile FramedConnection framedConnection;

    public int successCount;

    public BufferedSource source;

    public BufferedSink sink;

    public int allocationLimit;

    public final List> allocations =new ArrayList();

    public boolean noNewStreams;

    public long idleAtNanos =9223372036854775807L;

    public RealConnection(Route route) {

this.route = route;

    }

2)然后在ConnectInterceptor,也就是负责建立连接的拦截器中,首先会找可用连接,也就是从连接池中去获取连接,具体的就是会调用到ConectionPool的get方法。

需要看下这个源码:ConnectInterceptor的intercept()方法

publicResponseintercept(Chainchain)throwsIOException{RealInterceptorChainrealChain=(RealInterceptorChain)chain;Requestrequest=realChain.request();StreamAllocationstreamAllocation=realChain.streamAllocation();// We need the network to satisfy this request. Possibly for validating a conditional GET.booleandoExtensiveHealthChecks=!request.method().equals("GET");// 1HttpCodechttpCodec=streamAllocation.newStream(client,chain,doExtensiveHealthChecks);RealConnectionconnection=streamAllocation.connection();returnrealChain.proceed(request,streamAllocation,httpCodec,connection);}

来源:https://www.jianshu.com/p/7cdd598d382b

第五拦截器:CallServerInterceptor

主要的工作就是把请求的Request写入到服务端,而后从服务端读取Response。

(1)、写入请求头

(2)、写入请求体

(3)、读取响应头

(4)、读取响应体

用来发起请求并且得到响应。上一个阶段已经握手成功,HttpStream 流已经打开,所以这个阶段把 Request 的请求信息传入流中,并且从流中读取数据封装成 Response 返回

面试官:自定义过哪些拦截器?

自定义拦截器:通常情况下拦截器用来添加,移除或者转换请求或者响应的头部信息。比如将域名替换为ip地址,将请求头中添加host属性,也可以添加我们应用中的一些公共参数,比如设备id、版本号等等

okhttp拦截器主要在以下几种情况使用:

自定义拦截器不一定要继承基本的5大拦截器,而是继承Interceptor

网络请求、响应日志输出

在Header中统一添加cookie、token

设置网络缓存

1.添加日志拦截器

可以用系统的,或者通过添加自己写的拦截器

2.在Header中统一添加cookie、token  

public class HeaderInterceptor implements Interceptor 

面试官:okhttp是如何实现连接池复用的?

OkHttp的底层是通过Java的Socket发送HTTP请求与接受响应的(这也好理解,HTTP就是基于TCP协议的),但是OkHttp实现了连接池的概念,

KeepAlive

当然大量的连接每次连接关闭都要三次握手四次分手的很显然会造成性能低下,因此http有一种叫做keepalive connections的机制,它可以在传输数据后仍然保持连接,当客户端需要再次获取数据时,直接使用刚刚空闲下来的连接而不需要再次握手

我们可以Reuqst的header中将Connection设置为keepalive来复用连接。

什么时候用到链接池复用?

同一端口和同一域名。socket复用。减少3次握手,4次挥手。连接池源码分析。

即对于同一主机的多个请求,其实可以公用一个Socket连接,而不是每次发送完HTTP请求就关闭底层的Socket,这样就实现了连接池的概念。而OkHttp对Socket的读写操作使用的OkIo库进行了一层封装。

2 okhttp连接池复用是具体的一个连接还是同一个域名下的连接都可以复用?

是同一个域名下连接都可以复用,服务器和PC浏览器同一个域名下只能建立6个TCP 连接,为了让同一个网页中的图片快速加载,所以要把图片放到不同的域名下,这样就可以实现>6个的连接请求。

源码中同一域名下默认是5个TCP接连,超过后会等待(这个是分发器要求的5)不同域名下最多64个请求,但是大部分时候同一个域名比较多

会从很多常用的连接问题中自动恢复。如果您的服务器配置了多个IP地址,当第一个IP连接失败的时候,OkHttp会自动尝试下一个IP,此外OkHttp还处理了代理服务器问题和SSL握手失败问题。

如上代码设置相当于没有复用 keep-alive导致每次请求都需要重新进行 DNS解析,3次握手4次挥手操作,这样是非常浪费性能的,源码中默认的是5个空闲TCP接连,并且活跃时间为5分钟。

通一个请求地址?

Okhttp支持5个并发KeepAlive,默认链路生命为5分钟(链路空闲后,保持存活的时间)。

创建连接池实在OkHttpClient初始化的时候,ConnectinoPool通过直接new出的

public ConnectionPool() {

this(5, 5L, TimeUnit.MINUTES);

}

public final class ConnectionPool {

private static final Executor executor;

    private final int maxIdleConnections;

    private final long keepAliveDurationNs;

    private final Runnable cleanupRunnable;

    private final Deque connections;

    final RouteDatabase routeDatabase;

    boolean cleanupRunning;

    public ConnectionPool() {

this(5, 5L, TimeUnit.MINUTES);

    }

RealConnection get(Address address, StreamAllocation streamAllocation) {

assert Thread.holdsLock(this);

    Iterator var3 =this.connections.iterator();

    RealConnection connection;

    do {

if (!var3.hasNext()) {

return null;

        }

connection = (RealConnection)var3.next();

    }while(connection.allocations.size() >= connection.allocationLimit || !address.equals(connection.route().address) || connection.noNewStreams);

    streamAllocation.acquire(connection);

    return connection;

}

主要的变量有必要说明一下:

什么时候链接?什么时候获取?什么时候添加?什么时候移除

在第四个链接拦截器的时候添加和获取的,还有链接的。然后移除通过一个线程一直移除

executor线程池,类似于CachedThreadPool,需要注意的是这种线程池的工作队列采用了没有容量的SynchronousQueue

Deque<RealConnection>,双向队列,双端队列同时具有队列和栈性质,经常在缓存中被使用,里面维护了RealConnection也就是socket物理连接的包装。

RouteDatabase,它用来记录连接失败的Route的黑名单,当连接失败的时候就会把失败的线路加进去。

      ConnectionPool: 连接池

static {

executor =new ThreadPoolExecutor(0, 2147483647, 60L, TimeUnit.SECONDS, new SynchronousQueue(), Util.threadFactory("OkHttp ConnectionPool", true));

}

可以看出连接池复用的核心就是用Deque<RealConnection>来存储连接

里面的线程池干嘛的?去清理无用链接。Executor的线程池是用来清理闲置的连接的

在cleanupRunnable的run方法会不停的调用cleanup清理并返回下一次清理的时间间隔。然后进入wait,等待下一次的清理。那么cleanup()是怎么计算时间间隔的?

链路的概念

Collection

HttpCodec

StreamAllocation

ConnectionPool

HttpEngine在发起请求之前,会先调用nextConnection()来获取一个Connection对象,如果可以从ConnectionPool中获取一个Connection对象,就不会新建,

如果无法获取,就会调用createnextConnection()来新建一个Connection对象,

这就是okhttp多路复用的核心,不像之前的网络框架,无论有没有,都会新建Connection对象。

https://www.jianshu.com/p/8928f4b128f1

https://www.cnblogs.com/tony-yang-flutter/p/12383267.html

复用第一要素:ConnectionPool 要点(对链接队列进行添加,移除操作)

ConnectionPool提供对Deque<RealConnection>进行操作的方法分别为put、get、connectionBecameIdle、evictAll几个操作。分别对应放入连接、获取连接、移除连接、移除所有连接操作。

管理http和http/2的链接,以便减少网络请求延迟。同一个address将共享同一个connection。该类实现了复用连接的目标。

ConnectionPool内部以队列方式存储连接

连接池最多维持5个连接,且每个链接最多活5分钟

每次添加链接的时候回执行一次清理任务,清理空闲的链接(RealConnection)。

连接池的清理和回收:我们在put新连接到队列的时候会先执行清理闲置连接的线程

复用第二要素:RealConnection 要点

RealConnection是socket物理连接的包装,

真实连接:public final class RealConnectionextends Listenerimplements Connection {

代表着链接socket的链路,如果拥有了一个RealConnection就代表了我们已经跟服务器有了一条通信链路

实现了三次握手等操作。

Put方法:

void put(RealConnection connection) {

assert (Thread.holdsLock(this));

    if (!cleanupRunning) {

cleanupRunning =true;

        executor.execute(cleanupRunnable);

    }

connections.add(connection);

}

历connections缓存列表,当某个连接计数的次数小于限制的大小并且request的地址和缓存列表中此连接的地址完全匹配。则直接复用缓存列表中的connection作为request的连接。

在RealConnectionPool中维护了一个线程池,来进行回收和复用;connections是一个记录连接的双端队列;routeDatabase是记录路由失败的线路,cleanupRunnable是用来进行自动回收连接的。

然后我们重点看一下cleanupRunnable:首先是一个死循环,一直执行,通过cleanup方法进行回收连接,并返回了下次清理的间隔时间(以纳米为单位,下次调用这个方法的时间),-1表示不需要再进行回收,则跳出循环,否则一直进行回收,同时让连接池RealConnectionPool等待

根据连接中的引用计数来计算空闲连接数和活跃连接数,然后标记出空闲的连接,如果空闲连接keepAlive时间超过5分钟,或者空闲连接数超过5个,则从Deque中移除此连接。接下来根据空闲连接或者活跃连接来返回下次需要清理的时间数:如果空闲连接大于0则返回此连接即将到期的时间,如果都是活跃连接并且大于0则返回默认的keepAlive时间5分钟,如果没有任何连接则跳出循环并返回-1

Clear方法:

首先会对缓存中的连接进行遍历,以寻找一个闲置时间最长的连接,然后根据该连接的闲置时长和最大允许的连接数量等参数来决定是否应该清理该连接。同时注意上面的方法的返回值是一个时间,如果闲置时间最长的连接仍然需要一段时间才能被清理的时候,会返回这段时间的时间差,然后会在这段时间之后再次对连接池进行清理。

复用第三要素:Connection 要点

复用第四要素:StreamAllocation 要点(计数对象。判断是否是空闲的 )。

如何判断闲置的连接:主要通过弱引用和计数的方式

在遍历缓存列表的过程中

使用连接数目inUseConnectionCount 和闲置连接数目idleConnectionCount 的计数累加值都是通过pruneAndGetAllocationCount() 是否大于0来控制的。

那么很显然pruneAndGetAllocationCount() 方法就是用来识别对应连接是否闲置的。>0则不闲置。否则就是闲置的连接。

在RealConnection中保存了StreamAllocationd的一个列表,作为连接上流的计数器。如果列表大小为0,表示连接是空闲的,可以回收;否则连接还在用,不能关闭。

对StreamAllocation使用了弱引用包装。只要弱引用还存在,说明连接还在用。

总结:清理闲置连接的核心主要是引用计数器List<Reference<StreamAllocation>> 和 选择排序的算法以及excutor的清理线程池。

一个链接对应多个流,链接和流的对应关系通过StreamAllocation来记录。

判断connection.allocations列表中能否有StreamAllocation,假如没有就是空闲连接,否则不是。

它里面维护了List<Reference<StreamAllocation>>的引用。List中StreamAllocation的数量也就是socket被引用的计数,如果计数为0的话,说明此连接没有被使用就是空闲的,需要通过下文的算法实现回收;如果计数不为0,则表示上层代码仍然引用,就不需要关闭连接。

在okhttp中,在高层代码的调用中,使用了类似于引用计数的方式跟踪Socket流的调用,这里的计数对象是StreamAllocation,它被反复执行aquire与release操作,这两个函数其实是在改变RealConnection中的List<Reference<StreamAllocation>> 的大小。(StreamAllocation.Java)。

List中StreamAllocation的数量也就是socket被引用的计数

public final class StreamAllocation {

  public void acquire(RealConnection connection) {

    connection.allocations.add(new WeakReference<>(this));

  }

  private void release(RealConnection connection) {

    for (int i = 0, size = connection.allocations.size(); i < size; i++) {

      Reference<StreamAllocation> reference = connection.allocations.get(i);

      if (reference.get() == this) {

        connection.allocations.remove(i);

        return;

      }

    }

    throw new IllegalStateException();

  }

复用第五要素:RouteDatabase,它用来记录连接失败的Route的黑名单,当连接失败的时候就会把失败的线路加进去

面试官:okhttp是如何保证通信安全的?

面试官:okhttp如何方法https里面怎么处理SSL?

客户端默认信任全部证书

X509TrustManager

SSLSocketFactory

客户端默认信任全部证书

面试官:OKhttp针对网络层有哪些优化?

另外一个博客

面试官:okhttp,在访问一个界面没有结束,关闭activity。还是会,这个要验证一下,那么下一次进去重新访问接口还是--------

网络请求如何取消?底层是怎么实现的

还是会执行

面试官:如何修改服务器返回的数据类型修改

Response的body().string()方法获取返回回来的json数据(也可以是其他类型的数据(XML类型) 这个需要和服务器端商量好)

面试官:delay和delayqueue

面试官:OkHttp中为什么使用构建者模式?

使用多个简单的对象一步一步构建成一个复杂的对象;

优点: 当内部数据过于复杂的时候,可以非常方便的构建出我们想要的对象,并且不是所有的参数我们都需要进行传递;

缺点: 代码会有冗余

好封装

面试官:OkHttp中模板方法设计模式是怎样的?

面试官: okhttp的缺点?

就是责任链模式的缺点:当然它也有缺点:因为调用链路长,而且存在嵌套,遇到问题排查其它比较麻烦。

面试官:okio是用来做什么的?

写流

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

推荐阅读更多精彩内容