一、XMPP概念
1) XMPP: 只是一套即时通讯的协议接口,并没有任务的实现代码
2) 实现XMPP协议的服务器: Openfire …等等
二、所有的XMPP相关操作、代理回调函数、都写在AppDelegate中, 便于维护
XMPP协议中的比较常用的类:
1) XMPPStream
2) XMPPJID
3) XMPPPresence
) XMPPRoster
- XMPPMesage(封装一个xml message)
) XMPPRoom
) IQ请求 — 基于XML格式的对象化
- XMLEelement
- IQ请求的set\get, result\error
- set: 发送数据给别人
- get: 从别人那获取数据
- result: IQ请求成功后的响应数据
- error: IQ请求失败
(注意: result元素的ID 一定要与 set/get请求的ID 一致)
) 各种模块对应的CoreDataStorage
三、XMPPStream对象: 基本上所有的操作通过该对象执行
1) 单例模式设计
在AppDelegate.h中声明获取单例静态XMPPStream对象方法
+ (XMPPStrean *)sharedXMPPStream;
在AppDelegate.m中实现单例方法
+ (XMPPStrean *)sharedXMPPStream {
static XMPPStream *sharedXMPPStream = nil;
if (sharedXMPPStream) {
//实例化XMPPStream
sharedXMPPStream = [[XMPPStream alloc] init];
//给XMPPStream添加代理和操作队列
[sharedXMPPStream addDelegate:代理对象 delegateQueue:给一个单例队列];
//给XMPPStream设置主机名
[sharedXMPPStream setHostName:@“XMPP服务器所在计算机的IP地址 (如果在当前机器上测试程序, 填写127.0.0.1)”];
//给XMPPStream设置主机的端口
[sharedXMPPStream setHostPort:XMPP服务器所在计算机的端口号];
}
}
四、使用XMPPStream对象, 登录XMPP服务器
//第一步, 设置当前登录用户的XMPPJID(XMPP服务器是根据XMPPJID来区分每一个用户和连接XMPP服务器)
[[self sharedXMPPStream] setMyJID:[XMPPJID jidWithString:@“用户名@XMPP服务器配置的名字(注意:是服务器的名字)”] ];
//第二步, 连接到XMPP服务器
[[self sharedXMPPStream] connectWithTimeout:连接超时时间 error:nil];
//第三步, 进入到XMPP服务器连接后的回调函数(连接成功)
- (void)xmppStream:(XMPPStream *)sender socketDidConnect:(GCDAsyncSocket *)socket {
//第四步, 验证登录密码
[[self sharedXMPPStream] authenticateWithPassword:保存的用户密码 error:nil ];
}
//第五步, 验证成功的回调函数 或 验证失败的回调函数
- (void)xmppStreamDidAuthenticate:(XMPPStream *)sender{
//第六步, 发送用户上线通知
XMPPPresence *prescence = [XMPPPresence presence];
[[self sharedXMPPStream] sendElement:prescence];
//进入app主界面
}
- (void)xmppStream:(XMPPStream *)sender didNotAuthenticate:(DDXMLElement *)error{
//验证不通过,返回登录界面
}
//第六步, 其他在线用户接收当当前用户的上线通知
- (void)xmppStream:(XMPPStream *)sender didReceivePresence:(XMPPPresence *)presence{
//保存到用户的在线用户数组
//刷新tableView显示更新在线用户
}
五、使用XMPPStream对象, 注册XMPP账号
//第一步, 因为没有账号, 所以用户名为空的形式 建立XMPP服务器连接 [[self sharedXMPPStream] connectWithTimeout:连接超时时间 error:nil];
//第二步, 设置要注册的用户名
[[self sharedXMPPStream] setMyJID:[NSString stringWithFormat:@“zhangsan@xmpp服务器名字”]]
//第三步, 进行注册
[[self sharedXMPPStream] registerWithPassword:用户登录密码 error:nil];
//第四步, 注册成功的回调函数 或 注册失败的回调函数
//成功
- (void)xmppStreamDidRegister:(XMPPStream *)sender {
//登录XMPP服务器
[[self sharedXMPPStream] authenticateWithPassword:保存的用户密码 error:nil ];
}
//失败
- (void)xmppStream:(XMPPStream *)sender didNotRegister:(NSXMLElement *)error {
//继续注册
}
六、好友操作
一)把好友操作的XMPP模块激活到当前XMPPStream对象
1)实例化好友操作模块
XMPPRosterCoreDataStorage *rosterCoreDataStorage = [XMPPRosterCoreDataStorage sharedInstance];
XMPPRoster *roster = [[XMPPRoster alloc] initWithRosterStorage:rosterCoreDataStorage];
//设置XMPPRoster自动接收好友订阅的请求 (可以让对方自动收到好友添加的请求)
[roster setAutoAcceptKnownPresenceSubscriptionRequests:YES];
2)注册到XMPPStream对象
[roster activate:_xmppStream];
3)给好友操作模块添加代理对象和操作队列
[roster addDelegate:self delegateQueue:给一个全局单例队列];
二)添加好友
//第一步, 构造要添加的好友的XMPPJID
XMPPJID *JID = [XMPPJID jidWithString:[NSString stringWithFormat:@“待加好友的用户名@XMPP服务器名字或XMPP服务器IP地址”]];
//第二步, 发送加好友请求
[[self sharedXMPPStream] subscribePresenceToUser:JID];
//第三步, 被添加好友的用户接收到好友添加请求的回调函数执行
- (void)xmppRoster:(XMPPRoster *)sender didReceivePresenceSubscriptionRequest:(XMPPPresence *)presence {
//取得好友状态
NSString *presenceType = [presence type];
//构造好友添加请求的用户的XMPPJID
XMPPJID *fromUserJID = [XMPPJID jidWithString:[[presence from] user]];
//处理添加好友请求
[roster acceptPresenceSubscriptionRequestFrom:fromUserJID andAddToRoster:YES];
}
//第四步, 自动发送上线通知
三) 删除好友
//第一步, 构造要删除好友的XMPPJID
XMPPJID *removeUserJID = [XMPPJID jidWithString:[NSString stringWithFormat:@"%@@%@",userName, @"qq.xzh.com"] ];
//第二步, 删除好友
[roster removeUser:removeUserJID];
四)查询好友列表
//第一步, 实例化NSFetchedResultsController
NSFetchedResultsController *fetchVC = [[NSFetchedResultsController alloc]
initWithFetchRequest:[NSFetchRequest fetchRequestWithEntityName:@"XMPPUserCoreDataStorageObject"]
managedObjectContext:[[APPDelegate对象 getXMPPRosterCoreDataStorage] mainThreadManagedObjectContext]
sectionNameKeyPath:nil
cacheName:nil];
//第二步, 设置NSFetchRequest的排序方式
//第三步, 执行查询
[fetchVC performFetch:nil];
//第四步, 更新tableView显示好友列表
1)numberOfSectionsInTableView
return _fetchRsultVC.sections.count;
2)numberOfRowsInSection
id<NSFetchedResultsSectionInfo> dataList = _fetchRsultVC.sections[section];
return [dataList numberOfObjects];
3)cellForRowAtIndexPath
XMPPUserCoreDataStorageObject *user = [_fetchRsultVC objectAtIndexPath:indexPath];
[cell.detailTextLabel setText:user.displayName];
4)didSelectRowAtIndexPath
XMPPUserCoreDataStorageObject *user = [_fetchRsultVC objectAtIndexPath:indexPath];
pushViewController时,传递user参数
六、消息操作
一)发送消息
1)文本消息
发送消息的xml格式:
<message type="chat" to="xiaoming@example.com">
<body>Hello World!<body />
<message />
代码:
//1) 构造消息体XML根元素 -- <message />
NSXMLElement *message = [NSXMLElement elementWithName:@"message"];
//2) 给<message /> 添加参数to以及参数值
[message addAttributeWithName:@"to" stringValue:@“接收消息的用户JID字符串(zhangsan@xmpp服务器名字)”];
//3)给<message /> 添加参数type以及参数值
[message addAttributeWithName:@"type" stringValue:@"chat"];
//4) 构造消息体XML子元素 -- <body />
NSXMLElement *body = [NSXMLElement elementWithName:@"body"];
//5) 给 <body />元素设置值
[body setStringValue:@"聊天消息内容”];
//6) <message /> 添加子元素 <body />
[message addChild:body];
//7) 发送<message />
[[self sharedXMPPStream] sendElement:message];
2)发送其他自定义格式的消息 (使用带内传输 — 传输XMPP XML字符串流)
传输途径:
客户端A发送XML数据流 --> XMPP服务器 --> 客户端B接收XML数据流
发送消息的xml格式:(自定义XML)
<message type=“不是系统的chat, 自己定义一个消息类型“ to="xiaoming@example.com">
<自定义消息体标签>将传输的文件使用base64压缩成的NSString</自定义消息体标签>
<message />
代码:
NSXMLElement *message = [NSXMLElement elementWithName:@"message"];
[message addAttributeWithName:@"to" stringValue:@“接收消息的用户JID字符串(zhangsan@xmpp服务器名字)”];
[message addAttributeWithName:@“type” stringValue:@“自定义消息类型”];
//1. 将文件转换成NSString(文件->NSData->使用base64压缩转换成NSString)
NSData *imageData = UIImagePNGRepresentation(image);
NSString *picStr = [imageData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];
//2. 构造自定义消息体标签(NSXMLElement)
NSXMLElement *imageString = [NSXMLElement elementWithName:@"imageString"];
//3. 给小消息体设置value
[imageString setStringValue:picStr];
//6) <message /> 添加子元素 自定义消息体元素
[message addChild:imageString];
//7) 发送<message />
[[self sharedXMPPStream] sendElement:message];
//发送Message后, 执行对方用户的didReceiveMessage回调函数
//解析出我们自己定义的消息
<message type="chat" to="xiaoming@example.com">
<imageString>文件的字符串流<imageString />
<message />
if ([[[message attributeForName:@"type"] stringValue] isEqualToString:@“自定义消息类型”]) {
//1) 取出图片的NSString
NSString *picString = [[message elementForName:@"imageString"] stringValue];
//2)将图片字符串->base64->NSData
NSData *imageData = [[NSData alloc] initWithBase64EncodedString:picString options:NSDataBase64DecodingIgnoreUnknownCharacters];
//3)将NSData->UIImage
// UIImage *image = [[UIImage alloc] initWithData:imageData];
//4)更新UI
__weak NSData *weakImageData = imageData;
__weak XMPPOperation *weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
if (weakSelf.receivePic != nil) {
weakSelf.receivePic(weakImageData);
}
});
}
3)发送其他自定义格式的消息 (使用带外传输 — 使用Socket5传输文件)
例子:客户端A 向 客户端B , 发送文件
//第一步, A询问B是否可以建立Socket连接(IQ请求包含一个SI请求包)
//1. 发送IQ请求格式
<iq type='set' id=‘当前IQ请求的操作ID’ to='test@dd.antkingdom.com' >
<si xmlns='http://jabber.org/protocol/si' id='si操作ID profile='http://jabber.org/protocol/si/profile/file-transfer'>
<file xmlns='http://jabber.org/protocol/si/profile/file-transfer' name='backup.txt' size='2043'/>
<feature xmlns='http://jabber.org/protocol/feature-neg'>
<x xmlns='jabber:x:data' type='form'>
<field var='stream-method' type='list-single'>
<option><value>http://jabber.org/protocol/bytestreams</value></option>
</field>
</x>
</feature>
</si>
</iq>
//2. 组装上面的xml格式
NSXMLElement *iq = [NSXMLElement elementWithName:@"iq"];
[iq addAttributeWithName:@"type" stringValue:@"set"];
[iq addAttributeWithName:@"id" stringValue:@"请求ID"];
[iq addAttributeWithName:@"to" stringValue:toJID];
NSXMLElement *si = [NSXMLElement elementWithName:@"si" xmlns:@"http://jabber.org/protocol/si"];
[si addAttributeWithName:@"id" stringValue:@"si的ID"];
[si addAttributeWithName:@"profile" stringValue:@"http://jabber.org/protocol/si/profile/file-transfer"];
NSXMLElement *file = [NSXMLElement elementWithName:@"file" xmlns:@"http://jabber.org/protocol/si/profile/file-transfer"];
[file addAttributeWithName:@"name" stringValue:@"要传输的文件名字"];
[file addAttributeWithName:@"size" stringValue:@"要传输的文件大小"];
NSXMLElement *feature = [NSXMLElement elementWithName:@"feature" xmlns:@"http://jabber.org/protocol/feature-neg"];
NSXMLElement *x = [NSXMLElement elementWithName:@"x" xmlns:@"jabber:x:data"];
[x addAttributeWithName:@"type" stringValue:@"from"];
NSXMLElement *field = [NSXMLElement elementWithName:@"field"];
[field addAttributeWithName:@"var" stringValue:@"stream-method"];
[field addAttributeWithName:@"type" stringValue:@"list-single"];
NSXMLElement *option = [NSXMLElement elementWithName:@"option"];
NSXMLElement *value = [NSXMLElement elementWithName:@"value"];
[value setStringValue:@"http://jabber.org/protocol/bytestreams"];
[option addChild:value];
[field addChild:option];
[x addChild:field];
[feature addChild:x];
[si addChild:feature];
[si addChild:file];
[iq addChild:si];
//3. 发送IQ请求
[[self sharedXMPPStream] sendElement:iq];
//第二步, 在B的接收IQ请求的回调函数中(包含一个SI响应包)
//SI请求包的XML格式
<iq id="gaim8215f9ef" to="si请求包发送者jid字符串" type="result">
<si id="gaim8215f9f0" xmlns="http://jabber.org/protocol/si">
<feature xmlns="http://jabber.org/protocol/feature-neg">
<x type="submit" xmlns="jabber:x:data">
<field var="stream-method">
<value>http://jabber.org/protocol/bytestreams<value>
</field>
</x>
</feature>
</si>
</iq>
SI响应包 XML结构:
<iq id="iq_13" to="iphone@xxxxx/xiff" from="android@xxxxx/Spark 2.6.3" type="result">
<si xmlns="http://jabber.org/protocol/si">
<feature xmlns="http://jabber.org/protocol/feature-neg">
<x xmlns="jabber:x:data" type="submit">
<field var="stream-method">
<value>http://jabber.org/protocol/bytestreams</value>
<value>http://jabber.org/protocol/ibb</value>
</field>
</x>
</feature>
</si>
</iq>
//第三步, B收到SI请求包
- (BOOL)xmppStream:(XMPPStream *)sender didReceiveIQ:(XMPPIQ *)iq {
if ([[[iq attributeForName:@"type"] stringValue]isEqualToString:@"set"]) {//set形式的IQ请求, 传递数据到对方
//1)记录客户端Socket的XMPPJID字符串
customJID = [[iq from] user];
//第四步, B给A发送SI响应包
[self sendSIResponse:iq];//自定义sendSIResponse函数,按照XML格式组装IQ请求, 并发送
//第五步, 请求XMPP服务器, 查询可用的代理服务器列表
[self requestXMPPServerForProxy];//自定义requestXMPPServerForProxy函数
}
}
- (void)requestXMPPServerForProxy {
/*
查询代理服务器列表XML:
<iq id="iq_15" type="get">
<query xmlns="http://jabber.org/protocol/disco#items" />
</iq>
*/
NSXMLElement *iq = [NSXMLElement elementWithName:@"iq"];
[iq addAttributeWithName:@"type" stringValue:@"get"];
NSXMLElement *query = [NSXMLElement elementWithName:@"query" xmlns:@"http://jabber.org/protocol/disco#items"];
[iq addChild:query];
[[self sharedXMPPStreamInstance] sendElement:iq];
}
//第六步, 1) A收到B发送到SI响应包 , 2) B收到XMPP服务器发回的代理服务器列表
1) A收到B发送到SI响应包
//A的接收IQ请求的回调函数中
- (BOOL)xmppStream:(XMPPStream *)sender didReceiveIQ:(XMPPIQ *)iq {
}
2) B收到XMPP服务器发回的代理服务器列表
//B的接收IQ请求的回调函数中
/*
返回的XMPP代理服务器列表XML:
<iq type="result" id="iq_15" to="iphone@192.168.1.xxx/xiff">
<query xmlns="http://jabber.org/protocol/disco#items">
<item jid="proxy.xxxxxx" name="Socks 5 Bytestreams Proxy"/>
<item jid="pubsub.xxxxxx" name="Publish-Subscribe service"/>
<item jid="conference.xxxxxxx" name="公共房间"/>
<item jid="search.xxxxxx" name="User Search"/>
</query>
</iq>
*/
- (BOOL)xmppStream:(XMPPStream *)sender didReceiveIQ:(XMPPIQ *)iq {
if ([iq.type isEqualToString:@"result"]) {
if ([iq elementForName:@"query"] != nil) {
NSXMLElement *query = [iq elementForName:@"query"];
if([[query attributeStringValueForName:@"xmlns"] isEqualToString:@"http://jabber.org/protocol/disco#items"]) {
//1. 选中某一个代理服务器
NSArray *items = [query elementsForName:@"item"];
NSString *jid = @"";
NSString *name = @"";
for (NSXMLElement *item in items) {
if ([[[item elementForName:@"name"] stringValue] isEqualToString:@"Socks 5 Bytestreams Proxy"]) {
name = @"Socks 5 Bytestreams Proxy";
jid = [[item elementForName:@"jid"] stringValue];
}
//2. 请求XMPP服务器, 查询选中的代理服务器的相信信息(ip地址, port)
if (jid && name) {
NSXMLElement *iq_new = [NSXMLElement elementWithName:@"iq"];
[iq_new addAttributeWithName:@"id" stringValue:@"iqID"];
[iq_new addAttributeWithName:@"type" stringValue:@"get"];
[iq_new addAttributeWithName:@"to" stringValue:jid];
NSXMLElement *query = [NSXMLElement elementWithName:@"query" xmlns:@"http://jabber.org/protocol/disco#items"];
[iq_new addChild:query];
[[self sharedXMPPStreamInstance] sendElement:iq_new];
}
}
}
}
}
//第七步, B收到XMPP服务器发回的代理服务器具体信息
- (BOOL)xmppStream:(XMPPStream *)sender didReceiveIQ:(XMPPIQ *)iq {
if ([iq.type isEqualToString:@"result"]) {
if ([iq elementForName:@"query"] != nil) {
if ([[query attributeStringValueForName:@"xmlns"] isEqualToString:@"http://jabber.org/protocol/bytestreams"]) {
// B取出XMPP服务器的代理服务器数据
if ([query elementForName:@"streamhost"]) {
NSString *ipAddress = @"";
NSString *port = @"";
NSString *jid = @"";
//1) 取出服务器发回的代理服务器详细数据(ip和port)
if ([query childCount] > 0) {
NSXMLElement *streamhost = [query elementForName:@"streamhost"];
ipAddress = [streamhost attributeStringValueForName:@"host"];
port = [streamhost attributeStringValueForName:@"port"];
jid = [streamhost attributeStringValueForName:@"jid"];
}
//2)服务Socket创建服务器Socket, 连接得到的代理服务器
[self createSocketAndStartListeningWithJID:jid ip:ipAddress port:port];
//3)通知客户方连接代理服务器
[self notifyCostemToConnectProxyWithWithJID:jid ip:ipAddress port:port customJID:customJID];
}
}
}
}
}
//B创建服务器Socket连接代理服务器
- (void)createSocketAndStartListeningWithJID:(NSString *)jid ip:(NSString *)ip port:(NSString *)port{
//1)创建Socket
TURNSocket *serverSocket = [[TURNSocket alloc] initWithStream:[self sharedXMPPStreamInstance] toJID:[XMPPJID jidWithString:jid]];
//2)保存当前Socket对象
[_serverSocketArray addObject:serverSocket];
//3)开启Socket
[serverSocket startWithDelegate:self delegateQueue:给一个Socket对象的队列];
}
//B通知A建立Socket连接代理服务器
<iq to="android@192.168.1.xxxx/Spark 2.6.3" type="set" id="iq_19" from="iphone@192.168.1.xxx/xiff">
<query xmlns="http://jabber.org/protocol/bytestreams" mode="tcp" sid="82B0C697-C1DE-93F9-103E-481C8E7A3BD8">
<streamhost port="7777" host="192.168.1.xxx" jid="proxy.192.168.1.xxx" />
</query>
</iq>
- (void)notifyCostemToConnectProxyWithWithJID:(NSString *)jid ip:(NSString *)ip port:(NSString *)port customJID:cJid{
NSXMLElement *iq = [NSXMLElement elementWithName:@"iq"];
[iq addAttributeWithName:@"type" stringValue:@"set"];
[iq addAttributeWithName:@"id" stringValue:@"iqID"];
[iq addAttributeWithName:@"from" stringValue:@"服务方XMPPJID"];
[iq addAttributeWithName:@"to" stringValue:cJid];
NSXMLElement *query = [NSXMLElement elementWithName:@"query" xmlns:@"http://jabber.org/protocol/bytestreams"];
[query addAttributeWithName:@"mode" stringValue:@"tcp"];
[query addAttributeWithName:@"sid" stringValue:@"任意生成"];
NSXMLElement *streamhost = [NSXMLElement elementWithName:@"streamhost"];
[streamhost addAttributeWithName:@"jid" stringValue:jid];
[streamhost addAttributeWithName:@"port" stringValue:port];
[streamhost addAttributeWithName:@"ip" stringValue:ip];
[query addChild:streamhost];
[iq addChild:query];
[[self sharedXMPPStreamInstance] sendElement:iq];
}
//第八步, A接收到B的连接通知, 建立Socket连接到代理服务器
//第九步, A,B 分别向代理服务器激活文件流
//第十步, A,B 通过代理服务器来中转传输文件
六、聊天室操作
//第一步, 创建XMPPRoom
//1. 聊天室也是一个XMPPJID标识
XMPPJID *roomJID = [XMPPJID jidWithString:@“room01@qq.xzh.com”];
//2. 实例化XMPPRoom
XMPPRoomCoreDataStorage *roomStorage = [XMPPRoomCoreDataStorage sharedInstance];
XMPPRoom *room = [[XMPPRoom alloc] initWithRoomStorage: mid:roomJID];
//3. 激活XMPPRoom
[room active:_xmppStream];
//4. 给XMPPRoom添加代理和操作队列
[room addDelegate:self delegateQueue:给一个单例队列];
//第二步, 聊天室创建成功的回调函数
- (void)xmppRoomDidCreate(XMPPRoom *)sender {
}
//第三步, 加入聊天室
[room joinRoomWithNickname:@“昵称” history:nil];
//第四步, 加入聊天室成功的回调函数中 , 获取聊天室信息
- (void)xmppRoomDidJoin:(XMPPRoom *)sender {
[sender fetchConfigurationForm];
[sender fetchBanList];
[sender fetchMembersList];
[sender fetchModerastorList];
}
//第五步, 如果房间存在, 执行代理对象的回调函数
- (void)xmppRoom:(XMPPRoom *)sender didNotFetchBanList:(XMPPIQ *)iqError;
- (void)xmppRoom:(XMPPRoom *)sender didNotFetchMembersList:(XMPPIQ *)iqError;
- (void)xmppRoom:(XMPPRoom *)sender didNotFetchModeratorsList:(XMPPIQ *)iqError;
//第六步, 撤销聊天室
[room reactive:_xmppStream];
//聊天室撤掉的回调函数
- (void)xmppRoomDidLeave:(XMPPRoom *)sender {
}
//第七步, 其他的代理方法
//新人加入群聊
- (void)xmppRoom:(XMPPRoom *)sender occupantDidJoin:(XMPPJID *)occupantJID
{
}
//有人退出群聊
- (void)xmppRoom:(XMPPRoom *)sender occupantDidLeave:(XMPPJID *)occupantJID
{
}
//有人在群里发言
- (void)xmppRoom:(XMPPRoom *)sender didReceiveMessage:(XMPPMessage *)message fromOccupant:(XMPPJID *)occupantJID
{
}