iOS實現XMPP通訊(二)XMPP編程


項目概述

  • 這是一個可以登錄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。后台配置如下:

輸入賬號、密碼並按下注冊或登錄按鈕后,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();        
    }
}

登錄界面如下:

獲取好友列表

  • 要獲取到好友列表需要根據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;
}

獲取好友列表界面如下:

單聊界面

  • 當我們獲取到好友列表后,針對某一好友進行聊天,我們得區分自己與好友,項目采用的是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;
}

單聊界面如下:


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM