iOS實現XMPP通訊(二)XMPP程式設計

奔跑的鴻發表於2021-10-13

專案概述

  • 這是一個可以登入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。後臺配置如下:

iOS實現XMPP通訊(二)XMPP程式設計

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

登入介面如下:

iOS實現XMPP通訊(二)XMPP程式設計

獲取好友列表

  • 好友是事先用Spark客戶端新增的。要獲取到好友列表可以根據xmpp的花名冊格式來編寫xml包,然後將編寫好的xml包傳送給伺服器,即向伺服器發起獲取好友花名冊的請求。以下是在ListViewController的viewDidLoad方法中的程式碼:
- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self getList];
}
//向伺服器請求好友列表
-(void)getList {
    //以下包含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:@"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="hong@192.168.2.2/t7i1lbc63">
          <query xmlns="jabber:iq:roster" ver="-1497960644">
            <item jid="ming@192.168.2.2" name="ming" subscription="to">
              <group>Friends</group>
            </item>
            <item jid="wang@192.168.2.2" 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];
}

獲取好友列表介面如下:

iOS實現XMPP通訊(二)XMPP程式設計

單聊介面

  • 當我們獲取到好友列表後,針對某一好友進行聊天,我們得區分自己與好友,專案採用的是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="hong@192.168.2.2/t7i1lbc63" id="2222" to="wang@192.168.2.2" 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="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>
     */
    //設定回撥
    [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;
}

單聊介面如下:

iOS實現XMPP通訊(二)XMPP程式設計

相關文章