QT從入門到入土(九)——TCP/IP網路通訊(以及檔案傳輸)

唯有自己強大發表於2021-08-27

引言

TCP/IP通訊(即SOCKET通訊)是通過網線將伺服器Server端客戶機Client端進行連線,在遵循ISO/OSI模型的四層層級構架的基礎上通過TCP/IP協議建立的通訊。控制器可以設定為伺服器端或客戶端。

關於TCP/IP協議可詳看:TCP/IP協議詳解 - 知乎 (zhihu.com)

 總的來說,TCP/IP通訊有兩個部分:

  • 客戶端伺服器

  • QTcpServer(監聽套接字)QTcpSocket(通訊套接字)

監聽套接字,顧名思義,監聽關於各種通訊的狀態,一旦進行通訊,監聽套接字會啟動通訊套接字,進行通訊

客戶端使用connectToHost函式主動連線伺服器後,伺服器會觸發newConnectio這個槽函式,並進行取出QTcpServer(監聽套接字),將相關內容取出並賦給QTcpSocket(通訊套接字)。
客戶端向伺服器傳送資料,觸發readyRead(),進行處理,彼此傳遞時,原理都是這樣的。

對雙方來說都起作用的部分:

  1. 一旦建立連線,就會觸發connected,伺服器特殊一點,觸發的是newConnectio
  2. 互傳資料也是一樣的,一旦接受到,就會觸發readyread

伺服器中,需要監聽套接字以及通訊套接字,監聽套接字用於監聽客戶端是否給伺服器傳送請求

本篇博文做了初步的學習與嘗試,編寫了一個客戶端和伺服器基於視窗通訊以及檔案傳輸的小例程。


 一,客戶端

客戶端的程式碼比伺服器稍簡單,總的來說,使用QT中的QTcpSocket類與伺服器進行通訊只需要以下5步:

(1)建立QTcpSocket套接字物件

    socket = new QTcpSocket(this);

(2)使用這個物件連線伺服器

    QString ip = ui.lineEdit_ip->text();//獲取ip
    int port = ui.lineEdit_2->text().toInt();//獲取埠資料
    socket->connectToHost(ip, port);

(3)使用write函式向伺服器傳送資料

    QByteArray data = ui.lineEdit_3->text().toUtf8();//獲取lineEdit控制元件中的資料併傳送給伺服器
    socket->write(data);

 (4)當socket接收緩衝區有新資料到來時,會發出readRead()訊號,因此為該訊號新增槽函式以讀取資料

 connect(socket, &QTcpSocket::readyRead, this, &QTcpClinet::ReadData);
void QTcpClinet::ReadData()
{
    QByteArray buf = socket->readAll();
    ui.textEdit->append(buf);
}

(5)斷開與伺服器的連線(關於close()和disconnectFromHost()的區別,可以按F1看幫助)

socket->disconnectFromHost();

客戶端例程:(新建一個qt專案QTcpClinet(客戶機))

  • ui介面

本地迴路ip:127.0.0.1 可以連線到本地ip(電腦內部迴圈的ip)

如果要和區域網其他ip連線 -> 在執行(win+R)+cmd+ipconfig ->ipv4地址 檢視本機ip

  • QTcpClinet.h
#include <QtWidgets/QWidget>
#include "ui_QTcpClinet.h"
#include"QTcpSocket.h"
#pragma execution_character_set("utf-8")
class QTcpClinet : public QWidget
{
    Q_OBJECT

public:
    QTcpClinet(QWidget *parent = Q_NULLPTR);
    ~QTcpClinet();
public slots:
    void on_btn_connect_clicked();
    void ReadData();
    void on_btn_push_clicked();
private:
    Ui::QTcpClinetClass ui;
    QTcpSocket* socket;//建立socket指標
};
  • QTcpClinet.cpp
#include "QTcpClinet.h"

QTcpClinet::QTcpClinet(QWidget *parent)
    : QWidget(parent)
{
    ui.setupUi(this);
    socket = new QTcpSocket(this);
}

QTcpClinet::~QTcpClinet()
{
    delete this->socket;//回收記憶體
}

void QTcpClinet::on_btn_connect_clicked()
{
  if (ui.btn_connect->text()==tr("連線伺服器"))
  {
    QString ip = ui.lineEdit_ip->text();//獲取ip
    int port = ui.lineEdit_2->text().toInt();//獲取埠資料
    //取消已有的連線
    socket->abort();
    //連線伺服器
    socket->connectToHost(ip, port);
    bool isconnect = socket->waitForConnected();//等待直到連線成功
    //如果連線成功
    if (isconnect)
    {
        ui.textEdit->append("The connection was successful!!");
        ui.btn_push->setEnabled(true);//按鈕使能
        //修改按鍵文字
        ui.btn_connect->setText("斷開伺服器連線");
        //接收緩衝區(伺服器)資訊
        connect(socket, &QTcpSocket::readyRead, this, &QTcpClinet::ReadData);
    }
    else
    {
        ui.textEdit->append("The connection falied!!");
    }
  }
  else
  {
      //斷開連線
      socket->disconnectFromHost();
      ui.btn_connect->setText("連線伺服器");
      ui.btn_push->setEnabled(false);//關閉傳送按鈕使能
  }

}

//接收緩衝區資訊函式
void QTcpClinet::ReadData()
{
    QByteArray buf = socket->readAll();
    ui.textEdit->append(buf);
}
//傳送按鈕事件
void QTcpClinet::on_btn_push_clicked()
{
    QByteArray data = ui.lineEdit_3->text().toUtf8();//獲取lineEdit控制元件中的資料併傳送給伺服器
    socket->write(data);
    //判斷是否寫入成功
    bool iswrite = socket->waitForBytesWritten();
    if (iswrite)
    {
        //寫入成功
    }
    else
    {
        //沒有寫入成功
    }
}

二,伺服器(需要一直執行哦)

伺服器除了使用到了QTcpSocket類,還需要用到QTcpSever類。即便如此,也只是比客戶端複雜一點點,用到了6個步驟:

(1)建立QTcpSever物件

    server = new QTcpServer(this);

(2)偵聽一個埠,使得客戶端可以使用這個埠訪問伺服器

    server->listen(QHostAddress::Any, 6677);//監聽所有ip和6677埠

(3)當伺服器被客戶端訪問時,會發出newConnection()訊號,因此為該訊號新增槽函式,並用一個QTcpSocket物件接受客戶端訪問

connect(server, &QTcpServer::newConnection, this, &TcpServer::ClientConnect);
void TcpServer::ClientConnect()
{
    //解析所有客戶連線
    while (server->hasPendingConnections())
    {
        //連線上後通過socket(QTcpSocket物件)獲取連線資訊
        socket = server->nextPendingConnection();
        QString str = QString("[ip:%1,port:%2]").arg(socket->peerAddress().toString()).arg(socket->peerPort());//監聽客戶端是否有訊息傳送
        connect(socket, &QTcpSocket::readyRead, this, &TcpServer::ReadData1);
    }
}

(4)使用socket的write函式向客戶端傳送資料

socket->write(data);

(5)當socket接收緩衝區有新資料到來時,會發出readRead()訊號,因此為該訊號新增槽函式以讀取資料

//監聽客戶端是否有訊息傳送
connect(socket, &QTcpSocket::readyRead, this, &TcpServer::ReadData1);
//獲取客戶端向伺服器傳送的資訊
void TcpServer::ReadData1()
{
    QByteArray buf = socket->readAll();//readAll最多接收65532的資料
    QString str = QString("[ip:%1,port:%2]").arg(socket->peerAddress().toString()).arg(socket->peerPort());
    ui.textEdit_server->append(str +QString(buf));
    //socket->write("ok");//伺服器接收到資訊後返回一個ok
}

(6)取消偵聽

server->close();

 伺服器例程:(新增一個新的qt專案TcpServer(伺服器))

  • ui介面

  •   TcpServer.h
#include <QtWidgets/QWidget>
#include"ui_TcpServer.h"
#include"qtcpserver.h"
#include"qtcpsocket.h"

class TcpServer : public QWidget
{
    Q_OBJECT

public:
    TcpServer(QWidget *parent = Q_NULLPTR);
    ~TcpServer();
public slots:
    void on_btn_server_clicked();
    void on_btn_listen_clicked();
private:
    Ui::TcpServerClass ui;
    QTcpServer* server;
    QTcpSocket* socket;//一個客戶端對應一個socket
    void ClientConnect();
    void ReadData1();
    
};
  • TcpServer.cpp
#include "TcpServer.h"
#include"qstring.h"
#include"qdebug.h"
#pragma execution_character_set("utf-8")
TcpServer::TcpServer(QWidget *parent)
    : QWidget(parent)
{
    ui.setupUi(this);
    server = new QTcpServer(this);
   //客戶機連線訊號槽
    connect(server, &QTcpServer::newConnection, this, &TcpServer::ClientConnect);
}

TcpServer::~TcpServer()
{
    server->close();
    server->deleteLater();
}

void TcpServer::on_btn_listen_clicked()
{
    if (ui.btn_listen->text()=="偵聽")
    {
        //從輸入框獲取埠號
        int port = ui.lineEdit_port->text().toInt();
        //偵聽指定埠的所有ip
        if (!server->listen(QHostAddress::Any, port))
        {
            //若出錯,則輸出錯誤資訊
            qDebug() << server->errorString();
            return;
        }
        //修改按鍵文字
        ui.btn_listen->setText("取消偵聽");    
    }
    else
    {
        socket->abort();
        //取消偵聽
        server->close();
        //修改按鍵文字
        ui.btn_listen->setText("偵聽");
    }
}

void TcpServer::ClientConnect()
{
    //解析所有客戶連線
    while (server->hasPendingConnections())
    {
        //連線上後通過socket獲取連線資訊
        socket = server->nextPendingConnection();
        QString str = QString("[ip:%1,port:%2]").arg(socket->peerAddress().toString()).arg(socket->peerPort());
        //提示連線成功
        ui.textEdit_server->append(str+"Connect to the server");
        //核取方塊選項為連線伺服器的ip
        ui.comboBox->addItem(str);
        //將socket地址放入combobox屬性內
        //ui.comboBox->setItemData(ui.comboBox->count()-1, QVariant((int)socket));
        //監聽客戶端是否有訊息傳送
        connect(socket, &QTcpSocket::readyRead, this, &TcpServer::ReadData1);
    }
}

//獲取客戶端向伺服器傳送的資訊
void TcpServer::ReadData1()
{
    QByteArray buf = socket->readAll();//readAll最多接收65532的資料
    QString str = QString("[ip:%1,port:%2]").arg(socket->peerAddress().toString()).arg(socket->peerPort());
    ui.textEdit_server->append(str +QString(buf));
}

//伺服器向客戶端傳送資訊
void TcpServer::on_btn_server_clicked()
{
  if(ui.comboBox->count()== 0)return;
  //QTcpSocket* skt=  (QTcpSocket*)ui.comboBox->itemData(ui.comboBox->currentIndex()).value<int>();
  socket->write(ui.lineEdit1->text().toUtf8());
}

注意:write中需要寫入char型別的元素或QByteArray型別的元素

效果展示:

三,TCP/IP檔案傳輸

上文實現了訊息的傳輸,由於socket->readAll();(readAll最多接收65532的資料),因此對於大檔案的傳輸用此方法是不可取的。

TCP/IP檔案傳輸的思路:

  1. 客戶端和伺服器連線
  2. 客戶端選擇檔案,併傳送檔案給伺服器(傳送的是檔案的幀頭,格式:檔名&大小)
  3. 伺服器觸發readyRead,然後解析檔案幀頭(獲取檔名和大小),並返回客戶端一個ok訊息
  4. 客戶端觸發readyRead,然後傳送檔案資料,通過progressBar顯示進度
  5. 伺服器再次觸發readyRead,接收檔案資料,並儲存(通過ishead判斷接收的是檔案幀頭還是檔案資料)

程式碼實現:

新建伺服器專案(TcpServer)

  • TcpServer.h
#pragma once

#include <QtWidgets/QWidget>
#include "ui_TcpServer.h"
#include"qtcpserver.h"
#include"qtcpsocket.h"
#pragma execution_character_set("utf-8")
class TcpServer : public QWidget
{
    Q_OBJECT

public:
    TcpServer(QWidget *parent = Q_NULLPTR);
    void hasConnect();
private:
    Ui::TcpServerClass ui;
    QTcpServer* server;
    QTcpSocket* socket;
    bool ishead;
    QString fileName;
    int fileSize;//接收檔案的總大小
    int recvSize;//當前接收檔案的大小
    QByteArray filebuf;//當前接收的檔案資料
};
  • TcpServer.cpp
#include "TcpServer.h"
#include"qfile.h"
TcpServer::TcpServer(QWidget *parent)
    : QWidget(parent)
{
    ishead = true;
    ui.setupUi(this);
    server = new QTcpServer(this);
    //監聽1122埠的ip
    server->listen(QHostAddress::Any, 1122);
    //如果有使用者連線觸發槽函式
    connect(server, &QTcpServer::newConnection, this, &TcpServer::hasConnect);
}

void TcpServer::hasConnect()
{
    while (server->hasPendingConnections()>0)//判斷當前連線了多少人
    {
        //用socket和我們的客戶端連線,一個客戶端對應一個套接字socket
        socket = server->nextPendingConnection();
        //伺服器介面上輸出客戶端資訊
        ui.textEdit->append(QString("%1:新使用者連線").arg(socket->peerPort()));
        //如果客戶端傳送資訊過來了,觸發匿名函式
        connect(socket, &QTcpSocket::readyRead, [=]() {
            QByteArray buf = socket->readAll();
            //用一個標誌位ishead判斷是頭還是資料位
            if (ishead)
            {
                //如果是頭,解析頭(檔名,檔案大小)
                QString str = QString(buf);
                ui.textEdit->append(str);
                QStringList strlist = str.split("&");
                fileName = strlist.at(0);//解析幀標頭檔案名
                fileSize = strlist.at(1).toInt();//解析幀標頭檔案大小
                ishead = false;//下次接收到的檔案就是我們的資料
                recvSize = 0;
                filebuf.clear();
                socket->write("ok");
            }
            else
            {
                //根據檔名和檔案大小接收和儲存檔案
                filebuf.append(buf);
                recvSize += buf.size();//每接收一次檔案,當前檔案大小+1
                //當接收檔案大小等於總檔案大小,即檔案資料接收完畢
                if (recvSize>=fileSize)
                {
                    //儲存檔案
                    QFile file(ui.lineEdit->text() + fileName);
                    file.open(QIODevice::WriteOnly);
                    file.write(filebuf);
                    file.close();
                    ishead = true;
                }
            }
            });
    }
}

新建客戶端專案(QTcpClient)

  • QTcpClient.h
#include <QtWidgets/QWidget>
#include"ui_QTcpClient.h"
#include"qtcpsocket.h"
#pragma execution_character_set("utf-8")
class QTcpClient : public QWidget
{
    Q_OBJECT

public:
    QTcpClient(QWidget *parent = Q_NULLPTR);
public slots:
    void on_btn_connect_clicked();
    void on_btn_choose_clicked();
    void on_btn_open_clicked();
    
private:
    Ui::QTcpClientClass ui;
    QTcpSocket* socket;
};
  • QTcpClient.cpp
#include "QTcpClient.h"
#include"qfiledialog.h"
#include"qfileinfo.h"
QTcpClient::QTcpClient(QWidget *parent)
    : QWidget(parent)
{
    ui.setupUi(this);
    socket = new QTcpSocket(this);
    
}
void QTcpClient::on_btn_connect_clicked()
{
    QString ip = ui.lineEdit_ip->text();//獲取ip
    int port = ui.lineEdit_port->text().toInt();//獲取埠資料
    socket->connectToHost(ip, port);//連線伺服器
    //等待連線成功
    if (socket->waitForConnected())
    {
        ui.textEdit->append("<font color='green'>連線伺服器成功!</font>");    
        ui.btn_open->setEnabled(true);
        
        //如果伺服器傳送資訊到客戶端,觸發匿名函式
        connect(socket, &QTcpSocket::readyRead, [=]() {
            //讀取伺服器傳送的資訊(即緩衝區資訊)
            QByteArray buf = socket->readAll();
            if (buf=="ok")
            {
                QFile file = (ui.label_path->text());
                if (!file.open(QIODevice::ReadWrite))
                {
                    //讀取檔案失敗
                    return;
                }
                qint64 currentlen = 0;//當前已經傳送的大小
                qint64 allLength = file.size();//總檔案大小
                do
                {
                    char data[1024];
                    qint64 msize = file.read(data, 1024);//讀檔案放入打他陣列中,返回讀取到的大小
                    socket->write(data, msize);//把讀取到的data資料傳送給伺服器
                    currentlen += msize;//實時獲取當前傳送的檔案大小
                    ui.progressBar->setValue(currentlen *100 / allLength);//更新介面進度條
                } while (currentlen < allLength);//當傳送檔案等於檔案大小時,傳送完畢,迴圈結束
            }
            });

    }
    else
    {
        ui.textEdit->append("<font color='red'>連線伺服器失敗!</font>");
    }
}
//選擇檔案事件
void QTcpClient::on_btn_choose_clicked()
{
    QString path = QFileDialog::getOpenFileName(this, "開啟檔案", "", "(*.*)");
    ui.label_path->setText(path);
}
//傳送檔案事件
void QTcpClient::on_btn_open_clicked()
{
    QFileInfo info(ui.label_path->text());
    //用QFileInfo::fileName,size獲取檔名和大小 格式:檔名&大小
    //伺服器用該格式解析檔名和大小
    QString head = QString("%1&%2").arg(info.fileName()).arg(info.size());
    //將該格式傳送給伺服器 toUtf8:QString轉QByteArray或char型別
    socket->write(head.toUtf8()); 
}

效果展示:

 

 

參考博文:(5條訊息) Qt網路程式設計之TCP通訊(二)檔案傳輸_毓樹麟風的部落格-CSDN部落格

                  (5條訊息) QTCpSocket檔案傳輸_Apollon_krj的部落格-CSDN部落格

相關文章