XMPP 的使用

1、XMPP

  • XMPP 是一个基于 Socket 通信的即时通讯的协议,它规范了即时通信在网络上数据的传输格式,比如登录,获取好友列表等等的格式。XMPP 在网络传输的数据是 XML 格式。

  • 开发架构:

  • iOS 框架:XMPPFramework

  • 服务器:Openfire

  • 数据库:MySQL

2、XMPPFramework 框架简介

2.1 XMPPFramework 简介

  • XMPPFramework 是一个 OS X/iOS 平台的开源项目,使用 Objective-C 实现了 XMPP 协议(RFC-3920),同时还提供了用于读写 XML 的工具,大大简化了基于 XMPP 的通信应用的开发。

2.2 XMPPFramework 结构

  • 1、XMPPFramework 的目录结构如下:

目录 说明
Authentication 授权,与授权验证相关,如用户名密码等
Categories 分类,XMPP 自己写的一些分类,尤其是 NSXMLElement+XMPP 扩展是必备的
Core 核心,这里是 XMPP 的核心文件目录,我们最主要的目光还是要放在这个目录上
Extensions 扩展,XMPP 的扩展模块,用于扩展各种协议和各种独立的功能,其下每个子目录都是对应的一个单独的子功能
Utilities 工具,都是辅助类,我们开发者不用关心这里
Vendor 第三方库,这个目录是 XMPP 所引用的第三方类库,我们也不用关心这里
  • 虽然这里有很多个目录,但是我们在开发中基本只关心 Core 和 Extensions 这两个目录下的类。

  • 在 Core 中:

目录 说明
XMPPElement 是一个基类,延展出三个子类
XMPPIQ 请求,用户登录,用户注册,添加好友等
XMPPMessage 消息,用来发各种消息等
XMPPPresence 展现,用户上线下线提示等
XMPPStream 流,非常常用,大部分类的加载都在写在流的懒加载里
  • 在 Extensions 中:
目录 说明
CoreDataStorage coreData 存储
Reconnect 重新连接
Roster 好友管理
SystemInputActivityMonitor 系统输入的活动监控
  • 在 Vendor 中:
文件夹 说明
CocoaAsyncSocket 异步 Socket
CocoaLumberjack ⽇志相关
KissXML XML 解析
  • 2、XMPPFramework 中常用的类:
说明
XMPPStream XMPP 基础服务类
XMPPRoster 好友列表类
XMPPUserCoreDataStorageObject 管理用户的类
XMPPRosterCoreDataStorage 好友列表(用户账号)在 core data 中的操作类
XMPPvCardCoreDataStorage 好友名片(昵称,签名,性别,年龄等信息)在 core data 中的操作类
XMPPvCardTemp 好友名片实体类,从数据库里取出来的都是它
xmppvCardAvatarModule 好友头像
XMPPReconnect 如果失去连接,自动重连
XMPPRoom 提供多用户聊天支持
XMPPPubSub 发布订阅
XMPPMessageArchiving 其中有数据表
XMPPMessageArchiving_Message_CoreDataObject 取出当前信息的类
  • 3、XMPPFramework 几个常用到的扩展协议:
协议 协议简介
XEP-0006 使能与网络上某个 XMPP 实体间的通信
XEP-0009 在两个 XMPP 实体间传输 XML-RPC 编码请求和响应
XEP-0012 最后的活动(判断上线,离开断开)
XEP-0045 多人聊天相关协议
XEP-0054 名片格式的标准文档,个人信息设置
XEP-0060 提供通用公共订阅功能
XEP-0065 两个 XMPP 用户之间建立一个带外流,主要用于文件传输,sockets5 字节流
XEP-0066 二进制数据传输(特殊信息的发送)
XEP-0082 日期和时间信息的标准化表示
XEP-0085 聊天对话中通知用户状态,聊天状态通知
XEP-0100 表述了 XMPP 客户端与提供传统的 IM 服务的代理网关之间交换的最佳实践
XEP-0115 广播和动态发现客户端、设备、或一般实体能力
XEP-0136 为服务端备份和检索 XMPP 消息定义机制和偏好设置,聊天记录归档
XEP-0153 用于交换用户头像,基于名片的头像
XEP-0184 消息送达回执协议
XEP-0199 XMPP ping 协议(用来 ping 服务器和 ping 自己)
XEP-0202 用于交换实体间的本地时间信息
XEP-0203 用于延迟发送
XEP-0224 引起另一个用户注意的协议
XEP-0335 JSON 容器(可能以后某些信息传输将用 JSON 格式)
  • XMPP 的扩展协议 Jingle 使得其支持语音和视频,目前 iOS 尚不支持。

    • iOS 发送附件(图片,语音,文档…)时比较麻烦,XMPP 框架没有提供发送附件的功能,需要自己实现。

    • iOS 发送附件实现方法:

      • 1、将获取到的图片/音频文件通过 base64 加密,直接通过 xmpp 的消息体发送过去,然后解码。
      • 2、通过 http 请求的方式将图片/音频文件上传到服务器,然后将图片/音频文件的下载地址通过 xmpp 消息体发送过去,另外一个客户端下载。

      • 音频文件建议转码为 amr,这种格式的音频文件比较小。

2.3 XMPPJID 类

  • 登录需要到账号,而所谓的账号其实就是用户唯一标识符(JID),在 XMPP 中使用 XMPPJID 类来表示。

  • JID 一般由三部分构成:用户名,域名和资源名,格式为 user@domain/resource,例如:`test@example.com/Anthony`。对应于 XMPPJID 类中的三个属性 user、domain、resource。

  • 如果没有设置主机名(HOST),则使用 JID 的域名(domain)作为主机名,而端口号是可选的,默认是 5222,一般也没有必要改动它。

2.4 XMPPStream 类

  • 我们要与服务器连接,就必须通过 XMPPStream 类了,它提供了很多的 API 和属性设置,通过 socket 来实现的。Verdor 目录包含了 CocoaAsyncSocket 这个非常有名的 socket 编程库。XMPPStream 类还遵守并实现了 GCDAsyncSocketDelegate 代理,用于客户端与服务器交互。

    1
    @interface XMPPStream : NSObject <GCDAsyncSocketDelegate>
  • 当我们创建 XMPPStream 对象后,我们需要设置代理,才能回调我们的代理方法,这个是支持 multicast delegate,也就是说对于一个 XMPPStream 对象,可以设置多个代理对象,其中协议是XMPPStreamDelegate。

    1
    - (void)addDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue;
  • 而当我们不希望某个 XMPPStream 对象继续接收到代理回调时,我们通过这样的方式来移除代理。

    1
    2
    - (void)removeDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue;
    - (void)removeDelegate:(id)delegate;
  • 接下来,我们要设置主机和端口,通过设置这两个属性。

    1
    2
    3
    4
    5
    // 主机,可选设置,如果没有设置默认会使用 domain
    @property (readwrite, copy) NSString *hostName;

    // 端口号,默认为 5222
    @property (readwrite, assign) UInt16 hostPort;
  • XMPPStream 有 XMPPJID 类对象作为属性,标识用户,因为我们后续很多操作都需要到 myJID。

    1
    @property (readwrite, copy) XMPPJID *myJID;
  • 而管理用户在线状态的就交由 XMPPPresence 类了,它同样被作为 XMPPStream 的属性,组合到 XMPPStream 中,后续很多关于用户的操作是需要到处理用户状态的。

    1
    @property (strong, readonly) XMPPPresence *myPresence;

2.5 XMPPStreamDelegate

  • 这个协议是非常关键的,我们的很多主要操作都集中在这个协议的代理回调上。它分为好几种类型的代理 API,比如授权的、注册的、安全的等。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    @protocol XMPPStreamDelegate
    @optional

    // 将要与服务器连接
    - (void)xmppStreamWillConnect:(XMPPStream *)sender;

    // 已经与服务器连接,
    // 当 TCP Socket 已经与远程主机连接上时会回调此方法
    // 若 App 要求在后台运行,需要设置 XMPPStream's enableBackgroundingOnSocket 属性
    - (void)xmppStream:(XMPPStream *)sender socketDidConnect:(GCDAsyncSocket *)socket;

    // 当 TCP 与服务器建立连接后会回调此方法
    - (void)xmppStreamDidStartNegotiation:(XMPPStream *)sender;

    // TLS 传输层协议在将要验证安全设置时会回调
    // 参数 settings 会被传到 startTLS,此方法可以不实现的
    // 若服务端使用自签名的证书,需要在 settings 中添加 GCDAsyncSocketManuallyEvaluateTrust = YES
    - (void)xmppStream:(XMPPStream *)sender willSecureWithSettings:(NSMutableDictionary *)settings;

    // 上面的方法执行后,下一步就会执行这个代理回调
    // 用于在 TCP 握手时手动验证是否受信任
    - (void)xmppStream:(XMPPStream *)sender didReceiveTrust:(SecTrustRef)trust
    completionHandler:(void (^)(BOOL shouldTrustPeer))completionHandler;

    // 当 stream 通过了 SSL/TLS 的安全验证时,会回调此代理方法
    - (void)xmppStreamDidSecure:(XMPPStream *)sender;

    // 当 XML 流已经完全打开时(也就是与服务器的连接完成时)会回调此代理方法。此时可以安全地与服务器通信了
    - (void)xmppStreamDidConnect:(XMPPStream *)sender;

    // 注册新用户成功时的回调
    - (void)xmppStreamDidRegister:(XMPPStream *)sender;

    // 注册新用户失败时的回调
    - (void)xmppStream:(XMPPStream *)sender didNotRegister:(NSXMLElement *)error;

    // 授权通过时的回调,也就是登录成功的回调
    - (void)xmppStreamDidAuthenticate:(XMPPStream *)sender;

    // 授权失败时的回调,也就是登录失败时的回调
    - (void)xmppStream:(XMPPStream *)sender didNotAuthenticate:(NSXMLElement *)error;

    // 将要绑定 JID resource 时的回调,这是授权程序的标准部分
    // 当验证 JID 用户名通过时,下一步就验证 resource。若使用标准绑定处理,return nil 或者不要实现此方法
    - (id <XMPPCustomBinding>)xmppStreamWillBind:(XMPPStream *)sender;

    // 如果服务器出现 resouce 冲突而导致不允许 resource 选择时,会回调此代理方法
    // 返回指定的 resource 或者返回 nil 让服务器自动帮助我们来选择。一般不用实现它
    - (NSString *)xmppStream:(XMPPStream *)sender alternativeResourceForConflictingResource:(NSString *)conflictingResource;

    // 将要接收 IQ(消息查询)时的回调
    - (XMPPIQ *)xmppStream:(XMPPStream *)sender willReceiveIQ:(XMPPIQ *)iq;

    // 将要接收到消息时的回调
    - (XMPPMessage *)xmppStream:(XMPPStream *)sender willReceiveMessage:(XMPPMessage *)message;

    // 将要接收到用户在线状态时的回调
    - (XMPPPresence *)xmppStream:(XMPPStream *)sender willReceivePresence:(XMPPPresence *)presence;

    // 通过实现此代理方法,可以知道被过滤的原因,有一定的帮助
    // 当 xmppStream:willReceiveX: (也就是前面这三个 API 回调后),过滤了 stanza,会回调此代理方法
    - (void)xmppStreamDidFilterStanza:(XMPPStream *)sender;

    // 在接收了 IQ(消息查询后)会回调此代理方法
    - (BOOL)xmppStream:(XMPPStream *)sender didReceiveIQ:(XMPPIQ *)iq;

    // 在接收了消息后会回调此代理方法
    - (void)xmppStream:(XMPPStream *)sender didReceiveMessage:(XMPPMessage *)message;

    // 在接收了用户在线状态消息后会回调此代理方法
    - (void)xmppStream:(XMPPStream *)sender didReceivePresence:(XMPPPresence *)presence;

    // 在接收 IQ/messag、presence 出错时,会回调此代理方法
    - (void)xmppStream:(XMPPStream *)sender didReceiveError:(NSXMLElement *)error;

    // 将要发送 IQ(消息查询时)时会回调此代理方法
    - (XMPPIQ *)xmppStream:(XMPPStream *)sender willSendIQ:(XMPPIQ *)iq;

    // 在将要发送消息时,会回调此代理方法
    - (XMPPMessage *)xmppStream:(XMPPStream *)sender willSendMessage:(XMPPMessage *)message;

    // 在将要发送用户在线状态信息时,会回调此方法
    - (XMPPPresence *)xmppStream:(XMPPStream *)sender willSendPresence:(XMPPPresence *)presence;

    // 在发送 IQ(消息查询)成功后会回调此代理方法
    - (void)xmppStream:(XMPPStream *)sender didSendIQ:(XMPPIQ *)iq;

    // 在发送消息成功后,会回调此代理方法
    - (void)xmppStream:(XMPPStream *)sender didSendMessage:(XMPPMessage *)message;

    // 在发送用户在线状态信息成功后,会回调此方法
    - (void)xmppStream:(XMPPStream *)sender didSendPresence:(XMPPPresence *)presence;

    // 在发送 IQ(消息查询)失败后会回调此代理方法
    - (void)xmppStream:(XMPPStream *)sender didFailToSendIQ:(XMPPIQ *)iq error:(NSError *)error;

    // 在发送消息失败后,会回调此代理方法
    - (void)xmppStream:(XMPPStream *)sender didFailToSendMessage:(XMPPMessage *)message error:(NSError *)error;

    // 在发送用户在线状态失败信息后,会回调此方法
    - (void)xmppStream:(XMPPStream *)sender didFailToSendPresence:(XMPPPresence *)presence error:(NSError *)error;

    // 当修改了 JID 信息时,会回调此代理方法
    - (void)xmppStreamDidChangeMyJID:(XMPPStream *)xmppStream;

    // 当 Stream 被告知与服务器断开连接时会回调此代理方法
    - (void)xmppStreamWasToldToDisconnect:(XMPPStream *)sender;

    // 当发送了 </stream:stream> 节点时,会回调此代理方法
    - (void)xmppStreamDidSendClosingStreamStanza:(XMPPStream *)sender;

    // 连接超时时会回调此代理方法
    - (void)xmppStreamConnectDidTimeout:(XMPPStream *)sender;

    // 当与服务器断开连接后,会回调此代理方法
    - (void)xmppStreamDidDisconnect:(XMPPStream *)sender withError:(NSError *)error;

    // P2P 类型相关的
    - (void)xmppStream:(XMPPStream *)sender didReceiveP2PFeatures:(NSXMLElement *)streamFeatures;
    - (void)xmppStream:(XMPPStream *)sender willSendP2PFeatures:(NSXMLElement *)streamFeatures;

    - (void)xmppStream:(XMPPStream *)sender didRegisterModule:(id)module;
    - (void)xmppStream:(XMPPStream *)sender willUnregisterModule:(id)module;

    // 当发送非 XMPP 元素节点时,会回调此代理方法
    // 也就是说,如果发送的 element 不是 <iq>, <message> 或者 <presence>,那么就会回调此代理方法
    - (void)xmppStream:(XMPPStream *)sender didSendCustomElement:(NSXMLElement *)element;

    // 当接收到非 XMPP 元素节点时,会回调此代理方法
    // 也就是说,如果接收的element不是 <iq>, <message> 或者 <presence>,那么就会回调此代理方法
    - (void)xmppStream:(XMPPStream *)sender didReceiveCustomElement:(NSXMLElement *)element;

2.6 XMPPIQ 类

  • 消息查询(IQ)就是通过此类来处理的了。XMPP 给我们提供了 IQ 方便创建的类,用于快速生成 XML 数据。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    @interface XMPPIQ : XMPPElement

    // 生成 IQ

    + (XMPPIQ *)iq;
    + (XMPPIQ *)iqWithType:(NSString *)type;
    + (XMPPIQ *)iqWithType:(NSString *)type to:(XMPPJID *)jid;
    + (XMPPIQ *)iqWithType:(NSString *)type to:(XMPPJID *)jid elementID:(NSString *)eid;
    + (XMPPIQ *)iqWithType:(NSString *)type to:(XMPPJID *)jid elementID:(NSString *)eid child:(NSXMLElement *)childElement;
    + (XMPPIQ *)iqWithType:(NSString *)type elementID:(NSString *)eid;
    + (XMPPIQ *)iqWithType:(NSString *)type elementID:(NSString *)eid child:(NSXMLElement *)childElement;
    + (XMPPIQ *)iqWithType:(NSString *)type child:(NSXMLElement *)childElement;

    - (id)init;
    - (id)initWithType:(NSString *)type;
    - (id)initWithType:(NSString *)type to:(XMPPJID *)jid;
    - (id)initWithType:(NSString *)type to:(XMPPJID *)jid elementID:(NSString *)eid;
    - (id)initWithType:(NSString *)type to:(XMPPJID *)jid elementID:(NSString *)eid child:(NSXMLElement *)childElement;
    - (id)initWithType:(NSString *)type elementID:(NSString *)eid;
    - (id)initWithType:(NSString *)type elementID:(NSString *)eid child:(NSXMLElement *)childElement;
    - (id)initWithType:(NSString *)type child:(NSXMLElement *)childElement;

    // IQ 类型
    - (NSString *)type;

    // 判断 type 类型

    - (BOOL)isGetIQ;
    - (BOOL)isSetIQ;
    - (BOOL)isResultIQ;
    - (BOOL)isErrorIQ;

    // 当 type 为 get 或者 set 时,这个 API 是很有用的,用于指定是否要求有响应
    - (BOOL)requiresResponse;

    - (NSXMLElement *)childElement;
    - (NSXMLElement *)childErrorElement;

    @end
  • IQ 是一种请求/响应机制,从一个实体发送请求,另外一个实体接受请求并进行响应。例如,Client 在 stream 的上下文中插入一个元素,向 Server 请求得到自己的好友列表,Server 返回一个,里面是请求的结果。

  • <type></type> 有以下类别(可选设置如:<type>get</type>

    1
    2
    3
    4
    5
    6
     type    | 说明
    ---------|---------------------------------------------
    get | 获取当前域值。类似于 http get 方法
    set | 设置或替换 get 查询的值。类似于 http put 方法
    result | 说明成功的响应了先前的查询。类似于 http 状态码 200
    error | 查询和响应中出现的错误
  • 下面是一个 IQ 例子:

    1
    2
    3
    4
    5
    6
    <iqfrom="huangyibiao@welcome.com/ios"  
    id="xxxxxxx"
    to="biaoge@welcome.com/ios"
    type="get">
    <queryxmlns="jabber:iq:roster"/>
    </iq>

2.7 XMPPPresence 类

  • 这个类代表节点,我们通过此类提供的方法来生成 XML 数据。presence 它代表用户在线状态。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    @interface XMPPPresence : XMPPElement

    // Converts an NSXMLElement to an XMPPPresence element in place (no memory allocations or copying)
    + (XMPPPresence *)presenceFromElement:(NSXMLElement *)element;

    + (XMPPPresence *)presence;
    + (XMPPPresence *)presenceWithType:(NSString *)type;

    // type:用户在线状态,to:接收方的 JID
    + (XMPPPresence *)presenceWithType:(NSString *)type to:(XMPPJID *)to;

    - (id)init;
    - (id)initWithType:(NSString *)type;
    - (id)initWithType:(NSString *)type to:(XMPPJID *)to;

    - (NSString *)type;

    - (NSString *)show;
    - (NSString *)status;

    - (int)priority;

    - (int)intShow;

    - (BOOL)isErrorPresence;

    @end
  • presence 用来表明用户的状态,如:online、offline、away、dnd (请勿打扰) 等。当改变自己的状态时,就会在 stream 的上下文中插入一个 Presence 元素,来表明自身的状态。要想接受 presence 消息,必须经过一个叫做 presence subscription 的授权过程。

  • <type></type> 有以下类别(可选设置如:<type>subscribe</type>):

    1
    2
    3
    4
    5
    6
    7
    8
    9
     type           | 说明
    ----------------|----------------------------
    available | 上线
    unavailable | 下线
    away | 离开
    do not disturb | 忙碌
    subscribe | 订阅其他用户的状态
    probe | 请求获取其他用户的状态
    unavailable | 不可用,离线(offline)状态
  • <show></show> 节点有以下类别,如 <show>dnd</show>

    1
    2
    3
    4
    5
    6
     show  | 说明
    -------|---------------------------------------------
    chat | 聊天中
    away | 暂时离开
    xa | eXtend Away,长时间离开
    dnd | 勿打扰
  • <status></status> 节点

    • 这个节点表示状态信息,内容比较自由,几乎可以是所有类型的内容。常用来表示用户当前心情,活动,听的歌曲,看的视频,所在的聊天室,访问的网页,玩的游戏等等。
  • <priority></priority> 节点

    • 范围 -128~127。高优先级的 resource 能接受发送到 bare JID 的消息,低优先级的 resource 不能。优先级为负数的 resource 不能收到发送到 bare JID 的消息。
  • 发送一个用户在线状态的例子:

    1
    2
    3
    4
    <presencefrom="alice@wonderland.lit/pda"> 
    <show>dnd</show>
    <status>浏览器搜索</status>
    </presence>

2.8 XMPPMessage 类

  • XMPPMessage 是 XMPP 框架给我们提供的,方便用于生成 XML 消息的数据。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    @interface XMPPMessage : XMPPElement

    // Converts an NSXMLElement to an XMPPMessage element in place (no memory allocations or copying)
    + (XMPPMessage *)messageFromElement:(NSXMLElement *)element;

    + (XMPPMessage *)message;
    + (XMPPMessage *)messageWithType:(NSString *)type;
    + (XMPPMessage *)messageWithType:(NSString *)type to:(XMPPJID *)to;
    + (XMPPMessage *)messageWithType:(NSString *)type to:(XMPPJID *)jid elementID:(NSString *)eid;
    + (XMPPMessage *)messageWithType:(NSString *)type to:(XMPPJID *)jid elementID:(NSString *)eid child:(NSXMLElement *)childElement;
    + (XMPPMessage *)messageWithType:(NSString *)type elementID:(NSString *)eid;
    + (XMPPMessage *)messageWithType:(NSString *)type elementID:(NSString *)eid child:(NSXMLElement *)childElement;
    + (XMPPMessage *)messageWithType:(NSString *)type child:(NSXMLElement *)childElement;

    - (id)init;
    - (id)initWithType:(NSString *)type;
    - (id)initWithType:(NSString *)type to:(XMPPJID *)to;
    - (id)initWithType:(NSString *)type to:(XMPPJID *)jid elementID:(NSString *)eid;
    - (id)initWithType:(NSString *)type to:(XMPPJID *)jid elementID:(NSString *)eid child:(NSXMLElement *)childElement;
    - (id)initWithType:(NSString *)type elementID:(NSString *)eid;
    - (id)initWithType:(NSString *)type elementID:(NSString *)eid child:(NSXMLElement *)childElement;
    - (id)initWithType:(NSString *)type child:(NSXMLElement *)childElement;

    - (NSString *)type;
    - (NSString *)subject;
    - (NSString *)body;
    - (NSString *)bodyForLanguage:(NSString *)language;
    - (NSString *)thread;

    - (void)addSubject:(NSString *)subject;
    - (void)addBody:(NSString *)body;
    - (void)addBody:(NSString *)body withLanguage:(NSString *)language;
    - (void)addThread:(NSString *)thread;

    - (BOOL)isChatMessage;
    - (BOOL)isChatMessageWithBody;
    - (BOOL)isErrorMessage;
    - (BOOL)isMessageWithBody;

    - (NSError *)errorMessage;

    @end
  • message 是一种基本 推送 消息方法,它不要求响应。主要用于 IM、groupChat、alert 和 notification 之类的应用中。

  • <type></type> 有以下类别(可选设置如:<type>chat</type>):

    1
    2
    3
    4
    5
    6
    7
     type      | 说明
    -----------|--------------------------------------------------------------
    normal | 类似于 email,主要特点是不要求响应
    chat | 类似于 qq 里的好友即时聊天,主要特点是实时通讯
    groupchat | 类似于聊天室里的群聊
    headline | 用于发送 alert 和 notification
    error | 如果发送 message 出错,发现错误的实体会用这个类别来通知发送者出错了
  • <body></body> 节点

    • 所要发送的内容就放在 body 节点下
  • 消息节点的例子:

    1
    2
    3
    <messageto="lily@jabber.org/contact" type="chat"> 
    <body>您好?</body>
    </message>

3、XMPPFramework 框架使用

3.1 CocoaPods 导入框架

  • 1、通过 CocoaPods 导入第三方框架 XMPPFramework。

    • 在 Podfile 文件中加入如下代码,在终端中,使用命令 pod install 下载添加 XMPPFramework 框架。

      1
      2
      3
      4
      5
      6
      7
      8
      platform :ios, '8.0'

      target 'XMPPDemo' do

      use_frameworks!
      pod 'XMPPFramework', '~> 3.7.0'

      end
  • 2、在需要使用 XMPPFramework 的文件中导入以下头文件。

    1
    #import <XMPPFramework/XMPPFramework.h>

3.2 导入框架过程中问题解决

  • 1、用 Cocoapods 集成 XMPPFramework 遇 Module ‘KissXML’ not found 等问题解决方法。

    • 一般来说,通过 Coacopods 集成集成第三方框架,不会再有依赖库方面的问题,所以需要检查导入方式是否正确,最终找到原因,仔细看 githup 上导入说明

      1
      2
      3
      4
      5
      6
      Install

      The minimum deployment target is iOS 8.0 / macOS 10.8.

      The easiest way to install XMPPFramework is using CocoaPods. Remember to add to the top of your Podfile the
      use_frameworks! line (even if you are not using swift):
    • 因此,Podfile 里必须写入这一句

      1
      use_frameworks!
  • 2、Xcode8 之后 XMPP 重定义 Redefinition of module ‘dnssd’ 问题解决方法。

    • 在升级 Xcode 到 8 之后,原来的关于 XMPP 的项目运行报错,错误信息为: Redefinition of module ‘dnssd’。系统和XMPP框架同时用到了 ‘dnssd’,大概就是错误的原因。

    • 解决方案:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      # The version pushed to CocoaPods is very out of date, use master branch for now 
      pod 'XMPPFramework', :git => "https://github.com/robbiehanson/XMPPFramework.git", :branch => 'master'

      大概意思是需要更新 XMPP 框架,需要把 Podfile 文件中的
      pod 'XMPPFramework', '~> 3.6.6'

      pod 'XMPPFramework', :git => "https://github.com/robbiehanson/XMPPFramework.git", :branch => 'master'
      来替换

      或者直接改成
      pod 'XMPPFramework', '~> 3.7.0'
  • 3、在 pod update 的过程中有的童鞋会遇到下面这样的错误。

    • 这个是因为更新的 XMPP 框架中支持的最低版本为 iOS 8.0 / macOS 10.8。The minimum deployment target is iOS 8.0 / macOS 10.8.

    • 把 Podfile 文件中

      1
      platform:ios, '7.0'
- 的 7.0 改为 8.0 或以上。
  • 4、pod 更新完成了,出现下面这样的错误。

    • 到报错的工程里面搜一下

      1
      Enable Strict Checking of objc_msgSend Calls
- 改成相反的值就行了,别改没有报错的工程。

4、XMPPFramework 实现简单聊天

  • 聊天实现的原理就是,一个客户端通过 XMPP 协议把信息传给服务器,服务器再发消息发给另一个客户端。

4.1 用户注册

  • 初始化

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    /// 包含头文件
    #import <XMPPFramework/XMPPFramework.h>

    /// 遵守协议
    <XMPPStreamDelegate>

    /// 定义 XMPP 服务器相关信息
    #define HOST_DOMAIN @"jhq0228-macbookair.local"
    #define HOST_NAME @"jhq0228-macbookair.local"
    #define HOST_PORT 5222

    /// 注册的账号
    @property (nonatomic, copy) NSString *registerUserName;

    /// 注册的密码
    @property (nonatomic, copy) NSString *registerPassWord;

    /// XMPP 流
    @property (nonatomic, strong) XMPPStream *stream;

    /// 初始化
    self.stream = [[XMPPStream alloc] init];
    self.stream.hostName = HOST_NAME;
    self.stream.hostPort = HOST_PORT;
    [self.stream addDelegate:self delegateQueue:dispatch_get_main_queue()];
  • 与服务器建立链接

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    /// 与服务器建立链接
    [self connectToSercerWithUserName:self.registerUserName resource:nil];

    #pragma mark 与服务器连接通信

    /// 与服务器建立链接,自定义方法
    - (void)connectToSercerWithUserName:(NSString *)userName resource:(NSString *)resource {

    if ([self.stream isConnected]) {
    [self disconnectWithServer];
    }

    // jid
    self.stream.myJID = [XMPPJID jidWithUser:userName domain:HOST_DOMAIN resource:resource];

    NSError *error = nil;

    // 进行链接
    [self.stream connectWithTimeout:30.0 error:&error];

    if (error != nil) {
    NSLog(@"连接出现问题");
    }
    }
  • 进行注册

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    #pragma mark XMPPStreamDelegate 协议方法

    /// 与服务器连接成功
    - (void)xmppStreamDidConnect:(XMPPStream *)sender {

    NSError *error1 = nil;

    // 进行注册
    [self.stream registerWithPassword:self.registerPassWord error:&error1];

    if (error1 != nil) {
    NSLog(@"注册出现问题");
    }
    }

    /// 与服务器连接超时
    - (void)xmppStreamConnectDidTimeout:(XMPPStream *)sender {

    NSLog(@"连接服务器超时,请检查网络链接后再试!");
    }

    /// 注册成功
    - (void)xmppStreamDidRegister:(XMPPStream *)sender {

    NSLog(@"注册成功");
    }

    /// 注册失败
    - (void)xmppStream:(XMPPStream *)sender didNotRegister:(DDXMLElement *)error {

    NSLog(@"注册失败");
    }

4.2 用户登录、注销

  • 初始化

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    /// 包含头文件
    #import <XMPPFramework/XMPPFramework.h>

    /// 遵守协议
    <XMPPStreamDelegate>

    /// 定义 XMPP 服务器相关信息
    #define HOST_DOMAIN @"jhq0228-macbookair.local"
    #define HOST_NAME @"jhq0228-macbookair.local"
    #define HOST_PORT 5222

    /// 登录的账号
    @property (nonatomic, copy) NSString *loginUserName;

    /// 登录的密码
    @property (nonatomic, copy) NSString *loginPassWord;

    /// XMPP 流
    @property (nonatomic, strong) XMPPStream *stream;

    /// 初始化
    self.stream = [[XMPPStream alloc] init];
    self.stream.hostName = HOST_NAME;
    self.stream.hostPort = HOST_PORT;
    [self.stream addDelegate:self delegateQueue:dispatch_get_main_queue()];
  • 与服务器建立链接

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    /// 与服务器建立链接
    [self connectToSercerWithUserName:self.loginUserName resource:nil];

    #pragma mark 与服务器连接通信

    /// 与服务器建立链接,自定义方法
    - (void)connectToSercerWithUserName:(NSString *)userName resource:(NSString *)resource {

    if ([self.stream isConnected]) {
    [self disconnectWithServer];
    }

    // jid
    self.stream.myJID = [XMPPJID jidWithUser:userName domain:HOST_DOMAIN resource:resource];

    NSError *error = nil;

    // 进行连接
    [self.stream connectWithTimeout:30.0 error:&error];

    if (error != nil) {
    NSLog(@"连接出现问题");
    }
    }
  • 进行登录认证

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    #pragma mark XMPPStreamDelegate 协议方法

    /// 与服务器连接成功
    - (void)xmppStreamDidConnect:(XMPPStream *)sender {

    NSError *error = nil;

    // 进行登录认证
    [self.stream authenticateWithPassword:self.loginPassWord error:&error];

    if (error != nil) {
    NSLog(@"登录认证出现问题");
    }
    }

    /// 与服务器连接超时
    - (void)xmppStreamConnectDidTimeout:(XMPPStream *)sender {

    NSLog(@"连接服务器超时,请检查网络链接后再试!");
    }

    /// 登录成功
    - (void)xmppStreamDidAuthenticate:(XMPPStream *)sender {

    NSLog(@"登录成功");

    // 设置用户在线状态,如果没有添加,别人给你发的消息服务器默认为离线状态,是不会给你发送的
    XMPPPresence *presence = [XMPPPresence presenceWithType:@"available"];
    [self.stream sendElement:presence];
    }

    /// 登录失败
    - (void)xmppStream:(XMPPStream *)sender didNotAuthenticate:(DDXMLElement *)error {

    NSLog(@"登录失败");
    }
  • 与服务器断开链接,用户注销

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    #pragma mark 与服务器连接通信

    /// 与服务器断开链接,用户注销,自定义方法
    - (void)disconnectWithServer {

    // 断开链接
    [self.stream disconnect];
    }

    #pragma mark XMPPStreamDelegate 协议方法

    /// 注销成功
    - (void)xmppStreamDidDisconnect:(XMPPStream *)sender withError:(NSError *)error {

    NSLog(@"注销成功");

    // 设置用户下线状态
    XMPPPresence *presene = [XMPPPresence presenceWithType:@"unavailable"];
    [self.stream sendElement:presene];
    }
  • 用户登录信息本地化存储

    • 添加第三方框架 SAMKeychain

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      /// 包含头文件
      #import <SAMKeychain/SAMKeychain.h>

      /// 用户名和密码
      @property (nonatomic, copy) NSString *userName;
      @property (nonatomic, copy) NSString *userPasswd;

      /// 是否记住密码
      @property (nonatomic, assign, getter=isSavePasswd) BOOL savePasswd;

      /// 保存用户登录信息
      - (void)saveUserLoginInfo {

      NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];

      [userDefaults setObject:self.userName forKey:@"userNameKey"];
      [userDefaults setBool:self.isSavePasswd forKey:@"isSavePwdKey"];
      [userDefaults synchronize];

      if (self.isSavePasswd) {
      [SAMKeychain setPassword:self.userPasswd forService:[NSBundle mainBundle].bundleIdentifier account:self.userName];

      NSLog(@"保存用户登录信息");

      } else {
      self.userPasswd = nil;
      [SAMKeychain deletePasswordForService:[NSBundle mainBundle].bundleIdentifier account:self.userName];

      NSLog(@"不保存用户登录信息");
      }
      }

      /// 读取用户登录信息
      - (void)loadUserLoginInfo {

      NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];

      self.userName = [userDefaults objectForKey:@"userNameKey"];
      self.savePasswd = [userDefaults boolForKey:@"isSavePwdKey"];

      self.userPasswd = [SAMKeychain passwordForService:[NSBundle mainBundle].bundleIdentifier account:self.userName];
      }

4.3 好友管理

  • 初始化

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    /// 遵守协议
    <XMPPStreamDelegate, XMPPRosterDelegate, XMPPRosterMemoryStorageDelegate>

    /// 好友列表
    @property (nonatomic, strong) XMPPRoster *roster;

    /// 本地好友存储器
    @property (nonatomic, strong) XMPPRosterMemoryStorage *rosterMemoryStorage;

    // 添加好友模块
    self.rosterMemoryStorage = [[XMPPRosterMemoryStorage alloc] init];
    self.roster = [[XMPPRoster alloc] initWithRosterStorage:self.rosterMemoryStorage
    dispatchQueue:dispatch_get_global_queue(0, 0)];
    [self.roster activate:self.stream]; // 激活
    [self.roster addDelegate:self delegateQueue:dispatch_get_main_queue()]; // 设置代理
    [self.roster setAutoFetchRoster:YES]; // 设置好友同步策略,XMPP 一旦连接成功,自动同步好友到本地
    [self.roster setAutoAcceptKnownPresenceSubscriptionRequests:NO]; // 关掉自动接收好友请求,默认开启自动同意
  • 获取好友列表

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    // 手动同步好友列表到本地好友存储器
    [self.roster fetchRoster];

    // 获取好友列表,从本地好友存储器中读取好友信息
    NSArray *users = self.rosterMemoryStorage.unsortedUsers;

    // 获取好友账号名称
    NSString *userName = [user[0] jid].user;

    // 获取好友昵称
    NSString *userName = [users[0] nickname];

    // 获取好友在线状态
    BOOL userStatus = [user[0] isOnline];

    #pragma mark XMPPRosterDelegate 协议方法

    /// 开始同步好友列表到本地
    - (void)xmppRosterDidBeginPopulating:(XMPPRoster *)sender withVersion:(NSString *)version {

    }

    /// 同步到一个好友节点到本地
    - (void)xmppRoster:(XMPPRoster *)sender didReceiveRosterItem:(NSXMLElement *)item {

    }

    /// 同步好友列表到本地完成
    - (void)xmppRosterDidEndPopulating:(XMPPRoster *)sender {

    }
  • 刷新好友列表

    1
    2
    3
    4
    5
    6
    7
    #pragma mark - XMPPRosterMemoryStorageDelegate 协议方法

    /// 本地好友存储器发生改变
    - (void)xmppRosterDidChange:(XMPPRosterMemoryStorage *)sender {

    // 如果设置了自动同步,当服务器的好友列表发生改变时,会自动同步存入本地好友存储器
    }
  • 刷新好友状态

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    #pragma mark XMPPStreamDelegate 协议方法

    /// 好友状态改变
    - (void)xmppStream:(XMPPStream *)sender didReceivePresence:(XMPPPresence *)presence {

    // 收到对方取消定阅我的消息,对方删除我、对方状态改变

    if ([presence.type isEqualToString:@"unsubscribe"]) {

    // 从我的本地好友存储器中将对方移除
    [self.roster removeUser:presence.from];
    }
    }
  • 添加好友

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    /// 添加好友,自定义方法
    - (void)addFriendWithUserName:(NSString *)userName remarkName:(NSString *)remarkName {

    NSString *jidString = userName;

    // 判断有没有域名,如果没有域名,自己添加形成完整的 jid
    NSString *domainString = [NSString stringWithFormat:@"@%@", HOST_DOMAIN];
    if (![jidString containsString:domainString]) {
    jidString = [jidString stringByAppendingString:domainString];
    }

    XMPPJID *friendJID = [XMPPJID jidWithString:jidString];

    // 添加好友,remarkName 为备注名称
    [self.roster addUser:friendJID withNickname:remarkName];

    // [self.roster subscribePresenceToUser:friendJID];
    }
  • 收到添加好友申请

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    #pragma mark XMPPRosterDelegate 协议方法

    /// 收到添加好友请求
    - (void)xmppRoster:(XMPPRoster *)sender didReceivePresenceSubscriptionRequest:(XMPPPresence *)presence {

    NSString *name = [NSString stringWithFormat:@"添加 %@ 为好友?", presence.from.user];

    // 同意并添加对方为好友,YES 存入本地好友存储器
    [self.roster acceptPresenceSubscriptionRequestFrom:presence.from andAddToRoster:YES];

    // 拒绝添加对方为好友
    [self.roster rejectPresenceSubscriptionRequestFrom:presence.from];
    }
  • 删除好友

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    /// 删除好友,自定义方法
    - (void)removeFriendWithUserName:(NSString *)userName {

    NSString *jidString = userName;

    // 判断有没有域名,如果没有域名,自己添加形成完整的 jid
    NSString *domainString = [NSString stringWithFormat:@"@%@", HOST_DOMAIN];
    if (![jidString containsString:domainString]) {
    jidString = [jidString stringByAppendingString:domainString];
    }

    XMPPJID *friendJID = [XMPPJID jidWithString:jidString];

    // 删除好友
    [self.roster removeUser:friendJID];
    }

4.4 文本消息管理

  • 初始化

    1
    2
    3
    4
    5
    /// 遵守协议
    <XMPPStreamDelegate>

    /// 定义 XMPP 服务器相关信息
    #define HOST_DOMAIN @"jhq0228-macbookair.local"
  • 发送文本消息

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    /// 发送文本消息,自定义方法
    - (void)sendMessage:(NSString *)message toUser:(NSString *)userName {

    // 消息结构
    /*
    <message type="chat" to="xiaoming@example.com">
    <body>Hello World</body>
    </message>
    */

    NSString *jidString = userName; // 设置消息接收者
    NSString *domainString = [NSString stringWithFormat:@"@%@", HOST_DOMAIN];
    if (![jidString containsString:domainString]) {
    jidString = [jidString stringByAppendingString:domainString];
    }

    // 构建消息
    NSXMLElement *msg = [NSXMLElement elementWithName:@"message"];
    [msg addAttributeWithName:@"type" stringValue:@"chat"];
    [msg addAttributeWithName:@"to" stringValue:jidString];

    NSXMLElement *body = [NSXMLElement elementWithName:@"body"];
    [body setStringValue:message]; // 设置文本消息内容

    [msg addChild:body];

    // 发送
    [self.stream sendElement:msg];
    }
  • 接收文本消息

    1
    2
    3
    4
    5
    6
    7
    #pragma mark XMPPStreamDelegate 协议方法

    /// 接收到消息
    - (void)xmppStream:(XMPPStream *)sender didReceiveMessage:(XMPPMessage *)message {

    NSString *msg = [[message elementForName:@"body"] stringValue];
    }
  • 消息回执

    • 这个是 XEP-0184 协议的内容。

    • 发送消息时附加回执请求

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      // 消息结构
      /*
      <message
      from="northumberland@shakespeare.lit/westminster"
      id="richars2-4.1.247"
      to="kingrichard@royalty.england.lit/throne">
      <body>Hello World</body>
      <request xmlns="urn:xmpp:receipts"/>
      </message>
      */

      NSString *jidString = userName; // 设置消息接收者
      NSString *domainString = [NSString stringWithFormat:@"@%@", HOST_DOMAIN];
      if (![jidString containsString:domainString]) {
      jidString = [jidString stringByAppendingString:domainString];
      }

      // 构建消息
      NSString *siID = [XMPPStream generateUUID];
      XMPPJID *jid = [XMPPJID jidWithString:jidString];

      XMPPMessage *msg = [XMPPMessage messageWithType:@"chat" to:jid elementID:siID];

      NSXMLElement *receipt = [NSXMLElement elementWithName:@"request" xmlns:@"urn:xmpp:receipts"];
      [msg addChild:receipt]; // 设置消息回执

      [msg addBody:message]; // 设置消息内容

      // 发送
      [self.stream sendElement:msg];
    • 收到回执请求的消息,发送回执

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      /// 接收到消息,XMPPStreamDelegate 协议方法
      - (void)xmppStream:(XMPPStream *)sender didReceiveMessage:(XMPPMessage *)message {

      // 消息结构
      /*
      <message
      from="kingrichard@royalty.england.lit/throne"
      id="bi29sg183b4v"
      to="northumberland@shakespeare.lit/westminster">
      <received xmlns="urn:xmpp:receipts" id="richars2-4.1.247">
      </message>
      */

      // 回执判断
      NSXMLElement *request = [message elementForName:@"request"];

      if (request) {

      // 消息回执
      if ([request.xmlns isEqualToString:@"urn:xmpp:receipts"]) {

      // 组装消息回执
      XMPPMessage *msg = [XMPPMessage messageWithType:[message attributeStringValueForName:@"type"]
      to:message.from
      elementID:[message attributeStringValueForName:@"id"]];

      NSXMLElement *recieved = [NSXMLElement elementWithName:@"received" xmlns:@"urn:xmpp:receipts"];
      [msg addChild:recieved];

      // 发送回执
      [self.stream sendElement:msg];
      }

      } else {

      NSXMLElement *received = [message elementForName:@"received"];

      if (received) {

      // 消息回执
      if ([received.xmlns isEqualToString:@"urn:xmpp:receipts"]) {

      // 发送成功
      NSLog(@"message send success!");
      }
      }
      }

      // 消息处理
      // ...
      }

4.5 图片消息管理

  • 图片和语音文件发送的基本思路:

    • 先将图片/语音转化成二进制文件,然后将二进制文件进行 base64 编码,编码成字符串。在即将发送的 message 内添加一个子节点,节点的 stringValue(节点的值)设置这个编码后的字符串。
    • 然后消息发出后取出消息文件的时候,通过 messageType 先判断是不是图片/语音信息,如果是图片/语音信息先通过自己之前设置的节点名称,把这个子节点的 stringValue 取出来,应该是一个 base64 之后的字符串。
  • 选择图片

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    /// 遵守协议
    <UIImagePickerControllerDelegate, UINavigationControllerDelegate>

    UIImagePickerController *picker = [[UIImagePickerController alloc]init];
    picker.delegate = self;
    [self presentViewController:picker animated:YES completion:nil];

    #pragma mark - UIImagePickerControllerDelegate 代理方法

    - (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info {

    UIImage *image = info[UIImagePickerControllerOriginalImage];
    NSData *imageData = UIImagePNGRepresentation(image);

    // 发送图片消息,自定义方法
    [[XMPPManager defaultManager] sendMessage:imageData msgType:@"image" toUser:self.userName];

    [self dismissViewControllerAnimated:YES completion:nil];
    }
  • 发送图片消息

    • msgType 自定义消息类型,image:图片消息,audio:音频消息

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      // 发送图片消息
      [[XMPPManager defaultManager] sendMessage:imageData msgType:@"image" toUser:self.userName];

      /// 发送图片/音频消息,自定义方法
      - (void)sendMessage:(NSData *)msgData msgType:(NSString *)type toUser:(NSString *)userName {

      NSString *jidString = userName; // 设置消息接收者
      NSString *domainString = [NSString stringWithFormat:@"@%@", HOST_DOMAIN];
      if (![jidString containsString:domainString]) {
      jidString = [jidString stringByAppendingString:domainString];
      }

      XMPPJID *jid = [XMPPJID jidWithString:jidString];
      XMPPMessage *msg = [XMPPMessage messageWithType:@"chat" to:jid];

      [msg addBody:type];

      // 转换成 base64 的编码
      NSString *base64str = [msgData base64EncodedStringWithOptions:0];

      // 设置节点内容
      XMPPElement *attachment = [XMPPElement elementWithName:@"attachment" stringValue:base64str];

      // 包含子节点
      [msg addChild:attachment];

      // 发送消息
      [self.stream sendElement:msg];
      }
  • 接收图片消息

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    #pragma mark XMPPStreamDelegate 协议方法

    /// 接收到消息
    - (void)xmppStream:(XMPPStream *)sender didReceiveMessage:(XMPPMessage *)message {

    if ([message.body isEqualToString:@"image"]) {

    for (XMPPElement *node in message.children) {

    // 取出消息的解码
    NSString *base64str = node.stringValue;
    NSData *data = [[NSData alloc] initWithBase64EncodedString:base64str options:0];

    UIImage *image = [[UIImage alloc] initWithData:data];
    }
    }
    }

4.6 语音消息管理

  • 图片和语音文件发送的基本思路:

    • 先将图片/语音转化成二进制文件,然后将二进制文件进行 base64 编码,编码成字符串。在即将发送的 message 内添加一个子节点,节点的 stringValue(节点的值)设置这个编码后的字符串。
    • 然后消息发出后取出消息文件的时候,通过 messageType 先判断是不是图片/语音信息,如果是图片/语音信息先通过自己之前设置的节点名称,把这个子节点的 stringValue 取出来,应该是一个 base64 之后的字符串。
  • 录制/播放语音

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    /// 包含头文件
    #import <AVFoundation/AVFoundation.h>

    /// 录音器
    @property(nonatomic, strong) AVAudioRecorder *recorder;

    /// 录音时长
    @property(nonatomic, assign) NSTimeInterval recordTime;

    /// 录音地址
    @property(nonatomic, strong) NSURL *recordURL;

    /// 播放器
    @property(nonatomic, strong) AVAudioPlayer *player;

    /// 开始录音

    // 自定义方法
    - (IBAction)startRecord:(UIButton *)sender {

    // 创建录音文件保存路径
    NSString *urlStr = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
    self.recordURL = [NSURL URLWithString:[urlStr stringByAppendingPathComponent:@"myRecord.caf"]];

    // 创建录音格式设置
    NSMutableDictionary *dicM = [NSMutableDictionary dictionary];
    [dicM setObject:@(kAudioFormatLinearPCM) forKey:AVFormatIDKey]; // 设置录音格式
    [dicM setObject:@(8000) forKey:AVSampleRateKey]; // 设置录音采样率,8000 是电话采样率,对于一般录音已经够了
    [dicM setObject:@(1) forKey:AVNumberOfChannelsKey]; // 设置通道,这里采用单声道
    [dicM setObject:@(8) forKey:AVLinearPCMBitDepthKey]; // 每个采样点位数,分为 8、16、24、32
    [dicM setObject:@(YES) forKey:AVLinearPCMIsFloatKey]; // 是否使用浮点数采样
    NSDictionary *setting = [dicM copy];

    // 创建录音机
    self.recorder = [[AVAudioRecorder alloc] initWithURL:self.recordURL settings:setting error:NULL];

    // 开始录音
    [self.recorder record];
    }

    /// 停止录音

    // 自定义方法
    - (IBAction)stopRecord:(UIButton *)sender {

    NSTimeInterval time = self.recorder.currentTime;
    [self.recorder stop];

    if (time < 1.5) {
    NSLog(@"时间太短");
    } else {
    NSLog(@"录音完成");
    }
    }

    /// 播放录音

    // 自定义方法
    - (void)playAudioData:(NSData *)data {

    self.player = [[AVAudioPlayer alloc] initWithData:data error:NULL];
    self.player.numberOfLoops = 0;
    [self.player prepareToPlay];
    [self.player play];
    }
  • 发送语音消息

    • msgType 自定义消息类型,image:图片消息,audio:音频消息

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      // 发送语音消息
      NSData *audioData = [NSData dataWithContentsOfURL:self.recordURL];
      NSString *type = [NSString stringWithFormat:@"audio:%.1f秒", self.recordTime];
      [[XMPPManager defaultManager] sendMessage:audioData msgType:type toUser:self.userName];

      /// 发送图片/音频消息,自定义方法
      - (void)sendMessage:(NSData *)msgData msgType:(NSString *)type toUser:(NSString *)userName {

      NSString *jidString = userName; // 设置消息接收者
      NSString *domainString = [NSString stringWithFormat:@"@%@", HOST_DOMAIN];
      if (![jidString containsString:domainString]) {
      jidString = [jidString stringByAppendingString:domainString];
      }

      XMPPJID *jid = [XMPPJID jidWithString:jidString];
      XMPPMessage *msg = [XMPPMessage messageWithType:@"chat" to:jid];

      [msg addBody:type];

      // 转换成 base64 的编码
      NSString *base64str = [msgData base64EncodedStringWithOptions:0];

      // 设置节点内容
      XMPPElement *attachment = [XMPPElement elementWithName:@"attachment" stringValue:base64str];

      // 包含子节点
      [msg addChild:attachment];

      // 发送消息
      [self.stream sendElement:msg];
      }
  • 接收语音消息

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    #pragma mark XMPPStreamDelegate 协议方法

    /// 接收到消息
    - (void)xmppStream:(XMPPStream *)sender didReceiveMessage:(XMPPMessage *)message {

    if ([message.body hasPrefix:@"audio"]) {

    for (XMPPElement *node in message.children) {

    // 取出消息的解码
    NSString *base64str = node.stringValue;
    NSData *data = [[NSData alloc] initWithBase64EncodedString:base64str options:0];
    }
    }
    }

4.7 心跳检测

  • 为了监听服务器是否有效,增加心跳监听,用 XEP-0199 协议。

  • 在 XMPPFrameWork 框架下,封装了 XMPPAutoPing 和 XMPPPing 两个类都可以使用,因为 XMPPAutoPing 已经组合进了 XMPPPing 类,所以 XMPPAutoPing 使用起来更方便。

  • 初始化并启动 ping

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    /// 包含头文件
    #import <XMPPFramework/XMPPFramework.h>

    /// 遵守协议
    <XMPPAutoPingDelegate>>

    /// 心跳检测
    @property (nonatomic, strong) XMPPAutoPing *autoPing;

    // 添加心跳检测模块
    self.autoPing = [[XMPPAutoPing alloc] init]; // 发送的是一个 stream:ping,对方如果想表示自己是活跃的,应该返回一个 pong
    [self.autoPing activate:self.stream]; // 激活
    [self.autoPing addDelegate:self delegateQueue:dispatch_get_main_queue()];
    self.autoPing.pingInterval = 1000; // 定时发送 ping 时间
    self.autoPing.respondsToQueries = YES; // 不仅仅是服务器来得响应,如果是普通的用户,一样会响应
    self.autoPing.targetJID = [XMPPJID jidWithString:HOST_DOMAIN]; // 设置 ping 目标服务器
    // 如果为 nil,则监听 stream 当前连接上的那个服务器

    #pragma mark - XMPPAutoPingDelegate 协议方法

    /// 已经发送 ping
    - (void)xmppAutoPingDidSendPing:(XMPPAutoPing *)sender {

    NSLog(@"xmppAutoPingDidSendPing");
    }

    /// 接收到 pong
    - (void)xmppAutoPingDidReceivePong:(XMPPAutoPing *)sender {

    NSLog(@"xmppAutoPingDidReceivePong");
    }

    /// ping 超时
    - (void)xmppAutoPingDidTimeout:(XMPPAutoPing *)sender {

    NSLog(@"xmppAutoPingDidTimeout");
    }
  • 停止 ping

    1
    2
    3
    4
    // 停止 ping
    [self.autoPing deactivate];
    [self.autoPing removeDelegate:self];
    self.autoPing = nil;

4.8 自动重连

  • 当意外与服务器断开连接,自动重新连接上去,并且将上一次的信息自动加上去。

  • 初始化

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    /// 包含头文件
    #import <XMPPFramework/XMPPFramework.h>

    /// 遵守协议
    <XMPPReconnectDelegate>>

    /// 自动重连
    @property (nonatomic, strong) XMPPReconnect *reconnect;

    // 添加自动重连模块
    self.reconnect = [[XMPPReconnect alloc] init];
    [self.reconnect activate:self.stream]; // 激活
    [self.reconnect addDelegate:self delegateQueue:dispatch_get_main_queue()];
    self.reconnect.autoReconnect = YES; // 设置是否自动重新连接

    #pragma mark - XMPPReconnectDelegate 协议方法

    /// 设置是否自动重新连接
    - (BOOL)xmppReconnect:(XMPPReconnect *)sender shouldAttemptAutoReconnect:(SCNetworkConnectionFlags)connectionFlags {

    return YES;
    }

    /// 意外断开连接
    - (void)xmppReconnect:(XMPPReconnect *)sender didDetectAccidentalDisconnect:(SCNetworkConnectionFlags)connectionFlags {

    NSLog(@"didDetectAccidentalDisconnect");
    }

5、XMPPFramework 快速登录

6、XMPPFramework 重连以及其他问题

文章目录
  1. 1. 1、XMPP
  2. 2. 2、XMPPFramework 框架简介
    1. 2.1. 2.1 XMPPFramework 简介
    2. 2.2. 2.2 XMPPFramework 结构
    3. 2.3. 2.3 XMPPJID 类
    4. 2.4. 2.4 XMPPStream 类
    5. 2.5. 2.5 XMPPStreamDelegate
    6. 2.6. 2.6 XMPPIQ 类
    7. 2.7. 2.7 XMPPPresence 类
    8. 2.8. 2.8 XMPPMessage 类
  3. 3. 3、XMPPFramework 框架使用
    1. 3.1. 3.1 CocoaPods 导入框架
    2. 3.2. 3.2 导入框架过程中问题解决
  4. 4. 4、XMPPFramework 实现简单聊天
    1. 4.1. 4.1 用户注册
    2. 4.2. 4.2 用户登录、注销
    3. 4.3. 4.3 好友管理
    4. 4.4. 4.4 文本消息管理
    5. 4.5. 4.5 图片消息管理
    6. 4.6. 4.6 语音消息管理
    7. 4.7. 4.7 心跳检测
    8. 4.8. 4.8 自动重连
  5. 5. 5、XMPPFramework 快速登录
  6. 6. 6、XMPPFramework 重连以及其他问题
隐藏目录