目錄
1.程序設計
在同一局域網內的兩個設備,基於tcp網絡編程,實現可靠的、高速的文件傳輸,並且實時顯示傳輸進度和速度;採用客戶端、服務端形式,滿足雙向傳輸;具有可擴展性、可移植性。實測傳輸速度可達到9Mb/s。
2.程序效果
服務端
監聽端口:當計算機網絡底層收到tcp信息時,通過端口傳遞給相應的程序進行處理,也就是說一個端口只能被一個應用程序使用,但一個應用程序可以使用多個端口。
選擇監聽端口(爲了避免已被其他程序使用,可設大一點),點擊打開服務器,可更改接收文件的保存路徑,等到客戶端連接,客戶端連接成功後,可用鼠標拖動文件至中間空白處,即可將文件傳輸到客戶端。如下圖:
客戶端
輸入服務端的ip地址(下面的是我的ip,請根據服務端本地ip修改)和監聽的端口號,服務端的ip地址查看:右擊電腦右下角網絡圖標->打開網絡和共享中心->本地連接->詳細信息->IP4地址,正確填寫後點擊連接服務器,服務器端將顯示客戶端已連接,接下來就可以開始文件傳輸了。
程序窗口設計爲一直處於前置,以便拖拉文件;服務端、客戶端同時運行在同一電腦上時,也可以進行文件的傳輸(等於文件移動);實際的速度會存在差異,跟電腦配置和網絡狀態有關。
3.代碼設計
設計要點:
- QTcpSocket編程
- QFile文件操作
- UI界面設計
程序分爲客戶端、服務端兩部分:
服務端
1.初始化時,創建QTcpServer對象,並將綁定newConnection信號,當有客戶端連接時進入do_connect():
tcpServer = new QTcpServer(this);
connect(tcpServer,SIGNAL(newConnection()),this,SLOT(do_connect()));
2.點擊“打開服務器”按鈕後,獲取輸入框填寫的端口號,調用listen進行端口綁定並開始監聽客戶端的連接:
void MainWindow::on_pushButton_clicked()
{
if(ui->pushButton->text()==QString("服務器已打開")){
}else{
qint32 port=ui->lineEdit_2->text().toInt(NULL,10);
if(!tcpServer->listen(QHostAddress::Any,port))//開始監聽
{
QMessageBox::warning(this,tr("警告"), tcpServer->errorString());
tcpServer->close();
return;
}else{
//ui->lineEdit->setText(read_ip_address());
ui->label_3->setText("未有客戶端連接");
ui->pushButton->setText("服務器已打開");
qDebug() <<"【服務端】開始監聽端口:"<<port;
}
}
}
3.當有客戶端連接時進入do_connect接口,調用nexPenddingConnection獲取當前連接的客戶端的套接字,進行斷開信號和可讀信號的綁定,當連接發生斷開時進入槽函數do_disconnect,當客戶端發來信息時進入槽函數readMessage進行處理:
void MainWindow::do_connect()
{
clientConnection = tcpServer->nextPendingConnection();
connect(clientConnection, SIGNAL(disconnected()), this, SLOT(do_disconnect()));
connect(clientConnection,SIGNAL(readyRead()),this,SLOT(readMessage()));
ui->label_3->setText("客戶端已連接");
}
4.建立連接後就可以進行文件的傳輸了,將文件拖至發送區控件處,控件可獲取該文件的路徑,再判斷此時程序狀態是否滿足文件發送,滿足後調用sendfile函數,並把要發送的文件傳遞過去。
發送一個比較大的文件時,需要將文件數據分塊逐一發送,每次發送loadSize個字節。進入sendFile方法,打開發送文件,讀取文件名和文件大小,綁定bytesWritten信號和槽函數updataClientProgress,那麼將數據寫入clientConnection套接子後會調用槽函數updataClientProgress,該函數裏會繼續發剩下未發送的數據,這樣形成一個循環,直至數據發送爲止。在文件數據開始發送之前,先發送文件名字和文件大小,也以此作爲一個簡單的通知客戶端起始接收的“標誌”,讓客戶端對所需要接收的數據做準備:
void MainWindow::sendFile(QString filepath)
{
localFile = new QFile(filepath);
QFileInfo Fileinfo(*localFile);
fileName=Fileinfo.fileName();
if(!localFile->open((QFile::ReadOnly))){
qDebug() <<"【服務端】無法讀取發送文件";
return;
}else{
QString temp="正在發送:"+Fileinfo.fileName();
ui->label_3->setText(temp);
qDebug() <<"【服務端】開始發送,文件:" <<Fileinfo.fileName() <<"大小:" <<localFile->size();
}
connect(clientConnection,SIGNAL(bytesWritten(qint64)),this,SLOT(updateClientProgress(qint64)));
bytesToWrite=totalBytes = localFile->size();
status=ST_SENDING;
time.start();
timer->start(1000);
char fileinfo[1024]={0};
sprintf(fileinfo,"filename\"%s\"filesize\"%d\"",Fileinfo.fileName().toUtf8().data(),totalBytes);
char Startframe[]={0};
sprintf(Startframe,"@newfile\"%0.4d\"", strlen(fileinfo));//15Byte
//先發送文件信息
clientConnection->write(Startframe,15);
clientConnection->write(fileinfo,strlen(fileinfo));
}
void MainWindow::updateClientProgress(qint64 numBytes) //更新進度條,實現文件的傳送
{
bytesWritten +=numBytes;
if(bytesToWrite > 0)
{
outBlock = localFile->read(qMin(bytesToWrite,loadSize));
bytesToWrite -= (int)clientConnection->write(outBlock);
int sentRate=((float)bytesWritten/totalBytes)*100;
ui->progressBar->setValue(sentRate);
outBlock.resize(0);
#if debugenable
qDebug() <<tr("【服務端】已發送:%0字節,剩餘:%1字節").arg(bytesWritten).arg(bytesToWrite);
#endif
}
else
{
second_task();
timer->stop();
totalBytes = 0;
bytesWritten = 0;
bytesToWrite = 0;
status=ST_IDLE;
localFile->close(); //如果沒有發送任何數據,則關閉文件
ui->progressBar->setValue(100);
ui->label_3->setText("發送成功");
qDebug() <<fileName <<"文件發送成功.";
disconnect(clientConnection,SIGNAL(bytesWritten(qint64)),this,SLOT(updateClientProgress(qint64)));
}
}
客戶端
1.初始化時,創建QTcpSocket對象,綁定readRead信號和接收槽函數readMessage,綁定disconnected信號和連接斷開處理槽函數do_disconnect,綁定SocketError信號和錯誤處理槽函數ErrorOccurred:
tcpClient = new QTcpSocket(this);
connect(tcpClient,SIGNAL(readyRead()),this,SLOT(readMessage()));
connect(tcpClient,SIGNAL(disconnected()),this,SLOT(do_disconnect()));
connect(tcpClient,SIGNAL(error(QAbstractSocket::SocketError)),this,SLOT(ErrorOccurred(QAbstractSocket::SocketError)));
2.點擊“連接服務器”按鈕後,獲取輸入框輸入的ip地址和端口號,調用connectToHost接口連接到主機,如果連接失敗,過一段時間進入槽函數ErrorOccurred錯誤處理;連接成功後就可以進行文件的傳輸了:
void MainWindow::on_pushButton_clicked()
{
if(ui->pushButton->text()== QString("斷開連接"))
{
do_disconnect();
}else{//建立連接
QHostAddress hostAddress;
hostAddress = QHostAddress(ui->lineEdit->text());
tcpPort=ui->lineEdit_2->text().toShort(NULL,10);
tcpClient->abort();
tcpClient->connectToHost(hostAddress,tcpPort);
ui->label_4->setText("已連接服務器.");
ui->pushButton->setText("斷開連接");
ui->lineEdit->setDisabled(true);
ui->lineEdit_2->setDisabled(true);
ui->textEdit->setDisabled(false);
}
}
3.當接收到服務端發來信息後,先接收到的是文件名和文件大小,根據文件名,在指定的保存路徑filesavepath下創建本地文件localFile,再將staus狀態轉成ST_RECING接收狀態,那麼接下來收到的數據直接寫入localFile文件,每次接收到數據就更新進度條進度,當接收的字節數bytesReceived等於文件的總字節數totalBytes時,該文件接收完成。
void MainWindow::readMessage()
{
float useTime = time.elapsed();
if(status==ST_IDLE){//接收開始
char FrameHead[15]={0};
QString fileinfo;
QString s_filesize;
qint32 fileinfolen;
tcpClient->read(FrameHead,15);
sscanf(FrameHead,"@newfile\"%d\"",&fileinfolen);
fileinfo = QString(tcpClient->read(fileinfolen));
fileName = fileinfo.section('"',1,1);
s_filesize = fileinfo.section('"',3,3);
localFile = new QFile(filesavepath+fileName);
totalBytes = s_filesize.toInt();
if(!localFile->open(QFile::WriteOnly)){
qDebug()<<"【客戶端】文件創建失敗:" <<fileName;
do_disconnect();
return;
}
status=ST_RECING;
time.start();
timer->start(1000);
ui->progressBar->setValue(0);
ui->label_4->setText("正在接收:"+fileName);
qDebug() <<"【客戶端】開始接收,文件:" <<fileName <<"大小:" <<totalBytes;
return;
}
bytesReceived += tcpClient->bytesAvailable();
inBlock = tcpClient->readAll();
localFile->write(inBlock);
inBlock.resize(0);
int downedRate=((float)bytesReceived/totalBytes)*100;
ui->progressBar->setValue(downedRate);//更新進度條
#if 0//如果每次都計算會影響傳輸速度,故利用timer定時每過1s調用second_task函數來計算並顯示,參考second_task函數
double kbps = bytesReceived/1024; //B/MS
double speedks = (kbps/useTime)*1000;//k/s
QString speed;
if(speedks >= 1024){
speed.sprintf("速度:%.3fM/s",speedks/1024);
}else{
speed.sprintf("速度:%.3fk/s",speedks);
}
ui->label_5->setText(speed);
#endif
#if debugenable
qDebug() <<tr("【客戶端】:已接收 %1B(%2\%),用時:%3").arg(bytesReceived).arg(downedRate).arg((float)useTime/1000);
#endif
if(bytesReceived==totalBytes){
second_task();
timer->stop();
localFile->close();
totalBytes = 0;
bytesReceived = 0;
status=ST_IDLE;
ui->label_4->setText("接收完成.");
ui->progressBar->setValue(100);
qDebug() <<"【客戶端】接收完畢:" <<fileName;
qDebug() <<"【客戶端】:所用時間:" <<(float)useTime/1000 <<"s";
}
}
在源碼設計裏,服務端和客戶端都可以收發,相同部分不一一介紹,詳見源碼。
4.實驗可改進的地方:
- 一個服務端同時連接多個客戶端,進行文件收發服務,加入線程處理,提高傳輸效率。
- 加入應答機制,發送端發送固定字節數,需要等待接收端的應答後,才能繼續發送,保證數據發送的完整可靠、有序地傳輸。
水平有限,僅供參考,錯誤之處以及不足之處還望多多指教。
《睹一事於句中,反三隅於字外。 ———劉知幾》