XMPP实现3

注册,获取好友列表,聊天

我们的注册功能实现上跟登录功能的步骤大致相当

首先需要在我们的XMPPManager类中发送一个连接请求

连接成功后呢,我们需要调用一个方法

[self.xmppStream registerWithPassword:@“” error&error];

那么我们这个连接也是一样,需要一个用户名

这个时候我们可以像登录一样给注册写一个接口

//注册

(void)registerWithUserName:(NSString *)userName password:(NSString *)password;

我们可以来实现以下这个方法

我们的注册也需要获取以下这个XMPPJID的内容

这个和登录中得代码重复了,我们可以给他封装成一个方法,只需要调用就行了

把我们的- (void)connectToServer方法改一下

(void)connectToServerWithUserName:(NSString *)userName

然后把

XMPPJID *myJID = [XMPPJID jidWithUser:userName domain:kDomin resource:kResource];

self.xmppStream.myJID = myJID;

这段代码放到里面

然后在我们登录和注册的时候把方法改动一下WithUser

我们可以定义一个属性,注册的密码

在我们的注册方法内把注册密码等于password

接下来我们就需要在连接成功的方法内调用一下我们的注册方法

但是有一个问题,我们在登录的时候需要进行与服务器连接,注册的时候也需要进行连接

那我们这两个方法都在这个连接的方法中进行

我们怎么知道我们是要登录还是注册呢,通常情况下我们向服务器发送连接请求只有两种目的,

一种是登录,一种是注册,所以一旦链接成功之后呢要么进行登录,要么进行注册

所以我们可以用一个枚举来进行标识

//与服务器进行链接的目的(枚举)

typedef NS_ENUM(NSInteger, ConnectToServerPurpose)

{

ConnectToServerPurposeLogin,//登陆

ConnectToServerPurposeRegister, //注册

};

然后我们可以声明一个属性

@property (nonatomic, assign) ConnectToServerPurpose connectToServerPurpose;

然后在我们的登录和注册方法当中,给这个属性赋值

//登录

- (void)loginWithUserName:(NSString *)userName password:(NSString *)password

{

self.connectToServerPurpose = ConnectToServerPurposeLogin;//连接目的为登录

self.loginPassword = password;

[self connectToServerWithUserName:userName];

}

//注册

- (void)registerWithUserName:(NSString *)userName password:(NSString *)password

{

self.connectToServerPurpose = ConnectToServerPurposeRegister;//连接目的为注册

self.registerPassword = password;

[self connectToServerWithUserName:userName];

}

在我们与服务器进行连接的时候,我们可以来进行一个判断,

这里用到了我们的switch

//与服务器连接成功

- (void)xmppStreamDidConnect:(XMPPStream *)sender

{

NSLog(@"%s__%d__|成功",__FUNCTION__,__LINE__);

switch (self.connectToServerPurpose) {

case ConnectToServerPurposeLogin://登录的时候调用登录的方法

{

NSError *error = nil;

[self.xmppStream authenticateWithPassword:self.loginPassword error:&error];

if (nil != error) {

NSLog(@"%s__%d__|验证出错:%@",__FUNCTION__,__LINE__,error);

}

break;

}

case ConnectToServerPurposeRegister://注册的时候调用注册的方法

{

NSError *error = nil;

[self.xmppStream registerWithPassword:self.registerPassword error:&error];

if (nil != error) {

NSLog(@"%s__%d__|验证出错:%@",__FUNCTION__,__LINE__,error);

}

break;

}

default:

break;

}

}

接下来我们需要在注册的界面调用注册方法

首先我们引入头文件,和登录界面一样

首先获取用户名和密码,调用注册方法

[[XMPPManager defaultManager] registerWithUserName:uesrName password:password];

和登录界面一样,我们注册无非就两种结果,一种是成功,一种是失败

那我们需要让注册界面来接收这两个方法

然后我们要成为XMPPManager的代理

[[XMPPManager defaultManager].xmppStream addDelegate:self delegateQueue:dispatch_get_main_queue()];

我们要遵循XMPPManager的协议方法

- (void)xmppStreamDidRegister:(XMPPStream *)sender

{

NSLog(@"%s__%d___|注册成功",__FUNCTION__,__LINE__);

[self.navigationController popToRootViewControllerAnimated:YES];

}

- (void)xmppStream:(XMPPStream *)sender didNotRegister:(DDXMLElement *)error

{

NSLog(@"%s__%d___|注册失败:%@",__FUNCTION__,__LINE__,error);

}

这里需要注意的是,我们验证成功以后,要让我们的界面回到登录界面

我们登录一下,OK,登录成功。

下面我来讲一下如何获取好友列表

那么在XMPP框架中呢有一个类,叫做XMPPRoster

XMPPRoster是负责添加好友,删除好友,获取好友列表,跟好友所有相关的

我们来创建一个XMPPRoster的属性

然后我们进行一个初始化

self.xmppRoster = [[XMPPRoster alloc] initWithRosterStorage:(id)dispatchQueue:(dispatch_queue_t)];

这里需要两个参数,第一个参数呢,是需要遵循XMPPRosterStorage协议的一个对象

那么这个对象主要是对你的好友列表进行数据持久化存储以及对你好友信息进行存储的

在这个框架中有一个类,叫做XMPPRosterCoreDataStorage

创建一个对象

XMPPRoster *rosterCoreDataStorage = [XMPPRosterCoreDataStorage sharedInstance];

然后我们调用一下一个方法

[self.xmppRoster activate:self.xmppStream];

我们这个方法,里面需要一个xmppStream的对象,这里的activate可以理解为激活

因为我们的xmppStream是负责服务器与客户端之间的数据传输

那我们的好友信息都是需要通过服务器来获取,那自然也需要通过xmppStream(通信管道)来获取

只有这样你才能跟服务器进行交互。激活之后我们就可以来获取到好友列表了

接下来我们需要创建一个继承于UITbaleViewController的类,

我们命名为RosterTbaleViewController,

我们这个类需要一个数组来保存所有的好友。

我们展示好友是需要在RosterTbaleViewController里

我们的管理好友功能是在XMPPRoster协议内,协议方法去传输

所以我们这里要把RosterTbaleViewController成为XMPPRoster的代理对象

导入头文件,

添加代理

[[XMPPManager defaultManager].xmppRoster addDelegate:self delegateQueue:dispatch_get_main_queue()];

然后我们要遵循这个协议,并实现这组协议方法

首先是一个刚开始获取到好友列表的功能

//开始获取好友列表的时候执行

- (void)xmppRosterDidBeginPopulating:(XMPPRoster *)sender

{

NSLog(@"%s__%d__|",__FUNCTION__,__LINE__);

}

//正在获取好友列表的时候执行,每获取到一个好友就执行一次

- (void)xmppRoster:(XMPPRoster *)sender didReceiveRosterItem:(DDXMLElement *)item

{

NSLog(@"%s__%d__| item = %@",__FUNCTION__,__LINE__,item);

}

//已经获取完好友列表的时候执行

- (void)xmppRosterDidEndPopulating:(XMPPRoster *)sender

{

NSLog(@"%s__%d__|",__FUNCTION__,__LINE__);

}

然后不要忘了把我们的这个storyboard跟这个类进行关联。

运行一下成功地时候我们会看到我们的程序运行了xmppRoster:didRecieveRosterItem的方法

你有多少个好友就会运行多少次

如果没有走这个方法的话就出了问题了

—————————————————————

环境搭建补充:搜索服务需要我们去手动添加

如果没有添加的话会出现一个问题,搜索好友会弹出提示框

无法连接到搜索服务

解决方案:进入openfire后台管理器—>插件-->有效的插件-->安装search服务器,完成以后要刷新缓存:openfire后台管理器-->服务器-->缓存摘要-->刷新Roster缓存(或者刷新全部缓存)。但是如果openfire采用了集群,那么此处的缓存将不存在,需要刷新集群中的缓存。

有些同学就算这样设置了以后还是没法搜索(待解决)

—————————————————————

我们可以看到成功后打印出来的信息

他输出地信息是一个XML的结构

有个jid,有name等,我们主要获取到这个jid的信息,也就是他的唯一标示,那我们怎么获取呢

我们可以看到他的类型是一个DDXMLElement

这里面也用到了一个XML解析的第三方,

首先声明一个NSString类型的对象

NSString *jidStr = [[item attributeForName:@"jid"] stringValue];

然后我们要获取一个XMPPJID的对象来获取这个jid,并且给他加入到数组当中

XMPPJID *jid = [XMPPJID jidWithString:jidStr];

[self.allRoster addObject:jid];

然后我们要刷新我们的tableView

[self.tableView insertRowsAtIndexPaths:(NSArray *) withRowAnimation:(UITableViewRowAnimation)];

这里第一个参数是一个数组,我们先创建一个NSIndexPath,我们其实就是要插入一条数据

创建一个NSIndexPath对象

NSIndexPath *indexPath = [NSIndexPath indexPathForRow:self.allRoster.count - 1 inSection:0];

这里-1是因为我们要从0开始

然后给这个方法一个IndexPath

[self.tableView insertRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationBottom];

我们来设置一下Cell的信息

我们需要在Cell上展示我们的好友名,

XMPPJID *jid = self.allRoster[indexPath.row];

cell.textLabel.text = jid.user;

ok运行一下,我们的好友信息就展示在我们的TableView上了。

我们点击我们的好友,可以进入到我们的聊天界面

设置好我们的storyboard,与聊天界面进行关联,

设置一个barbuttonitem,提供我们聊天的信息

关联一下方法

我们需要一个可变数组,用来存放我们所有的聊天信息

然后在我们的XMPP框架中呢有一个XMPPMessageArchiving

他是负责处理聊天信息的,包括聊天信息的归档和存储

所以我们创建一个XMPPMessageArchiving的对象

然后我们对他进行一次初始化

self.xmppMessageArchiving = [[XMPPMessageArchiving alloc]initWithMessageArchivingStorage:(id) dispatchQueue:dispatch_get_main_queue()];

我们这里的(id) 和之前的xmppRoster类似

XMPPMessageArchivingStorage呢是对聊天信息的一个持久化存储

xmpp框架中呢有一个类与之对应XMPPMessageArchivingCoreDataStorage

我们来创建一个XMPPMessageArchivingCoreDataStorage对象

XMPPMessageArchivingCoreDataStorage *messageArchivingCoreDataStorage = [XMPPMessageArchivingCoreDataStorage sharedInstance];

在我们的xmppMessageArchiving初始化中给他放进去

然后他也需要激活一下

[self.xmppMessageArchiving activate:self.xmppStream];

因为你获取好友消息也是需要通过这个通信管道的

接下来我们就可以使用xmppMessageArchiving来与好友进行收发信息了。

回到我们的收发信息的这个TableView类里面

现在我们需要知道我们要给发送信息

我们可以选择一个好友,进入到这个好友的聊天界面就给这个好友发送信息

在我们的好友列表里有个数组,这个数组里就包含着我们的所有好友

我们可以把这个jid传给聊天界面

然后我们就可以知道我们要给哪个好友聊天了

因为这个jid是一个用户的唯一标示

可是我们要如何把这个数组传过去呢?

在我们的Roster这个类里面有一个方法

- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {

把这个方法打开

因为我们的好友列表界面是使用点击cell直接push到下个界面的

所以我们这个方法里地这个sender,就是我们具体的那个cell

我们先转换一下

首先我们要获取到我们的cell,然后我们就可以获取到他的具体位置

NSIndexPath *indexPath = [self.tableView indexPathForCell:cell];

这样就获取到具体位置,这样一来我们就可以访问这个数组和他对应的jid

下面的这个界面就要来有一个属性接收这个jid

在- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {

方法里有个segue的参数,我们初始化一个好友聊天的界面,这个给他赋值为跳转到的那个界面

然后我们就可以获取到这个jid,

XMPPJID *jid = self.allFriends[indexPath.row];

MessageTableViewController *messageTableView = segue.destinationViewController;

messageTableView.MessageForJID = jid;

然后我们怎么给他发信息呢?

XMPP框架里有一个类,叫做XMPPMessage,我们来创建一个他的对象

XMPPMessage *message = [XMPPMessage messageWithType:@“ ”to:self.chatToJID];

前面的这个type呢,相当于我们要聊天的信息,后面的这个to,就是我们要聊天的这个用户

既然是聊天信息,那我们就要有聊天内容

message呢有个方法,叫做addBody

然后呢,我们就需要通过XMPPStream来将这条信息放出去

因为我们说过XMPPStream是通信管道,负责客户端和服务端所有的传递

那么我们这个message如果想要发送出去必须要通过XMPPStream这个管道

这条信息发送出去后呢,有两种结果,还是成功和失败

接收成功和失败的方法呢,也是需要遵循一个协议来实现的

我们要想接收这个结果呢,需要成为XMPPStream的代理对象

然后我们就在下面实现具体的方法

//已发送

- (void)xmppStream:(XMPPStream *)sender didSendMessage:(XMPPMessage *)message

{

NSLog(@"%s__%d__|message = %@",__FUNCTION__,__LINE__,message);

}

//发送失败

- (void)xmppStream:(XMPPStream *)sender didFailToSendMessage:(XMPPMessage *)message error:(NSError *)error

{

NSLog(@"%s__%d__| 信息发送失败:%@",__FUNCTION__,__LINE__,error);

}

我们现在可以运行一下来看看

点击我们的按钮,可以看到控制台输出的是我们的didSendMessage的方法,证明我们已经发送成功

我们还可以接收好友发送的消息,那接收的方法也是我们协议中的一个方法

//接收消息

- (void)xmppStream:(XMPPStream *)sender didReceiveMessage:(XMPPMessage *)message

{

NSLog(@"%s__%d__| message = %@",__FUNCTION__,__LINE__,message);

}

这个怎么去进行测试呢?我们可以借助于Spark来进行测试

我们登录以后发现,虽然我们的模拟器上已经登录成功了

但是Spark里我们的状态是离线状态

那怎么设置我们的状态呢?

回到我们的APPDelegate类里面,

有一个方法

- (void)xmppStreamDidAuthenticate:(XMPPStream *)sender

{

NSLog(@"%s__%d__|",__FUNCTION__,__LINE__);

}

里添加

XMPPPresence *presence = [XMPPPresence presenceWithType:@"available"];

[[XMPPManager defaultManager].xmppStream sendElement:presence];

第一个方法呢,代表的含义是在线,后面的available的含义是状态

然后我们通过通信管道给服务器发送过去

再看Spark,我们已经在线了,不仅如此,我们还要在我们的登录成功方法内让他上线

这个时候我们可以通过Spark来进行发送信息,

从控制台我们可以看到运行了- (void)xmppStream:(XMPPStream *)sender didReceiveMessage:(XMPPMessage *)message得方法

那我们如何展示到界面上呢?

我们上次在初始化Roster的时候给了一个持久化存储的对象我们没有用

我们这次同样用了一个messageArchivingCoreDataStorage的对象

我们这次要用这个,使用CoreData来获取到这些数据

我们在使用coreData的时候用到一个属性叫做context,用来管理上下文

我们先声明一个属性在XMPPManager里

//负责检索聊天信息(管理对象上下文)

@property (nonatomic, strong) NSManagedObjectContext *messageArchivingManagedObjectContext;

然后获取一下咱们的上下文

self.messageArchivingManagedObjectContext = messageArchivingCoreDataStorage.mainThreadManagedObjectContext;

然后就可以在聊天界面使用了,如何使用管理上下文获取到聊天的信息呢?

我们写一个方法叫做reloadMessage

然后打一个fetch 他会有一个代码块,然后回车

NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];

NSEntityDescription *entity = [NSEntityDescription entityForName:@"<#Entity name#>" inManagedObjectContext:<#context#>];

[fetchRequest setEntity:entity];

// Specify criteria for filtering which objects to fetch

NSPredicate *predicate = [NSPredicate predicateWithFormat:@"<#format string#>", <#arguments#>];

[fetchRequest setPredicate:predicate];

// Specify how the fetched objects should be sorted

NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"<#key#>"

ascending:YES];

[fetchRequest setSortDescriptors:[NSArray arrayWithObjects:sortDescriptor, nil]];

NSError *error = nil;

NSArray *fetchedObjects = [<#context#> executeFetchRequest:fetchRequest error:&error];

if (fetchedObjects == nil) {

<#Error handling code#>

}

我们这些空出来的就是我们需要补全的信息

那我们的聊天信息对应的是哪个实体类呢?

我们可以点入我们的messageArchivingCoreDataStorage里

然后按卡曼特+shift+j,我们就可以定位到他的准确位置了

然后我们可以看到这个文件夹叫做CoreDataStorage

我们可以在这个文件夹里有一个实体文件,里面有一个对应我们Message的实体描述

点击那三个选项(中间有个小问号的按键),选中第三个,然后看到了这个实体的描述

我们给他复制一下,放入到实体的字符串中

还有一个context,我们可以给刚才的context放入进去

接收一下,放进去

接下来是我们的检索条件,那么既然这个CoreData用来检索我们的聊天信息的

这里不给设置任何的条件时,就会把我们所有的聊天信息展示出来

所以这呢只想获取到某一个好友的聊天信息,那么怎么设置这个条件呢?

我们可以看一看这个XMPPMessageArchiving_Message_CoreDataObject类里地属性

这里有一个bareJid的属性和bareJidStr的属性

我们使用bareJidStr,这个是对方的mid

后面这个地方,我们要给我们获取到的这个jid,但是我们接收到的是一个对象对吧

那后面就使用self.chatToJID.bare;

那么这样就获取到了跟好友的聊天信息,但是仅仅这样是不够的,我们还要给设置自己的

XMPPJID,因为这个应用可能你在用,也可能好友借过来用,家人也可能用一下,如果你们有共同好友的话呢,当别人用的时候就可能会把你的这个聊天信息也查出来,那这就麻烦了,信息泄露

所以这里要设置自己的JID,那我们自己的JID是哪个呢?

进入XMPPMessageArchiving_Message_CoreDataObject

下面这个streamBareJidStr就是我们自己的JID,

[XMPPManager defaultManager].xmppStream.myJID.bare

这句话呢,就是获取当前用户的JID

然后我们下面有一个NSSortDescriptor这个排序

我们这里肯定是要按时间去排序的,

我们找一下我们这个类有没有时间的属性呢,可以看到有个timestamp,一个时间戳

然后我们这个排序是一个升序

下面是监测请求,我们这里要给他context来管理上下文监测请求

如果数组为空呢,那证明没有监测到任何的内容,我们可以输出一下

如果不为空得时候,我们要先把当前的数组的聊天信息移除掉

然后呢,从这个数组中添加聊天信息,最后再刷新我们的tableview

[self.allMessages removeAllObjects];

[self.allMessages addObjectsFromArray:fetchedObjects];

[self.tableView reloadData];

获取到这个聊天信息后呢,我们需要在tableview上展示

我们之前有说过,我们聊天的时候,把对方发送的信息展示在左边,我们自己的展示在右边

那怎么知道这个信息是对方发过来的还是你发过去的呢?

我们再进这个类里看一下

这里有两个布尔值类型的对象

一个是isOutgoing,这个属性可以进行判断是出去的还是进来的

我们可以先判断一下属性

if (message.isOutgoing == YES) {

cell.detailTextLabel.text = message.body;

}else

{

cell.textLabel.text = message.body;

}

这里这个body就是我们的信息

仅仅这样还不够,我们还需要解决重用的问题

如果是发送出去呢,我们需要把接收进来的信息呢,为空

相同的如果是接收的,就把发送的设置为空

这样就可以了

我们还要在发送信息的时候重新加载一次标签信息

收到聊天信息的时候也要加载一次

[self reloadMessage];

然后在ViewDidLoad里也要加载一次

不要忘记对数组进行初始化

运行一下,崩溃掉了看一下崩溃信息

reason:Unable to parse the format string “bareJidStr == %@ streamBareJidStr”

我们这里失误,应该在

NSPredicate *predicate = [NSPredicate predicateWithFormat:@"bareJidStr == %@ streamBareJidStr", self.chatToJID.bare,[XMPPManager defaultManager].xmppStream.myJID.bare];

这里修改一下@"bareJidStr == %@ AND streamBareJidStr = %@”

再运行一下,这样就显示出来了。

我们可以使用spark来进行测试一下

但是我们发现我们发多少条信息,这个Cell都不会滚动,那如何让Cell滚动呢?

在加载聊天信息的时候我们可以让他滚动到最底部

[self.tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:self.allMessages.count - 1 inSection:0] atScrollPosition:UITableViewScrollPositionBottom animated:YES];

如果我们没有跟一些好友聊天,我们点击进去就会崩溃在这里

因为数组的个数为0,-1就变成-1了

所以我们在这要进行一个判断,如果fetchedObjects.count ==0

那我们让他return 什么也不执行

那到这呢,我们IOS端的XMPP就结束了。

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

推荐阅读更多精彩内容