这一章节会从基础模块开始逐步实现私聊功能
XMPP 基础模块
基础模块
1、 XMPPJID
: 身份模块,在 XMPP
中代表各个实体对象(例如:每一个用户、每一个房间),所有的消息都是根据 JID
进行分发传递的。
完整的格式:user@domain/resource
必要参数
-
user
: 使用XMPP
服务的实体 ID ,具有唯一性。 -
domain
: 服务器网关地址,可以是 IP 也可以是 域名
非必要参数
-
resource
: 用来表示一个特定的会话(与某个设备),连接(与某个地址),或者一个附属于某个节点ID实体相关实体的对象(比如多用户聊天室中的一个参加者)
/* XMPPFramework 中的初始化方法 */
[XMPPJID jidWithUser:userName domain:domain resource:@"iOS"];
2、XMPPStream
: 流模块(或者可以叫通信模块),XMPP
最基本的一个模块,可以用来加载其他核心(JID)或者拓展模块(Mut Chat)。通常在客户端的所有操作都是在此模块上进行的。
/* XMPPFramework 中的初始化方法 */
// 创建对象
_xmppStream = [[XMPPStream alloc] init];
// 配置服务器地址
[_xmppStream setHostName:XMPP_HOST];
// 配置服务器端口号(可以不配置,默认值为 5222)
[_xmppStream setHostPort:XMPP_PORT];
// 设置代理及回调队列
[_xmppStream addDelegate:self delegateQueue:dispatch_get_main_queue()];
// 设置心跳包发送间隔
[_xmppStream setKeepAliveInterval:30];
// 设置允许后台连接
[_xmppStream setEnableBackgroundingOnSocket:YES];
在 App 的一个完整的生命周期内应该只初始化一次此对象。
3、XMPPReconnect
: 断线重连模块,没有什么特殊的功能,但最好加上。可以避免因弱网情况下导致掉线而收不到消息。
/* XMPPFramework 中的初始化方法 */
// 创建对象
_xmppReconnect = [[XMPPReconnect alloc] initWithDispatchQueue:dispatch_get_main_queue()];
// 启用自动重连
[_xmppReconnect setAutoReconnect:YES];
// 激活模块,一定要加这句代码否则不会执行
[_xmppReconnect activate:_xmppStream];
4、XMPPStreamManagement
: 流管理模块,这个模块其实应该是和 XMPPReconnect
配套使用,作用是在客户端和服务器通讯消息中加入一个 </r>
标识,用来同步、确认消息是否被成功接收。
/* XMPPFramework 中的初始化方法 */
// 创建流状态缓存对象
_streamStorage = [[XMPPStreamManagementMemoryStorage alloc] init];
// 创建流管理对象
_xmppStreamManagement = [[XMPPStreamManagement alloc] initWithStorage:_streamStorage];
// 设置自动恢复
[_xmppStreamManagement setAutoResume:YES];
// 设置代理和返回队列
[_xmppStreamManagement addDelegate:self delegateQueue:dispatch_get_main_queue()];
// 激活模块
[_xmppStreamManagement activate:_xmppStream];
上面这几个模块是维持 XMPP
正常通讯所必须的,也如大家看到的在 XMPPFramework
中所有模块的加载都需要一个步骤:[xxx activate:_xmppStream]
所以当大家以后自己添加某个模块但没有执行的时候可以先检查一下是否添加了这句代码。
常用代理方法
一个简单的通讯其实只需要实现 XMPPStream
中以下几个常用的代理方法:
// 与服务器成功建立连接
- (void)xmppStreamDidConnect:(XMPPStream *)sender;
// 注册 XMPP 账号成功
- (void)xmppStreamDidRegister:(XMPPStream *)sender;
// 注册 XMPP 账号失败 ( error 失败原因 )
- (void)xmppStream:(XMPPStream *)sender didNotRegister:(NSXMLElement *)error;
// 登录成功
- (void)xmppStreamDidAuthenticate:(XMPPStream *)sender;
// 登录失败 ( error 失败原因 )
- (void)xmppStream:(XMPPStream *)sender didNotAuthenticate:(NSXMLElement *)error;
// 收到 <message></message> 类型的消息
- (void)xmppStream:(XMPPStream *)sender didReceiveMessage:(XMPPMessage *)message;
// 发送消息成功
- (void)xmppStream:(XMPPStream *)sender didSendMessage:(XMPPMessage *)message;
// 发送消息失败
- (void)xmppStream:(XMPPStream *)sender didFailToSendMessage:(XMPPMessage *)message error:(NSError *)error;
简单通讯
实现步骤
1.初始化流
- (void)initalize
{
// 初始化连接
_xmppStream = [[XMPPStream alloc] init];
[_xmppStream setHostName:XMPP_HOST];
[_xmppStream setHostPort:XMPP_PORT];
[_xmppStream addDelegate:self delegateQueue:dispatch_get_main_queue()];
[_xmppStream setKeepAliveInterval:30];
[_xmppStream setEnableBackgroundingOnSocket:YES];
// 接入断线重连模块
_xmppReconnect = [[XMPPReconnect alloc] initWithDispatchQueue:dispatch_get_main_queue()];
[_xmppReconnect setAutoReconnect:YES];
[_xmppReconnect activate:_xmppStream];
// 接入流管理模块
_streamStorage = [[XMPPStreamManagementMemoryStorage alloc] init];
_xmppStreamManagement = [[XMPPStreamManagement alloc] initWithStorage:_streamStorage];
[_xmppStreamManagement setAutoResume:YES];
[_xmppStreamManagement addDelegate:self delegateQueue:dispatch_get_main_queue()];
[_xmppStreamManagement activate:_xmppStream];
}
2.连接服务器
- (void)xmppConnect
{
NSError *connectError = nil;
if (![_xmppStream connectWithTimeout:XMPPStreamTimeoutNone error:&connectError]) {
NSLog(@"XMPP Connect With Error : %@", connectError);
}
}
3.登录账号
还记得上面提到的 XMPPStream
中的提到成功连接服务器的代理方法么?
- (void)xmppStreamDidConnect:(XMPPStream *)sender
{
// 一定要在成功连接服务器之后再登录
NSError *error = nil;
if (![_xmppStream authenticateWithPassword:_myPassword error:&error])
{
NSLog(@"Error Authenticate : %@", error);
}
}
// 登录成功
- (void)xmppStreamDidAuthenticate:(XMPPStream *)sender
{
NSLog(@"Authenticate Success !");
// 发送上线消息 ( 如果仅仅登录成功不发送上线消息的话是无法正常聊天的 )
XMPPPresence *presence = [XMPPPresence presence];
[_xmppStream sendElement:presence];
// 启用流管理
[_xmppStreamManagement enableStreamManagementWithResumption:YES maxTimeout:0];
}
// 登录失败
- (void)xmppStream:(XMPPStream *)sender didNotAuthenticate:(NSXMLElement *)error
{
NSLog(@"Did Not Authenticate : %@", error);
}
4.开始聊天
- (void)sendMessage:(NSString *)body to:(XMPPJID *)toJID
{
// 生成一个消息的唯一标识
NSString *uuId = [_xmppStream generateUUID];
// 初始化消息对象 (type 类型:私聊-@"chat",多人聊天-@"groupchat")
XMPPMessage *message = [XMPPMessage messageWithType:@"chat" to:toJID];
// 拼接消息体
[message addBody:body];
// 拼接消息 ID
[message addAttributeWithName:@"id" stringValue:uuId];
// 发送消息
[_xmppStream sendElement:message];
}
- (void)xmppStream:(XMPPStream *)sender didReceiveMessage:(XMPPMessage *)message
{
NSLog(@"Did Receive Message : %@", message);
}
- (void)xmppStream:(XMPPStream *)sender didSendMessage:(XMPPMessage *)message
{
NSLog(@"Send Message Success!");
}
- (void)xmppStream:(XMPPStream *)sender didFailToSendMessage:(XMPPMessage *)message error:(NSError *)error
{
NSLog(@"Fail Send Message : %@", error);
}
是不是很简单,轻轻松松几步让你开始聊天。
简单封装
IM 作为一个 App 的基本功能,肯定不会只在某一个界面去使用,那么做一下封装就显得很有必要了。下面直接上代码来实现一个单例:
XMPPManager.h
//
// XMPPManager.h
// MusicSampleChat
//
// Created by LonlyCat on 16/9/21.
// Copyright © 2016年 LonlyCat. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "XMPP.h"
#import "XMPPReconnect.h"
#import "XMPPStreamManagement.h"
#import "XMPPStreamManagementMemoryStorage.h"
#define XMPP_PORT 5222
#define XMPP_HOST 192.168.1.103 // 地址自己替换个可用的
typedef void(^MessageStatusHandle)(BOOL);
typedef void(^MessageReceiveHandle)(XMPPMessage *);
@interface XMPPManager : NSObject
/** 与服务器通讯链接 */
@property (nonatomic, strong) XMPPStream *xmppStream;
/** XMPP 用户对象(自己) */
@property (nonatomic, strong) XMPPJID *myJID;
/** XMPP 密码 */
@property (nonatomic, copy ) NSString *myPassword;
/** 断线重连模块 */
@property (nonatomic, strong) XMPPReconnect *xmppReconnect;
/** 流管理模块 */
@property (nonatomic, strong) XMPPStreamManagement *xmppStreamManagement;
@property (nonatomic, strong) XMPPStreamManagementMemoryStorage *streamStorage;
/**
登录
@param userName 用户名
@param password 密码
*/
- (void)loginWithName:(NSString *)userName password:(NSString *)password;
/**
登出
*/
- (void)logOut;
/**
上线
*/
- (void)goOnline;
/**
下线
*/
- (void)goOffline;
/**
发送消息
@param body 内容
@param toJID 发送对象
@param type 聊天类型 @"chat" / @"groupchat"
@param statusHandle 消息发送状态回调 YES 发送成功 NO 发送失败
@return XMPPMessage
*/
- (XMPPMessage *)sendMessage:(NSString *)body
to:(XMPPJID *)toJID
type:(NSString *)type
statusHandle:(MessageStatusHandle)statusHandle;
/**
添加一个收到消息的回调,key 不能为空
@param receiveHandle 回调
@param key 查询、删除关键字
@return 是否添加成功
*/
- (BOOL)addReceiveHandle:(MessageReceiveHandle)receiveHandle
forKey:(NSString *)key;
/**
根据关键字移除一个消息回调
*/
- (void)removeReceiveHandleByKey:(NSString *)key;
@end
XMPPManager* getXMPPManager();
XMPPManager.m
#import "XMPPManager.h"
@interface XMPPManager ()
@property (nonatomic, strong) NSMutableDictionary *statusHandleDic;
@property (nonatomic, strong) NSMutableDictionary *receiveHandlesDic;
@end
@implementation XMPPManager
+ (XMPPManager *)sharedInstance
{
static XMPPManager *manager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
manager = [[XMPPManager alloc] init];
manager.statusHandleDic = [NSMutableDictionary dictionary];
manager.receiveHandlesDic = [NSMutableDictionary dictionary];
});
return manager;
}
XMPPManager* getXMPPManager()
{
XMPPManager *manager = [XMPPManager sharedInstance];
return manager;
}
#pragma mark - 初始化
- (instancetype)init
{
self = [super init];
if (self) {
[self initalize];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(applicationWillTerminate:)
name:UIApplicationWillTerminateNotification
object:nil];
return self;
}
- (void)initalize
{
// 初始化连接
_xmppStream = [[XMPPStream alloc] init];
[_xmppStream setHostName:XMPP_HOST];
[_xmppStream setHostPort:XMPP_PORT];
[_xmppStream addDelegate:self delegateQueue:dispatch_get_main_queue()];
[_xmppStream setKeepAliveInterval:30];
[_xmppStream setEnableBackgroundingOnSocket:YES];
// 接入断线重连模块
_xmppReconnect = [[XMPPReconnect alloc] initWithDispatchQueue:dispatch_get_main_queue()];
[_xmppReconnect setAutoReconnect:YES];
[_xmppReconnect activate:_xmppStream];
// 接入流管理模块
_streamStorage = [[XMPPStreamManagementMemoryStorage alloc] init];
_xmppStreamManagement = [[XMPPStreamManagement alloc] initWithStorage:_streamStorage];
[_xmppStreamManagement setAutoResume:YES];
[_xmppStreamManagement addDelegate:self delegateQueue:dispatch_get_main_queue()];
[_xmppStreamManagement activate:_xmppStream];
}
#pragma mark - 登录状态
- (void)loginWithName:(NSString *)userName password:(NSString *)password
{
NSLog(@"userName : %@, password : %@",userName, password);
_myPassword = password;
NSString *domain = XMPP_HOST;
_myJID = [XMPPJID jidWithUser:userName domain:domain resource:@"iOS"];
[_xmppStream setMyJID:_myJID];
if ([_xmppStream isConnected]) {
[self xmppAuthen];
} else {
[self xmppConnect];
}
}
- (void)logOut
{
[self goOffline];
[_xmppStream disconnectAfterSending];
}
- (void)goOnline
{
XMPPPresence *presence = [XMPPPresence presence];
[_xmppStream sendElement:presence];
}
- (void)goOffline
{
XMPPPresence *presence = [XMPPPresence presenceWithType:@"unavailable"];
[_xmppStream sendElement:presence];
}
#pragma mark - 消息发送 / 接收
- (IMMessageModel *)sendMessage:(NSString *)body
to:(XMPPJID *)toJID
type:(NSString *)type
statusHandle:(MessageStatusHandle)statusHandle
{
return [self sendMessage:body to:toJID type:type extend:nil statusHandle:statusHandle];
}
- (XMPPMessage *)sendMessage:(NSString *)body
to:(XMPPJID *)toJID
type:(NSString *)type
extend:(DDXMLElement *)extend
statusHandle:(MessageStatusHandle)statusHandle
{
NSString *uuId = [_xmppStream generateUUID];
XMPPMessage *message = [XMPPMessage messageWithType:type to:toJID];
[message addBody:body];
[message addAttributeWithName:@"id" stringValue:uuId];
[_xmppStream sendElement:message];
[_statusHandleDic setObject:statusHandle forKey:uuId];
return message;
}
- (BOOL)addReceiveHandle:(MessageReceiveHandle)receiveHandle
forKey:(NSString *)key
{
if (receiveHandle && [key isKindOfClass:[NSString class]]) {
[_receiveHandlesDic setObject:receiveHandle forKey:key];
return YES;
}
return NO;
}
- (void)removeReceiveHandleByKey:(NSString *)key
{
[_receiveHandlesDic removeObjectForKey:key];
}
#pragma mark - XMPPStreamDelegate
#pragma mark connect
- (void)xmppStreamDidConnect:(XMPPStream *)sender
{
NSLog(@"%s", __func__);
[self xmppAuthen];
}
- (void)xmppStreamDidDisconnect:(XMPPStream *)sender withError:(NSError *)error
{
NSLog(@"%s", __func__);
}
- (void)xmppStreamDidAuthenticate:(XMPPStream *)sender
{
NSLog(@"Authenticate Success !");
// 发送上线消息
[self goOnline];
// 启用流管理
[_xmppStreamManagement enableStreamManagementWithResumption:YES maxTimeout:0];
// 发送 xmpp 登录成功通知
[[NSNotificationCenter defaultCenter] postNotificationName:XMPPLoginSuccess object:nil];
}
- (void)xmppStream:(XMPPStream *)sender didNotAuthenticate:(NSXMLElement *)error
{
NSLog(@"Did Not Authenticate : %@", error);
[[NSNotificationCenter defaultCenter] postNotificationName:XMPPLoginFail object:error];
}
#pragma mark message
- (void)xmppStream:(XMPPStream *)sender didReceiveMessage:(XMPPMessage *)message
{
NSLog(@"Did Receive Message : %@", message);
if ([message isErrorMessage]) {
// 警告类消息
NSLog(@"error message : %@ ", [message errorMessage]);
return;
}
NSString *type = [[message attributeForName:@"type"] stringValue];
if ([message isChatMessageWithBody]) {
// 私聊消息
[self sendReceiveMessageNotification:message];
} else if ([message isGroupChatMessageWithBody]) {
// 群聊消息
[self sendReceiveMessageNotification:message];
} else if ([type isEqualToString:@"normal"]) {
// 常规类型(已知邀请群聊为常规类型,不用在此处理)
}
}
- (void)xmppStream:(XMPPStream *)sender didSendMessage:(XMPPMessage *)message
{
NSLog(@"Did Send Message !");
NSString *key = [message elementID];
[self handleMessageStatusByKey:key result:YES];
}
- (void)xmppStream:(XMPPStream *)sender didFailToSendMessage:(XMPPMessage *)message error:(NSError *)error
{
NSLog(@"Fail Send Message : %@", error);
NSString *key = [message elementID];
[self handleMessageStatusByKey:key result:NO];
}
#pragma mark - 私有方法
- (void)xmppConnect
{
NSError *connectError = nil;
if (![_xmppStream connectWithTimeout:XMPPStreamTimeoutNone error:&connectError]) {
NSLog(@"XMPP Connect With Error : %@", connectError);
}
}
- (void)xmppAuthen
{
NSError *error = nil;
if (![_xmppStream authenticateWithPassword:_myPassword error:&error]) {
NSLog(@"Error Authenticate : %@", error);
[[NSNotificationCenter defaultCenter] postNotificationName:XMPPLoginFail object:error];
}
}
- (void)handleMessageStatusByKey:(NSString *)string result:(BOOL)result
{
MessageStatusHandle statusHandle = [_statusHandleDic objectForKey:string];
if (statusHandle) {
statusHandle(result);
[_statusHandleDic removeObjectForKey:string];
statusHandle = nil;
}
}
- (void)sendReceiveMessageNotification:(XMPPMessage *)message
{
NSArray *receiveHandleArray = [_receiveHandlesDic allValues];
[receiveHandleArray enumerateObjectsUsingBlock:^(MessageReceiveHandle _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop)
{
obj(message);
}];
}
#pragma mark -- terminate
/**
* 申请后台时间来清理下线的任务
*/
- (void)applicationWillTerminate:(NSNotification *)noti
{
[self goOffline];
[_xmppStream disconnectAfterSending];
}
@end
有了这个封装好的单例我们就可以开始聊天了( 当然你首先得有一个搭好的 XMPP 服务器 )。
结尾
如果你是有一定经验的开发者,看完之后肯定能发现俩个问题:
-
XMPPMessage
的传值使用的是原始格式 - 消息对象没有进行存储
这俩个问题对应的能力应该是 iOS 开发者的基本功,所以我希望你能自己修改下上面的代码来解决。
PS : XMPPFramework
框架本身是实现了对 XMPPMessage
的存储,实现方式是使用的 CoreData
。虽然方便了开发者进行开发,但对于某些需求的定制造成了局限( 比如你想保存用户的聊天记录到服务器 )。所以在这里我还是推荐大家使用 realm
或者 SQLite
来实现数据的存储