时间过得很快,我的第一份iOS工作做的就是IM应用(选用的是XMPP),如今也忘得差不多了.利用空闲时间来重写一遍小Demo就当复习一下.
原理我就不介绍了,服务器的安装与配置可以参考XMPP的mysql和openfire环境配置或者配置介绍这篇
环境配置转自陈怀哲首发自简开始吧!
我做的Demo地址为:(gitHub)[https://github.com/piaojin/XmppIm]里面有很多BUG不要太介意
先来一个流程图:
必要介绍
JID
XMPP的地址叫做JabberID(简写为JID),它用来标示XMPP网络中的各个XMPP实体。JID由三部分组成:domain,node identifier和resource。JID中domain是必不可少的部分。注意:domain和user部分是不分大小写的,但是resource区分大小写。
|
|
domain:通常指网络中的网关或者服务器。
node identifier:通常表示一个向服务器或网关请求和使用网络服务的实体(比如一个客户端),当然它也能够表示其他的实体(比如在多用户聊天系统中的一个房间)。
resource:通常表示一个特定的会话(与某个设备),连接(与某个地址),或者一个附属于某个节点ID实体相关实体的对象(比如多用户聊天室中的一个参加者)。
JID种类有:
|
|
例子:
stpeter@jabber.org:表示服务器jabber.org上的用户stpeter。
room@service:一个用来提供多用户聊天服务的特定的聊天室。这里 “room“ 是聊天室的名字, ”service“ 是多用户聊天服务的主机名。
room@service/nick:加入了聊天室的用户nick的地址。这里 “room“ 是聊天室的名字, ”service“ 是多用户聊天服务的主机名,”nick“ 是用户在聊天室的昵称。
为了标示JID,XMPP也有自己的URI,例如xmpp:stpeter@jabber.org,默认规则是在JID前加 xmpp:。
通信原语
XMPP通信原语有3种:message、presence和iq。
5.1 message
message是一种基本推送消息方法,它不要求响应。主要用于IM、groupChat、alert和notification之类的应用中。
主要 属性如下:
5.1.1 type属性,它主要有5种类型:
normal:类似于email,主要特点是不要求响应;
chat:类似于qq里的好友即时聊天,主要特点是实时通讯;
groupchat:类似于聊天室里的群聊;
headline:用于发送alert和notification;
error:如果发送message出错,发现错误的实体会用这个类别来通知发送者出错了;
5.1.2 to属性:标识消息的接收方。
5.1.3 from属性:指发送方的名字或标示。为防止地址外泄,这个地址通常由发送者的server填写,而不是发送者。
载荷(payload):例如body,subject
例子:
|
|
5.2 presence
presence用来表明用户的状态,如:online、away、dnd(请勿打扰)等。当改变自己的状态时,就会在stream的上下文中插入一个Presence元素,来表明自身的状态。要想接受presence消息,必须经过一个叫做presence subscription的授权过程。
5.2.1 属性:
5.2.1.1 type属性,非必须。有以下类别
subscribe:订阅其他用户的状态
probe:请求获取其他用户的状态
unavailable:不可用,离线(offline)状态
5.2.1.2 to属性:标识消息的接收方。
5.2.1.3 from属性:指发送方的名字或标示。
5.2.2 载荷(payload):
5.2.2.1 show:
chat:聊天中
away:暂时离开
xa:eXtend Away,长时间离开
dnd:勿打扰
5.2.2.2 status:格式自由,可阅读的文本。也叫做rich presence或者extended presence,常用来表示用户当前心情,活动,听的歌曲,看的视频,所在的聊天室,访问的网页,玩的游戏等等。
5.2.2.3 priority:范围-128~127。高优先级的resource能接受发送到bare JID的消息,低优先级的resource不能。优先级为负数的resource不能收到发送到bare JID的消息。
例子:
|
|
5.3 iq (Info / Query)
一种请求/响应机制,从一个实体从发送请求,另外一个实体接受请求,并进行响应。例如,client在stream的上下文中插入一个元素,向Server请求得到自己的好友列表,Server返回一个,里面是请求的结果。
主要的属性是type。包括:
Get :获取当前域值。类似于http get方法。
Set :设置或替换get查询的值。类似于http put方法。
Result :说明成功的响应了先前的查询。类似于http状态码200。
Error: 查询和响应中出现的错误。
例子:
|
|
XMPPFramework结构与核心类
在进入下一步之前,先给大家讲讲XMPPFramework的目录结构,以便新手们更容易读懂文章。我们来看看下图:
虽然这里有很多个目录,但是我们在开发中基本只关心Core和Extensions这两个目录下的类。各个目录主要用来干嘛的?
Authentication:这一看名字就知道与授权验证相关的。
Categories:主要是一些扩展,尤其是NSXMLElement+XMPP扩展是必备的。
Core:这里是XMPP的核心文件目录,我们最主要的目光还是要放在这个目录上。
Extensions:这个目录是XMPP的扩展,用于扩展各种协议和各种独立的功能,其下每个子目录都是对应的一个单独的子功能。我们最常用到的功能有Reconnect、Roster、CoreDataStorage等。
Utilities:都是辅助类,我们开发者不用关心这里。
Vendor:这个目录是XMPP所引用的第三方类库,如CocoaAsyncSocket、KissXML等,我们也不用关心这里。
阅读到此,对XMPPFramework的结构有所了解了吧!
概念知识
登录需要到账号,而所谓的账号其实就是用户唯一标识符(JID),在XMPP中使用XMPPJID类来表示。那么,用户唯一标识(JID)有什么组成?
JID一般由三部分构成:用户名,域名和资源名,格式为user@domain/resource,例如: test@example.com /Anthony。对应于XMPPJID类中的三个属性user、domain、resource。
如果没有设置主机名(HOST),则使用JID的域名(domain)作为主机名,而端口号是可选的,默认是5222,一般也没有必要改动它。
XMPPStream类
我们要与服务器连接,就必须通过XMPPStream类了,它提供了很多的API和属性设置,通过socket来实现的。我们看到Verdor目录了吗,包含了CocoaAsyncSocket这个非常有名的socket编程库。XMPPStream类还遵守并实现了GCDAsyncSocketDelegate代理,用于客户端与服务器交互。
|
|
当我们创建XMPPStream对象后,我们需要设置代理,才能回调我们的代理方法,这个是支持multicast delegate,也就是说对于一个XMPPStream对象,可以设置多个代理对象,其中协议是XMPPStreamDelegate:
|
|
而当我们不希望某个XMPPStream对象继续接收到代理回调时,我们通过这样的方式来移除代理:
|
|
接下来,我们要设置主机和端口,通过设置这两个属性:
|
|
XMPPStream有XMPPJID类对象作为属性,标识用户,因为我们后续很多操作都需要到myJID:
|
|
而管理用户在线状态的就交由XMPPPresence类了,它同样被作为XMPPStream的属性,组合到XMPPStream中,后续很多关于用户的操作是需要到处理用户状态的:
|
|
XMPPStreamDelegate
这个协议是非常关键的,我们的很多主要操作都集中在这个协议的代理回调上。它分为好几种类型的代理API,比如授权的、注册的、安全的等:
|
|
到此,也就理解了XMPPStream五五六六了吧!!!
XMPPIQ
消息查询(IQ)就是通过此类来处理的了。XMPP给我们提供了IQ方便创建的类,用于快速生成XML数据。若头文件声明如下:
|
|
IQ是一种请求/响应机制,从一个实体从发送请求,另外一个实体接受请求并进行响应。例如,client在stream的上下文中插入一个元素,向Server请求得到自己的好友列表,Server返回一个,里面是请求的结果。
有以下类别(可选设置如:get ):
get :获取当前域值。类似于http get方法。
set :设置或替换get查询的值。类似于http put方法。
result :说明成功的响应了先前的查询。类似于http状态码200。
error: 查询和响应中出现的错误。
下面是一个IQ例子:
|
|
XMPPPresence
这个类代表节点,我们通过此类提供的方法来生成XML数据。它代表用户在线状态,它的头文件内容很少的:
|
|
presence用来表明用户的状态,如:online、away、dnd(请勿打扰)等。当改变自己的状态时,就会在stream的上下文中插入一个Presence元素,来表明自身的状态。要想接受presence消息,必须经过一个叫做presence subscription的授权过程。
有以下类别(可选设置如:subscribe ):
subscribe:订阅其他用户的状态
probe:请求获取其他用户的状态
unavailable:不可用,离线(offline)状态
节点有以下类别,如dnd :
chat:聊天中
away:暂时离开
xa:eXtend Away,长时间离开
dnd:勿打扰
节点
这个节点表示状态信息,内容比较自由,几乎可以是所有类型的内容。常用来表示用户当前心情,活动,听的歌曲,看的视频,所在的聊天室,访问的网页,玩的游戏等等。
节点
范围-128~127。高优先级的resource能接受发送到bare JID的消息,低优先级的resource不能。优先级为负数的resource不能收到发送到bare JID的消息。
发送一个用户在线状态的例子:
|
|
XMPPMessage
XMPPMessage是XMPP框架给我们提供的,方便用于生成XML消息的数据,其头文件如下:
|
|
message是一种基本 推送 消息方法,它不要求响应。主要用于IM、groupChat、alert和notification之类的应用中。
有以下类别(可选设置如: chat ):
normal:类似于email,主要特点是不要求响应;
chat:类似于qq里的好友即时聊天,主要特点是实时通讯;
groupchat:类似于聊天室里的群聊;
headline:用于发送alert和notification;
error:如果发送message出错,发现错误的实体会用这个类别来通知发送者出错了;
节点
所要发送的内容就放在body节点下
消息节点的例子:
|
|
吧啦吧啦一大堆,其实我是从别人的文章copy来的
登录
登录的流程是这样:
1.初始化一个xmppStream
2.连接服务器(成功或者失败)
3.成功的基础上,服务器验证(成功或者失败)
4.成功的基础上,发送上线消息
初始化相关类
|
|
|
|
以上就是登录流程,比较暴力直接贴了一堆代码
##添加好友
添加实际是发送一个IQ请求
1234567891011121314151617181920212223242526 #pragma mark 添加好友- (void)addFriend:(UserModel *)user{if(user){//这里的nickname是我对它的备注,并非他的个人资料中得nickname[[XMPPManager shareInstanceManager].xmppRoster addUser:user.jid withNickname:user.userName];}}#pragma mark ===== 好友模块=======/** 收到出席订阅请求(代表对方想添加自己为好友) */- (void)xmppRoster:(XMPPRoster *)sender didReceivePresenceSubscriptionRequest:(XMPPPresence *)presence{//添加好友一定会订阅对方,但是接受订阅不一定要添加对方为好友self.pj_newFriend.jid = presence.from;NSString *message = [NSString stringWithFormat:@"【%@】想加你为好友",presence.from.bare];UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:nil message:message delegate:self cancelButtonTitle:@"拒绝" otherButtonTitles:@"同意", nil];[alertView show];}- (void)xmppStream:(XMPPStream *)sender didReceivePresence:(XMPPPresence *)presence{//收到对方取消定阅我得消息if ([presence.type isEqualToString:@"unsubscribe"]) {//从我的本地通讯录中将他移除[self.xmppRoster removeUser:presence.from];}}XMPPRoster获取好友列表(获取的是在线的好友列表
XMPPRoster 可以处理和好友相关的事:获取好友列表,添加好友,接收好友请求,同意添加好友,拒绝添加好友
XMPPRosterCoreDataStorage用于存储好友(需要知道一些CoreData相关)
初始化相关类
12345678910111213141516 - (XMPPRoster *)xmppRoster{if(!_xmppRoster){_xmppRoster = [[XMPPRoster alloc] initWithRosterStorage:self.xmppRosterCoreDataStorage dispatchQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)];//关闭自动同步好友列表,在需要拉取好友列表的地方调用fetchRoster方法去拉取[_xmppRoster setAutoFetchRoster:NO];}return _xmppRoster;}- (XMPPRosterCoreDataStorage *)xmppRosterCoreDataStorage{if(!_xmppRosterCoreDataStorage){// 3.好友模块 支持我们管理、同步、申请、删除好友_xmppRosterCoreDataStorage = [XMPPRosterCoreDataStorage sharedInstance];}return _xmppRosterCoreDataStorage;}
在需要拉取在线好友的地方:
|
|
以上代码执行后会走一下回调
|
|
同步sqlite中的好友
|
|
接下来就进入聊天模块了
发送文字
123456 //发送消息的方法/*** This method handles sending an XML stanza.* If the XMPPStream is not connected, this method does nothing.**/- (void)sendElement:(NSXMLElement *)element;
自己封装消息并且发送
|
|
发送图片
发送方式:
1:首先将图片变成2进制(NSData)格式,然后利用Base64将其变为字符串,当文字发送,然后在发送端添加设置其属性,接收端通过判断其属性来判断传过来的到底是啥。如果是图片再用Base64将字符串解成NSData然后转成图片即可。
2:将图片直接转为2进制,然后利用ASI将其上传到服务器,然后发送端发送你图片所在的地址给接收端,然后接收端从此地址下载即可。
关于图片发送:
语音的话首先通过AVAudioRecorder录音,选择好格式(acc,amr)。微信就是用的amr转码。然后剩下的跟图片方案一样
图片和音频文件发送的基本思路就是:
先将图片转化成二进制文件,然后将二进制文件进行base64编码,编码后成字符串。在即将发送的message内添加一个子节点,节点的stringValue(节点的值)设置这个编码后的字符串。然后消息发出后取出消息文件的时候,通过messageType 先判断是不是图片信息,如果是图片信息先通过自己之前设置的节点名称,把这个子节点的stringValue取出来,应该是一个base64之后的字符串.
|
|
图片的缓存
|
|
发送语音
1234567891011121314151617181920212223242526 //发送语音消息,PJVoiceMessage为自定义语音类,里面包含了录制好的语音的位置- (void)sendVoiceMesage:(PJVoiceMessage *)voiceMessage{XMPPMessage *message = [XMPPMessage messageWithType:@"chat" to:_chatUserModel.jid];[message addBody:VoiceMessage];[message addSubject:VoiceMessage];voiceMessage.showMessageIn = ShowMessageInRight;NSData *voiceData = [NSData dataWithContentsOfFile:voiceMessage.localUrl];NSString *voiceBase64Str = [voiceData base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength];// 设置节点内容,语音的内容base64strXMPPElement *attachment = [XMPPElement elementWithName:VoiceMessage stringValue:voiceBase64Str];NSLog(@"voiceBase64Str:%@",voiceBase64Str);// 包含子节点[message addChild:attachment];// 语音的节点名称(语音的加载是通过语音名称在到缓存中查询加载)XMPPElement *voiceAttachment = [XMPPElement elementWithName:XMPPElementVoiceMessage stringValue:voiceMessage.voiceName];// 包含子节点[message addChild:voiceAttachment];[self.chatArray addObject:voiceMessage];[self.tableView reloadData];[self tableViewScrollToBottom];[[XMPPManager shareInstanceManager].xmppStream sendElement:message];}
收到语音消息后缓存语音
|
|