QNetworkAccessManager:使用串行QIODevice发布http多部分

7
我将尝试使用QNetworkAccessManager上传http multiparts到一个专用服务器。
multipart由描述正在上传的数据的JSON部分组成。
数据从串行QIODevice中读取,该设备对数据进行加密。
以下是创建multipart请求的代码:
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的文档中读到:
如果设备是顺序的(例如套接字,但不是文件),则应在设备发出finished()之后调用QNetworkAccessManager :: post()。
不幸的是,我不知道如何做到这一点。
请给予建议。
编辑:
QIODevice根本没有finished()插槽。而且,如果未调用QNetworkAccessManager :: post(),则根本不会从我的自定义IODevice中读取,因此该设备将无法发出此类事件。(陷入困境?)
编辑2:
似乎QNAM根本不支持连续设备。请参见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()必须返回正确的值才能正常工作。 - Dan Milburn
自那时以来,你解决了这个问题了吗,matejk? - László Papp
我设法解决了它,但不是以一种干净的方式。请看下面我的评论。 - matejk
4个回答

2

你需要重构你的代码,这样你通过post传递的变量才能在你发布的函数之外可用,然后你需要定义一个新的插槽,并在实现中添加执行post的代码。最后,你需要执行connect(p_encDevice, SIGNAL(finished()), this, SLOT(yourSlot())来将所有东西粘合在一起。

你已经接近成功了,只需要将其重构并添加一个新的插槽,你可以将其与QIODevice::finished()信号相连接。


Nicolas,谢谢。这是否有效地意味着在调用post之前,所有来自p_encDevice的数据都将被读入QNetworkAccessManager的内部缓冲区中?如果是这样的话,那么如果我将数据读入QByteArray并将其传递给QHttpPart::setBody,那么就会容易得多。 - matejk
它不会读入QNAM缓冲区,本质上你将继续像现在一样读入filePart,但你需要将filePart作为类成员,这样你创建的插槽才能访问它。 - Nicholas Smith
QIODevice根本没有finished插槽。而且,如果不调用QNetworkAccessManager :: post,则根本不会从我的自定义IODevice读取数据,因此该设备将无法发出此类事件。 - matejk
@matejk:确实,没有QIODevice :: finished()函数。我想知道尼古拉斯打算写什么... - László Papp

1

我认为问题在于QNetworkAccessManager在上传(POST, PUT)数据时不支持 分块传输编码,这意味着QNAM必须预先知道要上传的数据的长度,以便发送Content-Length头。这意味着:

  1. 要么数据不来自连续设备,而是来自随机访问设备,该设备通过 size() 正确报告其总大小;
  2. 要么数据来自连续设备,但设备已经缓冲了所有数据(这就是关于 finished() 的注释的含义),并将报告它(通过 bytesAvailable(),我想);
  3. 要么数据来自尚未缓冲所有数据的连续设备,这反过来又意味着
    1. 要么QNAM从设备中读取和缓冲所有数据(通过读取直到EOF)。
    2. 要么用户手动设置请求的Content-Length头。

(有关最后两个要点,请参见QNetworkRequest :: DoNotBufferUploadDataAttribute 的文档。)

所以,QHttpMultiPart在某种程度上也存在这些限制,很可能会在第3种情况下出现问题。假设您不可能在内存中缓冲来自“编码器”QIODevice的所有数据,是否有可能您可以提前知道编码数据的大小并在QHttpPart上设置内容长度?

(最后注意一点,您不应该使用QScopedPointer。这将在智能指针超出范围时删除QNR,但您不想这样做。您希望在QNR发出finished()时删除它)。


感谢提供更多尝试的选项。我设置了DoNotBufferUploadDataAttribute并显式设置了内容长度,因为我事先知道大小,但这确实没有帮助。输入流被读取到EOF,但然后一切都停止了。文档提到了signal finished(),但QIODevice根本没有发出该信号。 - matejk
我故意使用ScopedPointer,因为接下来的代码正在等待QNR完成。 - matejk
@matejk:是的,我也在主函数中使用QScopedPointer,在那里QNetworkReply无法被删除,因为Qt事件循环会防止它。是的,不幸的是没有finished()信号。 - László Papp

1

我手动创建http post数据比使用QHttpPartQHttpMultiPart更成功。 我知道这可能不是你想听的,而且有点混乱,但它绝对有效。 在这个例子中,我从一个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
@matejk:确切地说,readAll 实际上不需要比 QNAM 中的 readAll 和 post 更多的东西,但是当有大文件时,人们无法这样做。 - László Papp

0
qt-project的单独讨论和检查源代码来看,QNAM似乎根本不支持顺序。无论是文档还是代码都是错误的。

那么如何解决这个使用案例呢?我们需要在内存中妥协大量数据,还是开始使用mmap来代替呢? - László Papp
@LaszloPapp 我创建了加密设备,使其声称是非顺序的,但 read() 和 seek() 函数会验证当前位置,以确保数据按顺序读取。 QNAM 按顺序读取数据。 这不太好,但它能工作。 - matejk

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接