閱讀159 返回首頁    go 阿裏雲 go 技術社區[雲棲]


[Qt教程] 第38篇 網絡(八)TCP(二)

[Qt教程] 第38篇 網絡(八)TCP(二)

樓主
 發表於 2013-9-6 15:50:35 | 查看: 421| 回複: 3
TCP(二)


版權聲明

該文章原創於作者yafeilinux,轉載請注明出處!


導語

在上一節裏我們使用TCP服務器發送一個字符串,然後在TCP客戶端進行接收。在這一節將重新寫一個客戶端程序和一個服務器程序,這次實現客戶端進行文件的發送,服務器進行文件的接收。有了上一節的基礎,這一節的內容就很好理解了,注意一下幾個信號和槽的關聯即可。當然,我們這次要更深入了解一下數據的發送和接收的處理方法。



環境:Windows Xp + Qt 4.8.5+QtCreator 2.8.0



目錄


一、客戶端
二、服務器端



正文



一、客戶端
這次先講解客戶端,在客戶端裏需要與服務器進行連接,一旦連接成功,就會發出connected()信號,這時我們就進行文件的發送。
在上一節已經看到,發送數據時先發送了數據的大小信息。這一次,我們要先發送文件的總大小,然後文件名長度,然後是文件名,這三部分合稱為文件頭結構,最後再發送文件數據。所以在發送函數裏就要進行相應的處理,當然,在服務器的接收函數裏也要進行相應的處理。對於文件大小,這次使用了qint64,它是64位的,可以表示一個很大的文件了。


1.新建QtGui項目
名稱為tcpSender,基類選擇QWidget,類名為Widget,完成後打開tcpSender.pro添加一行代碼:QT += network 。

2.我們在widget.ui文件中將界麵設計如下。


01.jpg



這裏“主機”後的Line EditobjectNamehostLineEdit;“端口”後的Line EditobjectNameportLineEdit;下麵的Progress BarobjectNameclientProgressBar,其value屬性設為0;“狀態”LabelobjetNameclientStatusLabel;“打開”按鈕的objectNameopenButton;“發送”按鈕的objectNamesendButton


3.widget.h文件中進行更改。

1)添加頭文件包含#include <QtNetwork>

2)添加private變量:
QTcpSocket *tcpClient;
    QFile *localFile;  //要發送的文件
    qint64 totalBytes;  //數據總大小
    qint64 bytesWritten;  //已經發送數據大小
    qint64 bytesToWrite;   //剩餘數據大小
    qint64 loadSize;   //每次發送數據的大小
    QString fileName;  //保存文件路徑
QByteArray outBlock;  //數據緩衝區,即存放每次要發送的數據

3)添加私有槽函數:
private slots:
    void send();  //連接服務器
    void startTransfer();  //發送文件大小等信息
    void updateClientProgress(qint64); //發送數據,更新進度條
    void displayError(QAbstractSocket::SocketError); //顯示錯誤
void openFile();  //打開文件


4.widget.cpp文件中進行更改
添加頭文件:#include <QFileDialog>

1)在構造函數中添加代碼:
loadSize = 4*1024;
totalBytes = 0;
bytesWritten = 0;
bytesToWrite = 0;
tcpClient = new QTcpSocket(this);
//當連接服務器成功時,發出connected()信號,我們開始傳送文件
connect(tcpClient,SIGNAL(connected()),this,SLOT(startTransfer()));
//當有數據發送成功時,我們更新進度條
connect(tcpClient,SIGNAL(bytesWritten(qint64)),this,
       SLOT(updateClientProgress(qint64)));
connect(tcpClient,SIGNAL(error(QAbstractSocket::SocketError)),this,
       SLOT(displayError(QAbstractSocket::SocketError)));
//開始使”發送“按鈕不可用
ui->sendButton->setEnabled(false);

我們主要是進行了變量的初始化和幾個信號和槽函數的關聯。


2)實現打開文件函數。
void Widget::openFile()   //打開文件
{
    fileName = QFileDialog::getOpenFileName(this);
    if(!fileName.isEmpty())
    {
       ui->sendButton->setEnabled(true);
       ui->clientStatusLabel->setText(tr("打開文件 %1 成功!")
                                       .arg(fileName));
    }
}
該函數將在下麵的“打開”按鈕單擊事件槽函數中調用。

(3)實現連接函數。
void Widget::send()   //連接到服務器,執行發送
{
    ui->sendButton->setEnabled(false);
    bytesWritten = 0;
    //初始化已發送字節為0
    ui->clientStatusLabel->setText(tr("連接中..."));
    tcpClient->connectToHost(ui->hostLineEdit->text(),
                             ui->portLineEdit->text().toInt());//連接
}
該函數將在“發送”按鈕的單擊事件槽函數中調用。


(4)實現文件頭結構的發送。
void Widget::startTransfer()  //實現文件大小等信息的發送
{
    localFile = new QFile(fileName);
    if(!localFile->open(QFile::ReadOnly))
    {
       qDebug() << "open file error!";
       return;
    }
   
    //文件總大小
    totalBytes = localFile->size();
   
    QDataStream sendOut(&outBlock,QIODevice::WriteOnly);
    sendOut.setVersion(QDataStream::Qt_4_6);
QString currentFileName = fileName.right(fileName.size()
- fileName.lastIndexOf('/')-1);
   
    //依次寫入總大小信息空間,文件名大小信息空間,文件名
    sendOut << qint64(0) << qint64(0) << currentFileName;
   
    //這裏的總大小是文件名大小等信息和實際文件大小的總和
    totalBytes += outBlock.size();
   
    sendOut.device()->seek(0);
    //返回outBolock的開始,用實際的大小信息代替兩個qint64(0)空間
    sendOut<<totalBytes<<qint64((outBlock.size() - sizeof(qint64)*2));
   
    //發送完頭數據後剩餘數據的大小
    bytesToWrite = totalBytes - tcpClient->write(outBlock);
   
    ui->clientStatusLabel->setText(tr("已連接"));
    outBlock.resize(0);
}


5)下麵是更新進度條,也就是發送文件數據。

//更新進度條,實現文件的傳送
void Widget::updateClientProgress(qint64 numBytes)
{
    //已經發送數據的大小
    bytesWritten += (int)numBytes;
   
    if(bytesToWrite > 0) //如果已經發送了數據
    {
   //每次發送loadSize大小的數據,這裏設置為4KB,如果剩餘的數據不足4KB,
   //就發送剩餘數據的大小
       outBlock = localFile->read(qMin(bytesToWrite,loadSize));
      
       //發送完一次數據後還剩餘數據的大小
       bytesToWrite -= (int)tcpClient->write(outBlock);
      
       //清空發送緩衝區
       outBlock.resize(0);
      
    } else {
       localFile->close(); //如果沒有發送任何數據,則關閉文件
    }
   
    //更新進度條
    ui->clientProgressBar->setMaximum(totalBytes);
    ui->clientProgressBar->setValue(bytesWritten);
   
    if(bytesWritten == totalBytes) //發送完畢
    {
     ui->clientStatusLabel->setText(tr("傳送文件 %1 成功")
.arg(fileName));
       localFile->close();
       tcpClient->close();
    }
}


(6)實現錯誤處理函數。
void Widget::displayError(QAbstractSocket::SocketError) //顯示錯誤
{
    qDebug() << tcpClient->errorString();
    tcpClient->close();
    ui->clientProgressBar->reset();
    ui->clientStatusLabel->setText(tr("客戶端就緒"));
    ui->sendButton->setEnabled(true);
}

(7)我們從widget.ui中分別進行“打開”按鈕和“發送”按鈕的單擊事件槽函數,然後更改如下。
void Widget::on_openButton_clicked() //打開按鈕
{
    openFile();
}
void Widget::on_sendButton_clicked() //發送按鈕
{
    send();
}

5.我們為了使程序中的中文不顯示亂碼,在main.cpp文件中更改。
添加頭文件:#include <QTextCodec>
main函數中添加代碼:QTextCodec::setCodecForTr(QTextCodec::codecForName("UTF-8"));

6.現在可以先運行程序。

7.程序整體思路分析。
我們設計好界麵,然後按下“打開”按鈕,選擇要發送的文件,這時調用了openFile()函數。然後點擊“發送”按鈕,調用send()函數,與服務器進行連接。當連接成功時就會發出connected()信號,這時就會執行startTransfer()函數,進行文件頭結構的發送,當發送成功時就會發出bytesWritten(qint64)信號,這時執行updateClientProgress(qint64 numBytes)進行文件數據的傳輸和進度條的更新。這裏使用了一個loadSize變量,我們在構造函數中將其初始化為4*10244字節,它的作用是,我們將整個大的文件分成很多小的部分進行發送,每部分為4字節。而當連接出現問題時就會發出error(QAbstractSocket::SocketError)信號,這時就會執行displayError()函數。對於程序中其他細節我們就不再分析,希望大家能自己編程研究一下。

二、服務器端
我們在服務器端進行數據的接收。服務器端程序是很簡單的,我們開始進行監聽,一旦發現有連接請求就發出newConnection()信號,然後我們便接受連接,開始接收數據。

1.新建QtGui應用
名稱為tcpReceiver,基類選擇QWidget,類名為Widget,完成後打開tcpReceiver.pro添加一行代碼:QT += network 。

2.我們更改widget.ui文件,設計界麵如下。
其中“服務器端”LabelobjectNameserverStatusLabel;進度條ProgressBarobjectNameserverProgressBar,設置其value屬性為0;“開始監聽”按鈕的objectNamestartButton
效果如下。


02.jpg



3.更改widget.h文件的內容。

1)添加頭文件包含:#include <QtNetwork>


2)添加私有變量:
     QTcpServer tcpServer;
    QTcpSocket *tcpServerConnection;
    qint64 totalBytes;  //存放總大小信息
    qint64 bytesReceived;  //已收到數據的大小
    qint64 fileNameSize;  //文件名的大小信息
    QString fileName;   //存放文件名
    QFile *localFile;   //本地文件
QByteArray inBlock;   //數據緩衝區

3)添加私有槽函數:
private slots:
    void on_startButton_clicked();
    void start();   //開始監聽
    void acceptConnection();  //建立連接
void updateServerProgress();  //更新進度條,接收數據

//顯示錯誤
void displayError(QAbstractSocket::SocketError socketError);



4.更改widget.cpp文件。


1)在構造函數中添加代碼:
totalBytes = 0;
    bytesReceived = 0;
fileNameSize = 0;

//當發現新連接時發出newConnection()信號
    connect(&tcpServer,SIGNAL(newConnection()),this,
SLOT(acceptConnection()));


2)實現start()函數。
void Widget::start() //開始監聽
{
    ui->startButton->setEnabled(false);
    bytesReceived =0;
    if(!tcpServer.listen(QHostAddress::LocalHost,6666))
    {
       qDebug() << tcpServer.errorString();
       close();
       return;
    }
    ui->serverStatusLabel->setText(tr("監聽"));
}

(3)實現接受連接函數。
void Widget::acceptConnection()  //接受連接
{
    tcpServerConnection = tcpServer.nextPendingConnection();
connect(tcpServerConnection,SIGNAL(readyRead()),this,
SLOT(updateServerProgress()));
    connect(tcpServerConnection,
SIGNAL(error(QAbstractSocket::SocketError)),this,
           SLOT(displayError(QAbstractSocket::SocketError)));
    ui->serverStatusLabel->setText(tr("接受連接"));
    tcpServer.close();
}

(4)實現更新進度條函數。
void Widget::updateServerProgress()  //更新進度條,接收數據
{
   QDataStream in(tcpServerConnection);
   in.setVersion(QDataStream::Qt_4_6);
   if(bytesReceived <= sizeof(qint64)*2)
   { //如果接收到的數據小於16個字節,那麼是剛開始接收數據,我們保存到//來的頭文件信息
       if((tcpServerConnection->bytesAvailable() >= sizeof(qint64)*2)
           && (fileNameSize == 0))
       { //接收數據總大小信息和文件名大小信息
           in >> totalBytes >> fileNameSize;
           bytesReceived += sizeof(qint64) * 2;
       }
       if((tcpServerConnection->bytesAvailable() >= fileNameSize)
           && (fileNameSize != 0))
       {  //接收文件名,並建立文件
           in >> fileName;
           ui->serverStatusLabel->setText(tr("接收文件 %1 ...")
                                           .arg(fileName));
           bytesReceived += fileNameSize;
           localFile= new QFile(fileName);
           if(!localFile->open(QFile::WriteOnly))
           {
                qDebug() << "open file error!";
                return;
           }
       }
       else return;
   }
   if(bytesReceived < totalBytes)
   {  //如果接收的數據小於總數據,那麼寫入文件
      bytesReceived += tcpServerConnection->bytesAvailable();
      inBlock= tcpServerConnection->readAll();
      localFile->write(inBlock);
      inBlock.resize(0);
   }
//更新進度條
   ui->serverProgressBar->setMaximum(totalBytes);
   ui->serverProgressBar->setValue(bytesReceived);
   
   if(bytesReceived == totalBytes)
   { //接收數據完成時
    tcpServerConnection->close();
    localFile->close();
    ui->startButton->setEnabled(true);
ui->serverStatusLabel->setText(tr("接收文件 %1 成功!")
.arg(fileName));
   }
}


5)錯誤處理函數。
void Widget::displayError(QAbstractSocket::SocketError) //錯誤處理
{
    qDebug() << tcpServerConnection->errorString();
    tcpServerConnection->close();
    ui->serverProgressBar->reset();
    ui->serverStatusLabel->setText(tr("服務端就緒"));
    ui->startButton->setEnabled(true);
}

(6)我們在widget.ui中進入“開始監聽”按鈕的單擊事件槽函數,更改如下。
void Widget::on_startButton_clicked() //開始監聽按鈕
{
    start();
}

5.我們為了使程序中的中文不顯示亂碼,在main.cpp文件中更改。
添加頭文件包含:#include <QTextCodec>
main函數中添加代碼:QTextCodec::setCodecForTr(QTextCodec::codecForName("UTF-8"));

6.運行程序,並同時運行tcpSender程序,效果如下。



03.jpg


我們先在服務器端按下“開始監聽”按鈕,然後在客戶端輸入主機地址和端口號,然後打開要發送的文件,點擊“發送”按鈕進行發送。


結語



在這兩節裏我們介紹了TCP的應用,可以看到服務器端和客戶度端都可以當做發送端或者接收端,而且數據的發送與接收隻要使用相對應的協議即可,它是可以根據用戶的需要來進行編程的,沒有固定的格式。《Qt及Qt Quick開發實戰精解》中的局域網聊天工具就是本節知識的擴展,大家可以從社區下載頁麵下載其源碼。




涉及到的源碼:  tcpSender.rar (3.55 KB, 下載次數: 12)  tcpReceiver.rar (3.02 KB, 下載次數: 12) 

最後更新:2017-04-03 14:54:11

  上一篇:go [Qt教程] 第39篇 網絡(九)進程和線程
  下一篇:go centos6.4服務器-搭建lnmp環境的經驗小結