netty服务端源码分析之server启动

netty echo服务端源码分析(一)netty echo服务端源码分析(二)分析完后,我们目前只构造了bossGroup和workGroup两个对象,同时把一些配置信息保存到了启动辅助类ServerBootStrap中,现在看看ServerBootStrap如何启动服务端。对用户来讲,启动服务端很简单,只要执行例子中的这条语句

// Start the server.
ChannelFuture f = b.bind(PORT).sync();

跟下去,最终会调用类AbstractBootstrapdoBind(final SocketAddress localAddress)函数,这个函数我们主要关心调用的下面两个函数:

// 创建NioServerSocketChannel实例,初始化,注册ServerSocketChannel到event loop的selector上
final ChannelFuture regFuture = initAndRegister();

// NioServerSocketChannel已经注册到 NioEventloop的selector上了
ChannelPromise promise = channel.newPromise();
// 为Server socket 绑定ip和端口号
doBind0(regFuture, channel, localAddress, promise);

先分析函数initAndRegister(),代码如下:

final ChannelFuture initAndRegister() {
    Channel channel = null;
    try {
        // 对于server, 通过反射返回NioServerSocketChannel的实例
        // 相当于channel = new NioServerSocketChannel()
        channel = channelFactory.newChannel();
        // ServerBootstrap.init
        init(channel);
    } catch (Throwable t) {
        // 无关代码省略
    }

    // config().group()返回的是ServerBootstrap的parent group, 即 bossGroup
    ChannelFuture regFuture = config().group().register(channel);
    if (regFuture.cause() != null) {
        // 无关代码省略
    }

    return regFuture;
}

根据netty echo服务端源码分析(二)的分析,channelFactory是类ReflectiveChannelFactory的实例,channelFactory.newChannel()会通过反射的方式创建NioServerSocketChannel的实例。

因为NioServerSocketChannel是服务端的核心类,我们先简单分析一下这个类。

先看看它的继承体系

NioServerSocketChannel的继承体系.png
// NioServerSocketChannel构造函数

public NioServerSocketChannel() {
    this(newSocket(DEFAULT_SELECTOR_PROVIDER));
}

public NioServerSocketChannel(ServerSocketChannel channel) {
    super(null, channel, SelectionKey.OP_ACCEPT);
    config = new NioServerSocketChannelConfig(this, javaChannel().socket());
}

// AbstractNioMessageChannel构造函数
protected AbstractNioMessageChannel(Channel parent, SelectableChannel ch, int readInterestOp) {
    super(parent, ch, readInterestOp);
}

// AbstractNioChannel构造函数
protected AbstractNioChannel(Channel parent, SelectableChannel ch, int readInterestOp) {
    super(parent);
    this.ch = ch;
    this.readInterestOp = readInterestOp;
    try {
        ch.configureBlocking(false);
    } catch (IOException e) {
        try {
            ch.close();
        } catch (IOException e2) {
            if (logger.isWarnEnabled()) {
                logger.warn(
                        "Failed to close a partially initialized socket.", e2);
            }
        }

        throw new ChannelException("Failed to enter non-blocking mode.", e);
    }
}

// AbstractChannel构造函数
protected AbstractChannel(Channel parent) {
    this.parent = parent;
    id = newId();
    unsafe = newUnsafe();
    pipeline = newChannelPipeline();
}

分析上面的代码可知,newSocket(DEFAULT_SELECTOR_PROVIDER)通过SelectorProvider.provider().openServerSocketChannel()打开一个ServerSocketChannel,在linux下,我们可以理解为JVM会调用linux操作系统的socket()函数创建了一个socket。这个地方要注意区别它跟前面NioServerSocketChannel实例的关系,他们都是一个socket channel,但是是两个不同的东西。newSocket(DEFAULT_SELECTOR_PROVIDER)返回的channel更多的是与操作系统socket相关联的一个东西,后面代码中经常遇到的通过javaChannel()返回的channel就是它,所以我们要进行底层的socket操作时,就是通过它。

然后设置channel需要监听的事件为OP_ACCEPT,也就是接受client的连接请求。将channel配置为非阻塞。最后在父类AbstractChannel构造函数中初始化了几个重要的属性域。ChannelId id初始化为DefaultChannelId的实例,用来唯一的标识一个channel,具体表达方式为:machineid+processid+sequence+timestamp+random。Unsafe = new NioMessageUnsafe()也是netty框架非常重要的一个。unsafe是netty又一个非常重要的对象,当我们要操作JVM的一些函数或功能时,就是通过unsafe来调用的。newUnsafe()函数的实现为:

protected AbstractNioUnsafe newUnsafe() {
    return new NioMessageUnsafe();
}

pipeline是netty框架的非常核心部分。netty的业务处理模型就是通过pipeline定义和实现。pipeline是一个有channel handler组成的业务处理链表,用户可以根据需要在线动态的添加业务处理、删除业务处理、重新调整业务处理顺序,从而灵活高效的实现不同的业务逻辑。newChannelPipeline()函数的实现为:

protected DefaultChannelPipeline newChannelPipeline() {
    return new DefaultChannelPipeline(this);
}

到此,netty服务端的三大核心组件eventloopgroup/eventloop、channel、pipeline/channelhandler都出现了,为了下面代码分析的方便,把他们之间的关系说明一下:
eventloopgroup包含一组eventloop,eventloop只属于一个eventloopgroup,channel注册到一个eventloop,它的所有事件和业务处理都在注册到的eventloop中进行,由于channel的所有业务处理都在一个线程中处理,不需要考虑多线程同步的问题,极大的简化了编程。每一个channel都有一个自己pipeline,pipeline是一个channelhandler链表。

initAndRegister()函数的channel创建分析完了,我们继续往下分析初始化channel的init(channel)这个函数调用。
init(Channel channel)函数在ServerBootstrap类中实现,我们重点分析下为channel的pipeline添加channelHandler的功能,代码如下:

// 返回的是DefaultChannelPipeline的实例
ChannelPipeline p = channel.pipeline();

// 为 ServerSocketChannel 的pipeline添加handler
p.addLast(new ChannelInitializer<Channel>() {
    @Override
    public void initChannel(final Channel ch) throws Exception {
        final ChannelPipeline pipeline = ch.pipeline();

        // 通过ServerBootstarp配置的handler添加到pipeline
        ChannelHandler handler = config.handler();
        if (handler != null) {
            pipeline.addLast(handler);
        }

        // 将handler ServerBootstrapAcceptor添加到pipeline
        ch.eventLoop().execute(new Runnable() {
            @Override
            public void run() {
                pipeline.addLast(new ServerBootstrapAcceptor(
                        ch, currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs));
            }
        });
    }
});

p.addLast()最终会调用DefaultChannelPipeline中定义的这个函数:

    public final ChannelPipeline addLast(EventExecutorGroup group, String name, ChannelHandler handler) {
        final AbstractChannelHandlerContext newCtx;
        synchronized (this) {
            // 检查handler是否支持添加到多个pipeline
            checkMultiplicity(handler);

            newCtx = newContext(group, filterName(name, handler), handler);

            // 将handler添加到pipeline链路的末尾
            addLast0(newCtx);

            // If the registered is false it means that the channel was not registered on an eventloop yet.
            // In this case we add the context to the pipeline and add a task that will call
            // ChannelHandler.handlerAdded(...) once the channel is registered.
            // channel还没有注册到selector,将状态设为pending,并放入pending队列
            if (!registered) {
                newCtx.setAddPending();
                callHandlerCallbackLater(newCtx, true);
                return this;
            }

            // 已注册,但不在eventloop线程中,将状态设为pending,并放入eventloop的task队列
            EventExecutor executor = newCtx.executor();
            if (!executor.inEventLoop()) {
                newCtx.setAddPending();
                executor.execute(new Runnable() {
                    @Override
                    public void run() {
                        callHandlerAdded0(newCtx);
                    }
                });
                return this;
            }
        }
        // channel已注册,并且当前线程就是eventloop线程,立即执行
        callHandlerAdded0(newCtx);
        return this;
    }

从函数中得知,先会检查要添加的handler是否支持同时添加到多个pipeline,如果不支持,同时已经添加过,则不允许添加。对于允许shared的handler要求是线程安全的,否则可能会导致程序出错。

然后将创建一个DefaultChannelHandlerContext实例,将handler封装到handlercontext中,然后将handlercontext添加到pipeline链表的末尾,即tailcontext的前面。

由于当前channel还没有注册,会进入分支

if (!registered) {
    newCtx.setAddPending();
    callHandlerCallbackLater(newCtx, true);
    return this;
}

这个分支先会将handlercontext的状态设为ADD_PENDING, 然后添加到任务链表pendingHandlerCallbackHead末尾,待后续处理。

好了,init(channel)分析完了,继续回到initAndRegister()函数,继续往下分析channel注册。

// config().group()返回的是ServerBootstrap的parent group, 即 bossGroup
ChannelFuture regFuture = config().group().register(channel);

跟踪config().group()可知,返回的就是netty echo服务端源码分析(一)中的bossGroup,跟踪它的register(channel)函数,最后调用到MultithreadEventLoopGroup类中的register(),内容如下:

public ChannelFuture register(Channel channel) {
    return next().register(channel);
}

next()函数的功能就是通过netty echo服务端源码分析(一)介绍的chooser使用round-robin算法选择一个eventloop。

NioEventLoop中跟踪register(channel),调用到SingleThreadEventLoop

public ChannelFuture register(final ChannelPromise promise) {
    ObjectUtil.checkNotNull(promise, "promise");
    promise.channel().unsafe().register(this, promise);
    return promise;
}

NioMessageUnsafe中跟踪register(),会发现调用的是AbstractUnsafe的这个版本:

public final void register(EventLoop eventLoop, final ChannelPromise promise) {
    if (eventLoop == null) {
        throw new NullPointerException("eventLoop");
    }
    if (isRegistered()) {
        promise.setFailure(new IllegalStateException("registered to an event loop already"));
        return;
    }
    if (!isCompatible(eventLoop)) {
        promise.setFailure(
                new IllegalStateException("incompatible event loop type: " + eventLoop.getClass().getName()));
        return;
    }

    AbstractChannel.this.eventLoop = eventLoop;

    if (eventLoop.inEventLoop()) {
        register0(promise);
    } else {
        try {
            eventLoop.execute(new Runnable() {
                @Override
                public void run() {
                    register0(promise);
                }
            });
        } catch (Throwable t) {
            logger.warn(
                    "Force-closing a channel whose registration task was not accepted by an event loop: {}",
                    AbstractChannel.this, t);
            closeForcibly();
            closeFuture.setClosed();
            safeSetFailure(promise, t);
        }
    }
}

从代码可知,如果当前线程是eventloop的线程,则直接执行register0(promise),但知道现在为止,我们的代码一直在main()函数的线程中执行,所以执行的分支是

eventLoop.execute(new Runnable() {
    @Override
    public void run() {
        register0(promise);
    }
});

继续跟踪下去调用的是SingleThreadEventExecutor的这个函数

public void execute(Runnable task) {
    if (task == null) {
        throw new NullPointerException("task");
    }

    boolean inEventLoop = inEventLoop();
    if (inEventLoop) {
        addTask(task);
    } else {
        startThread();
        addTask(task);
        if (isShutdown() && removeTask(task)) {
            reject();
        }
    }

    if (!addTaskWakesUp && wakesUpForTask(task)) {
        wakeup(inEventLoop);
    }
}

当前线程不是eventloop的线程,所以进入startThread()这个分支,我们看看startThread()的相关代码:

    private void startThread() {
        if (state == ST_NOT_STARTED) {
            if (STATE_UPDATER.compareAndSet(this, ST_NOT_STARTED, ST_STARTED)) {
                doStartThread();
            }
        }
    }

    private void doStartThread() {
        assert thread == null;
        executor.execute(new Runnable() {
            @Override
            public void run() {
                thread = Thread.currentThread();
                if (interrupted) {
                    thread.interrupt();
                }

                boolean success = false;
                updateLastExecutionTime();
                try {
                    SingleThreadEventExecutor.this.run();
                    success = true;
                } catch (Throwable t) {
                    logger.warn("Unexpected exception from an event executor: ", t);
                } finally {
                    // 省略代码。。。。
                }
            }
        });
    }

可见,如果线程状态为ST_NOT_STARTED,则创建一个新的线程(还记得netty echo服务端源码分析(一)中的ThreadPerTaskExecutor吗?),并在新创建的线程中执行NioEventLooprun()函数。

eventloop线程创建并启动后,通过调用addTask()将task任务添加到taskqueue中。

我们先不管eventloop线程是如何调用taskqueue中的任务的,看看register0(promise)的具体逻辑。

private void register0(ChannelPromise promise) {
    try {
        // check if the channel is still open as it could be closed in the mean time when the register
        // call was outside of the eventLoop
        if (!promise.setUncancellable() || !ensureOpen(promise)) {
            return;
        }
        boolean firstRegistration = neverRegistered;
        // 调用的函数 AbstractNioChannel.doRegister()
        doRegister();
        neverRegistered = false;
        registered = true;

        // Ensure we call handlerAdded(...) before we actually notify the promise. This is needed as the
        // user may already fire events through the pipeline in the ChannelFutureListener.
        pipeline.invokeHandlerAddedIfNeeded();

        safeSetSuccess(promise);
        pipeline.fireChannelRegistered();
        // Only fire a channelActive if the channel has never been registered. This prevents firing
        // multiple channel actives if the channel is deregistered and re-registered.
        if (isActive()) {
            if (firstRegistration) {
                pipeline.fireChannelActive();
            } else if (config().isAutoRead()) {
                // This channel was registered before and autoRead() is set. This means we need to begin read
                // again so that we process inbound data.
                //
                // See https://github.com/netty/netty/issues/4805
                beginRead();
            }
        }
    } catch (Throwable t) {
        // Close the channel directly to avoid FD leak.
        closeForcibly();
        closeFuture.setClosed();
        safeSetFailure(promise, t);
    }
}
protected void doRegister() throws Exception {
    boolean selected = false;
    for (;;) {
        try {
            selectionKey = javaChannel().register(eventLoop().unwrappedSelector(), 0, this);
            return;
        } catch (CancelledKeyException e) {
            if (!selected) {
                // Force the Selector to select now as the "canceled" SelectionKey may still be
                // cached and not removed because no Select.select(..) operation was called yet.
                eventLoop().selectNow();
                selected = true;
            } else {
                // We forced a select operation on the selector before but the SelectionKey is still cached
                // for whatever reason. JDK bug ?
                throw e;
            }
        }
    }
}

到此,doRegister()通过javaChannel().register()函数将channel注册到了selector,只是还没有将监听的OP_ACCEPT事件注册上去。

然后我们分析一下这行代码:

pipeline.invokeHandlerAddedIfNeeded();

我们分析把channel handler添加到pipeline中时,由于channel还没有注册,所以将一个任务添加到了pendingHandlerCallbackHead链表中,这个函数会将registered标志设为true,同时在eventloop中执行pendingHandlerCallbackHead链表中的所有任务:

        PendingHandlerCallback task = pendingHandlerCallbackHead;
        while (task != null) {
            task.execute();
            task = task.next;
        }

继续往下分析:

safeSetSuccess(promise);

设置channelFuture,表示完成了注册,main()函数的b.bind(PORT).sync()调用可以返回了。

继续往下:

pipeline.fireChannelRegistered();

调用链如下代码,可以看代码注释,了解处理过程

// DefaultChannelPipeline

public final ChannelPipeline fireChannelRegistered() {
    AbstractChannelHandlerContext.invokeChannelRegistered(head);
    return this;
}

// AbstractChannelHandlerContext

static void invokeChannelRegistered(final AbstractChannelHandlerContext next) {
    EventExecutor executor = next.executor();
    if (executor.inEventLoop()) {
        next.invokeChannelRegistered();
    } else {
        executor.execute(new Runnable() {
            @Override
            public void run() {
                next.invokeChannelRegistered();
            }
        });
    }
}

// HeadContext

private void invokeChannelRegistered() {
    if (invokeHandler()) {
        try {
            // 返回的context的channel handler,对于head来说,因为head本身实现了channelhandler接口,返回的就是head自己
            ((ChannelInboundHandler) handler()).channelRegistered(this);
        } catch (Throwable t) {
            notifyHandlerException(t);
        }
    } else {
        fireChannelRegistered();
    }
}

// 调用channel handler的相应函数
public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
    // 实际干活的函数
    invokeHandlerAddedIfNeeded();
    
    // 执行下一个context的相应函数
    ctx.fireChannelRegistered();
}

public ChannelHandlerContext fireChannelRegistered() {
    invokeChannelRegistered(findContextInbound());
    return this;
}

private AbstractChannelHandlerContext findContextInbound() {
    AbstractChannelHandlerContext ctx = this;
    do {
        ctx = ctx.next;
    } while (!ctx.inbound);
    return ctx;
}

这里可以总结一下pipleline的任务链传递和处理模式(XXX表示register、bind、read、write等具体事件):

pipeline.fireXXX -> headContext.invokeXXX -> channelHandler.XXX -> next Context.fireXXX -> next Context.invokeXXX -> next channelHandler.XXX

initAndRegister()算是分析完了,到现在为止,经过曲折的调用关系,我们创建了eventloop线程并让它运行,同时将register0任务提交到它的任务队列,但是我们先不分析register0任务是如何执行的,还记得开头的dobind()函数中除了调用initAndRegister()还调用了dobind0()函数吗,我们先来分析这个函数。

private static void doBind0(
        final ChannelFuture regFuture, final Channel channel,
        final SocketAddress localAddress, final ChannelPromise promise) {

    // This method is invoked before channelRegistered() is triggered.  Give user handlers a chance to set up
    // the pipeline in its channelRegistered() implementation.
    channel.eventLoop().execute(new Runnable() {
        @Override
        public void run() {
            if (regFuture.isSuccess()) {
                channel.bind(localAddress, promise).addListener(ChannelFutureListener.CLOSE_ON_FAILURE);
            } else {
                promise.setFailure(regFuture.cause());
            }
        }
    });
}

原来跟register0()任务一样,是将任务提交到eventloop线程的taskQueue队列,只不过这一次不用创建新的线程了。

下面分析channel的bind()函数,具体实现代码在类AbstractChannel中,这个函数的调用链如下,代码注释已经做了说明:

// AbstractChannel
public ChannelFuture bind(SocketAddress localAddress, ChannelPromise promise) {
    return pipeline.bind(localAddress, promise);
}

// DefaultChannelPipeline
public final ChannelFuture bind(SocketAddress localAddress, ChannelPromise promise) {
    // 因为bind是一个outbound事件,从pipeline链尾tailContext开始执行
    return tail.bind(localAddress, promise);
}

// tail context的父类AbstractChannelHandlerContext
public ChannelFuture bind(final SocketAddress localAddress, final ChannelPromise promise) {
    if (localAddress == null) {
        throw new NullPointerException("localAddress");
    }
    if (isNotValidPromise(promise, false)) {
        // cancelled
        return promise;
    }

// 应用程序没有添加outbound的情况下,找到的next  context是head context
    final AbstractChannelHandlerContext next = findContextOutbound();
    EventExecutor executor = next.executor();
    if (executor.inEventLoop()) {
        next.invokeBind(localAddress, promise);
    } else {
        safeExecute(executor, new Runnable() {
            @Override
            public void run() {
                next.invokeBind(localAddress, promise);
            }
        }, promise, null);
    }
    return promise;
}

// head context的父类AbstractChannelHandlerContext
    private void invokeBind(SocketAddress localAddress, ChannelPromise promise) {
        if (invokeHandler()) {
            try {
                ((ChannelOutboundHandler) handler()).bind(this, localAddress, promise);
            } catch (Throwable t) {
                notifyOutboundHandlerException(t, promise);
            }
        } else {
            bind(localAddress, promise);
        }
    }

// 还在head context的父类AbstractChannelHandlerContext
public void bind(
        ChannelHandlerContext ctx, SocketAddress localAddress, ChannelPromise promise)
        throws Exception {
    // 通过unsafe调用bind了,意味着会调用JVM的功能,操作底层的一些函数了
    unsafe.bind(localAddress, promise);
}

// AbstractUnsafe
public final void bind(final SocketAddress localAddress, final ChannelPromise promise) {
    assertEventLoop();

    if (!promise.setUncancellable() || !ensureOpen(promise)) {
        return;
    }

    // See: https://github.com/netty/netty/issues/576
    if (Boolean.TRUE.equals(config().getOption(ChannelOption.SO_BROADCAST)) &&
        localAddress instanceof InetSocketAddress &&
        !((InetSocketAddress) localAddress).getAddress().isAnyLocalAddress() &&
        !PlatformDependent.isWindows() && !PlatformDependent.maybeSuperUser()) {
        // Warn a user about the fact that a non-root user can't receive a
        // broadcast packet on *nix if the socket is bound on non-wildcard address.
        logger.warn(
                "A non-root user can't receive a broadcast packet if the socket " +
                "is not bound to a wildcard address; binding to a non-wildcard " +
                "address (" + localAddress + ") anyway as requested.");
    }

    boolean wasActive = isActive();
    try {
        // 做实际的bind工作
        doBind(localAddress);
    } catch (Throwable t) {
        safeSetFailure(promise, t);
        closeIfClosed();
        return;
    }

    if (!wasActive && isActive()) {
        invokeLater(new Runnable() {
            @Override
            public void run() {
                pipeline.fireChannelActive();
            }
        });
    }

    safeSetSuccess(promise);
}

// NioServerSocketChannel
protected void doBind(SocketAddress localAddress) throws Exception {
    if (PlatformDependent.javaVersion() >= 7) {
       // 最终通过JVM调用server socket的bind、listen等函数,启动服务端
        javaChannel().bind(localAddress, config.getBacklog());
    } else {
        javaChannel().socket().bind(localAddress, config.getBacklog());
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容