今天在实现文件传输功能的时候,发现图片文件数据传到本地后没有按预期显示出来。
核心代码如下:
void CFileTransferThread::slotStartTransferFile(QString srcPath, QString extend, QString ip, quint16 port)
{
//进入这个函数,根据srcPath找到目标文件,以只读方式打开,注意文件不能一次性读取
QFile file(srcPath);
file.open(QIODevice::ReadOnly);
if(!file.exists())
{
qDebug()<<"file is not exist";
return;
}
bool isFirstLine = true; //在第一行加入文件总长度
//文件存在
qint64 fileTotalSize = file.size(); //文件真实长度
while(!file.atEnd() && !file.error())
{
QByteArray line;
if(isFirstLine)
{
line = QString("<totalSize=%1>").arg(fileTotalSize).toUtf8();
line = line + file.readLine(65536).trimmed();
isFirstLine = false;
}
else
{
line = file.readLine(65536).trimmed(); //从文件中读数据,每次最多只读65536字节
}
CCommunicationCenter::sendPrivateMsg(NET_REPLY_FILE_TRANSFER,"","",line,ip,port); //这是自己实现的用来往指定socket发送数据的方法
}
file.close();
}
为了更好地观察传输前后数据的差异,便从网上down了一部小说,现在发送端的文件是这样的:
而接收端接收到的文件却是这样的:
可见发送端从文件中读取数据时,把所有空格、换行都移除了。很容易发现这是因为调用了trimmed()函数,查看帮助文档是这样介绍的:
Returns a byte array that has whitespace removed from the start and the end.
Whitespace means any character for which the standard C++ isspace() function returns true in the C locale. This includes the ASCII characters ‘\t’, ‘\n’, ‘\v’, ‘\f’, ‘\r’, and ’ '.
Example:
QByteArray ba(" lots\t of\nwhitespace\r\n ");
ba = ba.trimmed();
// ba == “lots\t of\nwhitespace”;
Unlike simplified(), trimmed() leaves internal whitespace alone.
返回一个去除了头尾空格符的字节数组。
空格符表示任何在标准C++的isspace()方法中返回值为true的字符。包括ASCII字符’\t’, ‘\n’, ‘\v’, ‘\f’, ‘\r’, 和’ ‘。
例如:
QByteArray ba(" lots\t of\nwhitespace\r\n ");
ba = ba.trimmed();
// ba == “lots\t of\nwhitespace”; 去除了字节数组起始的’ ‘和结尾的’\r\n ’
不同于simplified(), trimmed()仅保留内部空白。
但如果仅仅是把读取到的数据头尾的空白符移除,为什么接收端接收到的数据看上去会如此“紧凑”呢?
再来看看readLine()方法的帮助文档,它是这样介绍的:
This function reads a line of ASCII characters from the device, up to a maximum of maxSize - 1 bytes, stores the characters in data, and returns the number of bytes read. If a line could not be read but no error ocurred, this function returns 0. If an error occurs, this function returns the length of what could be read, or -1 if nothing was read.
A terminating ‘\0’ byte is always appended to data, so maxSize must be larger than 1.
Data is read until either of the following conditions are met:
- The first ‘\n’ character is read.
- maxSize - 1 bytes are read.
- The end of the device data is detected.
For example, the following code reads a line of characters from a file:
QFile file(“box.txt”);
if (file.open(QFile::ReadOnly)) {
char buf[1024];
qint64 lineLength = file.readLine(buf, sizeof(buf));
if (lineLength != -1) {
// the line is available in buf
}
}
The newline character (’\n’) is included in the buffer. If a newline is not encountered before maxSize - 1 bytes are read, a newline will not be inserted into the buffer. On windows newline characters are replaced with ‘\n’.
This function calls readLineData(), which is implemented using repeated calls to getChar(). You can provide a more efficient implementation by reimplementing readLineData() in your own subclass.
此函数从设备中读取一行ASCII字符(最大maxSize-1个字节),将字符存储在数据中,并返回读取的字节数。如果无法读取一行但没有发生错误,则此函数返回0。如果发生错误,则此函数返回能够读取到的长度,如果未读取则返回-1。
此函数从设备中读取数据,直到遇到以下任一情况:
- 读取到第一个’\n’字符(换行符)
- 读取到maxSize-1个字节
- 检测到设备数据的结尾
例如,以下代码从文件中读取一行字符:
QFile file(“box.txt”);
if (file.open(QFile::ReadOnly)) {
char buf[1024];
qint64 lineLength = file.readLine(buf, sizeof(buf));
if (lineLength != -1) {
// the line is available in buf
}
}
换行符包含在缓冲区中。如果在读取maxSize-1个字节之前未遇到换行符,则不会将换行符插入缓冲区。在Windows上,换行符替换为’\n’。
此函数调用readLineData(),该方法使用对getChar()的重复调用来实现。
意思就是,调用一次readLine()方法,在三种情况下它会停止从设备中读取数据:
第一种情况是遇到换行符’\n’;
第二种情况是读取到了第maxSize-1个字节,也就是达到设置的最大长度;
第三种情况是检测到设备的结尾,即没有可以读取的数据了。
回到发送端需要发送的文件:
前面几行可以看作是:
\n
------------\n
\n
第一章\n
\n
《梦里花落知多少》\n
……
由于对文件调用readLine()方法时,每次遇到\n都会停止继续读取数据,再调用trimmed()方法,去除每次读取到的数据头尾的空白符,最后就会得到:
------------第一章《梦里花落知多少》
而图片文件数据是用字节数组来表示的,调用trimmed()方法可能会引起无法预料的错误。因此这里不需要调用trimmed()方法。【看来还是不要盲目借鉴别人的代码hhh】
但是,仅调用readLine()还是有问题。。
现在简化发送端需要发送的数据,方便调试:
接收端接收到的数据:
最后一行是被吃了吗!!
经过调试后,确认发送端发送的数据无误,开始检查接收端接收的过程出了什么问题。
接收端的核心代码如下:
void CFileTransferThread::receiveFile(QByteArray data, QString ip, quint16 port)
{
m_file.open(QIODevice::WriteOnly);
if(data.contains("<totalSize="))
{
//把长度取出来
int indexLeft = data.indexOf("<totalSize="); //从这个位置继续往后找,找到>
while(indexLeft>0)
{
data.remove(0,indexLeft);
indexLeft = data.indexOf("<totalSize=");
}
int indexSize = data.indexOf("=",indexLeft) + 1;
int indexRight = data.indexOf(">",indexSize);
int diff = indexRight - indexSize;
m_expecteSize = data.mid(indexSize,diff).toInt(); //文件总长度
data.remove(indexLeft,indexRight-indexLeft+1); //将添加在首部的文件长度标识移除,还原文件数据
}
//已经获取文件总长度
if(m_expecteSize)
{
m_readSize = m_file.size();
if(m_expecteSize>m_readSize)
{
m_readSize += m_file.write(data);
}
else //文件读取完毕,重置成员变量并关闭文件
{
m_expecteSize = 0;
m_readSize = 0;
m_file.close();
}
}
}
在调试这段代码的时候依然没有发现有什么问题,但是在程序运行的过程中,打开目标文件却出现了奇怪的现象:
如果发送端用readLine()读取文件数据再发送给接收端,那么就像前面提到的会缺少最后一行,如果发送端用read()读取文件数据再发送给接收端,就是一片空白。
但是当关闭程序后,再打开文件,却能得到完整的文件数据。
而且在程序运行的过程中,如果想删除文件,会弹出提示:文件正在程序中被打开。
于是初步推断,在程序关闭前,文件一直没有关闭。
再看代码,只有在不满足if(m_expectSize>m_readSize)时,才会执行m_file.close(),而debug的时候,发现这段代码并没有执行。
else //文件读取完毕,重置成员变量并关闭文件
{
m_expecteSize = 0;
m_readSize = 0;
m_file.close();
}
经过一番推理(?),得出的结论是,在接收端接收到文件的最后一部分数据,并执行 m_readSize += m_file.write(data) 后,m_readSize确实等于m_expectSize,但是,此时发送端的文件数据已经发送完毕了,后续就不会再向接收端发送数据,接收端也就不会再调用receiveFile()方法,自然也就不会进行if else的判断,所以即使此时m_readSize == m_expectSize,也不会再执行m_file.close()。
所以,应该将代码改为先读取数据,再判断是否读取完毕:
void CFileTransferThread::receiveFile(QByteArray data, QString ip, quint16 port)
{
if(!m_file.isOpen())
m_file.open(QIODevice::WriteOnly);
if(data.contains("<totalSize="))
{
//把长度取出来
int indexLeft = data.indexOf("<totalSize="); //从这个位置继续往后找,找到>
while(indexLeft>0)
{
data.remove(0,indexLeft);
indexLeft = data.indexOf("<totalSize=");
}
int indexSize = data.indexOf("=",indexLeft) + 1;
int indexRight = data.indexOf(">",indexSize);
int diff = indexRight - indexSize;
m_expecteSize = data.mid(indexSize,diff).toInt(); //文件总长度
data.remove(indexLeft,indexRight-indexLeft+1);
}
//已经获取文件总长度
if(m_expecteSize)
{
//先读取,再判断
m_readSize += m_file.write(data);
if(m_expecteSize<=m_readSize)
{
m_expecteSize = 0;
m_readSize = 0;
m_file.close();
}
}
}
这样问题就解决啦,真是举步维艰啊(#`-_ゝ-)。
重点来了
为什么文件关闭后,打开目标文件才能显示出完整的数据呢?
是因为发送端发送最后一行数据时,也是接收端最后一次调用receiveFile(),此时它仍然满足m_readSize<m_expectSize,便会调用m_file.write(data)往本地文件中写入数据【注意!这里的写入,其实是先写入缓冲区】,但再也没有机会进入else语句去执行m_file.close()将文件关闭。又因为文件关闭时,会自动调用flush()方法将输出缓冲区的数据强制刷出,而原来的代码中,在读取最后一部分数据并写入缓冲区后,不仅没有调用m_file.close(),也没有手动调用m_file.flush(),导致最后一次写入输出缓冲区的数据没有刷出【原因后面会说】,在文件关闭前我们打开文件看到的就会是少了一行。
如果发送端使用read()往接收端发送数据,那么示例文件中的文字内容只需调用一次read()方法就能够读取完毕(数据比较少),相应的,接收端的receiveFile()方法也只会被调用一次,在文件没有关闭的情况下,如果没有手动调用flush()将输出缓冲区的数据强制刷出,输出缓冲区就会等到数据装满了才把数据写入文件,所以此时打开文件看到的就是一片空白。
总结
※ 在这次查bug的过程中,掌握了read()和readLine()的使用方法并了解了它们的区别。
※ 另外,之前读过的一些关于文件传输的代码都是先读取数据再判断是否接收完毕,当时并没有引起我特别的注意,结果这次我采用了先判断是否接收完毕再决定要不要读取数据,就出现了问题,因为接收端在满足 m_readSize == m_expectSize 的条件后,下一次就不会再进入读取数据的receiveFile()函数了,此时再判断是否接收完毕就是多此一举了。
※ 对输入输出缓冲区的作用和工作原理也有了更具象的理解。文件对象调用write()方法时,不仅仅是往文件写数据这么简单。数据会先被读到内存缓冲区,当缓冲区被数据装满时,才会将数据内容一并写入文件。但这样会出现的问题是,当数据很少,装不满缓冲区时,可能会导致数据的丢失。所以,为了保险起见,应该每次调用write()后都用 flush() 方法将缓冲区遗留的数据强制刷出。当然,Qt的帮助文档告诉我们,QFile类的 close() 方法会先调用 flush() 清空缓冲区,把数据写入文件,最后才关闭文件。所以,对文件进行读写操作之后,一定不要忘了调用 close() 关闭文件。
参考
输出流flush()用法
JAVA中的flush()方法
java,write()方法后写flush()的作用?
文件流关闭顺序问题
Qt的QFile类详解
write()方法和flush()的作用