QNetworkAccessManager:从串行 QIODevice 发布 http 多部分

Posted

技术标签:

【中文标题】QNetworkAccessManager:从串行 QIODevice 发布 http 多部分【英文标题】:QNetworkAccessManager: post http multipart from serial QIODevice 【发布时间】:2013-02-27 10:24:39 【问题描述】:

我正在尝试使用 QNetworkAccessManager 将 http 多部分上传到专用服务器。

多部分由描述上传数据的 JSON 部分组成。

数据从串行 QIODevice 中读取,该设备对数据进行加密。

这是创建多部分请求的代码:

QHttpMultiPart *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);

QHttpPart metaPart;
metaPart.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
metaPart.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"metadata\""));
metaPart.setBody(meta.toJson());
multiPart->append(metaPart);

QHttpPart filePart;
filePart.setHeader(QNetworkRequest::ContentTypeHeader, QVariant(fileFormat));
filePart.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"file\""));
filePart.setBodyDevice(p_encDevice);
p_encDevice->setParent(multiPart); // we cannot delete the file now, so delete it with the multiPart
multiPart->append(filePart);

QNetworkAccessManager netMgr;
QScopedPointer<QNetworkReply> reply( netMgr.post(request, multiPart) );
multiPart->setParent(reply.data()); // delete the multiPart with the reply

如果 p_encDevice 是 QFile 的实例,则该文件可以正常上传。

如果使用专门的加密 QIODevice(串行设备),那么所有数据都是从我的自定义设备中读取的。但是 QNetworkAccessManager::post() 没有完成(挂起)。

我在QHttpPart 的文档中读到:

如果设备是连续的(例如套接字,但不是文件), QNetworkAccessManager::post() 应该在设备完成后调用 发出完成()。

很遗憾,我不知道该怎么做。

请指教。

编辑:

QIODevice 根本没有finished() 槽。更重要的是,如果没有调用 QNetworkAccessManager::post(),则根本不会从我的自定义 IODevice 中读取数据,因此设备将无法发出这样的事件。 (第 22 条?)

编辑 2:

QNAM 似乎根本不适用于顺序设备。见discussion on qt-project。

编辑 3:

我设法“欺骗”了 QNAM,让它认为它正在从非顺序设备读取,但查找和重置功能阻止了查找。这将一直有效,直到 QNAM 真正尝试寻找。

bool AesDevice::isSequential() const

    return false;


bool AesDevice::reset()

    if (this->pos() != 0) 
        return false;
    
    return QIODevice::reset();


bool AesDevice::seek(qint64 pos)

    if (this->pos() != pos) 
        return false;
    
    return QIODevice::seek(pos);

【问题讨论】:

我认为合适的信号是QIODevice::readChannelFinished()。基本上QIODevice::bytesAvailable() 必须返回正确的值才能正常工作。 从那以后你解决了这个问题吗,matejk? 我设法解决了它,但不是以干净的方式。请参阅下面的评论。 【参考方案1】:

您需要大量重构代码,以便传递给post 的变量在您发布的函数之外可用,然后您需要使用代码定义的新槽来执行@ 987654322@里面的实现。最后,您需要使用connect(p_encDevice, SIGNAL(finished()), this, SLOT(yourSlot()) 将它们粘合在一起。

您基本上已经完成了,您只需要重构它并添加一个可以绑定到QIODevice::finished() 信号的新插槽。

【讨论】:

尼古拉斯,谢谢。这是否实际上意味着来自传入 p_encDevice 的所有数据都将在调用 post 之前被读入 QNetworkAccessManager 的内部缓冲区?如果是这样,那么将数据读入 QByteArray 并将其传递给 QHttpPart::setBody 会容易得多。 它不会读入 QNAM 缓冲区,本质上您将像现在一样继续读入 filePart 但您需要 filePart 成为类成员,以便您创建的插槽可以访问它。 QIODevice 根本没有完成插槽。更重要的是,如果没有调用 QNetworkAccessManager::post,则根本不会从我的自定义 IODevice 中读取数据,因此设备将无法发出这样的事件。 @matejk:确实,没有 QIODevice::finished()。我想知道尼古拉斯打算写什么……【参考方案2】:

与使用QHttpPartQHttpMultiPart 相比,手动创建http 帖子数据的成功率更高。我知道这可能不是你想听到的,而且有点乱,但它确实有效。在此示例中,我正在阅读QFile,但您可以在任何QIODevice 上调用readAll()。还有一点值得注意,QIODevice::size()会帮你检查是否所有的数据都被读取了。

QByteArray postData;
QFile *file=new QFile("/tmp/image.jpg");
if(!(file->open(QIODevice::ReadOnly)))
    qDebug() << "Could not open file for reading: "<< file->fileName();
    return;

//create a header that the server can recognize
postData.insert(0,"--AaB03x\r\nContent-Disposition: form-data; name=\"attachment\"; filename=\"image.jpg\"\r\nContent-Type: image/jpeg\r\n\r\n");
postData.append(file->readAll());
postData.append("\r\n--AaB03x--\r\n");
//here you can add additional parameters that your server may need to parse the data at the end of the url
QString check(QString(POST_URL)+"?fn="+fn+"&md="+md);
QNetworkRequest req(QUrl(check.toLocal8Bit()));
req.setHeader(QNetworkRequest::ContentTypeHeader,"multipart/form-data; boundary=AaB03x");
QVariant l=postData.length();
req.setHeader(QNetworkRequest::ContentLengthHeader,l.toString());
file->close();
//free up memory
delete(file);
//post the data
reply=manager->post(req,postData);
//connect the reply object so we can track the progress of the upload        
connect(reply,SIGNAL(uploadProgress(qint64,qint64)),this,SLOT(updateProgress(qint64,qint64)));

那么服务器就可以这样访问数据了:

<?php
$filename=$_REQUEST['fn'];
$makedir=$_REQUEST['md'];
if($_FILES["attachment"]["type"]=="image/jpeg")
if(!move_uploaded_file($_FILES["attachment"]["tmp_name"], "/directory/" . $filename))
    echo "File Error";
    error_log("Uploaded File Error");
    exit();
;
else
print("no file");
error_log("No File");
exit();

echo "Success.";
?>

我希望这些代码可以帮助到你。

【讨论】:

我目前阅读了完整的数据并将其发布,但是我负担不起,因为文件可能非常大(几 GB)。 @matejk:确切地说,readAll 实际上除了 readAll 和 QNAM 发布之外不需要任何其他东西,但是当文件很大时人们不能这样做。【参考方案3】:

我认为问题是 QNetworkAccessManager 在上传(POST,PUT)数据时不支持chunked transfer encoding。这意味着 QNAM 必须提前知道要上传的数据的长度,以便发送 Content-Length 标头。这意味着:

    数据不是来自顺序设备,而是来自随机访问设备,它们会通过size() 正确报告其总大小; 或者数据来自顺序设备,但是设备已经全部缓冲了(这就是finished()的注释的意思),会报告(我想是通过bytesAvailable()); 或者数据来自没有缓冲所有数据的顺序设备,这反过来意味着
      任一 QNAM 都会读取并缓冲来自设备的所有数据(通过读取直到 EOF) 或者用户手动设置请求的 Content-Length 标头。

(关于最后两点,请参阅 QNetworkRequest::DoNotBufferUploadDataAttribute 的文档。)

所以,QHttpMultiPart 以某种方式共享这些限制,并且它很可能在案例 3 中窒息。假设您不可能在内存中缓冲来自“编码器”QIODevice 的所有数据,您是否有可能知道提前编码数据并在 QHttpPart 上设置内容长度?

(最后一点,你不应该使用 QScopedPointer。当智能指针超出范围时,这将删除 QNR,但你不想这样做。你想在 QNR 发出时删除它完成())。

【讨论】:

感谢您尝试其他选项。我设置了 DoNotBufferUploadDataAttribute 并明确设置了内容长度,因为我提前知道了大小,但它确实没有帮助。输入流被读取到 EOF,但随后一切都停止了。文档中提到了信号finished(),而QIODevice 根本不发出。 我故意使用 ScopedPointer,因为后面的代码正在等待 QNR 完成。 @matejk:是的,我也在一个主函数中使用 QScopedPointer,因为 Qt 事件循环对此进行了辩护,所以 QNetworkReply 不能被它删除。是的,很遗憾,没有finished() 信号。【参考方案4】:

从qt-project 中的单独讨论和检查源代码来看,QNAM 似乎根本不适用于顺序。文档和代码都错了。

【讨论】:

那么如何解决这个用例呢?我们需要对内存中的大数据进行妥协,还是您开始对其进行映射? @LaszloPapp 我以声明为非顺序的方式创建了加密设备,但 read() 和 seek() 函数验证当前位置以确保按顺序读取数据方式。 QNAM 正在按顺序读取数据。这不是很好,但它有效。

以上是关于QNetworkAccessManager:从串行 QIODevice 发布 http 多部分的主要内容,如果未能解决你的问题,请参考以下文章

QT 从 QWebView 的 QNetworkAccessManager 读取数据

QNetworkAccessManager 可以从不同的线程获取/发布吗?

如何从 QNetworkAccessManager::networkAccessibleChanged() 获得信号?

QNetworkAccessManager 从 Internet 崩溃中下载图像预览

在 QNetworkAccessManager 中,何时从解析 DNS 的 IP 列表中选择 IP?

使用 QWebKit 的永久 cookie——从哪里获得 QNetworkAccessManager?