项目概述
-
这是一个可以登录jabber账号,获取好友列表,并且能与好友进行聊天的项目。
使用的是第三方库XMPPFramework框架来实现XMPP通讯。
项目地址:XMPP-Project
如果文章和项目对你有帮助,还请给个Star⭐️,你的Star⭐️是我持续输出的动力,谢谢啦😘 -
项目准备工作:搭建好Openfire服务器,安装客户端Spark,登录本项目的用户与登录Spark的另一用户进行XMPP通讯。
-
项目结构概述:
有三个视图控制器LoginViewController,FriendListViewController,ChatViewController。
LoginViewController:登录和注册xmpp账号界面
FriendListViewController:获取花名册(好友列表)界面
ChatViewController:和好友进行单聊界面
为此封装了XmppManager类,方便统一管理与服务器的连接、获取好友列表、添加好友、发送聊天消息、获取聊天消息等功能。 -
注意:由于XMPPFramework框架还依赖其他第三方库,如KissXML、CocoaAsyncSocket等,因此用cocoaPods添加XMPPFramework库时,
podfile必须添加use_frameworks!
,如下:
platform:ios , '8.0'
target 'XMPP' do
use_frameworks!
pod 'XMPPFramework', '~> 4.0.0'
end
注册登录
- xmpp的注册流程是:先连接xmpp服务器,连接成功后再向xmpp服务器注册账号、密码。
xmpp的登录流程是:先连接xmpp服务器,连接成功后再进行登录的鉴权,即校验密码的准确性。
XmppManager类提供了给LoginViewController注册和登录的接口,如下:
//注册
-(void)registerWithName:(NSString *)name andPassword:(NSString *)password result:(RegisterBlock)block{
self.registerBlock = [block copy];
[self connectHost:name andPassword:password andisLogin:NO];
}
//登录
-(void)loginWithName:(NSString *)name andPassword:(NSString *)password result:(LoginBlock)block{
self.loginBlock = [block copy];
[self connectHost:name andPassword:password andisLogin:YES];
}
这两个接口共同调用connectHost:andPassword:result:
方法,用于连接xmpp服务器(备注:islogin用来区分是登录还是注册),该方法如下:
//服务器地址(改成自己电脑的IP地址)
#define HOST @"192.168.2.2"
//端口号
#define KPort 5222
-(void)connectHost:(NSString *)usernameStr andPassword:(NSString *)passwordStr andisLogin:(BOOL)islogin{
self.usernameStr = usernameStr;
self.pswStr = passwordStr;
self.isLogin = islogin;
//判断当前没有连接服务器,如果连接了就断开连接
if ([self.xmppStream isConnected]) {
[self.xmppStream disconnect];
}
//设置服务器地址
[self.xmppStream setHostName:HOST];
//设置端口号
[self.xmppStream setHostPort:KPort];
//设置JID账号
XMPPJID *jid = [XMPPJID jidWithUser:self.usernameStr domain:HOST resource:nil];
[self.xmppStream setMyJID:jid];
//连接服务器
NSError *error = nil;
//该方法返回了bool值,可以作为判断是否连接成功,如果10s内顺利连接上服务器返回yes
if ([self.xmppStream connectWithTimeout:10.0f error:&error]) {
NSLog(@"连接成功");
}
//如果连接服务器超过10s钟
if (error) {
NSLog(@"error = %@",error);
}
}
由于我设置了电脑充当Openfire服务器,因而电脑当前WiFi的IP地址(比如192.168.3.133)就是Openfire服务器的地址,因而HOST参数要配置电脑当前WiFi的IP地址才能让手机连上Openfire服务器。
注意:由于首次配置Openfire后台服务器时,服务器名称设置了192.168.2.2(因为首次配置时电脑WiFi的IP地址为192.168.2.2),主机名配置127.0.0.1,因此192.168.2.2就作为Openfire服务器的主机名。不管HOST参数设置成什么,收发的XML包的域名(domain)都是192.168.2.2。
Openfire后台服务器配置的客户端连接端口默认是5222,因此这里KPort的值设为5222。后台配置如下:
![](https://i-blog.csdnimg.cn/blog_migrate/e9cc9c9a468139e57cb75b114cb182bc.png)
输入账号、密码并按下注册或登录按钮后,app会向XMPP服务器进行连接请求,服务器连接成功会有相应的回调,在连接成功的回调中进行密码校验或账号注册操作。即如下所示:
//除了上面可以判断是否连接上服务器外还能通过如下这种形式判断
-(void)xmppStreamDidConnect:(XMPPStream *)sender{
NSLog(@"连接服务器成功");
//这里要清楚,连接服务器成功并不是注册成功或登录成功【可以把“连接服务器成功”当做接收到当前服务器开启了的通知】
if (self.isLogin) {
//进行验证身份(或者叫进行登录)
[self.xmppStream authenticateWithPassword:self.pswStr error:nil];
}else{
//进行注册
[self.xmppStream registerWithPassword:self.pswStr error:nil];
}
}
对于注册成功或登录验证成功的回调结果,XmppManager类中有相应的回调方法:
//注册成功的回调
-(void)xmppStreamDidRegister:(XMPPStream *)sender{
NSLog(@"注册成功");
}
//登录成功(密码输入正确)的回调
-(void)xmppStreamDidAuthenticate:(XMPPStream *)sender{
NSLog(@"验证身份成功");
//发送一个登录状态
XMPPPresence *presence = [XMPPPresence presenceWithType:@"available"];
//发送一个xml包给服务器
//参数:DDXMLElement,XMPPPresence继承自它
[self.xmppStream sendElement:presence];
//跳转控制器
if (self.loginblock) {
self.loginblock();
}
}
登录界面如下:
![](https://i-blog.csdnimg.cn/blog_migrate/fb0035d4926374aa2af08311f6238f28.jpeg)
获取好友列表
- 要获取到好友列表需要根据xmpp的花名册格式来编写xml包,然后将xml包发送给服务器,即向服务器发起获取好友花名册的请求。以下是在FriendListViewController的viewDidLoad方法中的代码:
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
//设置回调block
[XmppManager defaultManager].friendListBlock = ^(NSArray *friends) {
NSLog(@"friendcount:%d", (int)friends.count);
[self.friendArr removeAllObjects];
[self.friendArr addObjectsFromArray:friends];
[self.tableView reloadData];
};
//向服务器请求好友列表
[[XmppManager defaultManager] requestFriends];//向服务器请求好友列表
}
XmppManager提供了请求获取好友接口requestFriends,当服务器返回好友列表时,XmppManager类会回调-(BOOL)xmppStream:(XMPPStream *)sender didReceiveIQ:(XMPPIQ *)iq
方法,代码如下:
//请求获取好友
-(void)requestFriends{
//以下包含iq节点和query子节点
/**
<iq from="hong@192.168.2.2/750tnmoq3l" id="1111" type="get">
<query xmlns="jabber:iq:roster"></query>
</iq>
*/
NSXMLElement *iq = [NSXMLElement elementWithName:@"iq"];
//拼接属性节点from,id,type
//属性节点"from"的值为jid账号
[iq addAttributeWithName:@"from" stringValue:[XmppManager defaultManager].xmppStream.myJID.description];
//id是消息的标识号,到时需要查找消息时可以根据id去找,id可以随便取值
[iq addAttributeWithName:@"id" stringValue:JFriendListID];
//类似http的Get请求,发出获取好友的请求。服务器的响应数据中type为result,id对应1111
[iq addAttributeWithName:@"type" stringValue:@"get"];
//query是单节点,xmlns为它的属性节点
NSXMLElement *query = [NSXMLElement elementWithName:@"query"];
//拼接属性节点xmlns,固定写法
[query addAttributeWithName:@"xmlns" stringValue:@"jabber:iq:roster"];
//iq添加query为它的子节点
[iq addChild:query];
//发送请求获取好友的xml包
[self.xmppStream sendElement:iq];
}
//服务器返回的IQ信息。比如花名册数据(即好友列表)
//该方法可能多次返回相似的数据,可通过id值过滤,判断服务器是响应什么请求
- (BOOL)xmppStream:(XMPPStream *)sender didReceiveIQ:(XMPPIQ *)iq{
NSLog(@"didReceiveIQ:%@",iq);
/**
第一次回调
<iq xmlns="jabber:client" type="result" id="1111" to="hong@192.168.2.2/2uc83c92op">
<query xmlns="jabber:iq:roster" ver="204617739">
<item jid="ming@192.168.2.2" subscription="both"/>
<item jid="wang@192.168.2.2" name="wang" ask="subscribe" subscription="from">
<group>我的联系人</group>
</item>
</query>
</iq>
第二次回调
<iq xmlns="jabber:client" type="get" id="515-72" to="hong@192.168.2.2/2uc83c92op" from="192.168.2.2">
<query xmlns="jabber:iq:version"></query>
</iq>
*/
//获取好友列表
//由于iq节点里面只有一个子节点query,所以可以直接用childElement获取其子节点query
NSXMLElement *query = iq.childElement;
if ([iq.elementID isEqualToString:JFriendListID]) {
NSLog(@"好友花名册");
NSArray *friends = [self.friendList copy];
//query.children:获得节点query的所有孩子节点
for (NSXMLElement *item in query.children) {
NSString *friendJidString = [item attributeStringValueForName:@"jid"];
BOOL shouldAdd = YES;
for (UserModel *model in friends) {
if ([friendJidString isEqualToString:model.jidUserName]) {
shouldAdd = NO;
break;
}
}
if (shouldAdd) {
UserModel *newmodel = [[UserModel alloc] init];
newmodel.jidUserName = friendJidString;
newmodel.status = 0;
//添加到数组中
[self.friendList addObject:newmodel];
}
}
if (self.friendListBlock) {
self.friendListBlock(self.friendList);
}
}
return YES;
}
获取好友列表界面如下:
![](https://i-blog.csdnimg.cn/blog_migrate/b27f2e05c118e2c3b37303e872b8de85.png)
单聊界面
- 当我们获取到好友列表后,针对某一好友进行聊天,我们得区分自己与好友,项目采用的是Message类,里面有如下属性:
@interface Message : NSObject
//内容
@property(nonatomic,copy)NSString *contentString;
//谁的信息
@property(nonatomic,assign)BOOL isOwn;
@end
isOwn用来区分自己与好友对方,contentString即表示自己或好友发送消息的内容。本次ChatViewController在tableView中只用了一种cell,实际开发还是建议区分开来。在ChatViewController的主要代码如下:
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
//获取信息模型
Message *model = self.messageArr[indexPath.row];
ChatCell *cell = [tableView dequeueReusableCellWithIdentifier:@"ChatCell"];
[cell setCellWithModel:model];
return cell;
}
cell内部根据isOwn区分自己和好友,进而调整子控件的frame,代码如下:
-(void)setCellWithModel:(Message *)model{
_contentLabel.text = model.contentString;
CGRect contentRect = [model.contentString boundingRectWithSize:CGSizeMake([UIScreen mainScreen].bounds.size.width-100-90, MAXFLOAT) options:NSStringDrawingUsesLineFragmentOrigin attributes:@{NSFontAttributeName:[UIFont systemFontOfSize:14]} context:nil];
CGFloat screenWidth = [UIScreen mainScreen].bounds.size.width;
CGFloat contentWidth = contentRect.size.width;
CGFloat contentHeight = contentRect.size.height;
CGFloat popWidth = contentWidth + 40;
CGFloat popHeight = contentHeight + 25;
if (model.isOwn) { //自己
_headerImageView.image = [UIImage imageNamed:@"icon01"];
//头像
_headerImageView.frame = CGRectMake(screenWidth-70, 10, 60, 60);
//气泡的图片
CGFloat popX = screenWidth - popWidth - 70;
_popoImageView.frame = CGRectMake(popX, 10, popWidth, popHeight);
UIImage * image = [UIImage imageNamed:@"chatto_bg_normal.png"];
image = [image stretchableImageWithLeftCapWidth:45 topCapHeight:12];
_popoImageView.image = image;
//聊天内容的label
_contentLabel.frame = CGRectMake(15, 10, contentWidth, contentHeight);
}else{ //好友
_headerImageView.image = [UIImage imageNamed:@"icon02"];
_headerImageView.frame = CGRectMake(10, 10, 60, 60);
_popoImageView.frame = CGRectMake(70, 10, popWidth, popHeight);
UIImage * image = [UIImage imageNamed:@"chatfrom_bg_normal.png"];
image = [image stretchableImageWithLeftCapWidth:45 topCapHeight:55];
_popoImageView.image = image;
_contentLabel.frame = CGRectMake(25, 10, contentWidth, contentHeight);
}
}
把自己说的文本用textField代理方法发送出去,即如下:
//点击return键发送信息
-(BOOL)textFieldShouldReturn:(UITextField *)textField{
if (textField.text.length == 0) {
return YES;
}
[[XmppManager defaultManager] sendMessageText:textField.text jidUserName:self.chatName];
Message *myMes = [[Message alloc] init];
myMes.contentString = textField.text;
myMes.isOwn = YES;
[self.messageArr addObject:myMes];
[self archiverWithArray:self.messageArr];
[self.tableView reloadData];
self.messageTF.text = @"";
[_tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:self.messageArr.count-1 inSection:0] atScrollPosition:UITableViewScrollPositionBottom animated:YES];
return YES;
}
XmppManager提供了发送消息的接口sendMessageText:jidUserName:,可将文本包装成XML消息包发送给服务器,如下:
//发送消息
-(void)sendMessageText:(NSString *)text jidUserName:(NSString *)jidUserName{
/*
<message from="hong@192.168.2.2/t7i1lbc63" id="2222" to="wang@192.168.2.2" type="chat">
<body>准备吃饭了</body>
</message>
*/
if (text.length == 0) {
return;
}
NSXMLElement *message = [NSXMLElement elementWithName:@"message"];
XMPPJID *jid = self.xmppStream.myJID;
//拼接属性节点
[message addAttributeWithName:@"from" stringValue:jid.description];
[message addAttributeWithName:@"id" stringValue:@"2222"];
[message addAttributeWithName:@"to" stringValue:jidUserName];
//什么类型xml包,chat表示单聊。lang表示语言,拼不拼接都无所谓
[message addAttributeWithName:@"type" stringValue:@"chat"];
NSXMLElement *body = [NSXMLElement elementWithName:@"body"];
//设置发送的信息
[body setStringValue:text];
//添加子节点
[message addChild:body];
//发送xml包请求
[self.xmppStream sendElement:message];
}
当好友发消息给我时,xmpp在XmppManager类会触发相应的回调,如下:
//收到服务器返回的聊天消息
-(void)xmppStream:(XMPPStream *)sender didReceiveMessage:(XMPPMessage *)message{
NSLog(@"message=%@",message);
/*
<message xmlns="jabber:client" to="hong@192.168.2.2/t7i1lbc63" id="bFTVn-127" type="chat" from="wang@192.168.2.2/HellodeMacBook-Pro.local">
<thread>ykBwqQ</thread>
<body>好的</body>
<x xmlns="jabber:x:event">
<offline/>
<composing/>
</x>
<active xmlns="http://jabber.org/protocol/chatstates"></active>
</message>
*/
if ([message.type isEqualToString:@"chat"]) { //表示聊天
NSXMLElement *body = [message elementForName:@"body"];
//NSLog(@"body = %@",body); //打印:body = <body>好的</body>
NSString *messageText = [body stringValue];
if (self.getMessageBlock) {
self.getMessageBlock(messageText);
}
}
}
以上getMessageBlock是ChatViewController用来获取好友聊天消息的Block,ChatViewController的viewDidLoad方法相关代码如下:
- (void)viewDidLoad {
[super viewDidLoad];
if ([self unarchiver]) {
[self.messageArr addObjectsFromArray:[self unarchiver]];
[self.tableView reloadData];
}
//设置回调
[XmppManager defaultManager].getMessageBlock = ^(NSString *messageText){
if (messageText==nil || [messageText isEqualToString:@""]) {
return;
}
Message *otherMes = [[Message alloc] init];
otherMes.contentString = messageText;
otherMes.isOwn = NO;
//添加到数组当中
[self.messageArr addObject:otherMes];
[self archiverWithArray:self.messageArr];
[self.tableView reloadData];
[self.tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:self.messageArr.count-1 inSection:0] atScrollPosition:UITableViewScrollPositionBottom animated:YES];
};
}
- 这里打算用归档(NSKeyedArchiver)的方式存储用户的聊天记录。
由于每条聊天记录都是一个Message模型,Message模型必须实现归档(encodeWithCoder:)和解档(initWithCoder:),这样才能使用NSKeyedArchiver把模型数组存储到沙盒中。
ChatViewController类中归档和解档代码如下:
-(void)archiverWithArray:(NSMutableArray *)array{
NSString *documentPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
NSString *filePath = [documentPath stringByAppendingFormat:@"/%@/%@", MessageHistory, self.chatName];
NSFileManager *fm = [NSFileManager defaultManager];
if (![fm fileExistsAtPath:filePath]) {
[fm createFileAtPath:filePath contents:nil attributes:nil];
}
[NSKeyedArchiver archiveRootObject:array toFile:filePath];
}
-(NSMutableArray *)unarchiver{
NSString *documentPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
NSString *filePath = [documentPath stringByAppendingFormat:@"/%@/%@", MessageHistory, self.chatName];
NSFileManager *fm = [NSFileManager defaultManager];
if ([fm fileExistsAtPath:filePath]) {
NSMutableArray *array = [NSKeyedUnarchiver unarchiveObjectWithFile:filePath];
return array;
}
return nil;
}
单聊界面如下:
![](https://i-blog.csdnimg.cn/blog_migrate/fbf5e0a8e8fac252e3a3e9f286e76a77.jpeg)