Vert.x3 Core手册 [Part 2. TCP 服务器与客户端 ]

本文依照 知识共享许可协议(署名-非商业性使用-禁止演绎) 发布。


编写TCP 服务器和客户端

Vert.x让你很轻松地编写非阻塞的TCP 服务器和客户端。

创建TCP 服务器

创建TCP 服务器最简单的方式是像下面这样,使用缺省选项:
NetServer server = vertx.createNetServer();

配置TCP 服务器

不使用缺省选项时,可以在创建时传入NetServerOptions对象:

NetServerOptions options = new NetServerOptions().setPort(4321);
NetServer server = vertx.createNetServer(options);

服务器启动监听

选择listen方法中的一个,可以让服务器监听传入的请求。

让服务器监听选项中指定的端口和主机地址:

NetServer server = vertx.createNetServer();
server.listen();

或者在方法调用时指定端口和主机,这将忽略配置项:

NetServer server = vertx.createNetServer();
server.listen(1234, "localhost");

缺省主机地址是0.0.0.0,其意义是在所有可用的地址上进行监听;缺省端口是0,这是一个特殊的值,它会指示服务器随机寻找一个可用的本地端口来使用。

实际的绑定是异步发生的。所以有可能在listen方法调用返回之后,服务器才开始监听。

如果想得到监听开始的通知,可以在调用listen方法时提供一个handler供回调执行:

NetServer server = vertx.createNetServer();
server.listen(1234, "localhost", res -> {
  if (res.succeeded()) {
    System.out.println("Server is now listening!");
  } else {
    System.out.println("Failed to bind!");
  }
});

随机监听某个端口

如果监听端口被设置为0,服务器将随机寻找一个端口。

想知道服务器实际监听的端口,可以调用actualPort方法。

NetServer server = vertx.createNetServer();
server.listen(0, "localhost", res -> {
  if (res.succeeded()) {
    System.out.println("Server is now listening on actual port: " + server.actualPort());
  } else {
    System.out.println("Failed to bind!");
  }
});

收到链接传入的通知

想在链接产生时收到通知,需要设置connectHandler

NetServer server = vertx.createNetServer();
server.connectHandler(socket -> {
  // Handle the connection in here
});

链接产生时,这个handler将被调用,参数是NetSocket的实例。

NetSocket是对实际链接的一个类socket的接口(socket-like interface),你可以读写数据,也可以关闭socket。

从socket读数据

要从socket读数据,只需在socket上设置handler

这样每次socket收到数据时,将会给传入一个buffer并调用handler。

NetServer server = vertx.createNetServer();
server.connectHandler(socket -> {
  socket.handler(buffer -> {
    System.out.println("I received some bytes: " + buffer.length());
  });
});

往socket写数据

write方法用来写入数据。

Buffer buffer = Buffer.buffer().appendFloat(12.34f).appendInt(123);
socket.write(buffer);

// Write a string in UTF-8 encoding
socket.write("some data");

// Write a string using the specified encoding
socket.write("some data", "UTF-16");

写入是异步的,有可能write方法已经返回而数据写入还未发生。

结束的handler(Closed handler)

要在socket关闭时得到通知,可以设置一个closeHandler

socket.closeHandler(v -> {
  System.out.println("The socket has been closed");
});

处理异常

可以设置一个exceptionHandler来捕获socket上发生的异常。

Event bus write handler

socket会在event bus上自动注册一个handler,这个handler会在收到buffer时将其写入socket。

这样你可以在不同的verticle里、甚至是不同的Vert.x实例里发送buffer到这个地址,而这些数据将被写入socket。

这个handler的地址可以用writeHandlerID方法获得。

本地和远程地址

NetSocket的本地地址用localAddress方法获取。

远程地址(即,链接另一端的地址),可以用remoteAddress方法获取。

发送文件或类路径里的资源

sendFile方法可以将文件或类路径里的资源直接写入socket。因为其可以借助操作系统内核支持的操作来完成,所以这是一种很有效的发送文件的方式。

请参阅本章,可以了解关于serving files from the classpath 的限制或者关闭这个特性。
socket.sendFile("myfile.dat");

Streaming sockets

NetSocket的实例也是ReadStreamWriteStream的实例,所以它们可以用于pump data(指将数据转发出去)或从其他流读写数据。

更多细节请参阅 streams and pumps

升级链接到 SSL/TLS

可以使用upgradeToSsl方法将一个非SSL/TLS的链接升级。

要使这个特性正常工作,服务器/客户端需要配置安全选项。请参阅SSL/TLS这一节获取更多细节。

关闭TCP 服务器

调用close方法可以关闭服务器。关闭服务器时,将会关闭所有打开的链接,并释放所有的服务器资源。

关闭也是异步的,所以close方法返回时关闭可能还没结束。若要在关闭完成时得到通知,需传入一个handler。

server.close(res -> {
  if (res.succeeded()) {
    System.out.println("Server is now closed");
  } else {
    System.out.println("close failed");
  }
});

verticles的自动清理

如果你是在verticle内部创建的TCP 服务器或客户端,那当verticle被卸载时,它们将被自动关闭。

扩展-共享(Scaling - sharing) TCP 服务器

TCP 服务器的handlers将一直在同一个event loop线程上执行。

这意味着如果在一个多核机器上运行时,只有一个实例被部署,你无法从多核中获得任何多余的好处。

为了利用到机器上的更多cpu核心,你需要部署你的TCP 服务器的多个实例。

可以通过编程的方式实例化多个实例:

for (int i = 0; i < 10; i++) {
  NetServer server = vertx.createNetServer();
  server.connectHandler(socket -> {
    socket.handler(buffer -> {
      // Just echo back the data
      socket.write(buffer);
    });
  });
  server.listen(1234, "localhost");
}

或者,如果你使用了verticle,只需要部署你的服务器verticle的多个实例即可。可以在命令行里加上-instance选项:
vertx run com.mycompany.MyVerticle -instances 10

再或者,以编程方式部署你的verticle:

DeploymentOptions options = new DeploymentOptions().setInstances(10);
vertx.deployVerticle("com.mycompany.MyVerticle", options);

一旦你这么做了,你会发现服务器功能如以前一样没变,但它利用上了多核资源,可以做更多事。

这时候,你也许会问:“一台主机的确定端口,怎么能有多个服务器监听呢?部署多个实例难道不会造成端口冲突吗?”

Vert.x在这里耍了点小花招。*

当你在同一个端口部署另一个服务器时,其实并没有真的创建一个新服务器监听这个端口。

相反,内部其实只维护了一个服务器,但是链接传入时,会用轮询的方式将链接分发给任意的connect handler。

因此,Vert.x的TCP 服务器可以在单线程的情况下,扩展到多个可用的cpu核心。

创建TCP 客户端

创建TCP客户端也和服务器类似:
NetClient client = vertx.createNetClient();

配置TCP 客户端

类似于服务器:

NetClientOptions options = new NetClientOptions().setConnectTimeout(10000);
NetClient client = vertx.createNetClient(options);

创建链接

指定了服务器的port和host后,可以使用connect方法创建到服务器的链接。之后handler将被调用,当链接成功创建,传入的参数会包含一个NetSocket;如果创建失败,传入的参数将包含失败对象。

NetClientOptions options = new NetClientOptions().setConnectTimeout(10000);
NetClient client = vertx.createNetClient(options);
client.connect(4321, "localhost", res -> {
  if (res.succeeded()) {
    System.out.println("Connected!");
    NetSocket socket = res.result();
  } else {
    System.out.println("Failed to connect: " + res.cause().getMessage());
  }
});

配置尝试连接的次数

客户端可以被配置成链接失败时自动重连。有两个方法,setReconnectIntervalsetReconnectAttempts

注意:当前Vert.x不会尝试重连,这两个特性仅仅在链接创建时可用。

NetClientOptions options = new NetClientOptions().
    setReconnectAttempts(10).
    setReconnectInterval(500);

NetClient client = vertx.createNetClient(options);

缺省情况下,创建多个链接是被禁止的。

配置服务器和客户端使用 SSL/TLS

TCP客户端/服务器通过配置可以使用Transport Layer Security(前身是大名鼎鼎的SSL)。

是否使用SSL/TLS 对API没有影响,在NetClientOptionsNetServerOptions实例上配置。

在服务端启用 SSL/TLS

通过设置ssl可以启用 SSL/TLS。

为服务端指定 key/certificate

为了客户端能校验SSL/TLS 服务的合法性,服务端通常会提供证书给客户端。

有好几种方式可以配置服务端的Certificates/keys 。

第一种方式是指定一个Java key-store的地址,这其中应该包含证书(the certificate)和私钥(private key)。

JDK中有一个实用的程序keytool可以管理Java key stores。

key store的密码也是必要的:

NetServerOptions options = new NetServerOptions().setSsl(true).setKeyStoreOptions(
    new JksOptions().
        setPath("/path/to/your/server-keystore.jks").
        setPassword("password-of-your-keystore")
);
NetServer server = vertx.createNetServer(options);

其二,你可以读取key store并将之存入buffer直接提供:

Buffer myKeyStoreAsABuffer = vertx.fileSystem().readFileBlocking("/path/to/your/server-keystore.jks");
JksOptions jksOptions = new JksOptions().
    setValue(myKeyStoreAsABuffer).
    setPassword("password-of-your-keystore");
NetServerOptions options = new NetServerOptions().
    setSsl(true).
    setKeyStoreOptions(jksOptions);
NetServer server = vertx.createNetServer(options);

PKCS#12 格式的Key/certificate,通常文件后缀名是.pfx.p12,它们的载入方式类同于Java key store:

NetServerOptions options = new NetServerOptions().setSsl(true).setPfxKeyCertOptions(
    new PfxOptions().
        setPath("/path/to/your/server-keystore.pfx").
        setPassword("password-of-your-keystore")
);
NetServer server = vertx.createNetServer(options);

支持以buffer格式进行配置:

Buffer myKeyStoreAsABuffer = vertx.fileSystem().readFileBlocking("/path/to/your/server-keystore.pfx");
PfxOptions pfxOptions = new PfxOptions().
    setValue(myKeyStoreAsABuffer).
    setPassword("password-of-your-keystore");
NetServerOptions options = new NetServerOptions().
    setSsl(true).
    setPfxKeyCertOptions(pfxOptions);
NetServer server = vertx.createNetServer(options);

还有一种分别提供私钥和证书的方式用到.pem文件。

NetServerOptions options = new NetServerOptions().setSsl(true).setPemKeyCertOptions(
    new PemKeyCertOptions().
        setKeyPath("/path/to/your/server-key.pem").
        setCertPath("/path/to/your/server-cert.pem")
);
NetServer server = vertx.createNetServer(options);

同样支持buffer:

Buffer myKeyAsABuffer = vertx.fileSystem().readFileBlocking("/path/to/your/server-key.pem");
Buffer myCertAsABuffer = vertx.fileSystem().readFileBlocking("/path/to/your/server-cert.pem");
PemKeyCertOptions pemOptions = new PemKeyCertOptions().
    setKeyValue(myKeyAsABuffer).
    setCertValue(myCertAsABuffer);
NetServerOptions options = new NetServerOptions().
    setSsl(true).
    setPemKeyCertOptions(pemOptions);
NetServer server = vertx.createNetServer(options);

请谨记以pem 配置时,私钥是未加密的。

Specifying trust for the server

为了验证客户端的身份,SSL/TLS 服务器可以使用证书授权(a certificate authority)。

有多种途径可为服务器配置证书授权。

Java trust store同样可以用keytool管理。

同样需要提供密码:

NetServerOptions options = new NetServerOptions().
    setSsl(true).
    setClientAuth(ClientAuth.REQUIRED).
    setTrustStoreOptions(
        new JksOptions().
            setPath("/path/to/your/truststore.jks").
            setPassword("password-of-your-truststore")
    );
NetServer server = vertx.createNetServer(options);

同样可以读入buffer再提供:

Buffer myTrustStoreAsABuffer = vertx.fileSystem().readFileBlocking("/path/to/your/truststore.jks");
NetServerOptions options = new NetServerOptions().
    setSsl(true).
    setClientAuth(ClientAuth.REQUIRED).
    setTrustStoreOptions(
        new JksOptions().
            setValue(myTrustStoreAsABuffer).
            setPassword("password-of-your-truststore")
    );
NetServer server = vertx.createNetServer(options);

PKCS#12格式的证书授权(Certificate authority )同样可用:

NetServerOptions options = new NetServerOptions().
    setSsl(true).
    setClientAuth(ClientAuth.REQUIRED).
    setPfxTrustOptions(
        new PfxOptions().
            setPath("/path/to/your/truststore.pfx").
            setPassword("password-of-your-truststore")
    );
NetServer server = vertx.createNetServer(options);

buffer格式的:

Buffer myTrustStoreAsABuffer = vertx.fileSystem().readFileBlocking("/path/to/your/truststore.pfx");
NetServerOptions options = new NetServerOptions().
    setSsl(true).
    setClientAuth(ClientAuth.REQUIRED).
    setPfxTrustOptions(
        new PfxOptions().
            setValue(myTrustStoreAsABuffer).
            setPassword("password-of-your-truststore")
    );
NetServer server = vertx.createNetServer(options);

.pem文件也可用:

NetServerOptions options = new NetServerOptions().
    setSsl(true).
    setClientAuth(ClientAuth.REQUIRED).
    setPemTrustOptions(
        new PemTrustOptions().
            addCertPath("/path/to/your/server-ca.pem")
    );
NetServer server = vertx.createNetServer(options);

对应的buffer格式的:

Buffer myCaAsABuffer = vertx.fileSystem().readFileBlocking("/path/to/your/server-ca.pfx");
NetServerOptions options = new NetServerOptions().
    setSsl(true).
    setClientAuth(ClientAuth.REQUIRED).
    setPemTrustOptions(
        new PemTrustOptions().
            addCertValue(myCaAsABuffer)
    );
NetServer server = vertx.createNetServer(options);

在客户端启用 SSL/TLS

要让客户端用上SSL,配置也很容易。这里的API与使用标准socket时极其相似。

调用setSSL(true)方法即可。

Client trust configuration

如果在客户端将trustAll 设置为true,客户端将会信任所有的服务器证书。这种情况下,链接仍然会被加密,不过容易受到‘中间人攻击’。换言之,其实你没法确定连上的是谁,这点需要加以注意。缺省值是false。

NetClientOptions options = new NetClientOptions().
    setSsl(true).
    setTrustAll(true);
NetClient client = vertx.createNetClient(options);

如果未曾设置trustAll ,那么必须配置客户端的trust store,其中应该包含客户端信任的服务器证书。

与服务端的配置类似,客户端的trust也有下面几种途径:

一是指定包含证书授权的Java trust-store的位置。

这是一个标准的Java key store,与服务端的一样。客户端trust store位置的设置通过JksOptions对象的path方法完成。客户端发起连接时,如果服务器的证书不在客户端的 trust store 里,则连接请求不会成功。

NetClientOptions options = new NetClientOptions().
    setSsl(true).
    setTrustStoreOptions(
        new JksOptions().
            setPath("/path/to/your/truststore.jks").
            setPassword("password-of-your-truststore")
    );
NetClient client = vertx.createNetClient(options);

buffer支持:

Buffer myTrustStoreAsABuffer = vertx.fileSystem().readFileBlocking("/path/to/your/truststore.jks");
NetClientOptions options = new NetClientOptions().
    setSsl(true).
    setTrustStoreOptions(
        new JksOptions().
            setValue(myTrustStoreAsABuffer).
            setPassword("password-of-your-truststore")
    );
NetClient client = vertx.createNetClient(options);

PKCS#12也类似:

NetClientOptions options = new NetClientOptions().
    setSsl(true).
    setPfxTrustOptions(
        new PfxOptions().
            setPath("/path/to/your/truststore.pfx").
            setPassword("password-of-your-truststore")
    );
NetClient client = vertx.createNetClient(options);

buffer支持:

Buffer myTrustStoreAsABuffer = vertx.fileSystem().readFileBlocking("/path/to/your/truststore.pfx");
NetClientOptions options = new NetClientOptions().
    setSsl(true).
    setPfxTrustOptions(
        new PfxOptions().
            setValue(myTrustStoreAsABuffer).
            setPassword("password-of-your-truststore")
    );
NetClient client = vertx.createNetClient(options);

.pem文件也可以:

NetClientOptions options = new NetClientOptions().
    setSsl(true).
    setPemTrustOptions(
        new PemTrustOptions().
            addCertPath("/path/to/your/ca-cert.pem")
    );
NetClient client = vertx.createNetClient(options);

同样还有buffer支持:

Buffer myTrustStoreAsABuffer = vertx.fileSystem().readFileBlocking("/path/to/your/ca-cert.pem");
NetClientOptions options = new NetClientOptions().
    setSsl(true).
    setPemTrustOptions(
        new PemTrustOptions().
            addCertValue(myTrustStoreAsABuffer)
    );
NetClient client = vertx.createNetClient(options);

为客户端指定key/certificate

如果服务端要验证客户端的身份,那么在发起连接时,客户端需要向服务端提交自己的证书。下面几种方式可以配置客户端:

其一是指定包含密钥和证书的Java key-store的位置。同样这也是一个常规的Java key store。仍然通过JksOptions对象的path方法设置。

NetClientOptions options = new NetClientOptions().setSsl(true).setKeyStoreOptions(
    new JksOptions().
        setPath("/path/to/your/client-keystore.jks").
        setPassword("password-of-your-keystore")
);
NetClient client = vertx.createNetClient(options);

buffer支持:

Buffer myKeyStoreAsABuffer = vertx.fileSystem().readFileBlocking("/path/to/your/client-keystore.jks");
JksOptions jksOptions = new JksOptions().
    setValue(myKeyStoreAsABuffer).
    setPassword("password-of-your-keystore");
NetClientOptions options = new NetClientOptions().
    setSsl(true).
    setKeyStoreOptions(jksOptions);
NetClient client = vertx.createNetClient(options);

PKCS#12格式的密钥/证书:

NetClientOptions options = new NetClientOptions().setSsl(true).setPfxKeyCertOptions(
    new PfxOptions().
        setPath("/path/to/your/client-keystore.pfx").
        setPassword("password-of-your-keystore")
);
NetClient client = vertx.createNetClient(options);

buffer支持:

Buffer myKeyStoreAsABuffer = vertx.fileSystem().readFileBlocking("/path/to/your/client-keystore.pfx");
PfxOptions pfxOptions = new PfxOptions().
    setValue(myKeyStoreAsABuffer).
    setPassword("password-of-your-keystore");
NetClientOptions options = new NetClientOptions().
    setSsl(true).
    setPfxKeyCertOptions(pfxOptions);
NetClient client = vertx.createNetClient(options);

.pem文件支持:

NetClientOptions options = new NetClientOptions().setSsl(true).setPemKeyCertOptions(
    new PemKeyCertOptions().
        setKeyPath("/path/to/your/client-key.pem").
        setCertPath("/path/to/your/client-cert.pem")
);
NetClient client = vertx.createNetClient(options);

buffer支持:

Buffer myKeyAsABuffer = vertx.fileSystem().readFileBlocking("/path/to/your/client-key.pem");
Buffer myCertAsABuffer = vertx.fileSystem().readFileBlocking("/path/to/your/client-cert.pem");
PemKeyCertOptions pemOptions = new PemKeyCertOptions().
    setKeyValue(myKeyAsABuffer).
    setCertValue(myCertAsABuffer);
NetClientOptions options = new NetClientOptions().
    setSsl(true).
    setPemKeyCertOptions(pemOptions);
NetClient client = vertx.createNetClient(options);

请谨记pem 配置中,私钥是未加密的。

撤销证书授权(Revoking certificate authorities)

被撤销的证书不应再被信任,这可以使用证书撤销列表( a certificate revocation list (CRL))来配置。crlPath用来配置crl列表:

NetClientOptions options = new NetClientOptions().
    setSsl(true).
    setTrustStoreOptions(trustOptions).
    addCrlPath("/path/to/your/crl.pem");
NetClient client = vertx.createNetClient(options);

buffer支持:

Buffer myCrlAsABuffer = vertx.fileSystem().readFileBlocking("/path/to/your/crl.pem");
NetClientOptions options = new NetClientOptions().
    setSsl(true).
    setTrustStoreOptions(trustOptions).
    addCrlValue(myCrlAsABuffer);
NetClient client = vertx.createNetClient(options);

配置加密算法套件( the Cipher suite )

缺省情况下,TLS 配置使用的是运行Vert.x的JVM自带的加密算法套件。它也可以用一组已启用的加密算法来配置:

NetServerOptions options = new NetServerOptions().
    setSsl(true).
    setKeyStoreOptions(keyStoreOptions).
    addEnabledCipherSuite("ECDHE-RSA-AES128-GCM-SHA256").
    addEnabledCipherSuite("ECDHE-ECDSA-AES128-GCM-SHA256").
    addEnabledCipherSuite("ECDHE-RSA-AES256-GCM-SHA384").
    addEnabledCipherSuite("CDHE-ECDSA-AES256-GCM-SHA384");
NetServer server = vertx.createNetServer(options);

NetServerOptionsNetClientOptions对象都可以指定加密算法套件。


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

推荐阅读更多精彩内容