iOS實現XMPP通訊(二)XMPP編程
項目概述
- 這是一個可以登錄jabber賬號,獲取好友列表,並且能與好友進行聊天的項目。
使用的是第三方庫XMPPFramework框架來實現XMPP通訊。
項目地址:XMPP-Project - 項目準備工作:搭建好Openfire服務器,安裝客戶端Spark,具體步驟請見:iOS實現XMPP通訊(一)搭建Openfire
這樣就可以登錄本項目與登錄Spark的另一用戶進行XMPP通訊。 - 項目結構概述:
有三個視圖控制器LoginViewController,ListViewController,ChatViewController
LoginViewController:登錄和註冊xmpp賬號界面
ListViewController:獲取花名冊(好友列表)界面
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類提供一個連接xmpp服務器的方法,當點擊LoginViewController的”註冊”和”登錄”按鈕時調用該方法。(備註: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);
}
}
HOST是Openfire後台服務器的主機名,我們在Openfire後台服務器中配置了主機名為127.0.0.1,讓電腦充當Openfire服務器,因此HOST的值為我電腦網絡的IP地址192.168.2.2。
Openfire後台服務器配置的客戶端連接端口默認是5222,因此這裡KPort的值設為5222。後台配置如下:
輸入賬號、密碼並按下註冊或登錄按鈕後,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];
}
}
附上LoginViewController的「註冊」按鈕和」登錄「按鈕的點擊事件便於理解:
- (IBAction)registerAction:(id)sender {
//註冊
[[XmppManager defaultManager] connectHost:self.usernameTF.text andPassword:self.pswTF.text andisLogin:NO];
}
- (IBAction)loginAction:(UIButton *)sender {
//登錄
[[XmppManager defaultManager] connectHost:self.usernameTF.text andPassword:self.pswTF.text andisLogin:YES];
}
對於註冊成功或登錄驗證成功的回調結果,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();
}
}
以上loginblock是用來進行視圖控制器間的跳轉用的,登錄界面採用storyboard搭建UI。LoginViewController點擊”登錄”按鈕跳轉到ListViewController,採用」登陸「按鈕拉線至ListViewController的方式,因而可以給該條segue跳轉線打上標記,如”ListViewController”,然後在LoginViewController的viewDidLoad方法中實現loginblock代碼塊,在代碼塊中藉助segue的標記實現跳轉,即:
- (void)viewDidLoad {
[super viewDidLoad];
//設置回調block
[XmppManager defaultManager].loginblock = ^{
[self performSegueWithIdentifier:@"ListViewController" sender:nil];
};
}
登錄界面如下:
獲取好友列表
- 好友是事先用Spark客戶端添加的。要獲取到好友列表可以根據xmpp的花名冊格式來編寫xml包,然後將編寫好的xml包發送給服務器,即向服務器發起獲取好友花名冊的請求。以下是在ListViewController的viewDidLoad方法中的代碼:
- (void)viewDidLoad {
[super viewDidLoad];
[self getList];
}
//向服務器請求好友列表
-(void)getList {
//以下包含iq節點和query子節點
/**
<iq from="[email protected]/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:@"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包
[[XmppManager defaultManager].xmppStream sendElement:iq];
}
對於花名冊返回的結果,XmppManager類有相應的回調方法:
//獲取到服務器返回的花名冊(即好友列表)
- (BOOL)xmppStream:(XMPPStream *)sender didReceiveIQ:(XMPPIQ *)iq{
//NSLog(@"%@",iq);
if (self.listblock) {
self.listblock(iq);
}
return YES;
}
以上listblock是用來向ListViewController回調iq結果(iq裏面含有好友賬號信息),即ListViewController的viewDidLoad方法最終代碼如下:
- (void)viewDidLoad {
[super viewDidLoad];
//設置回調block
[XmppManager defaultManager].listblock = ^(XMPPIQ *xmppiq){
//服務器返回的內容,進行解析xml,取出我們需要的好友名字(賬號)
/*
<iq xmlns="jabber:client" type="result" id="1111" to="[email protected]/t7i1lbc63">
<query xmlns="jabber:iq:roster" ver="-1497960644">
<item jid="[email protected]" name="ming" subscription="to">
<group>Friends</group>
</item>
<item jid="[email protected]" name="wang" subscription="both">
<group>Friends</group>
</item>
</query>
</iq>
*/
//獲取好友列表
NSXMLElement *query = xmppiq.childElement; //由於iq節點裏面只有一個子節點query,所以可以直接用childElement獲取其子節點query
//query.children:獲得節點query的所有孩子節點
for (NSXMLElement *item in query.children) {
NSString *friendJidString = [item attributeStringValueForName:@"jid"];
//添加到數組中
[self.friendArr addObject:friendJidString];
}
[self.tableView reloadData];
};
[self getList];
}
獲取好友列表界面如下:
單聊界面
- 當我們獲取到好友列表後,針對某一好友進行聊天,我們得區分自己與好友,項目採用的是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發送出去的,運用的是textField的代理方法,遵循xml消息包格式,我們編寫自己說的內容的xml消息包進行發送,即如下:
//點擊return鍵發送信息
-(BOOL)textFieldShouldReturn:(UITextField *)textField{
/*
<message from="[email protected]/t7i1lbc63" id="2222" to="[email protected]" type="chat">
<body>準備吃飯了</body>
</message>
*/
NSXMLElement *message = [NSXMLElement elementWithName:@"message"];
XMPPJID *jid = [XmppManager defaultManager].xmppStream.myJID;
//拼接屬性節點
[message addAttributeWithName:@"from" stringValue:jid.description];
[message addAttributeWithName:@"id" stringValue:@"2222"];
[message addAttributeWithName:@"to" stringValue:self.chatName];
[message addAttributeWithName:@"type" stringValue:@"chat"]; //什麼類型xml包,chat表示單聊。 lang表示語言,拼不拼接都無所謂
NSXMLElement *body = [NSXMLElement elementWithName:@"body"];
//設置發送的信息
[body setStringValue:textField.text];
//添加子節點
[message addChild:body];
//發送xml包請求
[[XmppManager defaultManager].xmppStream sendElement:message];
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;
}
當好友發消息給我時,xmpp在XmppManager類會觸發相應的回調,如下:
//收到服務器返回的消息回調
-(void)xmppStream:(XMPPStream *)sender didReceiveMessage:(XMPPMessage *)message{
//NSLog(@"message=%@",message);
if (self.chatblock) {
self.chatblock(message);
}
}
以上chatblock是用來向ChatViewController回調message結果(裏面含有聊天消息內容),ChatViewController的viewDidLoad方法如下:
- (void)viewDidLoad {
[super viewDidLoad];
if ([self unarchiver]) {
[self.messageArr addObjectsFromArray:[self unarchiver]];
[self.tableView reloadData];
}
/*
<message xmlns="jabber:client" to="[email protected]/t7i1lbc63" id="bFTVn-127" type="chat" from="[email protected]/HellodeMacBook-Pro.local">
<thread>ykBwqQ</thread>
<body>好的</body>
<x xmlns="jabber:x:event">
<offline/>
<composing/>
</x>
<active xmlns="//jabber.org/protocol/chatstates"></active>
</message>
*/
//設置回調
[XmppManager defaultManager].chatblock = ^(XMPPMessage *message){
NSXMLElement *body = [message elementForName:@"body"];
//NSLog(@"body = %@",body); //打印:body = <body>NIHAO</body>
if ([body stringValue]==nil || [[body stringValue] isEqualToString:@""]) {
return;
}
Message *otherMes = [[Message alloc] init];
otherMes.contentString = [body stringValue];
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;
}
單聊界面如下: