Netty中ServerBootstrap初始化源码解析

前言

之前的文章介绍了Netty的线程池NioEventLoopGroup的初始化过程,这次将分析Netty中同样非常重要的一个东西ServerBootstrap。

ServerBootstrap使用片段

        ThreadFactory bossThreadFactory = new ThreadFactoryBuilder().setNameFormat(getServerName() + " Server Acceptor NIO Thread#%d").build();
        ThreadFactory workerThreadFactory = new ThreadFactoryBuilder().setNameFormat(getServerName() + " Server Reactor NIO Thread#%d").build();

        this.bossGroup = new NioEventLoopGroup(numberOfThreads, bossThreadFactory);
        this.workerGroup = new NioEventLoopGroup(numberOfThreads, workerThreadFactory);

        ServerBootstrap bootstrap = new ServerBootstrap();
        bootstrap.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class).childHandler(new ChannelInitializer<SocketChannel>() {
            @Override
            protected void initChannel(SocketChannel ch) throws Exception {
                ChannelPipeline pipeline = ch.pipeline();
                pipeline.addLast("readTimeoutHandler", new ReadTimeoutHandler(DeviceServerListener.this.timeoutSeconds));
                pipeline.addLast("lineBasedFrameDecoder-" + maxLength, new LineBasedFrameDecoder(Integer.parseInt(maxLength)));// 按行('\n')解析成命令ByteBuf
                pipeline.addLast("stringPluginMessageDecoder", new StringDecoder(CharsetUtil.UTF_8));
                pipeline.addLast("stringToByteEncoder", new StringToByteEncoder());// 将JSON字符串类型消息转换成ByteBuf
                pipeline.addLast("deviceMessageDecoder", new DeviceMessageDecoder());// 将JSON字符串消息转成deviceMessage对象
                pipeline.addLast("deviceMessageEncoder", new DeviceMessageEncoder());// 将deviceMessage对象转成JSON字符串
                pipeline.addLast("deviceHeartBeatResponseHandler", new DeviceHeartBeatResponseHandler(heartTime));
                pipeline.addLast("deviceAuthResponseHandler",
                        new DeviceAuthResponseHandler(DeviceServerListener.this.timeoutSeconds, DeviceServerListener.serverInstanceName));
                pipeline.addLast("deviceMessageHandler", new DeviceMessageHandler());

                // log.debug("Added Handler to Pipeline: {}", pipeline.names());
            }
        }).option(ChannelOption.SO_BACKLOG, 1024).option(ChannelOption.SO_KEEPALIVE, true).option(ChannelOption.TCP_NODELAY, true)
                .option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT).childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
                .option(ChannelOption.RCVBUF_ALLOCATOR, AdaptiveRecvByteBufAllocator.DEFAULT);

        // Start the server. Bind and start to accept incoming connections.
        this.channelFuture = bootstrap.bind(serverPort).sync();

这是Netty服务端比较标准的初始化片段,可以看到这其中ServerBootstrap有着非常重要的戏份,它就像是Netty的启动器一样,其中的NioEventLoopGroup之前已经分析过了,那么直接来看ServerBootstrap的初始化。

源码解析

从以上片段可以看到初始化时首先通过ServerBootstrap的无参构造函数创建一个对象,该构造函数没有任何的操作因此不做分析。接着是该对象的一串链式调用bootstrap.group().channel().childHandler().option(),我们来逐一看一下。
首先是group()方法

    public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup) {
        super.group(parentGroup);
        if (childGroup == null) {
            throw new NullPointerException("childGroup");
        }
        if (this.childGroup != null) {
            throw new IllegalStateException("childGroup set already");
        }
        this.childGroup = childGroup;
        return this;
    }

这里将传入的childGroup对象赋值给ServerBootstrapchildGroup属性,然后调用了父类AbstractBootstrapgroup()方法

    public B group(EventLoopGroup group) {
        if (group == null) {
            throw new NullPointerException("group");
        }
        if (this.group != null) {
            throw new IllegalStateException("group set already");
        }
        this.group = group;
        return (B) this;
    }

由这里看出该方法将传入的parentGroup赋值给了group属性。

接着来看channel()方法

    public B channel(Class<? extends C> channelClass) {
        if (channelClass == null) {
            throw new NullPointerException("channelClass");
        }
        return channelFactory(new BootstrapChannelFactory<C>(channelClass));
    }

这里调用了channelFactory(new BootstrapChannelFactory<C>(channelClass)),来跟一下

    public B channelFactory(ChannelFactory<? extends C> channelFactory) {
        if (channelFactory == null) {
            throw new NullPointerException("channelFactory");
        }
        if (this.channelFactory != null) {
            throw new IllegalStateException("channelFactory set already");
        }

        this.channelFactory = channelFactory;
        return (B) this;
    }

这里完成了对AbstractBootstrap类的channelFactory属性的赋值,赋值对象为new BootstrapChannelFactory<C>(channelClass)

public abstract class AbstractBootstrap<B extends AbstractBootstrap<B, C>, C extends Channel> implements Cloneable

这里的C根据AbstractBootstrap的类定义为Channel的子类,因此实际传入的channelClass值为NioServerSocketChannel.class

接着来看childHandler()方法

    public ServerBootstrap childHandler(ChannelHandler childHandler) {
        if (childHandler == null) {
            throw new NullPointerException("childHandler");
        }
        this.childHandler = childHandler;
        return this;
    }

没有过多操作,也仅是对ServerBootstrapchildHandler属性赋值,但这里传入的childHandler稍微有点复杂。传入的对象为new ChannelInitializer<SocketChannel>()同时覆写了其initChannel(SocketChannel ch)方法,该方法初始化了一个处理链用于处理接收和发送双向的消息,这块比较重要,后续将会单独进行分析,在此暂不深入。

最后来看一下option()方法

    public <T> B option(ChannelOption<T> option, T value) {
        if (option == null) {
            throw new NullPointerException("option");
        }
        if (value == null) {
            synchronized (options) {
                options.remove(option);
            }
        } else {
            synchronized (options) {
                options.put(option, value);
            }
        }
        return (B) this;
    }

这里维护了一个options数组将要设置的项都放进了该数组中,其定义如下

private final Map<ChannelOption<?>, Object> options = new LinkedHashMap<ChannelOption<?>, Object>();

这里可以看出放入对象的key值限定为ChannelOption<?>对象,大致都有下面这些

    public static final ChannelOption<ByteBufAllocator> ALLOCATOR = valueOf("ALLOCATOR");//根据PooledByteBufAllocator或UnpooledByteBufAllocator设置对象池开启与否(4.1版本开始默认开)
    public static final ChannelOption<RecvByteBufAllocator> RCVBUF_ALLOCATOR = valueOf("RCVBUF_ALLOCATOR");//设置接收池
    public static final ChannelOption<MessageSizeEstimator> MESSAGE_SIZE_ESTIMATOR = valueOf("MESSAGE_SIZE_ESTIMATOR");

    public static final ChannelOption<Integer> CONNECT_TIMEOUT_MILLIS = valueOf("CONNECT_TIMEOUT_MILLIS");
    public static final ChannelOption<Integer> MAX_MESSAGES_PER_READ = valueOf("MAX_MESSAGES_PER_READ");
    public static final ChannelOption<Integer> WRITE_SPIN_COUNT = valueOf("WRITE_SPIN_COUNT");
    public static final ChannelOption<Integer> WRITE_BUFFER_HIGH_WATER_MARK = valueOf("WRITE_BUFFER_HIGH_WATER_MARK");
    public static final ChannelOption<Integer> WRITE_BUFFER_LOW_WATER_MARK = valueOf("WRITE_BUFFER_LOW_WATER_MARK");

    public static final ChannelOption<Boolean> ALLOW_HALF_CLOSURE = valueOf("ALLOW_HALF_CLOSURE");
    public static final ChannelOption<Boolean> AUTO_READ = valueOf("AUTO_READ");

    public static final ChannelOption<Boolean> SO_BROADCAST = valueOf("SO_BROADCAST");//允许发送广播数据报
    public static final ChannelOption<Boolean> SO_KEEPALIVE = valueOf("SO_KEEPALIVE");//是否启用心跳保活机制
    public static final ChannelOption<Integer> SO_SNDBUF = valueOf("SO_SNDBUF");//发送缓冲区大小
    public static final ChannelOption<Integer> SO_RCVBUF = valueOf("SO_RCVBUF");//接收缓冲区大小
    public static final ChannelOption<Boolean> SO_REUSEADDR = valueOf("SO_REUSEADDR");//是否允许重复使用本地地址和端口
    public static final ChannelOption<Integer> SO_LINGER = valueOf("SO_LINGER");//保证阻塞close()的调用,直到数据完全发送
    public static final ChannelOption<Integer> SO_BACKLOG = valueOf("SO_BACKLOG");//临时存放已完成三次握手的请求队列的最大长度

    public static final ChannelOption<Integer> SO_TIMEOUT = valueOf("SO_TIMEOUT");

    public static final ChannelOption<Integer> IP_TOS = valueOf("IP_TOS");
    public static final ChannelOption<InetAddress> IP_MULTICAST_ADDR = valueOf("IP_MULTICAST_ADDR");
    public static final ChannelOption<NetworkInterface> IP_MULTICAST_IF = valueOf("IP_MULTICAST_IF");
    public static final ChannelOption<Integer> IP_MULTICAST_TTL = valueOf("IP_MULTICAST_TTL");
    public static final ChannelOption<Boolean> IP_MULTICAST_LOOP_DISABLED = valueOf("IP_MULTICAST_LOOP_DISABLED");

    public static final ChannelOption<Boolean> TCP_NODELAY = valueOf("TCP_NODELAY");//是否开启Nagle算法,即是否等数据累积到一定程度再发送数据

这里很大一部分是tcp中的一些参数设置,比较常用的几个参数都在源码上做了注释,其中对性能影响很大的一项是ALLOCATOR,根据一些资料显示开启对象池性能远高于不开启,而Netty从4.1版本开始也将默认选项设为了开启对象池。
这里我们顺便来看一下valueOf方法的实现

    public static <T> ChannelOption<T> valueOf(String name) {
        checkNotNull(name, "name");
        ChannelOption<T> option = names.get(name);
        if (option == null) {
            option = new ChannelOption<T>(name);
            ChannelOption<T> old = names.putIfAbsent(name, option);
            if (old != null) {
                option = old;
            }
        }
        return option;
    }

可以看到这个方法主要是维护了names这个数组,其定义如下

private static final ConcurrentMap<String, ChannelOption> names = PlatformDependent.newConcurrentHashMap();

来看一下PlatformDependent.newConcurrentHashMap()的具体实现

    public static <K, V> ConcurrentMap<K, V> newConcurrentHashMap() {
        if (CAN_USE_CHM_V8) {
            return new ConcurrentHashMapV8<K, V>();
        } else {
            return new ConcurrentHashMap<K, V>();
        }
    }

看到这里我又一次被震惊了,Netty自己实现了一个ConcurrentHashMapV8用于应对java8以下的版本,要知道这可是一个6000多行代码的类。。。不得不说Netty的开发者真的不愧优化狂魔的称号。

分析完了bootstrap.group().channel().childHandler().option()这一串链式调用,接着来看下一串bootstrap.bind(serverPort).sync()。这里首先来看bind()方法

    public ChannelFuture bind(int inetPort) {
        return bind(new InetSocketAddress(inetPort));
    }

这里根据端口号创建了一个InetSocketAddress对象,并调用了bind(SocketAddress localAddress)方法

    public ChannelFuture bind(SocketAddress localAddress) {
        validate();
        if (localAddress == null) {
            throw new NullPointerException("localAddress");
        }
        return doBind(localAddress);
    }

这里的validate()方法校验了AbstractBootstrap类的groupchannelFactory属性是否为空,然后执行doBind()方法

    private ChannelFuture doBind(final SocketAddress localAddress) {
        final ChannelFuture regFuture = initAndRegister();// 创建Channel并注册到线程池
        final Channel channel = regFuture.channel();
        if (regFuture.cause() != null) {
            return regFuture;
        }

        if (regFuture.isDone()) {
            // 注册完成且注册成功
            ChannelPromise promise = channel.newPromise();
            doBind0(regFuture, channel, localAddress, promise);
            return promise;
        } else {
            // 由于注册是异步事件,可能此时没有注册完成,那么使用异步操作
            final PendingRegistrationPromise promise = new PendingRegistrationPromise(channel);
            regFuture.addListener(new ChannelFutureListener() {
                @Override
                public void operationComplete(ChannelFuture future) throws Exception {
                    Throwable cause = future.cause();
                    if (cause != null) {
                        // 注册过程中有异常则失败
                        promise.setFailure(cause);
                    } else {
                        // 注册完成且成功
                        promise.executor = channel.eventLoop();

                        doBind0(regFuture, channel, localAddress, promise);
                    }
                }
            });
            return promise;
        }
    }

首先看一下regFuture对象如何通过initAndRegister()方法生成的

    final ChannelFuture initAndRegister() {
        Channel channel = null;
        try {
             // 创建一个Channel
            channel = channelFactory().newChannel();
            // 初始化处理器Handler
            init(channel);
        } catch (Throwable t) {
            if (channel != null) {
                // channel can be null if newChannel crashed (eg SocketException("too many open files"))
                channel.unsafe().closeForcibly();
            }
            // Channel还没有注册到线程池,使用默认线程GlobalEventExecutor
            return new DefaultChannelPromise(channel, GlobalEventExecutor.INSTANCE).setFailure(t);
        }
        // 将channel注册到Reactor线程池
        ChannelFuture regFuture = group().register(channel);
        if (regFuture.cause() != null) {
            if (channel.isRegistered()) {
                channel.close();
            } else {
                channel.unsafe().closeForcibly();
            }
        }
        return regFuture;
    }

channelFactory().newChannel()这里调用的是AbstractBootstrapchannelFactory属性的newChannel()方法,根据之前赋值的channelFactory对象的情况来看,这里最终得到的是NioServerSocketChannel的一个实例对象。得到channel对象后,接着调用init(channel)进行了初始化,该方法由子类ServerBootstrap实现。

    @Override
    void init(Channel channel) throws Exception {
        final Map<ChannelOption<?>, Object> options = options();
        synchronized (options) {
            channel.config().setOptions(options);//将options中逐一设置到DefaultChannelConfig对象上
        }

        final Map<AttributeKey<?>, Object> attrs = attrs();
        synchronized (attrs) {
            for (Entry<AttributeKey<?>, Object> e: attrs.entrySet()) {
                @SuppressWarnings("unchecked")
                AttributeKey<Object> key = (AttributeKey<Object>) e.getKey();
                channel.attr(key).set(e.getValue());
            }
        }

        ChannelPipeline p = channel.pipeline();

        final EventLoopGroup currentChildGroup = childGroup;
        final ChannelHandler currentChildHandler = childHandler;
        final Entry<ChannelOption<?>, Object>[] currentChildOptions;
        final Entry<AttributeKey<?>, Object>[] currentChildAttrs;
        synchronized (childOptions) {
            currentChildOptions = childOptions.entrySet().toArray(newOptionArray(childOptions.size()));
        }
        synchronized (childAttrs) {
            currentChildAttrs = childAttrs.entrySet().toArray(newAttrArray(childAttrs.size()));
        }

        p.addLast(new ChannelInitializer<Channel>() {
            @Override
            public void initChannel(Channel ch) throws Exception {
                final ChannelPipeline pipeline = ch.pipeline();
                ChannelHandler handler = handler();
                if (handler != null) {
                    pipeline.addLast(handler);
                }

                ch.eventLoop().execute(new Runnable() {
                    @Override
                    public void run() {//为了把ServerBootstrapAcceptor放在处理链的最末端,该类主要功能是将mainReactor接受的Channel传递给subReactor
                        pipeline.addLast(new ServerBootstrapAcceptor(
                                currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs));
                    }
                });
            }
        });
    }

这个方法看似很长其实主要做了两件事:一、将父类和子类的options和attrs进行赋值;二、构建channelpipeline属性的处理链。这里主要来看一下addLast()方法,由DefaultChannelPipeline进行具体实现,来看一下

    @Override
    public final ChannelPipeline addLast(ChannelHandler... handlers) {
        return addLast(null, handlers);
    }

    @Override
    public final ChannelPipeline addLast(EventExecutorGroup executor, ChannelHandler... handlers) {
        if (handlers == null) {
            throw new NullPointerException("handlers");
        }

        for (ChannelHandler h: handlers) {
            if (h == null) {
                break;
            }
            addLast(executor, null, h);
        }

        return this;
    }

    @Override
    public final ChannelPipeline addLast(EventExecutorGroup group, String name, ChannelHandler handler) {
        final AbstractChannelHandlerContext newCtx;
        synchronized (this) {
            checkMultiplicity(handler);//判断handler是否已添加并且是否是sharable的

            newCtx = newContext(group, filterName(name, handler), handler);//filterName(name, handler)自动生成name,newContext方法对newCtx对象的handler、inbound、outbound、name、pipeline属性进行了赋值

            addLast0(newCtx);//将newCtx加到tail节点之前

            if (!registered) {//一旦channel注册到eventloop上就将registered置为true,并且不可再改变
                newCtx.setAddPending();//CAS地修改newCtx的handlerState的值
                callHandlerCallbackLater(newCtx, true);//添加newCtx到pendingHandlerCallbackHead的next节点
                return this;
            }

            EventExecutor executor = newCtx.executor();
            if (!executor.inEventLoop()) {
                newCtx.setAddPending();
                executor.execute(new Runnable() {
                    @Override
                    public void run() {
                        callHandlerAdded0(newCtx);
                    }
                });
                return this;
            }
        }
        callHandlerAdded0(newCtx);
        return this;
    }

最终执行的是三参的addLast()方法并且传入的groupname均为空。该方法中主要根据channel是否注册到eventloop上做不同的处理,初始化handler链。
回到initAndRegister()方法中,分析完了init(channel)再来看另一个很重要的将channel注册到Reactor线程池的register()方法。该方法在NioEventLoopGroup的父类MultithreadEventLoopGroup中实现

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

    @Override
    public EventLoop next() {
        return (EventLoop) super.next();
    }

    @Override
    public EventExecutor next() {
        return chooser.next();
    }

这里的chooser上一篇文章中有分析过,其根据线程池设置的线程数是否2的幂次方有不同的实现,而next()方法返回的是数组中的一个NioEventLoop对象,该用类的register()方法,最终会返回一个注册了该channelDefaultChannelPromise对象,具体这里就不再深入了。
到此initAndRegister()方法就分析完毕,回到doBind()方法来继续分析doBind0()方法

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

        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());
                }
            }
        });
    }

这里能看到,只有regFuture.isSuccess()也就是channel注册成功时时才会执行绑定操作channel.bind(localAddress, promise).addListener(ChannelFutureListener.CLOSE_ON_FAILURE),否则直接向promise写注册失败,这里的promise是一个DefaultChannelPromise类型的对象,该类继承自Future,可以认为是一种特殊的Future对象。bind(localAddress, promise)方法最终会绑定在tail节点上,最终交由DefaultChannelPipeline的内部类的unsafe去进行绑定,调用链非常深在此就不做展开了。
最后跳回来看一下bootstrap.bind(serverPort).sync()中的sync(),之前分析过bootstrap.bind(serverPort)返回的是promise,因此sync()方法由DefaultChannelPromise实现

    @Override
    public ChannelPromise sync() throws InterruptedException {
        super.sync();
        return this;
    }

接着跟到DefaultPromise

    @Override
    public Promise<V> sync() throws InterruptedException {
        await();
        rethrowIfFailed();
        return this;
    }

    @Override
    public Promise<V> await() throws InterruptedException {
        if (isDone()) {
            return this;
        }

        if (Thread.interrupted()) {
            throw new InterruptedException(toString());
        }

        checkDeadLock();

        synchronized (this) {
            while (!isDone()) {
                incWaiters();
                try {
                    wait();
                } finally {
                    decWaiters();
                }
            }
        }
        return this;
    }

可以看出正常情况下该方法会始终在while循环中,导致执行到sync()方法的线程阻塞,因此bootstrap.bind(serverPort).sync()以后的代码都是不可达的,这点值得注意。

总结

ServerBootstrap的使用方式非常固定,大部分常规使用都会应用该初始化代码模板。主要做的事情就是赋值ServerBootstrap的各个属性,并且创建Channel、绑定用户定义的Handler、以及将该Channel注册到一个eventloop中(这里特别要强调的是Channel只能被绑定到一个eventloop中),最后绑定本地端口监听IO事件。

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

推荐阅读更多精彩内容