在Android中使用 asmack 实现文件的接收和发送 - 移动开发

博主:xiaoweixiaowei 2023-02-10 条评论

本篇主要是基于最近帮助朋友在 Android 中使用 asmack 库实现文件的接收和发送 功能时,写了个参考示例,这里做个记录,以便于自己以后参考。


文件传输相关的XEP协议参考:

2XEP-0095StreamInitiation: http://xmpp.org/extensions/xep-0095.html

2XEP-0096SI FileTransfer: http://xmpp.org/extensions/xep-0096.html

2XEP-0030ServiceDiscovery: http://xmpp.org/extensions/xep-0030.html

2XEP-0065SOCKS5Bytestreams: http://xmpp.org/extensions/xep-0065.html

2XEP-0066Out of BandData: http://xmpp.org/extensions/xep-0066.html

2XEP-0047In-BandBytestreams: http://xmpp.org/extensions/xep-0047.html

本实例中主要采用的是SI + IBB 的方式发送和接收文件。

1,首先在连接和登录之前设置 ProviderManager,同时设置好全局的文件传输管理器(FileTransferManager) fileTransferManager,如下:

   /**
      *
登录 IM 服务器,同时负责  连接 和 登录
      */
     public boolean login(String user, String password)
     {
         ConnectionConfiguration conf = new  ConnectionConfiguration(
                  IM_HOST, IM_PORT, IM_SERVICE_NAME);
         conf.setDebuggerEnabled(true);
         conf.setCompressionEnabled(false);
         conf.setSendPresence(true);
          conf.setSASLAuthenticationEnabled(false);
          conf.setSecurityMode(ConnectionConfiguration.SecurityMode.disabled);
         connection = new  XMPPConnection(conf);

         ProviderManager pm =  ProviderManager.getInstance();

         // Private Data Storage
          pm.addIQProvider(”query”, ”jabber:iq:private”, new  PrivateDataManager.PrivateDataIQProvider());

         // Roster Exchange
          pm.addExtensionProvider(”x”, ”jabber:x:roster”, new  RosterExchangeProvider());

         // Message Events
          pm.addExtensionProvider(”x”, ”jabber:x:event”, new  MessageEventProvider());

         // Chat State
          pm.addExtensionProvider(”active”,  ”http://jabber.org/protocol/chatstates”, new  ChatStateExtension.Provider());
          pm.addExtensionProvider(”composing”,  ”http://jabber.org/protocol/chatstates”, new  ChatStateExtension.Provider());
          pm.addExtensionProvider(”paused”,  ”http://jabber.org/protocol/chatstates”, new  ChatStateExtension.Provider());
         pm.addExtensionProvider(”inactive”,  ”http://jabber.org/protocol/chatstates”, new  ChatStateExtension.Provider());
          pm.addExtensionProvider(”gone”,  ”http://jabber.org/protocol/chatstates”, new  ChatStateExtension.Provider());

         // XHTML
         pm.addExtensionProvider(”html”,  ”http://jabber.org/protocol/xhtml-im”, new  XHTMLExtensionProvider());

         // Group Chat Invitations
          pm.addExtensionProvider(”x”, ”jabber:x:conference”, new  GroupChatInvitation.Provider());

         // Service Discovery # Items
          pm.addIQProvider(”query”,  ”http://jabber.org/protocol/disco#items”, new  DiscoverItemsProvider());

         // Service Discovery # Info
          pm.addIQProvider(”query”,  ”http://jabber.org/protocol/disco#info”, new DiscoverInfoProvider());

         // Data Forms
          pm.addExtensionProvider(”x”, ”jabber:x:data”, new  DataFormProvider());

         // MUC User
          pm.addExtensionProvider(”x”,  ”http://jabber.org/protocol/muc#user”, new MUCUserProvider());

         // MUC Admin
          pm.addIQProvider(”query”,  ”http://jabber.org/protocol/muc#admin”, new MUCAdminProvider());

         // MUC Owner
          pm.addIQProvider(”query”,  ”http://jabber.org/protocol/muc#owner”, new MUCOwnerProvider());

         // Delayed Delivery
         pm.addExtensionProvider(”x”,  ”jabber:x:delay”, new DelayInformationProvider());

         // Version
         try {
              pm.addIQProvider(”query”, ”jabber:iq:version”,
                      Class.forName(”org.jivesoftware.smackx.packet.Version”));
         } catch (ClassNotFoundException e)  {
             // Not  sure what’s happening here.
         }

         // VCard
          pm.addIQProvider(”vCard”, ”vcard-temp”, new  VCardProvider());

         // Offline Message Requests
          pm.addIQProvider(”offline”, ”http://jabber.org/protocol/offline”,  new OfflineMessageRequest.Provider());

         // Offline Message Indicator
          pm.addExtensionProvider(”offline”,  ”http://jabber.org/protocol/offline”, new  OfflineMessageInfo.Provider());

         // Last Activity
          pm.addIQProvider(”query”, ”jabber:iq:last”, new  LastActivity.Provider());

         // User Search
          pm.addIQProvider(”query”, ”jabber:iq:search”, new  UserSearch.Provider());

         // SharedGroupsInfo
          pm.addIQProvider(”sharedgroup”, ”http://www.jivesoftware.org/protocol/sharedgroup”,  new SharedGroupsInfo.Provider());

         // JEP-33: Extended Stanza  Addressing
          pm.addExtensionProvider(”addresses”,  ”http://jabber.org/protocol/address”, new  MultipleAddressesProvider());

// FileTransfer
         pm.addIQProvider(”si”,  ”http://jabber.org/protocol/si”, new StreamInitiationProvider());
          pm.addIQProvider(”query”,  ”http://jabber.org/protocol/bytestreams”, new  BytestreamsProvider());

         // Privacy
         pm.addIQProvider(”query”,  ”jabber:iq:privacy”, new PrivacyProvider());

          pm.addIQProvider(”command”,  ”http://jabber.org/protocol/commands”,
                  new AdHocCommandDataProvider());

          pm.addExtensionProvider(”malformed-action”,
                  ”http://jabber.org/protocol/commands”,
                  new AdHocCommandDataProvider.MalformedActionError());

          pm.addExtensionProvider(”bad-locale”,
                  ”http://jabber.org/protocol/commands”,
                  new AdHocCommandDataProvider.BadLocaleError());

          pm.addExtensionProvider(”bad-payload”,
                  ”http://jabber.org/protocol/commands”,
                  new AdHocCommandDataProvider.BadPayloadError());

          pm.addExtensionProvider(”bad-sessionid”,
                  ”http://jabber.org/protocol/commands”,
                  new AdHocCommandDataProvider.BadSessionIDError());

          pm.addExtensionProvider(”session-expired”,
                  ”http://jabber.org/protocol/commands”,
                  new AdHocCommandDataProvider.SessionExpiredError());

         try
         {
              connection.connect();
              connection.login(user, password);
         }
         catch (XMPPException e)
         {
              e.printStackTrace();
             connection  = null;
             return  false;
         }

         fileTransferManager = new  FileTransferManager(connection);
         ServiceDiscoveryManager sdm =  ServiceDiscoveryManager.getInstanceFor(connection);
         if (sdm == null)
             sdm = new  ServiceDiscoveryManager(connection);
          sdm.addFeature(”http://jabber.org/protocol/disco#info”);
          sdm.addFeature(”jabber:iq:privacy”);
          FileTransferNegotiator.setServiceEnabled(connection, true);
         return true;
     }

在Android中使用 asmack 实现文件的接收和发送 - 移动开发

2,发送文件,如下:

           Button btnSendFile  = (Button) rootView.findViewById(R.id.send_file_btn);
              btnSendFile.setOnClickListener(new View.OnClickListener()
             {
                  @Override
                  public void onClick(View v)
                  {
                      Toast.makeText(getActivity(), ”send file”,  Toast.LENGTH_SHORT).show();
new SendFileTask().execute(new Uri[]{ uriFile });
                  }
             });

 // 将发送文件的操作采用 AsyncTask 的方式实现(SendFileTask):
     public class SendFileTask extends AsyncTask<Uri, Integer,  Long> {

         @Override
         protected Long  doInBackground(Uri… params) {

             if  (params.length < 1) {
                  return Long.valueOf(-1);
             }

             Uri  uriFile = params[0];
              FileTransferManager ftm = MainHelloIM.getInstance().getFileTransferManager();
             if (ftm !=  null)
             {
OutgoingFileTransfer oft =  ftm.createOutgoingFileTransfer(”blue@test.cn/Spark”);
                  try
                  {
                      String[] proj = { MediaStore.Images.Media.DATA };
                      Cursor actualp_w_picpathcursor = managedQuery(uriFile,proj,null,null,null);
                      int actual_p_w_picpath_column_index
                              = actualp_w_picpathcursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
                      actualp_w_picpathcursor.moveToFirst();
                      String filePath = actualp_w_picpathcursor.getString(actual_p_w_picpath_column_index);
                      File fileToSend = new File(filePath);
                      if (fileToSend.exists())
                      {
oft.sendFile(fileToSend, ”recv my file!”);
                          while (!oft.isDone())
                          {
                              if (oft.getStatus().equals(FileTransfer.Status.error))
                              {
                                  Log.e(TAG, ”send failed”);
                              }
                              else
                              {
                                  Log.i(TAG, ”status:” + oft.getStatus() + ”|progress:” +  oft.getProgress());
                              }
                              Thread.sleep(1000);
                          }
                      }
                  }
                  catch (XMPPException e)
                  {
                      e.printStackTrace();
                  }
                  catch (InterruptedException e)
                  {
                      e.printStackTrace();
                  }
             }

             return  Long.valueOf(0);
         }
     }

3,接收文件,主要通过设置 文件传输监听器 来实现:

            FileTransferManager ftm = MainHelloIM.getInstance().getFileTransferManager();
ftm.addFileTransferListener(new FileTransferListener()
             {
                  @Override
                  public void fileTransferRequest(FileTransferRequest fileTransferRequest)
                  {
                      Log.i(TAG, ”has file”);
IncomingFileTransfer transfer =  fileTransferRequest.accept();
                      File sdDir = Environment.getExternalStorageDirectory();
                      String filePath = sdDir.toString() + ”/” +  fileTransferRequest.getFileName();
                      try
                      {
transfer.recieveFile(new File(filePath));
                      }
                      catch (XMPPException e)
                      {
                          e.printStackTrace();
                      }
                  }
             });

下面为完整的文件传输的消息交互流程:


 /**
 *
前面主要遵循标准 XEP-0095 来协商采用的文件传输方式
 */

//—– negotiation profile and stream

hhy -> blue
// hhy 需要发送 p_w_picpath/png 格式的文件 bookCater.png 给 blue
 <iq id=”VFDHn-4” to=”blue@sharexun.cn/Spark”  from=”hhy@sharexun.cn/Smack” type=”set”>
     <si xmlns=”http://jabber.org/protocol/si”
         id=”jsi_7839940461539037483”
         mime-type=”p_w_picpath/png”
          profile=”http://jabber.org/protocol/si/profile/file-transfer”>
         <file  xmlns=”https://cache.yisu.com/upload/information/20200311/46/202455.jpg” size=”448” >
              <desc>recv my file!</desc>
         </file>
         <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>
                      <option>
                          <value>http://jabber.org/protocol/ibb</value>
                      </option>
                  </field>
             </x>
         </feature>
     </si>
 </iq>

blue -> hhy
// blue 返回的结果显示 blue 支持 bytestreams 和 ibb 两种方式传输文件
 <iq id=”VFDHn-4” to=”hhy@sharexun.cn/Smack”  from=”blue@sharexun.cn/Spark” 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>

//—– service discovery

hhy -> blue
 <iq id=”VFDHn-5” to=”blue@sharexun.cn/Spark”  type=”get”>
     <query xmlns=”http://jabber.org/protocol/disco#info”></query>
 </iq>

blue -> hhy
 <iq id=”VFDHn-5” to=”hhy@sharexun.cn/Smack”  type=”result” from=”blue@sharexun.cn/Spark”>
     <query  xmlns=”http://jabber.org/protocol/disco#info”>
         <identity  category=”client” name=”Smack” type=”pc”/>
         <feature  var=”http://jabber.org/protocol/xhtml-im”/>
         <feature  var=”http://jabber.org/protocol/muc”/>
         <feature  var=”http://jabber.org/protocol/bytestreams”/>
         <feature  var=”http://jabber.org/protocol/commands”/>
         <feature var=”http://jabber.org/protocol/si/profile/file-transfer”/>
         <feature  var=”http://jabber.org/protocol/si”/>
         <feature  var=”http://jabber.org/protocol/ibb”/>
     </query>
 </iq>

hhy -> [server]
 <iq id=”VFDHn-6” to=”sharexun.cn”  type=”get”>
     <query  xmlns=”http://jabber.org/protocol/disco#items”></query>
 </iq>

blue -> hhy
 <iq id=”VFDHn-5” to=”hhy@sharexun.cn/Smack”  type=”result” from=”blue@sharexun.cn/Spark”>
     <query  xmlns=”http://jabber.org/protocol/disco#info”>
         <identity category=”client”  name=”Smack” type=”pc”/>
         <feature  var=”http://www.xmpp.org/extensions/xep-0166.html#ns”/>
         <feature  var=”urn:xmpp:tmp:jingle”/>
     </query>
 </iq>

[server] -> hhy
 <iq type=”result” id=”VFDHn-6”  from=”sharexun.cn” to=”hhy@sharexun.cn/Smack”>
     <query  xmlns=”http://jabber.org/protocol/disco#items”>
         <item  jid=”proxy.sharexun.cn” name=”Socks 5 Bytestreams  Proxy”/>
         <item  jid=”pubsub.sharexun.cn” name=”Publish-Subscribe  service”/>
         <item  jid=”search.sharexun.cn” name=”User Search”/>
         <item  jid=”conference.sharexun.cn” name=”…………”/>
     </query>
 </iq>

hhy -> [server proxy]
 <iq id=”VFDHn-7” to=”proxy.sharexun.cn”  type=”get”>
     <query  xmlns=”http://jabber.org/protocol/disco#info”></query>
 </iq>

[server proxy] -> hhy
 <iq type=”result” id=”VFDHn-7”  from=”proxy.sharexun.cn” to=”hhy@sharexun.cn/Smack”>
     <query  xmlns=”http://jabber.org/protocol/disco#info”>
         <identity  category=”proxy” name=”SOCKS5 Bytestreams Service”  type=”bytestreams”/>
         <feature  var=”http://jabber.org/protocol/bytestreams”/>
         <feature  var=”http://jabber.org/protocol/disco#info”/>
     </query>
 </iq>

hhy -> [server pubsub]
 <iq id=”VFDHn-8” to=”pubsub.sharexun.cn”  type=”get”>
     <query xmlns=”http://jabber.org/protocol/disco#info”></query>
 </iq>

[server pubsub] -> hhy
 <iq type=”result” id=”VFDHn-8”  from=”pubsub.sharexun.cn” to=”hhy@sharexun.cn/Smack”>
     <query  xmlns=”http://jabber.org/protocol/disco#info”>
         <identity  category=”pubsub” name=”Publish-Subscribe service”  type=”service”/>
         <feature  var=”http://jabber.org/protocol/pubsub”/>
         <feature  var=”http://jabber.org/protocol/pubsub#collections”/>
         <feature  var=”http://jabber.org/protocol/pubsub#config-node”/>
         <feature var=”http://jabber.org/protocol/pubsub#create-and-configure”/>
         <feature  var=”http://jabber.org/protocol/pubsub#create-nodes”/>
         <feature  var=”http://jabber.org/protocol/pubsub#delete-nodes”/>
         <feature  var=”http://jabber.org/protocol/pubsub#get-pending”/>
         <feature  var=”http://jabber.org/protocol/pubsub#instant-nodes”/>
         <feature  var=”http://jabber.org/protocol/pubsub#item-ids”/>
         <feature  var=”http://jabber.org/protocol/pubsub#meta-data”/>
         <feature  var=”http://jabber.org/protocol/pubsub#modify-affiliations”/>
         <feature  var=”http://jabber.org/protocol/pubsub#manage-subscriptions”/>
         <feature  var=”http://jabber.org/protocol/pubsub#multi-subscribe”/>
         <feature  var=”http://jabber.org/protocol/pubsub#outcast-affiliation”/>
         <feature  var=”http://jabber.org/protocol/pubsub#persistent-items”/>
         <feature  var=”http://jabber.org/protocol/pubsub#presence-notifications”/>
         <feature  var=”http://jabber.org/protocol/pubsub#publish”/>
         <feature  var=”http://jabber.org/protocol/pubsub#publisher-affiliation”/>
         <feature  var=”http://jabber.org/protocol/pubsub#purge-nodes”/>
         <feature  var=”http://jabber.org/protocol/pubsub#retract-items”/>
         <feature  var=”http://jabber.org/protocol/pubsub#retrieve-affiliations”/>
         <feature  var=”http://jabber.org/protocol/pubsub#retrieve-default”/>
         <feature  var=”http://jabber.org/protocol/pubsub#retrieve-items”/>
         <feature  var=”http://jabber.org/protocol/pubsub#retrieve-subscriptions”/>
         <feature  var=”http://jabber.org/protocol/pubsub#subscribe”/>
         <feature  var=”http://jabber.org/protocol/pubsub#subscription-options”/>
         <feature  var=”http://jabber.org/protocol/pubsub#default_access_model_open”/>
         <feature  var=”http://jabber.org/protocol/disco#info”/>
     </query>
 </iq>

hhy -> [server search]
 <iq id=”VFDHn-9” to=”search.sharexun.cn”  type=”get”>
     <query  xmlns=”http://jabber.org/protocol/disco#info”></query>
 </iq>

[server search] -> hhy
 <iq type=”result” id=”VFDHn-9”  from=”search.sharexun.cn” to=”hhy@sharexun.cn/Smack”>
     <query  xmlns=”http://jabber.org/protocol/disco#info”>
         <identity  category=”directory” type=”user” name=”User  Search”/>
         <feature  var=”jabber:iq:search”/>
         <feature  var=”http://jabber.org/protocol/disco#info”/>
         <feature  var=”http://jabber.org/protocol/rsm”/>
     </query>
 </iq>

hhy -> [server conference]
 <iq id=”VFDHn-10” to=”conference.sharexun.cn”  type=”get”>
     <query xmlns=”http://jabber.org/protocol/disco#info”></query>
 </iq>

[server conference] -> hhy
 <iq type=”result” id=”VFDHn-10”  from=”conference.sharexun.cn”  to=”hhy@sharexun.cn/Smack”>
     <query  xmlns=”http://jabber.org/protocol/disco#info”>
         <identity  category=”conference” name=”…………”  type=”text”/>
         <identity  category=”directory” name=”Public Chatroom Search”  type=”chatroom”/>
         <feature  var=”http://jabber.org/protocol/muc”/>
         <feature  var=”http://jabber.org/protocol/disco#info”/>
         <feature var=”http://jabber.org/protocol/disco#items”/>
         <feature  var=”jabber:iq:search”/>
         <feature  var=”http://jabber.org/protocol/rsm”/>
     </query>
 </iq>

//—– 首先是采用 bytestreams 方式传输文件的

hhy -> [server proxy]
 <iq id=”VFDHn-11” to=”proxy.sharexun.cn”  type=”get”>
     <query  xmlns=”http://jabber.org/protocol/bytestreams”/>
 </iq>

[server proxy] -> hhy
 <iq type=”result” id=”VFDHn-11”  from=”proxy.sharexun.cn” to=”hhy@sharexun.cn/Smack”>
     <query  xmlns=”http://jabber.org/protocol/bytestreams”>
         <streamhost  jid=”proxy.sharexun.cn” host=”127.0.0.1”  port=”7777”/>
     </query>
 </iq>

 hhy -> blue
// 这里要求 blue 采用 proxy server 方式来传输文件
 <iq id=”VFDHn-12” to=”blue@sharexun.cn/Spark”  type=”set”>
     <query  xmlns=”http://jabber.org/protocol/bytestreams” sid=”jsi_7839940461539037483”  mode = ”tcp”>
         <streamhost  jid=”hhy@sharexun.cn/Smack” host=”127.0.0.1”  port=”7777”/>
         <streamhost  jid=”proxy.sharexun.cn” host=”127.0.0.1”  port=”7777”/>
     </query>
 </iq>

 blue -> hhy
// 可以看到 blue 无法连接到 代理服务器,并返回了 404 的错误响应
 <iq id=”VFDHn-12” to=”hhy@sharexun.cn/Smack”  from=”blue@sharexun.cn/Spark” type=”error”>
     <query  xmlns=”http://jabber.org/protocol/bytestreams”  sid=”jsi_7839940461539037483” mode=”tcp”>
         <streamhost  jid=”hhy@sharexun.cn/Smack” host=”127.0.0.1”  port=”7777”/>
         <streamhost  jid=”proxy.sharexun.cn” host=”127.0.0.1”  port=”7777”/>
     </query>
     <error code=”404” type=”CANCEL”>
         <item-not-found  xmlns=”urn:ietf:params:xml:ns:xmpp-stanzas”/>
         <text  xmlns=”urn:ietf:params:xml:ns:xmpp-stanzas”  xml:lang=”en”>Could not establish socket with any provided  host</text>
     </error>
 </iq>

//—– 上面方式不行,采用 IBB 方式传输

hhy -> blue
// 在上面无法采用代理服务器的方式后,这里协商采用 IBB 的方式来传输文件
 // 并且这里提示会采用 iq stanza 来传输文件内容

 <iq id=”VFDHn-13” to=”blue@sharexun.cn/Spark”  type=”set”>
     <open xmlns=”http://jabber.org/protocol/ibb”  block-size=”4096” sid=”jsi_7839940461539037483”  stanza=”iq”/>
 </iq>

blue -> hhy
// blue 确认采用此方式传输文件
 <iq id=”VFDHn-13” to=”hhy@sharexun.cn/Smack”  from=”blue@sharexun.cn/Spark” type=”result”/>

hhy -> blue
// hhy 使用 iq/data 方式来传输文件
 <iq id=”VFDHn-14” to=”blue@sharexun.cn/Spark”  type=”set”>
     <data xmlns=”http://jabber.org/protocol/ibb”  seq=”0” sid=”jsi_7839940461539037483”>
         iVBORw0KGgoAAAANSUhEUgAAACwAAAAsCAMAAAApWqozAAAAdVBMVEU0m/I0m/I0m/I0m/I0m/I0m/I0m/I0m/I0m/I0m/I0m/I0m/I0m/I0m/I0m/I0m/I0m/I0m/I0m/I0m/I0m/I0m/I0m/L///9bqfRrsPR6t/WVxPeu0fnj7/1JovPE3fq51/nZ6fyiy/jt9P2IvvbP4/v2+v54PNP4AAAAFnRSTlMABQYJCg4PEZOWmJmiqaqu7/Hy9/n8hZw2JQAAAORJREFUeF7l08luwlAMhWG3acvQeYh955sJ3v8RmxAsQErEyYoF//qTdTampRUroOJAV58lA5Xvzz3+YLA3oscSxb9ELwx3jm2FYy+SUdyIHLWvJ7rEKaoOMpHiC11hmM1+AB7DbHsQLZ+lbEix5iUnRjF3zDhmCCc3lhDsZMzdFpsQQo1gLdwLtgbHjXgUpyySQeyjiLQQ1sx1vDvSaIHL7Wgr1tjMYF3RupO1cRbnwe6Sztfq2c2Bx9wJd9M4xYY1pXvP17/bh0Nd4gkMtBg//KH2h5i2KN70+On1G6Ff64IW9Q/DwZoIf1KdbAAAAABJRU5ErkJggg==
     </data>
 </iq>

blue -> hhy
// 确认已经接收完成
 <iq id=”VFDHn-14” to=”hhy@sharexun.cn/Smack”  from=”blue@sharexun.cn/Spark” type=”result”/>

hhy -> blue
// hhy 关闭本次传输
 <iq id=”VFDHn-15” to=”blue@sharexun.cn/Spark”  type=”set”>
     <close xmlns=”http://jabber.org/protocol/ibb”  sid=”jsi_7839940461539037483”/>
 </iq>

blue -> hhy
// 本次文件传输结束
 <iq id=”VFDHn-15” to=”hhy@sharexun.cn/Smack”  from=”blue@sharexun.cn/Spark” type=”result”/>

The End

发布于:2023-02-10,除非注明,否则均为 主机评测原创文章,转载请注明出处。