XMPP iOS 基本操作笔记

1 篇文章 0 订阅
1 篇文章 0 订阅

一、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:@"siID"];

    [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)记录客户端SocketXMPPJID字符串

        customJID = [[iq from] user];

        

        //第四步, BA发送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) 取出服务器发回的代理服务器详细数据(ipport)

                    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  

{  



  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值