Netty 源码解析 ——— 服务端启动流程 (上)

本文是Netty文集中“Netty 源码解析”系列的文章。主要对Netty的重要流程以及类进行源码解析,以使得我们更好的去使用Netty。Netty是一个非常优秀的网络框架,对其源码解读的过程也是不断学习的过程。

本文主要对Netty的启动流程的源码进行解析,首先来一段启动流程的代码(红框就为启动流程的代码):

重要类介绍

在讲解源码之前,先对该篇文字这涉及到的几个Netty中的重要的类进行简单的介绍。

EventLoopGroup

事件循环组(NioEventLoopGroup是异步事件循环组)。EventLoopGroup中包含了多个EventLoop。主要提供了两类方法:
① next()方法用于返回下一个EventLoop来使用。
② register方法,来将一个Channel注册到EventLoop当中,同时返回一个ChannelFuture,当注册完成的时候这个ChannelFuture将得到一个通知。可以见该方法是一个异步的方法,在调用完register后会立即返回,然后我们根据ChannelFuture中的相关方法来判断注册操作是否完成。

NioEventLoopGroup ——— 异步事件循环组

它是MultithreadEventLoopGroup的一个实现,它是用于基于Channel的NIO Selector。

EventLoop

事件循环类。将处理一个已经注册到该EventLoop的Channel的所有I/O操作。

NioEventLoop

NioEventLoop是一个SingleThreadEventLoop的实现,该类注册Channel到一个Selector中,并使用了IO多路复用在这个NioEventLoop中。

在NioEventLoop中的整个生命周期只会和一个线程绑定。因此与这个NioEventLoop关联的Channel的IO事件都会在这个线程中完成。

SingleThreadEventExecutor:
一个继承了OrderedEventExecutor的抽象类,它会执行所有提交的任务在一个单一的
线程中。而OrderedEventExecutor作为一个标记接口,它会执行所有提交的任务以有序/连续的方式。
SingleThreadEventExecutor通过持有一个MpscQueue taskQueue成员变量,来维护提交上来的任务。
SingleThreadEventExecutor中持有的executor成员变量是一个ThreadPerTaskExecutor对象,通过ThreadPerTaskExecutor来启动一个线程作为SingleThreadEventExecutor执行器(该执行器来自于NioEventLoopGroup)中执行任务的线程,也就是当前EventLoop生命周期所关联的线程。
SingleThreadEventExecutor中还维护有该线程的五个状态:a)ST_NOT_STARTED;b)ST_STARTED;c)ST_SHUTTING_DOWN;d)ST_SHUTDOWN;e)ST_TERMINATED。

SingleThreadEventExecutor的execute(Runnable task)方法:


execute方法会接收一个个任务,将任务依次放入taskQueue中。并且如果当前执行提交任务的线程不是SingleThreadEventExecutor执行任务的那个唯一线程(即,EventLoop所关联的线程)并且该唯一线程还未被创建启动,则通过ThreadPerTaskExecutor.execute(Runnable)来创建并启动执行任务的唯一线程。
也就是说,SingleThreadEventExecutor的任务线程会在满足如下条件时被创建并执行:
a) 提交任务的线程不为EventLoop所关联的线程
b) EventLoop所关联的线程还不存在,即EventLoop所关联的线程的状态为ST_NOT_STARTED
值得说明的一点是,我们的启动程序“serverBootstrap.bind(5566)”就满足👆的条件,也就会触发EventLoop所关联的线程创建并执行。至于具体bind流程,我们下面会进行详细说明,往下看你就晓得为什么bind(int port)满是于上面的条件了。

SingleThreadEventExecutor的doStartThread()方法:会调用SingleThreadEventExecutor.this.run();而这是SingleThreadEventExecutor的一个抽象方法,实际上会调用NioEventLoop类的run()方法,是的我们又回到了NioEventLoop类中,这是一个很重要的方法。

NioEventLoop.run() ———— 事件循环:
该方法通过Java NIO Selector的多路复用来实现对多个Channel的监控,该方法还对epoll空轮询bug进行了解决,并且在处理完Selector返回的可执行事件后,会处理taskQueue中的任务以及定时或周期性任务。注意,这里不要混淆了,selector完成的是我们注册的Channel所感兴趣的的读/写事或者acceptor、connect事件是否已经可以处理,这些都是会造成堵塞的I/O事件,是会通过操作系统底层来通知我们事件可执行了。而taskQueue中的任务是我们程序通过execute方法提交的任务,同时可以执行的定时任务或周期性任务也会被先放入taskQueue中,然后EventLoop关联的线程再从taskQueue中依次取出任务来执行。

比如,我们的启动流程中serverBootstrap.bind(5566),该方法中涉及到注册操作(即,将构建好的Channel注册到EventLoop上),执行该注册操作的线程不是NioEventLoop所关联的线程,此时这个操作会被封装为一个任务放入taskQueue中,然后NioEventLoop.run()方法就会从taskQueue中取出该任务:完成已经构建好ServerSocketChannel注册到NioEventLoop中的Selector上。然后在绑定操作中就会将ServerSocketChannel和对应端口进行绑定,并注册该ServerSocketChannel感兴趣的事件为SelectionKey.OP_ACCEPT。那么,在这之后,我们就会通过Selector来实现对ACCEPT事件的监控。

好了关于NioEventLoop絮絮叨叨了这么多,总的来说,目前我们只要知道:
① NioEventLoop是一个基于JDK NIO的异步事件循环类,它负责处理一个Channel的所有事件在这个Channel的生命周期期间。
② NioEventLoop的整个生命周期只会依赖于一个单一的线程来完成。一个NioEventLoop可以分配给多个Channel,NioEventLoop通过JDK Selector来实现I/O多路复用,以对多个Channel进行管理。
③ 如果调用Channel操作的线程是EventLoop所关联的线程,那么该操作会被立即执行。否则会将该操作封装成任务放入EventLoop的任务队列中。
④ 所有提交到NioEventLoop的任务都会先放入队列中,然后在线程中以有序(FIFO)/连续的方式执行所有提交的任务。
⑤ NioEventLoop的事件循环主要完成了:a)已经注册到Selector的Channel的监控,并在感兴趣的事件可执行时对其进行处理;b)完成任务队列(taskQueue)中的任务,以及对可执行的定时任务和周期性任务的处理(scheduledTaskQueue中的可执行的任务都会先放入taskQueue中后,再从taskQueue中依次取出执行)。

关于 EventLoop 和 EventLoopGroup
Q:笔者起初在查看EventLoop和EventLoopGroup的源码时,有一个困惑,我想大家也许和我会有这一样的困惑:正如我们前面所说的,EventLoopGroup维护一组EventLoop,可以通过EventLoopGroup的next()方法来获取EventLoop来使用。但为什么EventLoop又要反过来继承EventLoopGroup了?
A:是这样的,Netty在将一个Channel注册到EventLoop的时候,首先会调用EventLoopGroup实现类的register方法来从EventLoopGroup中得到一个EventLoop,然后在调用EventLoop的register方法来真正完成将Channel注册到这个EventLoop上。而register方法是声明在EventLoopGroup接口类中,所以EventLoop继承了EventLoopGroup。

关于NioEventLoop和OioEventLoop
Netty使用了一个公共的API层,该API涵盖了所以的传输实现,包括了OIO、NIO、Local transport和Embedded transport。
关于OIO传输,Channel和EventLoop的关系是一一对应的。因此在构建EventLoopGroup时不会提前构建好EventLoop,而是在每次执行注册Channel操作的时候,才创建一个EventLoop(如果有空闲的EventLoop的话会先使用空闲的EventLoop,而空闲的EventLoop来自于某个Channel从EventLoop上注销后,使得该EventLoop不再于任何Channel关联而处于空闲状态),并将该Channel注册到该EventLoop上。并且在OIO传输模式下,是不支持通过EventLoopGroup的next()方法来获取EvetLoop的,该方法是适用于提前在EventLoopGroup中构建好已知数量的EventLoop的。如前面所说,OIO传输模式下EventLoop与Channel是一一对应的,因此EventLoop的数量是无法预知的。源码如下:
OioEventLoopGroup是OIO传输模式使用的EventLoopGroup的实现类,而OioEventLoopGroup继承了ThreadPerChannelEventLoopGroup




关于NIO传输,Channel和EventLoop是多对一的关系,也就是可以将一个EventLoop分配给多个Channel。在这种模式下,EventLoop的个数是有限的,并且会在构建EventLoopGroup对象的时候就将相应数目(默认为操作系统可运行处理器数的2倍)的EventLoop对象创建出来。并且在每次注册Channel的时候,以轮询的方式获取已经构建好的EventLoop对象,并将Channel注册到这个EventLoop上。源码如下:
NioEventLoopGroup是NIO传输模式使用的EventLoopGroup的实现类,而NioEventLoopGroup继承了MultithreadEventLoopGroup
next()实现从已经构建好的NioEventLoop数组中以轮询的方式获取一个NioEventLoop。然后执行NioEventLoop的register方法来将该Channel注册到它上面。

ServerBootstrap

ServerBootstrap它使得我们可以轻松地去启动ServerChannel。

ServerChannel:
ServerChannel是一个Channel,它会试图接受另一端发过来的连接请求并且通过接受这个请求来创建它的child Channel。child Channel表示真正与客户端连接的Channel。

服务端通过ServerBootstrap来引导启动,而客户端通过Bootstrap来引导启动。AbstractBoostrap处理两个种类应用的共同引导步骤,然后特定的步骤客户端由Bootstrap、服务端有ServerBootstrap分别处理。

Q:为什么AbstractBoostrap类是可克隆的?
A:在某些时候你需要创建许多相似或完全相同设置的channels。为了支持在无需创建一个新的bootstrap实例并能配置每个channel的模式,AbstractBootstrap被标志为Cloneable。在一个已经配置好的bootstrap上调用clone()方法将返回另一个可立即使用的boostrap实例。
注意,这里创建的只是一个bootstrap的EventLoopGroup的浅拷贝,所以这个EventLoopGroup会被所有拷贝生成的channels共享。这是可以理解的,作为拷贝而来的channel经常都是短暂的,一个典型的场景是一个channel的创建用于生成一个HTTP请求。

源码解析

NioEventLoopGroup的构建

EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();

前面我们已经对NioEventLoop的结构和作用进行了介绍,这里我们直接跟到NioEventLoop构造方法的底层实现进行分析。
NioEventLoopGroup构造方法最终会调用如下方法:


传入的参数详解:

  • int nThreads :将使用在该实例上的线程个数。

    传入的值为:默认构造方法中,该参数的值为MultithreadEventLoopGroup类中定义DEFAULT_EVENT_LOOP_THREADS常量值。

    ① 如果系统属性中有“io.netty.eventLoopThreads”属性值,则返回;否则取可用处理器核心 * 2(处理器超线程数) * 2的值(即,NettyRuntime.availableProcessors() * 2)。

    ② 取第①步结果与1中大的那个值。
    这里简单补充下处理器核心数、超线程等的概念:
    总核数 = 物理CPU个数 X 每颗物理CPU的核数
    总逻辑CPU数 = 物理CPU个数 X 每颗物理CPU的核数 X 超线程数(超线程数就是2,但前提得是支持超线程)

  • Executor executor :使用的执行器,如果为“null”,则表示使用默认的执行器
    传入的值为:null
  • EventExecutorChooserFactory chooserFactory :EventExecutor选择器工厂
    传入的值为:DefaultEventExecutorChooserFactory.INSTANCE
    DefaultEventExecutorChooserFactory通过简单的轮询来获取下一个EventExecutor。
  • SelectorProvider selectorProvider :Selector选择器

    传入的值为:SelectorProvider.provider();

    ① 如果配置了“java.nio.channels.spi.SelectorProvider”属性,则通过该属性值load对应的SelectorProvider对象,如果构建失败则抛异常。

    ② 如果provider类已经安装在了对系统类加载程序可见的jar包中,并且该jar包的源码目录META-INF/services包含有一个java.nio.channels.spi.SelectorProvider提供类配置文件,则取文件中第一个类名进行load以构建对应的SelectorProvider对象,如果构建失败则抛异常。
    ③ 如果上面两种情况都不存在,则返回系统默认的SelectorProvider,即,sun.nio.ch.DefaultSelectorProvider.create();
    ④ 随后在调用该方法,即SelectorProvider.provider()。则返回第一次调用的结果。

  • SelectStrategyFactory selectStrategyFactory :选择策略工厂
    传入的值为:DefaultSelectStrategyFactory.INSTANCE
    DefaultSelectStrategyFactory:使用了默认选择策略的工厂。
  • RejectedExecutionHandler rejectedHandler :拒绝执行器

    传入的值为:RejectedExecutionHandlers.reject(),该方法返回一个REJECT常量实例
    比如,当EventLoop执行关闭操作后,还有任务提交上来,则需要拒绝该任务,这时候就会REJECT的rejected方法,抛出一个RejectedExecutionException异常。

方法体详解:
① executor = new ThreadPerTaskExecutor(newDefaultThreadFactory()):构建默认的执行器。并且传入的一个默认的线程工厂给ThreadPerTaskExecutor。
ThreadPerTaskExecutor继承了Executor,它会使用newDefaultThreadFactory()所返回的线程工厂来创建线程。

👆使用了“命令模式”和“代理模式
“命令模式”:execute(Runnable command)这里使用到了命令模式,任务的请求和执行分离开来了。
“代理模式”:这里execute方法实现并不是由ThreadPerTaskExecutor来执行的,而是交由ThreadFactory来执行的。ThreadFactory是通过构造方法传递进来的。

② children = new EventExecutor[nThreads]:根据传入了的nThreads为数组长度,创建EventExecutor数组对象,将该数组对象赋值给MultithreadEventExecutorGroup成员属性children。

③ 对children数组元素进行初始化,即构建一个个NioEventLoop实例:children[i] = newChild(executor, args);

主要完成了对NioEventLoop类的创建。NioEventLoop构造函数中完成了NioEventLoop中的Selector的启用(即,provider.openSelector());以及对EventLoop类中几个成员属性的设置,包括SelectorProvider ———— Selector提供器、selector ———— 封装后的Selector、unwrappedSelector ———— 未封装的Selector、selectStrategy ———— 选择策略类。构建成员属性tailTasks、taskQueue,两队列均为MpscUnboundedArrayQueue实例。设置成员属性addTaskWakesUp为false。

④ chooser = chooserFactory.newChooser(children):设置EventExecutorChoose ———— 事件执行器选择器。该选择器会以轮询的方式从上面构建好的EventExecutor[] children数组中获取EventExecutor。

👆这里当EventExecutor[]数组长度是2^N时使用PowerOfTwoEventExecutorChooser选择器,否则使用GenericEventExecutorChooser。
这两个选择器都是实现了以轮询的方式从EventExecutor[]数组中依次获取EventExecutor来使用,不同的地方在于它们实现的细节上。PowerOfTwoEventExecutorChooser因为数组长度是2^N,因此使用了’&’位的与操作来计算应该获取的EventExecutor在数组中的索引值。而GenericEventExecutorChooser则是通过’%’取模运算得到应该获取的EventExecutor在数组中的索引值。当然,’&’运算性能是高于’%’运算的。

⑤ 为EventExecutor[] children数组中的EventExecutor设置终止操作的监听事件。
该终止事件会发生在,当前EventLoopGroup所管理的所有EventLoop都终止后。

⑥ 根据EventExecutor[] children数组构建一个不可修改的集合,并赋值给MultithreadEventExecutorGroup的成员属性readonlyChildren。

启动类设置

ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class)
        .handler(new LoggingHandler(LogLevel.INFO)).childHandler(new MyServerInitializer());

ServerBootstrap是netty提供给我们的辅助类而已,它本身也没有做任何多余的事情,只用来设定一些相关的属性、参数。

serverBootstrap.group(bossGroup, workerGroup):

设置EventLoopGroup的parentGroup和childGroup。这些EventLoopGroup是用于处理ServerChannel和Channel的所有的事件以及I/O操作。

super.group(parentGroup):

这个EventLoopGroup用于处理将要创建的那个Channel的所有的事件。
该方法会将我们传进来的bossGroup赋值给AbstractBootstrap的成员变量volatile EventLoopGroup group。

总的来说:
serverBootstrap.group(bossGroup, workerGroup)完成了将传进来的两个EventLoopGroup对象分别给ServeBootstrap的父类AbstractBootstrap和ServerBootstrap本身的两个成员变量赋值。也就是说,我们前面创建的两个NioEventLoopGroup在ServerBootstrap中有相应的引用去指向这个具体的实例。
在AbstractBootstrap中保持的是一个bossGroup的成员变量;而workerGroup是在ServerBootstrap中持有的一个成员变量。
parentGroup(即,bossGroup)的作用就是接收远端发过来的连接,它不处理这个连接,它把处理的任务丢给childGroup(即,workerGroup)来完成。

serverBootstrap.channel(NioServerSocketChannel.class):

这个Class(👈传入的参数)用于创建一个Channel的实例。你可以使用该方法(即,channel(Class<? extends C> channelClass))或者使用channelFactory(io.netty.channel.ChannelFactory<? extends C> channelFactory)方法如果Channel实现没有无参的构造方法。
也就是说使用该方法构建的Channel是具有无参构造方法的,否则就要用channelFactory(...)方法。

new ReflectiveChannelFactory<C>(channelClass):

它是一个ChannelFactory,它会通过以反射的形式调用Channel默认的构造方法来实例化一个新的Channel。

channelFactory(new ReflectiveChannelFactory<C>(channelClass)):

将channelFactory参数设置给AbstractBootstrap的成员变量channelFactory。

总的来说:
serverBootstrap.channel(NioServerSocketChannel.class)完成了将NioServerSocketChannel的ChannelFactory赋值给AbstractBootstrap的成员变量channelFactory。该ChannelFactory是一个ReflectiveChannelFactory实例,它能够通过以反射的形式调用Channel默认的构造方法来实例化一个新的Channel。

serverBootstrap.handler(new LoggingHandler(LogLevel.INFO)):

将我们传入的LoggingHandler赋值给AbstractBootstrap的成员变量handler。handler用于ServerChannel(即,NioServerSocketChannel)上。

serverBootstrap.childHandler(new MyServerInitializer()):

将我们传入的MyServerInitializer赋值给ServerBootstrap的成员变量childHandler。childHandler用于child Channel(即,NioSocketChannel)上。

在配置启动类的过程中需要注意两点:
① Channel 和 EventLoopGroup 的兼容性
EventLoopGroup 和 Channel 都实现了 NIO 和 OIO 的传输。

这个兼容性是需要维护的,你不能够混合不同前缀(nio or oio)的组件使用,比如像NioEventLoopGroup 和 OioSocketChannel,否则将会触发IllegalStateException异常。
② 必须设定的配置
在你调用bing()或connect()方法前下面的方法必须被调用,否则将会触发IllegalStateException异常。以下是必须要设置的组件(下面组件的设置并没有任何顺序要求)。
a) group()
b) channel() or channelFactory()
c) handler() or childHandler()

总结:

到目前为止我们已经对👇四步程序代码进行了详细的解析

EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class)
        .handler(new LoggingHandler(LogLevel.INFO)).childHandler(new MyServerInitializer());

这四步代码主要完成了:
① NioEventLoopGroup的构建。在NIO模式中我们会在构建EventLoopGroup的时候就将EventLoop一并的给构建处理。因为在NIO模式中,EventLoop和Channel的关系是一对多的关系,多个Channel可以注册到同一个EventLoop中,因此每个EventLoopGroup中的EventLoop的个数是固定的,而在OIO模式下,我们就无法在构建EventLoopGroup的同时将EventLoop也构建出来了。因为在OIO模式中,EventLoop和Channel是一对一的关系,每个Channel都会注册到一个唯一的EventLoop中。因此在OIO模式中,EventLoop的个数随着Channel的增加而增加,而EventLoop的构建也放到了将Channel注册到EventLoop时进行。
而造成上面实现不同的本质原因还是应用传输协议模式的不同。NIO模式是非阻塞模式,底层使用了Selector来通过更少的线程同时管理大量的连接;而OIO模式是阻塞模式,每一个连接都需要一个线程来处理。
② 通过ServerBootstrap对服务的配置进行设置。其中a) group();b) channel() or channelFactory();c) childHandler() 配置是不可缺少的。在上面的源码解析中,我们可以总结出,涉及到对child Channel(即,服务端接收客户端的请求连接后生产的child Channel,该child Channel就是真正和客户端连接的Channel)的配置都会设置在ServerBootstrap中,比如childGroup、childHandler;而AbstractBootstrap中则保持了ServerChannel相关的配置,比如bossGroup、handle、channelFactory。

目前为止已经将ServerBootstrap所必须的成员变量都设置好了。但仅仅是配置而已,并未对ServerChannel进行构建等等。这些都是在bind操作会触发的。下一篇我们就会解析启动流程中的bind操作。

后记

本文主要对Netty服务端的启动流程源码进行了部分的解析。建议大家可以看看 Netty in action ——— 异步和事件驱动Netty in action ——— BootstrappingNetty in action ——— 事件循环 和 线程模式Netty in action ——— 传输协议这几篇文章,这几篇文章是在笔者写源码解析时涉及到的一些知识点的理论性文章,主要来自于《Netty in action》一书。
若文章有任何错误,望大家不吝指教:)

参考

圣思园《精通并发与Netty》

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

推荐阅读更多精彩内容