1.实现思路
要实现文件传输功能,需要在基础的 TCP 通信的代码上进行修改。
主要有两个要处理的点:文件的读写,TCP 收发。
对于文件的读写,每次只读取部分数据进行发送,然后 seek 到紧邻的位置便于下次读取;接收端写文件更简单,收到文件数据写文件就行了(准备传输的时候会把文件名和文件长度发到接收端)。
要实现 TCP 发送和接收文件,需要拟定通信协议,用于从字节流中取出数据帧(多次发送的数据连在一起,或者一次发送的分多次收到,就需要我们先把接收的数据缓存起来,根据协议逐帧解析和校验)。我写了个很简陋的协议:
传输协议
帧结构:帧头4+帧长2+帧类型1+帧数据N+帧尾2(没有校验段,懒得写)
帧头:4字节定值 0x0F 0xF0 0x00 0xFF
帧长:2字节数据段长度值 arr[4]*0x100+arr[5] 前面为高位后面为低位
帧类型:1字节
- 0x01 [S/C]准备发送文件,后跟四字节文件长度和N字节utf8文件名,长度计算同帧长一样前面为高位后面为低位
- 0x02 [C]文件数据
- 0x03 [S/C]发送结束,客户端无数据段,服务端接收返回1字节数据段:成功=1,失败=0,可扩展失败原因枚举
- 0x04 [S/C]取消发送
(服务端收到0x01 0x03开始和结束发送两个命令要进行应答,回同样的命令码无数据段)
帧尾:2字节定值 0x0D 0x0A
发送数据时在命令或数据的头尾加上帧头帧尾即可:
//帧头+长度+类型
char frameHead[7] = { 0x0F, (char)0xF0, 0x00, (char)0xFF, 0x00, 0x00, 0x00 };
//帧尾
char frameTail[2] = { 0x0D, 0x0A };
void ServerOperate::sendData(char type, const QByteArray &data)
{
if(!socket->isValid())
return;
frameHead[6]=type;
const quint64 data_size=data.count();
frameHead[5]=data_size%0x100;
frameHead[4]=data_size/0x100;
//发送头+数据+尾
socket->write(frameHead,7);
socket->write(data);
socket->write(frameTail,2);
}
对于数据的解析,校验帧头帧长帧尾(这里偷懒没有设计校验字段)之后,通过帧类型字段分别处理。以接收端为例:
//dataTemp为QByteArray成员变量
void ServerOperate::operateReceiveData(const QByteArray &data)
{
static QByteArray frame_head=QByteArray(frameHead,4);
//这里只是简单的处理,所以用了QByteArray容器做缓存
dataTemp+=data;
//处理数据
while(true){
//保证以帧头为起始
while(!dataTemp.startsWith(frame_head)&&dataTemp.size()>4){
dataTemp.remove(0,1); //左边移除一字节
}
//小于最小帧长
if(dataTemp.size()connectToHost(QHostAddress(address),port);
}else{
emit logMessage("socket->state() != QAbstractSocket::UnconnectedState");
}
}
void ClientOperate::disconnectTcp()
{
doDisconnect();
}
void ClientOperate::startFileTransfer()
{
//之前如果打开了先释放
doCloseFile();
if(!socket->isValid())
return;
const QString file_path=getFilePath();
//无效路径
if(file_path.isEmpty() || !QFile::exists(file_path)){
emit logMessage("无效的文件路径"+file_path);
return;
}
file=new QFile(this);
file->setFileName(file_path);
//打开失败
if(!file->open(QIODevice::ReadOnly)){
doCloseFile();
emit logMessage("打开文件失败"+file_path);
return;
}
sendSize=0;
fileSize=file->size();
if(fileSize>0%0x100;
file_size[2]=data_size>>8%0x100;
file_size[1]=data_size>>16%0x100;
file_size[0]=data_size>>24;
//把文件大小和文件名发送给服务端,然后等待确认命令的返回
QFileInfo info(file_path);
sendData(0x01,QByteArray(file_size,4)+info.fileName().toUtf8());
}
void ClientOperate::cancelFileTransfer()
{
//关闭文件
doCancel();
//发送停止传输指令
sendData(0x04,QByteArray());
}
void ClientOperate::initOperate()
{
socket=new QTcpSocket(this);
//收到数据,触发readyRead
connect(socket,&QTcpSocket::readyRead,[this]{
//没有可读的数据就返回
if(socket->bytesAvailable()readAll());
});
//连接状态改变
connect(socket,&QTcpSocket::connected,[this]{
setConnected(true);
emit connectStateChanged(true);
emit logMessage(QString("已连接服务器 [%1:%2]")
.arg(socket->peerAddress().toString())
.arg(socket->peerPort()));
});
connect(socket,&QTcpSocket::disconnected,[this]{
setConnected(false);
emit connectStateChanged(false);
emit logMessage(QString("与服务器连接已断开 [%1:%2]")
.arg(socket->peerAddress().toString())
.arg(socket->peerPort()));
});
timer=new QTimer(this);
//通过定时器来控制数据发送
connect(timer,&QTimer::timeout,[this]{
if(!socket->isValid()){
doCancel();
emit logMessage("Socket不可操作,发送终止");
return;
}
if(!file||!file->isOpen()){
doCancel();
emit logMessage("文件操作失败,发送终止");
return;
}
const qint64 read_size=file->read(fileBuffer,4096);
//socket->write(fileBuffer,read_size);
sendFile(fileBuffer,read_size);
sendSize+=read_size;
file->seek(sendSize);
if(!socket->waitForBytesWritten()){
doCancel();
emit logMessage("文件发送超时,发送终止");
return;
}
//避免除零
if(fileSize>0){
emit progressChanged(sendSize*100/fileSize);
}
if(sendSize>=fileSize){
doCancel();
emit logMessage("文件发送完成");
emit progressChanged(100);
sendData(0x03,QByteArray());
return;
}
});
}
void ClientOperate::doDisconnect()
{
//断开socket连接,释放资源
socket->abort();
doCloseFile();
}
void ClientOperate::doCloseFile()
{
if(file){
file->close();
delete file;
file=nullptr;
}
}
void ClientOperate::doCancel()
{
timer->stop();
if(file){
//关闭文件
doCloseFile();
}
}
void ClientOperate::sendData(char type,const QByteArray &data)
{
//传输协议
//帧结构:帧头4+帧长2+帧类型1+帧数据N+帧尾2(没有校验段,懒得写)
//帧头:4字节定值 0x0F 0xF0 0x00 0xFF
//帧长:2字节数据段长度值 arr[4]*0x100+arr[5] 前面为高位后面为低位
//帧类型:1字节
//- 0x01 准备发送文件,后跟四字节文件长度和N字节utf8文件名,长度计算同帧长一样前面为高位后面为低位
//- 0x02 文件数据
//- 0x03 发送结束
//- 0x04 取消发送
//(服务端收到0x01 0x03开始和结束发送两个命令要进行应答,回同样的命令码无数据段)
//帧尾:2字节定值 0x0D 0x0A
if(!socket->isValid())
return;
frameHead[6]=type;
const quint64 data_size=data.count();
frameHead[5]=data_size%0x100;
frameHead[4]=data_size/0x100;
//发送头+数据+尾
socket->write(frameHead,7);
socket->write(data);
socket->write(frameTail,2);
}
void ClientOperate::sendFile(const char *data, int size)
{
if(!socket->isValid())
return;
frameHead[6]=(char)0x02;
const quint64 data_size=size;
frameHead[5]=data_size%0x100;
frameHead[4]=data_size/0x100;
//发送头+数据+尾
socket->write(frameHead,7);
socket->write(data,size);
socket->write(frameTail,2);
}
void ClientOperate::operateReceiveData(const QByteArray &data)
{
static QByteArray frame_head=QByteArray(frameHead,4);
//这里只是简单的处理,所以用了QByteArray容器做缓存
dataTemp+=data;
//处理数据
while(true){
//保证以帧头为起始
while(!dataTemp.startsWith(frame_head)&&dataTemp.size()>4){
dataTemp.remove(0,1); //左边移除一字节
}
//小于最小帧长
if(dataTemp.size()isListening();
}
void ServerOperate::listen(const QString &address, quint16 port)
{
if(server->isListening())
doDislisten();
//启动监听
const bool result=server->listen(QHostAddress(address),port);
emit listenStateChanged(result);
emit logMessage(result?"服务启动成功":"服务启动失败");
}
void ServerOperate::dislisten()
{
doDislisten();
emit listenStateChanged(false);
emit logMessage("服务关闭");
}
void ServerOperate::cancelFileTransfer()
{
//关闭文件
doCancel();
//发送停止传输指令
sendData(0x04,QByteArray());
}
void ServerOperate::initOperate()
{
server=new QTcpServer(this);
//监听到新的客户端连接请求
connect(server,&QTcpServer::newConnection,this,[this]{
//如果有新的连接就取出
while(server->hasPendingConnections())
{
//nextPendingConnection返回下一个挂起的连接作为已连接的QTcpSocket对象
QTcpSocket *new_socket=server->nextPendingConnection();
emit logMessage(QString("新的客户端连接 [%1:%2]")
.arg(new_socket->peerAddress().toString())
.arg(new_socket->peerPort()));
//demo只支持一个连接,多余的释放掉
if(socket){
new_socket->abort();
new_socket->deleteLater();
emit logMessage("目前已有客户端连接,新连接已释放");
continue;
}else{
socket=new_socket;
}
//收到数据,触发readyRead
connect(socket,&QTcpSocket::readyRead,[this]{
//没有可读的数据就返回
if(socket->bytesAvailable()readAll());
});
//连接断开,销毁socket对象
connect(socket,&QTcpSocket::disconnected,[this]{
emit logMessage(QString("客户端连接已断开 [%1:%2]")
.arg(socket->peerAddress().toString())
.arg(socket->peerPort()));
socket->deleteLater();
socket=nullptr;
});
}
});
}
void ServerOperate::doDislisten()
{
//关闭服务,断开socket连接,释放资源
server->close();
if(socket){
socket->abort();
}
if(file){
file->close();
}
}
void ServerOperate::doCloseFile()
{
if(file){
file->close();
delete file;
file=nullptr;
}
}
void ServerOperate::doCancel()
{
if(file){
//关闭文件
doCloseFile();
}
}
bool ServerOperate::readyReceiveFile(qint64 size, const QString &filename)
{
//重置状态
fileSize=size;
receiveSize=0;
if(file){
doCloseFile();
}
//创建qfile用于写文件
file=new QFile(this);
QString file_path=getFilePath();
if(file_path.isEmpty())
file_path=QApplication::applicationDirPath();
file->setFileName(file_path+"/"+filename);
//Truncate清掉原本内容
if(!file->open(QIODevice::WriteOnly)){
emit logMessage("创建文件失败,无法进行接收"+file->fileName());
return false;
}
emit logMessage("创建文件成功,准备接收"+file->fileName());
return true;
}
void ServerOperate::onReceiveFile(const char *data, qint64 size)
{
if(!file||!file->isOpen()){
doCancel();
//发送停止传输指令
sendData(0x04,QByteArray());
emit logMessage("文件操作失败,取消接收");
return;
}
if(size>0){
const qint64 write_size = file->write(data,size);
//感觉这个waitForBytesWritten没啥用啊
if(write_size!=size && !file->waitForBytesWritten(3000)){
doCancel();
//发送停止传输指令
sendData(0x04,QByteArray());
emit logMessage("文件写入超时,取消接收");
return;
}
}
receiveSize+=size;
//避免除零
if(fileSize>0){
emit progressChanged(receiveSize*100/fileSize);
}
if(receiveSize>=fileSize){
doCancel();
emit logMessage("文件接收完成");
emit progressChanged(100);
return;
}
}
void ServerOperate::sendData(char type, const QByteArray &data)
{
//传输协议
//帧结构:帧头4+帧长2+帧类型1+帧数据N+帧尾2(没有校验段,懒得写)
//帧头:4字节定值 0x0F 0xF0 0x00 0xFF
//帧长:2字节数据段长度值 arr[4]*0x100+arr[5] 前面为高位后面为低位
//帧类型:1字节
//- 0x01 准备发送文件,后跟四字节文件长度和N字节utf8文件名,长度计算同帧长一样前面为高位后面为低位
//- 0x02 文件数据
//- 0x03 发送结束
//- 0x04 取消发送
//(服务端收到0x01 0x03开始和结束发送两个命令要进行应答,回同样的命令码无数据段)
//帧尾:2字节定值 0x0D 0x0A
if(!socket->isValid())
return;
frameHead[6]=type;
const quint64 data_size=data.count();
frameHead[5]=data_size%0x100;
frameHead[4]=data_size/0x100;
//发送头+数据+尾
socket->write(frameHead,7);
socket->write(data);
socket->write(frameTail,2);
}
void ServerOperate::operateReceiveData(const QByteArray &data)
{
static QByteArray frame_head=QByteArray(frameHead,4);
//这里只是简单的处理,所以用了QByteArray容器做缓存
dataTemp+=data;
//处理数据
while(true){
//保证以帧头为起始
while(!dataTemp.startsWith(frame_head)&&dataTemp.size()>4){
dataTemp.remove(0,1); //左边移除一字节
}
//小于最小帧长
if(dataTemp.size()
关注
打赏
最近更新
- 深拷贝和浅拷贝的区别(重点)
- 【Vue】走进Vue框架世界
- 【云服务器】项目部署—搭建网站—vue电商后台管理系统
- 【React介绍】 一文带你深入React
- 【React】React组件实例的三大属性之state,props,refs(你学废了吗)
- 【脚手架VueCLI】从零开始,创建一个VUE项目
- 【React】深入理解React组件生命周期----图文详解(含代码)
- 【React】DOM的Diffing算法是什么?以及DOM中key的作用----经典面试题
- 【React】1_使用React脚手架创建项目步骤--------详解(含项目结构说明)
- 【React】2_如何使用react脚手架写一个简单的页面?