深入理解OkHttp源码及设计思想

前言

用OkHttp很久了,也看了很多人写的源码分析,在这里结合自己的感悟,记录一下对OkHttp源码理解的几点心得。

整体结构

网络请求框架虽然都要做请求任务的封装和管理,但是最大的难点在于网络请求任务的多样性,因为网络层情况复杂,不仅要考虑功能性的建立Socket连接、文件流传输、TLS安全、多平台等,还要考虑性能上的Cache复用、Cache过期、连接池复用等,这些功能如果交错在一起,实现和维护都会有很大的问题。

为了解决这个问题,OkHttp采用了分层设计的思想,使用多层拦截器,每个拦截器解决一个问题,多层拦截器套在一起,就像设计模式中的装饰者模式一样,可以在保证每层功能高内聚的情况下,解决多样性的问题。

OkHttp使用了外观模式,开发者直接操作的主要就是OkHttpClient,其实如果粗略划分的话,整个OkHttp框架从功能上可以分为三部分:

1.请求和回调:具体的类就是Call、RealCall(及其内部类AsyncCall)、Callback等。

2.分发器及线程池:具体的类就是Dispatcher、ThreadPoolExecutor等。

3.拦截器:实现了分层设计+链式调用,具体的类就是Interceptor+RealInterceptorChain。

至于更具体的操作,均由拦截器实现,包括应用层拦截器、网络层拦截器等,开发者也可以自己扩展新的拦截器。

请求

网络请求其实可以分为数据和行为两部分,数据即我们的请求数据和返回数据,行为则是发起网络请求,以及得到处理结果。

数据(Request和Response)

在OkHttp中,用Request定义请求数据,用Response定义返回数据,这两个类都使用了建造者模式,把对象的创建和使用分离开,但这两个类更接近于数据模型,主要用来读写数据,不做请求动作。

行为(Call/RealCall/AsyncCall和Callback)

在OkHttp中,用Call和Callback定义网络请求,用Call去发起网络请求,用Callback去接收异步返回,(如果是同步请求,就直接返回Response数据)。

其中,Call是个接口,真正的实现类是RealCall,RealCall如果需要异步处理,还会先包装为RealCall的内部类AsyncCall,然后再把AsyncCall交给线程池。

在具体执行过程中,把数据对象交给行为对象去操作:

在RealCall行为中调用enqueue去发起异步网络请求,此时需要传参Request数据对象;返回的Callback会传递Response数据对象。

如果RealCall行为中调用的是execute同步网络请求,就直接返回Response数据对象。

RealCall只是对请求做了封装,真正处理请求的是分发器Dispatcher。

分发器及线程池

对于网络请求RealCall来说,需要可并行、可回调、可取消,因为OkHttp统一使用Dispatcher分发器来分发所有的Call请求,分发给多个线程进行执行(所以Dispatcher也叫反向代理),所以,这几个问题就需要交给Dispatcher来处理,对于Dispatcher来说,可并行、可回调、可取消的问题可以进一步被分解为以下几个问题,并分别处理:

1.有没有必要管理所有的请求

不论是同步请求还是异步请求,都是耗时操作,所以是个需要观测的行为,比如请求结束需要处理,请求本身可能取消等,都需要管理起来。

而且,不论是正在运行的,还是等待运行的,都需要管理。

2.如何管理所有的请求

为了管理所有的请求,Dispatcher采用了队列+生产+消费的模式。

为同步执行提供了runningSyncCalls来管理所有的同步请求;

为异步执行提供了runningAsyncCalls和readyAsyncCalls来管理所有的异步请求。

其中readyAsyncCalls是在当前可用资源不足时,用于缓存请求的。

由于这三个队列的使用场景类似于栈,偶尔需要删除功能,所以OkHttp使用了ArrayDeque双端队列来管理,ArrayDeque的设计和实现非常精妙,感兴趣的可以深入了解一下。

https://www.jianshu.com/p/132733115f95

3.如何确保多个队列之间能顺畅地调度

对于多线程情况下的队列调度,其实就是数据移动和失败阻塞的这两个问题。

对于数据移动来说,就是要考虑多线程下队列数据移动的问题。

对于同步请求来说,只有1个队列,不存在数据移动,数据移动的场景在两个异步队列,每当有一个异步请求finish了,就需要从待处理readyAsyncCalls队列移动到runningAsyncCalls队列,这在多线程场景下并不安全,需要加锁:

synchronized (this) {//加锁操作

if(!calls.remove(call))thrownewAssertionError("Call wasn't in-flight!");

if(promoteCalls) promoteCalls();

runningCallsCount = runningCallsCount();

idleCallback =this.idleCallback;

}

在promoteCalls时,会把call从ready队列转移到running队列:

privatevoidpromoteCalls(){

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

...

for(Iterator 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.

}

}

另外这个移动的操作放在finish函数里,会存在另一个问题,就是如何确保会执行这个finish函数,避免造成失败阻塞

对于失败阻塞来说,因为网络请求失败是很常见的场景,必须能在失败时避免阻塞队列。

OkHttp的处理是为Call对象的execute函数写try finally,在RealCall的execute函数里,在finally中调用client.dispatcher.finish(call),确保队列不阻塞。

这其实类似AsyncTask的处理方式,AsyncTask也是使用了try finally,在finally中scheduleNext,确保队列不阻塞。

4.如何实现多线程

io是个耗时但是不耗CPU的操作,是典型的需要并行处理的场景。

OkHttp不出意外地采用了线程池实现并行,这一点类似于AsyncTask,但不像AsyncTask使用了全局唯一的线程池,每个OkHttpClient都有自己的线程池。

不过,与AsyncTask不同的是,OkHttp的同步执行不进线程池,在RealCall执行同步execute任务时,只是在Dispatcher的runningSyncCalls中记录这个call,然后直接在当前线程执行了拦截器的操作。

至于异步执行,就是在RealCall中enqueue时调用Dispatcher的enqueue,然后调用线程池executeService().execute(call),这里面的call是RealCall的内部类AsyncCall,实现异步调用。

5.在这个过程中,用哪些方式提升效率

OkHttp主要针对队列和线程池做了优化:

循环数组

因为Dispatcher中的三个队列需要频繁出栈和入栈,所以采用了性能良好的循环数组ArrayDeque管理队列。

阻塞队列

因为Dispatcher自己用队列管理了排队的请求,所以Dispatcher中的线程池其实不需要缓存队列,那么这个线程池的任务其实是尽快地把元素转交给线程池中的io线程,所以采用了容量为0的阻塞队列SynchronousQueue,SynchronousQueue与普通队列不同,不是数据等线程,而是线程等数据,这样每次向SynchronousQueue里传入数据时,都会立即交给一个线程执行,这样可以提高数据得到处理的速度。

控制线程数量

因为线程本身也会消耗资源,所以每个线程池都需要控制线程数量,OkHttp的线程池更进一步,会针对每个Host主机的请求(避免全都卡死在某个Host上),分别控制线程数上限(5个),具体方法就是遍历所有runningAsyncCall队列中的每个Call,查询每个Call的Host,并做计数。

拦截器原理

在前面的步骤中,不管是同步请求还是异步请求,最终都会调用拦截器来处理网络请求。

//RealCall源码

Response result = getResponseWithInterceptorChain();

这就是OkHttp的核心,Interceptor拦截器。

在OkHttp中,Call、Callback和Dispatcher虽然很有用,但对于解决复杂的网络请求没有太多作用,使用了分层设计的拦截器Interceptor才是解决复杂网络请求的核心,这也是OkHttp的核心设计。

分层设计

我们都知道,真实情况中的网络行为其实非常复杂,纵跨软件、协议、数据包、电信号、硬件等,所以网络层的第一个基础知识就是IOS七层模型,明确了各层的功能范围,每一层各司其职,层与层依次依赖,实际上降低了开发和维护的难度与成本。

OkHttp也采用了分层设计思想,每层Interceptor的输入都是Request,输出都是Response,所以可以一层层地加工Request,再一层层地加工Response。

由于各个Interceptor之间不是组合关系,不能像ViewTree那样递归调用,所以需要一个链把这些拦截器全部串起来,为此,入口RealCall会执行网络请求的getResponseWithInterceptorChain函数,主要就是一层层地组织Interceptor,组成一个链,然后用chain.proceed去调用它。

ResponsegetResponseWithInterceptorChain() throws IOException{

// Build a full stack of interceptors.

List interceptors =newArrayList<>();

interceptors.addAll(client.interceptors());//自定义应用拦截器

interceptors.add(retryAndFollowUpInterceptor);//重试/重定向

interceptors.add(newBridgeInterceptor(client.cookieJar()));//应用请求转网络请求

interceptors.add(newCacheInterceptor(client.internalCache()));//缓存

interceptors.add(newConnectInterceptor(client));//连接

if(!forWebSocket) {

interceptors.addAll(client.networkInterceptors());//自定义网络拦截器

}

interceptors.add(newCallServerInterceptor(forWebSocket));//服务端连接

Interceptor.Chain chain =newRealInterceptorChain(//组成链

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

returnchain.proceed(originalRequest);//从RealCall的Request开始链式处理

}

如何实现链式处理

我们看到,链式处理的入口是RealInterceptorChain的proceed函数:

publicResponseproceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,

RealConnection connection

) throws IOException{

...

RealInterceptorChain next =newRealInterceptorChain(//在chain中前进一步

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

Interceptor interceptor = interceptors.get(index);

Response response = interceptor.intercept(next);//调用拦截器

...

returnresponse;

}

而拦截器在执行过程中,会再调用chain

@Override

publicResponseintercept(Chain chain)throwsIOException{

...

Response networkResponse = chain.proceed(requestBuilder.build());

...

这样,就形成一个chain.process(intreceptor)-->interceptor.intercept(chain)-->chainprocess(intreceptor)-->interceptor.intercept(chain)的循环,这个过程中,chain不断消费,直至最后一个拦截器,最后这个拦截器一定是CallServerInterceptor,CallServerInterceptor不再调用chain.process,链式调用结束。

拦截器的层次设计

了解过拦截器和链式反应的基本原理,我们再来看看各拦截器的层次设计和具体实现,有很多可以借鉴的地方。

我们先回到RealCall中,看看拦截器的层次和分类:

ResponsegetResponseWithInterceptorChain() throws IOException{

// Build a full stack of interceptors.

List interceptors =newArrayList<>();

interceptors.addAll(client.interceptors());//自定义应用拦截器

interceptors.add(retryAndFollowUpInterceptor);//重试/重定向

interceptors.add(newBridgeInterceptor(client.cookieJar()));//应用请求转网络请求

interceptors.add(newCacheInterceptor(client.internalCache()));//缓存

interceptors.add(newConnectInterceptor(client));//连接

if(!forWebSocket) {

interceptors.addAll(client.networkInterceptors());//自定义网络拦截器

}

interceptors.add(newCallServerInterceptor(forWebSocket));//实现在线网络连接

Interceptor.Chain chain =newRealInterceptorChain(//组成链

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

returnchain.proceed(originalRequest);//从RealCall的Request开始链式处理

}

我们可以看到,OkHttp中拦截器的层次是这样的:

1.自定义应用拦截器

2.重试、重定向拦截器

3.应用/网络桥接拦截器

4.缓存拦截器

5.连接拦截器

6.自定义网络拦截器

7.在线网络请求拦截器

我们看到,我们开发者可以添加两种自定义Interceptor,一种是client.interceptors()应用层拦截器,一种是client.networkInterceptors()网络层拦截器

但其实这两种都是Interceptor,为什么可以分成是应用层和网络层呢?

因为在网络层拦截器上方,是ConnectionInterceptor连接拦截器,这个拦截器里会提供Address、ConnectionPool等资源,可以用于处理网络连接,networkInterceptors是添加在这之后的,可以参与真正的网络层数据的处理。

接下来,我们自顶向下,依次看看每层拦截器的实现

拦截器——自定义应用拦截器

OkHttp在最外围允许添加自定义的应用拦截器,我们可以拦截Request和Response,分别进行加工,例如在Request时统一添加Header和Url参数:

Request.Builder builder = chain.request().newBuilder();

builder.addHeader("Accept-Charset","UTF-8");

builder.addHeader("Accept"," application/json");

builder.addHeader("Content-type","application/json");

HttpUrl url=builder.build().url().newBuilder()

.addQueryParameter("mac", EquipmentUtils.getMac())

.build();

Requestrequest= builder.url(url).build();

还可以拦截Response内容,打印返回数据的日志:

longt1 = System.nanoTime();

Request request = chain.request();

Response response = chain.proceed(request);

longt2 = System.nanoTime();

//直接复制字节流,获取response的数据内容

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

sr.request(Long.MAX_VALUE);

Buffer buf = sr.buffer().clone();//copy副本读取,不能读取原文

String content = buf.readString(Charset.forName("UTF-8"));

buf.clear();

Log.i(TAG,"net layer received response of url: "+ request.url().url().toString()

+"\nresponse: "+ content

+"\nspent time: "+ (t2 - t1) /1e6d);

开发者可以扩展针对请求数据和返回数据,自由开发功能。

拦截器——重试/重定向

虽然前面有开发者自定义的应用拦截器,但是真正准备处理网络连接,是从OkHttp自己定义的RetryAndFollowUpInterceptor开始的,因为OkHttp正是把这个拦截器作为真正的入口,创建StreamAllocation对象,在StreamAllocation对象中准备了网络连接的Address、连接池等资源,后续的拦截器,使用的都是这个StreamAllocation对象。

StreanAllocation

StreamAllocation是OkHttp中用来定义和传递网络资源,并建立网络连接的对象,内部包含:

Address:规定如何连接服务器,包括DNS、协议、URL等。

Route:存储建立连接的目标IP和端口InetSocketAddress,以及代理服务器。

ConnectionPool:存储和复用已存在的连接,复用时根据Address查找对应的连接。

StreamAllocation会通过findConnection创建连接,或复用已存在的连接,期间会调用RealConnection,根据设置建立TLS连接、处理握手协议等,最底层是根据当前运行的平台,直接操作Socket。

每个Host不超过5个连接,每个连接不超过5分钟。

重试/重定向

网络环境本质上是不稳定的,已建立的连接可能突然不可用,或者连接可用但是服务器报错,这就需要重试/重定向功能,这也是RetryAndFollowUpInterceptor拦截器的分层功能。

重试

如果整个链式调用出现了RouteException或IOException,就会调用recover函数重新建立连接;

重定向

如果服务器返回错误码如301,要求重定向,就会调用followUpRequest函数,新建一个Request,然后重定向,再走一遍整个调用链。

while

intercept函数中的这些主要逻辑都在while(true)循环中,最大循环上限是20。

拦截器——应用转网络的桥接功能

BridgeInterceptor是个桥梁,这主要是指他会自动处理一些网络层特有的Header信息,例如Host属性,是HTTP1.1必须的,但应用层并不关心这个属性,这就是由BridgeInterceptor自动处理的。

BridgeInterceptor中处理的Header属性包括Host、Connection的Keep-Alive、gzip透明压缩、User-Agent描述、Cookie策略等。

当然,因为OkHttp采用了外观模式,所以很多属性需要通过client设置和获取。

拦截器——缓存功能

在网络请求中使用缓存是非常必要提速手段,OkHttp专门用了CacheInterceptor拦截器来处理这个功能。

缓存的使用注意包括存储、查询和有效性检查,在OkHttp中:

存储,使用client外观模式来设置存储Cache数据的InternalCache实现类,在走请求链获取Response时记录cache。

查询,在存储Cache数据的InternalCache实现类中,根据Request过滤,来查找Cache。

有效性检查,利用工具类CacheStrategy的getCandidate函数,来判断Cache数据的各项指标是否达到条件。

拦截器——连接功能

在RetryAndFollowUpInterceptor入口处,我们已经分析过,在OkHttp中,连接功能由StreamAlloc实现,提供Address地址、Route路由、RealConnection连接、ConnectionPool线程池复用、身份验证、协议、握手、平台、安全等功能。

在ConnectionInterceptor这一层,其实还没有真正连接网络,它的具体功能很简单,就是准备好request请求、streamAllocation连接资源、httpCodec传输工具、connection连接,为最底层的网络连接服务。

其中,httpCodec通过sink提供了OKio封装过的基于socket的OutputStream,通过source提供了OKio封装的基于socket的InputStream,最终就是通过这个sink提交Request,用这个source获取Response。

拦截器——自定义网络拦截器

主要区别

自定义的网络层拦截器相比应用层拦截器,能直接监测到在线网络请求的数据交换过程。

例如,Http有url重定向机制,如果Http返回码为301,就需要根据Header中Location字段的新url,重新发起一次请求,这样的话,总共会有两次请求。

在应用层的拦截器看来,第一次请求并没有返回有效数据,它只会抓到一次请求,也就是第二次的请求。

但是在网络层的拦截器看来,两次都是网络请求,所以它会抓到两次请求。

用途扩展

根据网络层拦截器的特点,我们可以扩展如下功能:

1.模拟各种网络情况

网络接口不只是可用不可用的问题,还存在速度波动的问题,一个稳健的App应该能hold住波动的甚至是断断续续的网络,但是这样的网络非常不好模拟,我们可以在网络拦截器层自由设定网络返回值和返回时间,辅助我们检查App在处理网络数据时的健壮性。

2.模拟多个备用地址切换

无论是为了灾备,还是为了节省DNS解析时间,App都会有多个备用地址,有些就是ip地址,当网络出现问题时,要自动切换到备用地址,就可以在网络层模拟出301返回,直接重定向到备用地址。

3.模拟数据辅助开发/测试

在开发过程中,我们可以用gradle多环境的方法,增加一个mock的productFlavor,在这个环境下添加一个mockInterceptor,把指向官网的地址重定向为指向开发测试网址,甚至直接mock返回数据,换掉在线数据,这样可以检测整个网络层的全部功能(编码、缓存、切换、报错等),把mock数据的内容和App的反馈结合的话,还可以做到针对网络数据的半自动/自动化的测试验证。

拦截器——在线网络请求功能

前面所有的拦截器,都是在准备或处理网络连接前后的数据,只有CallServerInterceptor这个拦截器,是真正连接在线服务的。

它使用ConnectionInterceptor提供的HttpCodec传输工具来发出Request,获取Response,然后用ResponseBuilder生成最终的Response,再层层传递给外层的拦截器。

HttpCodec本身是一个接口,实例是StreamAllocation利用RealConnection生产的,RealConnection根据连接池中的可用连接,利用Okio生产source和sink:

privatevoidconnectSocket(intconnectTimeout,intreadTimeout)throwsIOException{

Proxy proxy = route.proxy();

Address address = route.address();

rawSocket = proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.HTTP

? address.socketFactory().createSocket()

:newSocket(proxy);

rawSocket.setSoTimeout(readTimeout);

...

//用Okio生产

source = Okio.buffer(Okio.source(rawSocket));

sink = Okio.buffer(Okio.sink(rawSocket));

...

}

Okio的source是socket.inputStream,sink是socket.outputStream。

所以,真正在传输数据时,就是用Okio的sink去传socket,用source去取socket,底层其实也是socket操作。

其他特性

以上是OkHttp的主要内容,此外,OkHttp还有一些很有意思的特性。

1.返回数据阅后即焚

在OkHttp中,如果要拦截ResponseBody的数据内容(比如写日志),会发现该数据读过一次就会被情况,相当于是“阅后即焚:

//ResponseBody源码

publicfinalStringstring()throwsIOException{//底层不能自己消化异常,应该向上层抛出异常

BufferedSource source = source();

try{

Charset charset = Util.bomAwareCharset(source, charset());

returnsource.readString(charset);

//不做catch,异常全部抛出给上层

}finally{//确保原始字节数据得到处理

Util.closeQuietly(source);//阅后即焚,这样可以迅速腾出内存空间来

}

}

如果一定要拦截出数据内容,我们就不能直接读ResponseBody中的source,需要copy一个副本才行:

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

sr.request(Long.MAX_VALUE);

Buffer buf = sr.buffer().clone();//copy副本读取,不能读取原文

String content = buf.readString(Charset.forName("UTF-8"));

buf.clear();

Response也提供了专门获取ResponsBody数据的函数peekBody,实现原理也是copy:

//Response源码

publicResponseBodypeekBody(longbyteCount)throwsIOException{

BufferedSource source = body.source();

source.request(byteCount);

Buffer copy = source.buffer().clone();

...

returnResponseBody.create(body.contentType(), result.size(), result);

}

参考

深入解析OkHttp3

OkHttp3源码分析[综述]

Okhttp-wiki 之 Interceptors 拦截器

如果您觉得不错,请别忘了转发、分享、点赞让更多的人去学习,在顺便给大家推荐一个架构交流群:617434785,里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化这些成为架构师必备的知识体系。还能领取免费的学习资源。相信对于已经工作和遇到技术瓶颈的码友,在这个群里会有你需要的内容。

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

推荐阅读更多精彩内容