說明:在即時通訊系列XMPP之搭建本地服務器中闡述了經過XAMPP、Openfire工具搭建基於XMPP的服務器端,並使用spark軟件進行XMPP客戶端的調試。在本文中主要闡述將XMPP添加到工程中,實現iOS中基於XMPP的即時通訊。 ####項目環境配置 使用CocoaPods導入XMPP框架,若是不會CocoaPods,點擊CocoaPods安裝及使用教程,或者手動導入XMPP框架。數據庫
####建立XMPP管理類 爲了讓XMPP不會對工程中其它業務代碼具備侵入性,方便咱們管理整個關於XMPP的邏輯處理,一般會採用單例模式新建一個專門的管理類,爲此,在工程中建立一個繼承自NSObject的XMPPManger,爲了可以在代碼中方便調用XMPPStreamDelegate的方法,讓其遵照XMPPStreamDelegate協議。 XMPP框架中全部的鏈接、登入、註冊、受權、失去鏈接等操做的回調都是經過XMPPStream來管理,因此XMPPManger必須有一個XMPPStream實例。其它的還包括進行添加好友、刪除好友、獲取好友列表等功能的XMPPRoster;消息保存組件XMPPMessageArchiving等。數組
######XMPPManager.hbash
@interface XMPPManager : NSObject<XMPPStreamDelegate>
/**
* 建立全局惟一的管理者對象
* @return 返回一個單例對象
*/
+ (XMPPManager *)sharedXMPPManager;
/**
* 信息管道
*/
@property (nonatomic,strong)XMPPStream *stream;
/**
* 進行添加好友 刪除好友 獲取好友列表等功能
*/
@property (nonatomic,strong)XMPPRoster *roster;
/**
* 消息保存組件
*/
@property (nonatomic,strong)XMPPMessageArchiving *messageArchiving;
/**
* 消息保存組件的CoreData上下文
*/
@property (nonatomic,strong)NSManagedObjectContext *managerObjectContext;
/**
* 登入
*
* @param userName 帳戶
* @param password 密碼
*/
- (void)loginWithUserName:(NSString *)userName password:(NSString *)password;
/**
* 註冊
*
* @param userName 帳戶
* @param password 密碼
*/
- (void)registWithUserName:(NSString *)userName password:(NSString *)password;
@end
複製代碼
######XMPPManager.m服務器
//服務器的地址
static NSString *const kHostName = @"127.0.0.1";
//端口號
static UInt16 const kHostPort = 5222;
//鏈接服務器類型
typedef NS_ENUM(NSUInteger,ConnectToServerStatus) {
ConnectToServerStatusLogin,
ConnectToServerStatusRegist,
};
@interface XMPPManager ()
//登入密碼
@property (nonatomic,strong)NSString *loginpassword;
//註冊密碼
@property (nonatomic,strong)NSString *registpassword;
//是登入仍是註冊
@property (nonatomic,assign)ConnectToServerStatus connectToSercerStatus;
@end
@implementation XMPPManager
+ (XMPPManager *)sharedXMPPManager{
static XMPPManager *xmppManager = nil;
static dispatch_once_t token;
dispatch_once(&token, ^{
xmppManager = [[XMPPManager alloc] init];
});
return xmppManager;
}
- (instancetype)init{
if ([super init]) {
//設置通訊管道屬性
self.stream = [[XMPPStream alloc] init];
self.stream.hostName = kHostName;
self.stream.hostPort = kHostPort;
//設置當前對象爲stream的代理
[self.stream addDelegate:self delegateQueue:dispatch_get_main_queue()];
//進行好友存儲
XMPPRosterCoreDataStorage *rosterStorage = [XMPPRosterCoreDataStorage sharedInstance];
self.roster = [[XMPPRoster alloc] initWithRosterStorage:rosterStorage dispatchQueue:dispatch_get_main_queue()];
//激活
[self.roster activate:self.stream];
//進行聊天信息存儲
XMPPMessageArchivingCoreDataStorage *messageArchivingStorage = [XMPPMessageArchivingCoreDataStorage sharedInstance];
self.messageArchiving = [[XMPPMessageArchiving alloc] initWithMessageArchivingStorage:messageArchivingStorage dispatchQueue:dispatch_get_main_queue()];
[self.messageArchiving activate:self.stream];
self.managerObjectContext = messageArchivingStorage.mainThreadManagedObjectContext;
}
return self;
}
//與服務器創建連接
-(void)connectToServerWithUser:(NSString *)user{
//要是正在連接的話那麼就先斷開鏈接
if ([self.stream isConnected]) {
[self disconnectServer];
}
XMPPJID *jid = [XMPPJID jidWithUser:user domain:@"DH_Fantasy" resource:@"iPhone"];
self.stream.myJID = jid;
NSError *error = nil;
[self.stream connectWithTimeout:30.0f error:&error];
if (nil != error) {
NSLog(@"%s__%d__連接出錯:%@",__FUNCTION__,__LINE__,error);
}
}
//與服務器斷開連接
-(void)disconnectServer{
[self.stream disconnect];
}
//登入
- (void)loginWithUserName:(NSString *)userName password:(NSString *)password{
self.connectToSercerStatus = ConnectToServerStatusLogin;
self.loginpassword = password;//將傳進來的password傳給self.password
[self connectToServerWithUser:userName];
}
//註冊
- (void)registWithUserName:(NSString *)userName password:(NSString *)password{
self.connectToSercerStatus = ConnectToServerStatusRegist;
self.registpassword = password;
[self connectToServerWithUser:userName];
}
#pragma mark XMPPStreamDelegate
//與服務器連接成功
-(void)xmppStreamDidConnect:(XMPPStream *)sender{
#pragma mark 判斷與服務器創建鏈接是登錄仍是註冊
switch (self.connectToSercerStatus) {
case ConnectToServerStatusLogin:
{
NSError *error = nil;
[self.stream authenticateWithPassword:self.loginpassword error:&error];
if (nil != error) {
NSLog(@"%s__%d__驗證出錯:%@",__FUNCTION__,__LINE__,error);
}
break;
}
case ConnectToServerStatusRegist:
{
NSError *err = nil;
[self.stream registerWithPassword:self.registpassword error:&err];
if (nil != err) {
NSLog(@"%s__%d__註冊出錯:%@",__FUNCTION__,__LINE__,err);
}
break;
}
default:
break;
}
}
//與服務器連接失敗
-(void)xmppStreamConnectDidTimeout:(XMPPStream *)sender{
NSLog(@"😂與服務器連接失敗");
}
- (void)xmppStreamWillConnect:(XMPPStream *)sender {
NSLog(@"🔌socket正在鏈接...");
}
- (void)xmppStream:(XMPPStream *)sender socketDidConnect:(GCDAsyncSocket *)socket {
NSLog(@"🍎socket鏈接成功");
// 鏈接成功以後,由客戶端xmpp發送一個stream包給服務器,服務器監聽來自客戶端的stream包,並返回stream feature包
}
- (void)xmppStream:(XMPPStream *)sender didNotAuthenticate:(NSXMLElement *)error {
NSLog(@"😂xmpp受權失敗:%@", error.description);
}
- (void)xmppStreamDidAuthenticate:(XMPPStream *)sender {
NSLog(@"🍎xmpp受權成功。");
// 只有進入到這裏,纔算是真正的能夠聊天了
}
- (void)xmppStreamDidDisconnect:(XMPPStream *)sender withError:(NSError *)error {
NSLog(@"😂xmpp失去鏈接。");
}
@end
複製代碼
####AppDelegate 在AppDelegate中主要設置了下進入APP時的登入狀態,在一些場景中,某些控制器必須爲登入狀態才能進入,用NSUserDefaults存儲爲Bool類型,這爲判斷是否登入提供了方便。app
######AppDelegate.m框架
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
//設置主窗口並顯示
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
self.window.backgroundColor = [UIColor whiteColor];
[self.window makeKeyAndVisible];
//設置進入APP爲沒登入狀態
[[NSUserDefaults standardUserDefaults] setBool:NO forKey:@"isLoginStatus"];
[[NSUserDefaults standardUserDefaults] synchronize];
//設置好友列表控制器爲根視圖
RosterTableViewController *VC = [[RosterTableViewController alloc] init];
UINavigationController *NC = [[UINavigationController alloc] initWithRootViewController:VC];
self.window.rootViewController = NC;
return YES;
}
複製代碼
####RosterTableViewController RosterTableViewController主要做用就是進行自動登入與顯示獲取到的好友。dom
######RosterTableViewController.msocket
@interface RosterTableViewController ()
//用於保存獲取到的好友
@property (nonatomic,strong)NSMutableArray *rosterArray;
@end
@implementation RosterTableViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.title = @"好友列表";
self.rosterArray = [[NSMutableArray alloc] init];
//判斷以前是否登入過
if (nil != [[NSUserDefaults standardUserDefaults]objectForKey:@"userName"]) {
//以前登入過,直接讀取用戶名和密碼進行鏈接
NSString *userName = [[NSUserDefaults standardUserDefaults]objectForKey:@"userName"];
NSString *password = [[NSUserDefaults standardUserDefaults]objectForKey:@"password"];
[[XMPPManager sharedXMPPManager]loginWithUserName:userName password:password];
[[XMPPManager sharedXMPPManager].stream addDelegate:self delegateQueue:dispatch_get_main_queue()];
}else{
//以前沒登入過則進入到登入窗口
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
LoginViewController *loginVC = [storyboard instantiateViewControllerWithIdentifier:@"login"];
[self.navigationController pushViewController:loginVC animated:YES];
}
[[XMPPManager sharedXMPPManager].roster addDelegate:self delegateQueue:dispatch_get_main_queue()];
}
#pragma mark UITableViewDataSource
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.rosterArray.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:(UITableViewCellStyleDefault) reuseIdentifier:@"cell"];
}
XMPPJID *jid = self.rosterArray[indexPath.row];
cell.textLabel.text = jid.user;
return cell;
}
#pragma mark 進入聊天界面
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
XMPPJID *jid = self.rosterArray[indexPath.row];
ChatViewController *chat = [[ChatViewController alloc] init];
chat.chatToJid = jid;
[self.navigationController pushViewController:chat animated:YES];
}
#pragma mark XMPPStreamDelegate
//受權成功
- (void)xmppStreamDidAuthenticate:(XMPPStream *)sender{
[[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"isLoginStatus"];
[[NSUserDefaults standardUserDefaults] synchronize];
//咱們驗證以後默認的狀態是離線的 因而咱們想顯示在線,須要告訴服務器本身的狀態
XMPPPresence *presence = [XMPPPresence presenceWithType:@"available"];
[[XMPPManager sharedXMPPManager].stream sendElement:presence];
}
//驗證失敗
-(void)xmppStream:(XMPPStream *)sender didNotAuthenticate:(DDXMLElement *)error{
NSLog(@"登入驗證失敗😂");
}
#pragma mark XMPPRosterDelegate
//剛開始獲取好友列表
-(void)xmppRosterDidBeginPopulating:(XMPPRoster *)sender{
NSLog(@"開始獲取好友列表");
}
//正在獲取好友列表
-(void)xmppRoster:(XMPPRoster *)sender didReceiveRosterItem:(DDXMLElement *)item{
NSLog(@"正在獲取好友列表...%@",item);
NSString *jidStr = [[item attributeForName:@"jid"]stringValue];
XMPPJID *jid =[XMPPJID jidWithString:jidStr];
[self.rosterArray addObject:jid];
//將數據添加進數組
[self.tableView insertRowsAtIndexPaths:@[[NSIndexPath indexPathForRow:self.rosterArray.count-1 inSection:0]] withRowAnimation:UITableViewRowAnimationLeft];
}
//已經完成好友列表獲取
-(void)xmppRosterDidEndPopulating:(XMPPRoster *)sender{
NSLog(@"已經完成好友列表獲取🍅");
}
複製代碼
####LoginViewController LoginViewController中使用XMPPManager單例在登入按鈕的點擊事件中調用- (void)loginWithUserName:(NSString ***)userName password:(NSString *)password;
方法進行登入。工具
######LoginViewController.mfetch
@interface LoginViewController ()
//登入帳戶
@property (weak, nonatomic) IBOutlet UITextField *userName;
//登入密碼
@property (weak, nonatomic) IBOutlet UITextField *password;
@end
@implementation LoginViewController
- (void)viewDidLoad {
[super viewDidLoad];
[[XMPPManager sharedXMPPManager].stream addDelegate:self delegateQueue:dispatch_get_main_queue()];
}
//登入操做
- (IBAction)loginAction:(id)sender {
[[XMPPManager sharedXMPPManager] loginWithUserName:self.userName.text password:self.password.text];
}
#pragma mark XMPPStreamDelegte
//驗證成功
- (void)xmppStreamDidAuthenticate:(XMPPStream *)sender{
//存儲帳戶密碼用於下次自動登入
[[NSUserDefaults standardUserDefaults] setObject:self.userName.text forKey:@"userName"];
[[NSUserDefaults standardUserDefaults] setObject:self.password.text forKey:@"password"];
//改變APP的登入狀態
[[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"isLoginStatus"];
[[NSUserDefaults standardUserDefaults] synchronize];
//驗證以後默認的狀態爲離線,須要手動告訴服務器本身的狀態
XMPPPresence *presence = [XMPPPresence presenceWithType:@"available"];
[[XMPPManager sharedXMPPManager].stream sendElement:presence];
//返回到根視圖
[self.navigationController popToRootViewControllerAnimated:YES];
}
//驗證失敗
-(void)xmppStream:(XMPPStream *)sender didNotAuthenticate:(DDXMLElement *)error{
NSLog(@"登入驗證失敗😂");
}
複製代碼
####RegisterViewController 註冊和登入比較類似,只需在註冊按鈕的點擊事件中使用XMPPManager單例調用- (void)registWithUserName:(NSString *)userName password:(NSString *)password;
方法進行註冊。
######RegisterViewController.m
@interface RegisterViewController ()
//註冊帳戶
@property (weak, nonatomic) IBOutlet UITextField *userName;
//註冊密碼
@property (weak, nonatomic) IBOutlet UITextField *password;
//密碼驗證
@property (weak, nonatomic) IBOutlet UITextField *rePassword;
@end
@implementation RegisterViewController
- (void)viewDidLoad {
[super viewDidLoad];
[[XMPPManager sharedXMPPManager].stream addDelegate:self delegateQueue:dispatch_get_main_queue()];
}
- (IBAction)registerAction:(id)sender {
//簡單驗證:用戶名不爲空且密碼和密碼驗證輸入一致
if (![self.userName.text isEqualToString:@""] && [self.rePassword.text isEqualToString:self.password.text]) {
[[XMPPManager sharedXMPPManager] registWithUserName:self.userName.text password:self.password.text];
}else{
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"註冊失敗" message:@"" preferredStyle:(UIAlertControllerStyleAlert)];
if ([self.userName.text isEqualToString:@""]) {
alertController.title = @"帳戶爲空";
alertController.message = @"請輸入帳戶";
}
if (![self.rePassword.text isEqualToString:self.password.text]) {
alertController.title = @"密碼驗證錯誤";
alertController.message = @"請從新輸入密碼驗證";
}
UIAlertAction *okAction = [UIAlertAction actionWithTitle:@"OK" style:(UIAlertActionStyleCancel) handler:^(UIAlertAction * _Nonnull action) {
[alertController dismissViewControllerAnimated:YES completion:nil];
}];
[alertController addAction:okAction];
[self presentViewController:alertController animated:YES completion:nil];
}
}
#pragma mark--XMPPStreamDelegate
//註冊成功
-(void)xmppStreamDidRegister:(XMPPStream *)sender{
NSLog(@"%s__%d__註冊成功",__FUNCTION__,__LINE__);
//存儲帳戶密碼用於下次自動登入
[[NSUserDefaults standardUserDefaults] setObject:self.userName.text forKey:@"userName"];
[[NSUserDefaults standardUserDefaults] setObject:self.password.text forKey:@"password"];
//改變APP的登入狀態
[[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"isLoginStatus"];
[[NSUserDefaults standardUserDefaults] synchronize];
//驗證以後默認的狀態爲離線,須要手動告訴服務器本身的狀態
XMPPPresence *presence = [XMPPPresence presenceWithType:@"available"];
[[XMPPManager sharedXMPPManager].stream sendElement:presence];
//註冊成功以後返回到登錄界面
[self.navigationController popToRootViewControllerAnimated:YES];
}
//註冊失敗
-(void)xmppStream:(XMPPStream *)sender didNotRegister:(DDXMLElement *)error{
NSLog(@"%s__%d__註冊失敗%@",__FUNCTION__,__LINE__,error);
}
複製代碼
####ChatViewController 在ChatViewController中須要公開一個屬性,用來接收好友列表頁面傳來的XMPPJID。 ######ChatViewController.h
@interface ChatViewController : UIViewController
@property (nonatomic,strong)XMPPJID *chatToJid;
@end
複製代碼
######ChatViewController.m
@interface ChatViewController ()<UITableViewDataSource,UITableViewDelegate>
//顯示消息
@property (weak, nonatomic) IBOutlet UITableView *messageContent;
//消息輸入框
@property (weak, nonatomic) IBOutlet UITextView *messageTextView;
//鍵盤底部約束
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *messageTextfieldConstraint;
//存儲消息數據
@property (nonatomic,strong) NSMutableArray *allMessageArray;
@end
@implementation ChatViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.messageTextView.text = @"";
self.messageContent.delegate = self;
self.messageContent.dataSource = self;
self.allMessageArray = [[NSMutableArray alloc] init];
//將歷史消息加入到數組
[self.allMessageArray addObjectsFromArray:[self fromDataBaseFetchResult]];
//監聽鍵盤frame的改變
[[NSNotificationCenter defaultCenter]addObserver:self selector:@selector(kbFrameWillChange:) name:UIKeyboardWillChangeFrameNotification object:nil];
[[XMPPManager sharedXMPPManager].stream addDelegate:self delegateQueue:dispatch_get_main_queue()];
[self reloadMessage];
self.messageContent.separatorStyle = UITableViewCellSeparatorStyleNone;
}
//消息發送按鈕
- (IBAction)sendMessageAction:(id)sender {
XMPPMessage *message = [XMPPMessage messageWithType:@"chat" to:self.chatToJid];
//要發送的消息添加到Body
[message addBody:self.messageTextView.text];
//發送消息
[[XMPPManager sharedXMPPManager].stream sendElement:message];
}
//鍵盤即將改變frame
- (void)kbFrameWillChange:(NSNotification *)noti{
//獲取窗口的高度
CGFloat windowH = [UIScreen mainScreen].bounds.size.height;
//鍵盤結束的frm
CGRect kbEndFrm = [noti.userInfo [UIKeyboardFrameEndUserInfoKey]CGRectValue];
//鍵盤結束的y值
CGFloat kbEndY = kbEndFrm.origin.y;
self.messageTextfieldConstraint.constant = windowH - kbEndY;
[self scrollsToBottomAnimated:YES];
}
- (void)scrollsToBottomAnimated:(BOOL)animated{
CGFloat offset = self.messageContent.contentSize.height - self.messageTextfieldConstraint.constant - 55;
if (offset > 0){
[self.messageContent setContentOffset:CGPointMake(0, offset) animated:animated];
}
}
#pragma mark XMPPStreamDelegate
//發送信息成功
-(void)xmppStream:(XMPPStream *)sender didSendMessage:(XMPPMessage *)message{
NSLog(@"發送成功🍅");
[self reloadMessage];//發送信息時刷新一次頁面
}
//發送信息失敗
-(void)xmppStream:(XMPPStream *)sender didFailToSendMessage:(XMPPMessage *)message error:(NSError *)error{
NSLog(@"發送失敗⚠️");
}
//收到信息
-(void)xmppStream:(XMPPStream *)sender didReceiveMessage:(XMPPMessage *)message{
NSLog(@"收到消息%@",message);
if ([message isChatMessageWithBody]) {
[self reloadMessage];//收到信息是刷新一次頁面
}
}
#pragma mark 加載聊天信息
-(void)reloadMessage{
//從數據庫讀取數據
NSArray *fetchedObjects = [self fromDataBaseFetchResult];
// 清空消息數組裏的全部數據
[self.allMessageArray removeAllObjects];
// 將新的聊天記錄添加到數組中
[self.allMessageArray addObjectsFromArray:fetchedObjects];
//將信息加載到tableView上
[self.messageContent reloadData];
//加載時滾動到最底部
[self scrollToBottomWithAnimated:YES];
}
//從數據庫讀取數據
- (NSArray *)fromDataBaseFetchResult{
//1.上下文
NSManagedObjectContext *managerObjectContext =[XMPPManager sharedXMPPManager].managerObjectContext;
//2.建立查詢請求
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"XMPPMessageArchiving_Message_CoreDataObject"];
//3.設置過濾條件(提取當前用戶jid,好友jid)
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"bareJidStr == %@ And streamBareJidStr == %@", self.chatToJid.bare,[XMPPManager sharedXMPPManager].stream.myJID.bare];
fetchRequest.predicate = predicate;
//4.設置排序(時間升序)
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"timestamp" ascending:YES];
fetchRequest.sortDescriptors = @[sortDescriptor];
//5.執行請求
NSError *error = nil;
NSArray *fetchedResult = [managerObjectContext executeFetchRequest:fetchRequest error:&error];
return fetchedResult;
}
//滾動到最底部
- (void)scrollToBottomWithAnimated:(BOOL)animated{
if (animated && [self.messageContent numberOfSections] > 0) {
NSInteger lastSectionIndex = [self.messageContent numberOfSections] - 1;
NSInteger lastRowIndex = [self.messageContent numberOfRowsInSection:lastSectionIndex] - 1;
if (lastRowIndex > 0) {
NSIndexPath *lastIndexPath = [NSIndexPath indexPathForRow:lastRowIndex inSection:lastSectionIndex];
[self.messageContent scrollToRowAtIndexPath:lastIndexPath atScrollPosition: UITableViewScrollPositionBottom animated:animated];
}
}
}
#pragma mark UITableViewDataSourceDelegate
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
return self.allMessageArray.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
if (nil == cell) {
cell = [[UITableViewCell alloc] initWithStyle:(UITableViewCellStyleValue1) reuseIdentifier:@"cell"];
}
XMPPMessageArchiving_Message_CoreDataObject *chatMessage = self.allMessageArray[indexPath.row];
//判斷聊天信息是發出去的 仍是接受進來的,在cell上顯示不同的樣式
if (chatMessage.isOutgoing == YES) {
cell.detailTextLabel.text = chatMessage.body;
cell.textLabel.text = @"";
} else {
cell.textLabel.text = chatMessage.body;
cell.detailTextLabel.text = @"";
}
self.messageTextView.text = @"";
return cell;
}
複製代碼
以上就是將XMPP添加到工程,實現基於XMPP的登入、註冊、聊天功能,還有一大批別的功能有待探索,以後將進行持續的更新。
####總結 1.XMPP框架中全部的鏈接、登入、註冊、受權、失去鏈接等操做的回調都是經過XMPPStream來管理; 2.使用XMPP開發即時通訊,大部分功能都是經過回調XMPPStreamDelegate協議中的方法來實現。
聯繫做者:簡書·DH_Fantasy 新浪微博·DH_Fantasy 版權聲明:自由轉載-非商用-非衍生-保持署名(CC BY-NC-ND 3.0)