文件传输在 Smack 4.1.0 beta-1 上失败

File Transfer fails on Smack 4.1.0 beta-1

我正在开发一个涉及两个 Smack 客户端的应用程序,其中一个尝试将 jar 文件发送到另一个客户端。我把它写成桌面应用程序的一部分,而不是 Android 应用程序(我想我会做出这种区分,因为 Whosebug 上的大多数 Smack 查询都是 Android 相关的)。我使用 Openfire 3.9.3 作为 XMPP 服务器。对于所有代码示例,我使用以下 Smack 库(版本 4.1.0- beta 1):smack-java7、smack-tcp、smack-extensions 和 smack-sasl-provided。这是 OutgoingFileTransfer 的代码(注意所有代码示例中的信息,例如 Smack 用户名已被匿名化):

// LOG is of type org.apache.logging.log4j.Logger
// transferManager is the FileTransferManager for this XMPPTCPConnection
private void sendJarFile(final String to)
{
    LOG.info("Sending " + jarFileName + " to " + to);
    OutgoingFileTransfer jarTransfer = 
            transferManager.createOutgoingFileTransfer(to);
    final File jarFile = new File(jarFileName);
    try
    {
        jarTransfer.sendFile(jarFile, "The current jar file");
        while (!jarTransfer.isDone())
        {
            LOG.info("File transfer status: " + jarTransfer.getStatus());
            Thread.sleep(500);
        }
        LOG.info("File transfer to " + to + " is done");
        // Now that the file transfer is done check for errors
        // or exceptions
        FileTransfer.Error error = jarTransfer.getError();
        if (error != null)
        {
            LOG.error(error.getMessage());
        }
        Exception exception = jarTransfer.getException();
        if (exception != null)
        {
            if (exception instanceof XMPPException.XMPPErrorException)
            {
                XMPPException.XMPPErrorException errorException = 
                        (XMPPException.XMPPErrorException)exception;
                XMPPError xmppError = errorException.getXMPPError();
                LOG.error(xmppError);
                LOG.error("Descriptive text: " + xmppError.getDescriptiveText());
                LOG.error("Condition: " + xmppError.getCondition());
                LOG.error("Type: " + xmppError.getType());
            }
        }
    }
    // For now, just catching and logging exceptions. Exception handling
    // will be added in top-level classes
    catch (SmackException e)
    {
        LOG.error("Exception trying to send jar file", e);
    }
    catch (InterruptedException e)
    {
        // Do nothing
    }
    catch (Exception e)
    {
        LOG.error("Exception trying to send jar file", e);
    }
}

这段代码的输出是:

Sending test.jar to receiver@local_openfire/Smack

14:09:43.748 INFO - File transfer status: Initial

14:09:44.249 INFO - File transfer status: Negotiating Stream

14:09:44.751 INFO - File transfer status: Negotiating Stream

// This message continues for several seconds, until finally

14:09:53.805 INFO - File transfer status: Negotiating Stream

14:09:54.308 INFO - File transfer to receiver@local_openfire/Smack is done

14:09:54.309 ERROR - org.jivesoftware.smack.packet.XMPPError@30e95075

14:09:54.310 ERROR - Descriptive text: null

14:09:54.310 ERROR - Condition: service-unavailable

14:09:54.310 ERROR - Type: cancel

对于 IncomingFileTransfer,代码是:

@Override
public void fileTransferRequest(FileTransferRequest request)
{
    final String requestorId = request.getRequestor();
    LOG.info("FileTransferRequest from: " + requestorId);
    // Only respond to requests from the sender
    if (requestorId.contains(senderId))
    {
        final IncomingFileTransfer transfer = request.accept();
        LOG.info("FileTransferRequest accepted");
        try
        {
            final String fileName = transfer.getFileName();
            transfer.recieveFile(new File(fileName));
            LOG.info("Incoming file transfer: " + fileName);
            LOG.info("Transfer status is: " + transfer.getStatus());
            while (!transfer.isDone())
            {
                final double progress = transfer.getProgress();
                final double progressPercent = progress * 100.0;
                String percComplete = String.format("%1$,.2f", progressPercent);
                LOG.info("Transfer status is: " + transfer.getStatus());
                LOG.info("File transfer is " + percComplete + "% complete");
                Thread.sleep(1000);
            }

            // Now that the file transfer is done check for errors
            // or exceptions
            FileTransfer.Error transferError = transfer.getError();
            if (transferError != null)
            {
                LOG.error("Transfer error occurred: " + transferError.getMessage());
            }
            Exception transferException = transfer.getException();
            if (transferException != null)
            {
                LOG.error("Transfer exception occurred: " + transferException);
                if (transferException instanceof SmackException.NoResponseException)
                {
                    SmackException.NoResponseException smackException = (SmackException.NoResponseException)transferException;
                    smackException.printStackTrace();
                }
            }
            LOG.info("FileTransfer complete");
            provisioningComplete = true;
        }
        // For now just logging exceptions
        catch (SmackException e)
        {
            LOG.error("SmackException trying to receive jar file", e);
        }
        catch (InterruptedException e)
        {
            // Do nothing
        }
        catch (IOException e)
        {
            LOG.error("IOException trying to receive jar file", e);
        }

    }
    else
    {
        LOG.warn("FileTransferRequest rejected");
        try
        {
            request.reject();
        }
        catch (NotConnectedException e)
        {
            LOG.warn("NotConnectedException when rejecting FileTransferRequest");
        }
    }
}

这段代码的输出是:

14:09:43.766 INFO - FileTransferRequest from: sender@local_openfire/Smack

14:09:43.767 INFO - FileTransferRequest accepted

14:09:43.768 INFO - Incoming file transfer: test.jar

14:09:43.769 INFO - Transfer status is: Negotiating Transfer

14:09:43.770 INFO - Transfer status is: Negotiating Stream

14:09:43.770 INFO - File transfer is 0.00% complete

14:09:44.771 INFO - Transfer status is: Negotiating Stream

14:09:44.771 INFO - File transfer is 0.00% complete

14:09:45.776 INFO - Transfer status is: Negotiating Stream

14:09:45.776 INFO - File transfer is 0.00% complete

14:09:46.778 INFO - Transfer status is: Negotiating Stream

14:09:46.778 INFO - File transfer is 0.00% complete

14:09:47.782 INFO - Transfer status is: Negotiating Stream

14:09:47.783 INFO - File transfer is 0.00% complete

14:09:48.784 ERROR - Transfer exception occurred: org.jivesoftware.smack.SmackException: Error in execution

14:09:48.784 INFO - FileTransfer complete

在这段代码有运行之后,在接收端,我在当前工作目录中有一个名为"test.jar"的文件,文件大小为0字节。我在不同机器上的发送者和接收者,以及同一台机器上的发送者和接收者都试过了。我最初使用 Smack 4.0.6,但切换到最新的代码库(撰写本文时为 4.1.0-beta 1),希望这个错误可能已经得到解决。没有这样的运气。我将不胜感激任何建议。谢谢!

2015 年 1 月 30 日更新

我不再在发件人端看到 XMPPError。相反,发件人仍然停留在文件传输状态:协商流状态。但是,接收方收到以下错误:

SmackException.NoResponseException: 包回复超时没有收到回复。超时为 5000 毫秒(~5 秒)

我可以看到如何增加 OutgoingFileTransfer 的超时 class,但不是 IncomingFileTransfer。

更新 2-02-2015

我使用了 Smack 调试工具并捕获了原始 XML 节。为了简洁起见,我只包括那些与文件传输相关的(即不存在或花名册数据包)。他们在这里:

<iq to="receiver@smack_server/Smack" id="NK8Lh-11" type="set" 
from="sender@smack_server/Smack">
    <si xmlns="http://jabber.org/protocol/si" id="jsi_3077759398544954943" 
    mime-type="text/plain" profile="http://jabber.org/protocol/si/profile/file-transfer">
        <file xmlns="http://jabber.org/protocol/si/profile/file-transfer" name="test.txt" 
        size="37">
            <desc>A test 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>

<iq to="sender@smack_server/Smack" id="NK8Lh-11" 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>


<iq to="receiver@smack_server/Smack" id="NK8Lh-13" type="get" from="sender@smack_server/Smack">
    <query xmlns="http://jabber.org/protocol/disco#info"/>
</iq>

<iq to="sender@smack_server/Smack" id="NK8Lh-13" type="result">
  <query xmlns="http://jabber.org/protocol/disco#info">
    <identity category="client" name="Smack" type="pc"/>
    <feature var="http://jabber.org/protocol/disco#items"/>
    <feature var="vcard-temp"/>
    <feature var="http://jabber.org/protocol/bytestreams"/>
    <feature var="http://jabber.org/protocol/ibb"/>
    <feature var="http://jabber.org/protocol/si"/>
    <feature var="http://jabber.org/protocol/xhtml-im"/>
    <feature var="jabber:x:data"/>
    <feature var="urn:xmpp:time"/>
    <feature var="jabber:iq:privacy"/>
    <feature var="http://jabber.org/protocol/si/profile/file-transfer"/>
    <feature var="urn:xmpp:ping"/>
    <feature var="jabber:iq:last"/>
    <feature var="http://jabber.org/protocol/commands"/>
    <feature var="http://jabber.org/protocol/muc"/>
    <feature var="http://jabber.org/protocol/xdata-validate"/>
    <feature var="http://jabber.org/protocol/xdata-layout"/>
    <feature var="http://jabber.org/protocol/disco#info"/>
  </query>
</iq>

<iq to="receiver@smack_server/Smack" id="NK8Lh-25" type="set" from="sender@smack_server/Smack">
    <query xmlns="http://jabber.org/protocol/bytestreams" sid="jsi_3077759398544954943" mode="tcp">
        <streamhost jid="sender@smack_server/Smack" host="ipv6_addr1" port="7778"/>
        <streamhost jid="sender@smack_server/Smack" host="ipv4_addr1" port="7778"/>
        <streamhost jid="sender@smack_server/Smack" host="ipv6_addr2" port="7778"/>
        <streamhost jid="proxy.smack_server" host="ipv4_addr2" port="7777"/>
    </query>
</iq>

<iq to="receiver@smack_server/Smack" id="NK8Lh-26" type="set" from="sender@smack_server/Smack">
    <open xmlns="http://jabber.org/protocol/ibb" block-size="4096" sid="jsi_3077759398544954943" stanza="iq"/>
</iq>

据我所知,阅读规范后,一切似乎都在按预期进行。发送方发送初始 SI 请求,接收方以支持的协议(即字节流和 IBB)进行响应,然后发送方向接收方查询所有迪斯科项目,接收方以特征列表进行响应,然后发送方发送各种流主机,然后发送方通过 IBB 发送数据块。从那里,接收者得到 SmackException: Error in execution method,这是由 SmackException.NoResponseException 引起的,消息是 5 秒内没有收到响应。看起来这个问题在这一点上基本上被忽略了,但希望有人能检查出来,我真的很感激任何帮助。谢谢!

我已经解决了这个问题!下载 Smack 4.0.6 源代码并在 Receiver 端调试数据包解析后,我发现一切正常。正在解析 IQ 和数据包,XMPPTCPConnection 正在正确处理数据包。这个问题原来是某种竞争条件。我认为问题出在这里:

// transfer is of type IncomingFileTransfer, created by
// FileTransferRequet.accept()
final String fileName = transfer.getFileName();
transfer.recieveFile(new File(fileName));
LOG.info("Incoming file transfer: " + fileName);
LOG.info("Transfer status is: " + transfer.getStatus());
while (!transfer.isDone())
{
    final double progress = transfer.getProgress();
    final double progressPercent = progress * 100.0;
    String percComplete = String.format("%1$,.2f", progressPercent);
    LOG.info("Transfer status is: " + transfer.getStatus());
    LOG.info("File transfer is " + percComplete + "% complete");
    Thread.sleep(1000);
}

由于某种原因,while 循环消耗了所有周期,XMPPTCPConnection 无法及时响应 Open IQ 和 Data IQ。所以我把进度监控移到了一个新的线程中,一切都很完美。作为参考,我在 Mac OS X 上使用 Java 版本 1.8.0_31(64 位)并且还在 Java 版本 1.8.[= Windows 7 Professional(64 位)上的 15=](64 位)。澄清一下,我的解决方案适用于 Smack 4.0.6。我还确认它在 Java 7 (1.7.0_51) 64 位上正常工作。

也许这个例子会给你提供一些思路:

private void sendFile(String to,String file){
    /*
     * This sends a file to someone
     * @param to the xmmp-account who receives the file, the destination  
     * @param file the path from the file
     */
    File f=new File(file);
    FileTransferManager manager = new FileTransferManager(conn);
    OutgoingFileTransfer transfer =
            manager.createOutgoingFileTransfer(to);

    // Send the file
    try {
        transfer.sendFile(f,"I have a file for you?");
    } catch (XMPPException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
        sendMessage(logAccount,"Sorry,couldn't deliver the file");
    }

} 

您可以在 Examples for org.jivesoftware.smackx.filetransfer.OutgoingFileTransfer 中找到更多信息。

还有一个可能有用的文件接收示例:

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); 
public void startRecvFileListen(XMPPConnection conn){
    FileTransferManager manager = new FileTransferManager(conn);
    manager.addFileTransferListener(new FileTransferListener() {
        public void fileTransferRequest(FileTransferRequest request) {
            final IncomingFileTransfer inTransfer = request.accept();
            try {
                System.out.println("filename: "+request.getFileName());
                String filePath = "D:\datas\smackclient\"+request.getFileName();
                inTransfer.recieveFile(new File(filePath));
                new Thread(){
                    @Override
                    public void run(){
                        long startTime = System.currentTimeMillis();
                        while(!inTransfer.isDone()){
                            if (inTransfer.getStatus().equals(Status.error)){
                                System.out.println(sdf.format(new Date())+"error!!!"+inTransfer.getError());
                            }else{
                                double progress = inTransfer.getProgress();
                                progress*=100;
                                System.out.println(sdf.format(new Date())+"status="+inTransfer.getStatus());
                                System.out.println(sdf.format(new Date())+"progress="+nf.format(progress)+"%");
                            }
                            try {
                                Thread.sleep(1000);
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                        System.out.println("used "+((System.currentTimeMillis()-startTime)/1000)+" seconds  ");
                    }
                }.start();
            } catch (XMPPException e) {
                JOptionPane.showMessageDialog(null, "failed", "error", JOptionPane.ERROR_MESSAGE);
                e.printStackTrace();
            }
        }
    });
    System.out.println(connection.getUser()+"--"+connection.getServiceName());
}