8.3 TCP文件传输
8.3.1 TCP服务器端设计
当服务器端发送文件给客户端时,要先选中一个文件,读取这个文件的大小与名称,以便于客户端可以提前准备好一个同名同大小的储存区域。
在客户端接收到文件的名字和大小后,要经过一段时间的延时,避免传输黏包问题导致的传输数据丢失,通常调用定时器进行延时,保证数据传输的完整性。
按照之前数据传输的TCP服务器设计,首先是需要俩个套接字,一个用于监听,另一个用于数据收发,此处类似,只是基于之前的操作进行扩充。
根据流程分析所需ui结构,首先是一个文件选择按钮,用于选择将要发送的文件,然后是文件发送按钮,用于文件发送。还有就是文本编辑区,用于显示客户端连接与文件发送情况。
连接操作:
在界面的初始界面文件选择按钮与文件发送按钮不可使能,当监听套接字监听到客户端的连接后,恢复文件选择按钮。此外,在监听到客户端连接成功之后,首先是通过监听套接字获取与客户端进行通信所使用的的通信套接字,获取客户端的IP和端口号,由于IP原本形式为QHostAddress,所以使用toString()进行类型转换。然后使用Qstring进行数据组包,将要显示的数据进行字符串使用Qstring函数进行拼接,然后将拼接好的字符串在文本编辑区进行显示,具体代码:
//如果客户端和服务器连接
//tcpserver自动触发,newconnection
connect(tcpServer,&QTcpServer::newConnection,
[=]()
{
//取出建立好连接的套接字
tcpSocket=tcpServer->nextPendingConnection();
//获取对方的ip和端口
QString ip=tcpSocket->peerAddress().toString();
quint16 port=tcpSocket->peerPort();
//数据组包
QString str;
str=QString("[%1:%2] successful Link! ").arg(ip).arg(port);
ui->textEdit->setText(str); //显示到编辑器
//成功连接后才能选择文件,首先恢复按钮
ui->buttonFile->setEnabled(true);
}
);
文件选择:
在客户端与服务器端连接成功之后,选择文件按钮点击之后弹出文件选择对话框,文件对话框的父组件为this,对话框的主题为“open”,文件的上层目录为../,通过文件对话框下的获取文件名函数打开,该函数返回一个字符串形式的文件路径。
若文件路径不为空,首先将文件信息变量初始化,然后通过文件信息函数获取文件名和文件大小。获取方式是先新建一个文件信息获取变量,将文件路径作为传入参数,之后在信息函数类中调用子函数,通过返回值获取文件信息。
之后在已定义好的文件变量中设置要操作的文件文件路径,然后将文件文件以只读形式打开,然后在文本编辑区中添加路径显示,最后取消文件选择按钮使能,使能文件发送按钮。
//选择文件按钮
void ServerWidget::on_buttonFile_clicked()
{
QString filepath=QFileDialog::getOpenFileName(this,"open","../");
if(false==filepath) //选择文件路径有效
{
//先初始化文件名和大小
fileName.clear();
fileSize=0;
sendSize=0;
// //获取文件信息
QFileInfo info(filepath); //出错
fileName=info.fileName();//获取文件名字
fileSize=info.size();//获取文件大小
//只读方式打开文件
//指定文件的名字
file.setFileName(filepath);
//打开文件
bool isOk=file.open(QIODevice::ReadOnly);
if(false == isOk)
{
qDebug() << "fail 78";
}
//提示打开文件的路径
ui->textEdit->append(filepath);
ui->buttonFile->setEnabled(false);
ui->buttonSend->setEnabled(true);
}
else
{
qDebug()<<"error : 58";
}
}
文件发送:
文件发送通过发送按钮来实现,文件发送时先发送文件的名字与大小,之后开启定时器,目的是为了防止头数据与文本数据黏包造成数据丢失,定时器满之后调用数据发送函数。
//发送文件按钮
void ServerWidget::on_buttonSend_clicked()
{
//先发送文件头信息, 文件名##文件大小
QString head=QString("%1##%2").arg(fileName).arg(fileSize);
//发送头部信息
qint64 len=tcpSocket->write(head.toUtf8());
if(len>0) //头部信息发送成功
{
//发送真正的文件信息
//防止TCP黏包,定时器延时20ms
timer.start(20);
}
else
{
qDebug()<<"fail to send!";
file.close();
ui->buttonFile->setEnabled(true);
ui->buttonSend->setEnabled(false);
}
}
定时器溢出之后,通过槽函数进行数据处理:
connect(&timer,&QTimer::timeout,
[=]()
{
//进来先关闭定时器
timer.stop();
//发送文件
sendData();
} );
文件发送函数:
定义一个长度变量,一个缓存字符数组并将其初始化,将之前只读方式打开的文件中的数据写入到buf缓存区中,再将缓存区的数据通过通信套接字写入传给客户端,返回值为本次写入的数据量,当写入的数据长度>0时循环发送。最终发送的数据量等于文件大小时关闭文件,与客户端断开连接。
//数据发送函数
void ServerWidget::sendData()
{
qint64 len=0;
do{
//每次发送的数据大小
char buf[4*1024]={0};
len=0;
//往文件中读数据
len=file.read(buf,sizeof(buf));
//发数据,有多少发多少
len=tcpSocket->write(buf,len);
//将发送的数据累计
sendSize+=len;
}while(len>0);
//是否文件发送完毕
if(fileSize==sendSize)
{
ui->textEdit->append("file send finish!");
file.close();
//与客户端断开连接
tcpSocket->disconnectFromHost();
tcpSocket->close();
}
}
8.3.2 TCP文件传输客户端设计
客户端相对服务器端来说较为容易,少了一个监听套接字,只有一个通信套接字,文件接收的流程为:
第一步、接收头,分析字符串,分离出文件的大小与名字,然后在本地创建一个文件。
第二步、接收文件数据,对方发多少就接收多少,吧接收到的内容写到文件里。
.pro文件:
由于是网络通信,要添加network,其次为了使用C++11新增的lambda表达式,添加config=C++11
ClientWidget.h文件:
要定义一个通信套接字,必须的!所以先要包含一个通信套接字头文件。既然是客户端文件接收,那么定义一个文件对象指针进行写入数据操作也是必须的,为了获取接收的文件名字和大小,一个字符串形式的文件名和一个64位的大小变量也是必须的!再加一个进度条显示接收进度吧,那就再定义一个已接收的数据大小的变量。
啥?第一词接收的数据是文件名和大小,第二次才是真实的文件内容,为了区分,就再定义一个bool类型的标志信号好了。
ClientWidget.cpp文件:
按照上一篇文说的先给通信套接字开辟一个指针空间,格式咱就不啰嗦了。
客户端要和服务器连接:
连接就要写槽函数,首先是获取行编辑区服务器的ip和端口号,从文本编辑区获取的ip是字符串形式的可咋办?使用函数转换一个就可以了。
void ClientWidget::on_pushButton_3_clicked()
{
//获取服务器ip和端口
QString ip=ui->lineEditIP->text();
quint16 port=ui->lineEditPort->text().toUInt();
tcpSocket->connectToHost(QHostAddress(ip),port);
}
数据接收:
接收?啥时候接收,就是在通信套接字发出准备好接收的时候(废话,但是事实就是这样)。接收的是头文件还是文件内容?我只知道第一次是头文件,第二次是文件内容,使用标志字进行区分,在接收函数执行的上一层,将标志字设为true,表示接收的是头,接收完头之后将标志字设为false,为false的时候接收到的数据数据就是文件内容。
接收到头:
将接收到数据拆包,拆出文件名和文件大小,然后将接收数据的寄存区大小清零,并给文件名设置一个操作路径,之后初始化进度条。
接收到内容:
将缓存区的内容写入到上一步设置好的文件中,更新进度条,当接收到的数据等于发送的文件大小时,关闭文件,断开通信套接字。
isStart=true;
connect(tcpSocket,&QTcpSocket::readyRead,
[=]()
{
//取出接收的内容
QByteArray buf = tcpSocket->readAll();
if(true==isStart)
{
//接收头
isStart=false;
//初始化
//解析头部信息 QString buf=“hello##1024”
// QString str="hello##1024";
// str.section("##",0,0); //##为拆包标识 0,0从第0段开始,到第0段结束,##为段界
fileName =QString(buf).section("##",0,0);
fileSize =QString(buf).section("##",1,1).toInt();
recvSize=0;
//打开文件
file.setFileName(fileName);
bool isOk=file.open(QIODevice::WriteOnly);
if(false==isOk)
{
qDebug()<<"fail to open";
}
ui->progressBar->setMinimum(0);
ui->progressBar->setMaximum(fileSize/1024);
ui->progressBar->setValue(0);
}
else //文件信息
{
qint64 len=file.write(buf);
if(len>0)
{
recvSize+=len;
qDebug()<<len;
}
//更新进度条
ui->progressBar->setValue(recvSize/1024);
if(recvSize==fileSize)
{
//先给服务器发送接收文件完成的消息
tcpSocket->write("file done");
file.close();
QMessageBox::information(this,"finish","finish receiving!");
tcpSocket->disconnectFromHost();
tcpSocket->close();
}
}
}
);
运行结果: