• 641阅读
  • 2回复

Qt TCP网络编程——传输图片(附TCP连接逻辑以及完整代码) [复制链接]

上一主题 下一主题
离线fanxinglanyu
 

只看楼主 倒序阅读 楼主  发表于: 04-11

@[toc]
# 0 效果
![在这里插入图片描述](https://img-blog.csdnimg.cn/20200410171614765.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzMzc1NTk4,size_16,color_FFFFFF,t_70)
![在这里插入图片描述](https://img-blog.csdnimg.cn/2020041017163264.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzMzc1NTk4,size_16,color_FFFFFF,t_70)
![在这里插入图片描述](https://img-blog.csdnimg.cn/20200410171655147.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzMzc1NTk4,size_16,color_FFFFFF,t_70)
[完整代码地址](https://github.com/fanxinglanyu/QtTcpImage)
# 1 知识点
## 1.1 图片编码和解码
png编码为base64数据 :(用于客服端传输)

```cpp
QByteArray Client::getImageData(const QImage &image)
{
    QByteArray imageData;
    QBuffer buffer(&imageData);
    image.save(&buffer, "png");
    imageData = imageData.toBase64();

    return imageData;
}
```
base64数据解码为png :(用于客服端传输)
```cpp
QImage Server::getImage(const QString &data)
{
    QByteArray imageData = QByteArray::fromBase64(data.toLatin1());
    QImage image;
    image.loadFromData(imageData);
    return image;
}
```
## 1.2 图片显示(合理缩放图像以填充label)

```cpp
           QImage imageData = getImage(imageContent);
            QPixmap resImage = QPixmap::fromImage(imageData);
            QPixmap* imgPointer = &resImage;
            imgPointer->scaled(ui->imageLabel->size(), Qt::IgnoreAspectRatio);//重新调整图像大小以适应窗口
            //法二:替换前面的表达式
           // imgPointer->scaled(ui->imageLabel->size(), Qt::KeepAspectRatio);//设置pixmap缩放的尺寸

            ui->imageLabel->setScaledContents(true);//设置label的属性,能够缩放pixmap充满整个可用的空间。
            ui->imageLabel->setPixmap(*imgPointer);
```
![在这里插入图片描述](https://img-blog.csdnimg.cn/20200410171546563.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzMzc1NTk4,size_16,color_FFFFFF,t_70#pic_center)
## 1.3 TCP传输与接收
* 信号:
    - 客户端连接上服务器时,发送`SIGNAL(connected())`信号;
    - 客户端的连接被断开时,发送`SIGNAL(disconnected())`信号;
    - 客户端中的`tcpClient->write(outBlock);`,发送`readyRead()`信号【readyRead()当网络套接字上有新的网络数据有效负载时】;
    - 两个端出现错误时,发出`SIGNAL(error(QAbstractSocket::SocketError))`信号;
    - 每当有客户端连接服务器后,会发送`SIGNAL(newConnection())`信号;
* 连接逻辑
    - 1 首先服务器通过`(QTcpServer 对象).listen(ip地址,端口)`开始监听端口;
    - 2  客户端通过`(QTcpSocket* 对象)->connectToHost(ip地址,端口)`连接服务器,当连接上服务器时,对于客户端会发出`SIGNAL(connected())`信号,对于服务器端会发送`SIGNAL(newConnection())`信号;  
    - 3  服务器端收到`SIGNAL(newConnection())`信号后,使用`(QTcpServer对象).nextPendingConnection()` 获得连接套接字;                    
    - 4 客户端通过`(QTcpSocket *对象)->write(QByteArray对象)`发出`readyRead()`信号(把数据写入TCP包中);
    - 5  服务器端收到`readyRead()`信号后,开始处理信息,显示图片。
    - 6 服务器端断开连接时`(QTcpSocket*对象)->disconnectFromHost();`,会发出`SIGNAL(disconnected())`信号,然后客户端更新状态
## 1.4 TCP知识
如果只是发送空的图片,就是只有TCP报头的数据报,大小为20字节。

一般以太网帧的大小为[64,1518],除去帧头14字节(6字节目的MAC地址,6字节目的MAC地址,2字节Tye域值)、4字节帧尾(CRC校验),剩下大小为[64,1500]字节,不足64字节会被当作无效帧(因为争用期的存在,即确保在发送数据的同时检测到可能存在的冲突【CSMA/CD协议】)。

一个TCP报文大小最大为1500-20(IP头)-20(TCP头)=1460字节;
一个UPD报文最大为1500-20(IP头)-8(UPD头)=1482字节。

TCP全靠IP曾来分帧(流协议),TCP协议本身会进行拥赛和流利控制。

# 2 客户端
类的声明
```cpp
private:
    Ui::Client *ui;

    QTcpSocket *tcpClient;
    QFile *localFile;     // 要发送的文件
    qint64 totalBytes;    // 发送数据的总大小
//    qint64 bytesWritten;  // 已经发送数据大小
//    qint64 bytesToWrite;  // 剩余数据大小
    qint64 payloadSize;   // 每次发送数据的大小(64k) 未用到
    QString fileName;     // 保存文件路径
    QByteArray outBlock;  // 数据缓冲区,即存放每次要发送的数据块

    QImage image;//图片
    QString currentImageName;//图片名

    volatile bool isOk;

private slots:
    void openFile();//打开文件
    void send();//发送
    void connectServer();//连接服务器
    void startTransfer();//发送图片数据
    void displayError(QAbstractSocket::SocketError);//处理错误函数
    void tcpConnected();//更新isOk的值,按钮以及label的显示
    void tcpDisconnected();//断开连接处理的事件

    //图片转base64字符串
    QByteArray getImageData(const QImage&);

    void on_openButton_clicked();//打开图片
    void on_sendButton_clicked();//发送图片
    void on_connectButton_clicked();//连接或断开服务器
signals:
    void buildConnected();//连接上服务器后,发出的信号
```

类的构造函数:

```cpp
Client::Client(QWidget *parent) :
    QDialog(parent),
    ui(new Ui::Client)
{
    ui->setupUi(this);

    //地址和端口自动补全以及默认提示
    QStringList hostWordList, portWordList;
    hostWordList <<tr("127.0.0.1");
    portWordList << tr("6666");
    QCompleter* completerHost = new QCompleter(hostWordList, this);
    QCompleter* completerPort = new QCompleter(portWordList, this);

    ui->hostLineEdit->setCompleter(completerHost);
    ui->portLineEdit->setCompleter(completerPort);
    ui->hostLineEdit->setPlaceholderText(tr("127.0.0.1"));
    ui->portLineEdit->setPlaceholderText(tr("6666"));

    payloadSize = 64 * 1024; // 64KB
    totalBytes = 0;
//    bytesWritten = 0;
//    bytesToWrite = 0;
    isOk = false;

    ui->sendButton->setEnabled(false);

    tcpClient = new QTcpSocket(this);

    // 当连接服务器成功时,发出connected()信号,isOK置为true
    connect(tcpClient, SIGNAL(connected()), this, SLOT(tcpConnected()));
    //当点击发送按钮后(且isOK为true),发出buildConnected()信号,开始传送数据
    connect(this, SIGNAL(buildConnected()), this, SLOT(startTransfer()));
    //当断开连接时,发出disconnected(),isOK置为false
    connect(tcpClient, SIGNAL(disconnected()), this, SLOT(tcpDisconnected()));
    //显示错误
    connect(tcpClient, SIGNAL(error(QAbstractSocket::SocketError)),
            this, SLOT(displayError(QAbstractSocket::SocketError)));
}
```
其余成员函数以及槽函数的定义:

```cpp
void Client::openFile()
{
    fileName = QFileDialog::getOpenFileName(this);

    if (!fileName.isEmpty()) {

        //获得实际文件名
        currentImageName = fileName.right(fileName.size()
                                                 - fileName.lastIndexOf('/')-1);

        ui->clientStatusLabel->setText(tr("打开 %1 文件成功!").arg(currentImageName));

        if(isOk == true){
            ui->sendButton->setEnabled(true);
        }
    }
}

void Client::send()
{
    if(!isOk){
        ui->clientStatusLabel->setText(tr("请先连接服务器"));
        return;
    }else{
        //发射信号
        emit buildConnected();
        qDebug() << "emit buildConnected()" << endl;
    }
}

void Client::connectServer()
{
    // 初始化已发送字节为0
//    bytesWritten = 0;
    ui->clientStatusLabel->setText(tr("连接中…"));

//    //连接到服务器
    tcpClient->connectToHost(ui->hostLineEdit->text(),
                             ui->portLineEdit->text().toInt());

    isOk = true;
    qDebug() << "connectServer: isOk is ok" << endl;
}


void Client::startTransfer()
{
    QDataStream sendOut(&outBlock, QIODevice::WriteOnly);
    sendOut.setVersion(QDataStream::Qt_5_6);

   //获得图片数据
    QImage image(fileName);
    QString imageData = getImageData(image);

    qDebug() << "fileName: " <<fileName << endl;
//    qDebug() << "imageData" << imageData << endl;

    // 保留总大小信息空间、图像大小信息空间,然后输入图像信息
    sendOut << qint64(0) << qint64(0) << imageData;

    // 这里的总大小是总大小信息、图像大小信息和实际图像信息的总和
    totalBytes += outBlock.size();
    sendOut.device()->seek(0);

    // 返回outBolock的开始,用实际的大小信息代替两个qint64(0)空间
    sendOut << totalBytes << qint64((outBlock.size() - sizeof(qint64)*2));

    //发出readyRead()信号
    tcpClient->write(outBlock);

    qDebug() << "图片的内容大小: " << qint64((outBlock.size() - sizeof(qint64)*2)) << endl;
    qDebug() << "整个包的大小: " << totalBytes << endl;
//    qDebug() << "发送完文件头结构后剩余数据的大小(bytesToWrite): " << bytesToWrite <<endl;

    outBlock.resize(0);

    ui->clientStatusLabel->setText(tr("传送文件 %1 成功").arg(currentImageName));
    totalBytes = 0;
//    bytesToWrite = 0;
}

void Client::displayError(QAbstractSocket::SocketError)
{
    qDebug() << tcpClient->errorString();
    tcpClient->close();

    ui->clientStatusLabel->setText(tr("客户端就绪"));
    ui->sendButton->setEnabled(true);
}

void Client::tcpConnected()
{
    isOk = true;
    ui->connectButton->setText(tr("断开"));

    ui->clientStatusLabel->setText(tr("已连接"));
}

void Client::tcpDisconnected()
{
    isOk = false;
    tcpClient->abort();
    ui->connectButton->setText(tr("连接"));

    ui->clientStatusLabel->setText(tr("连接已断开"));
}

QByteArray Client::getImageData(const QImage &image)
{
    QByteArray imageData;
    QBuffer buffer(&imageData);
    image.save(&buffer, "png");
    imageData = imageData.toBase64();

    return imageData;
}

// 打开按钮
void Client::on_openButton_clicked()
{
    ui->clientStatusLabel->setText(tr("状态:等待打开文件!"));
    openFile();

}

// 发送按钮
void Client::on_sendButton_clicked()
{
    send();
}

void Client::on_connectButton_clicked()
{
    if (ui->connectButton->text() == tr("连接")) {
        tcpClient->abort();
        connectServer();
    } else {
        tcpClient->abort();
    }
}
```

布局:
![在这里插入图片描述](https://img-blog.csdnimg.cn/20200410180023699.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzMzc1NTk4,size_16,color_FFFFFF,t_70)
# 3 服务器
类声明:

```cpp
private:
    Ui::Server *ui;

    QTcpServer tcpServer;
    QTcpSocket *tcpServerConnection;
    qint64 totalBytes;     // 存放总大小信息
    qint64 bytesReceived;  // 已收到数据的大小
    qint64 fileNameSize;   // 文件名的大小信息
    qint64 imageSize; //图片大小

    QString fileName;      // 存放文件名
    QFile *localFile;      // 本地文件
    QByteArray inBlock;    // 数据缓冲区
    QString imageContent;

    QImage image;//图片


private slots:
    void start();//监听的事件
    void acceptConnection();//被客户端连接上后,创建套接字、接收数据、处理异常、关闭服务器
    void updateServerProgress();//接收并处理显示图片
    void displayError(QAbstractSocket::SocketError socketError);//错误处理

    //base64字符串转图片
    QImage getImage(const QString &);

    void on_startButton_clicked();//监听或断开监听
```
构造函数:

```cpp
    ui->setupUi(this);

    //端口自动补全以及默认提示
    ui->portLineEdit->setPlaceholderText(tr("6666"));//设置默认提示
    QStringList portWordList;
    portWordList << tr("6666");
    QCompleter* portCompleter = new QCompleter(portWordList, this);
    ui->portLineEdit->setCompleter(portCompleter);

    connect(&tcpServer, SIGNAL(newConnection()),
            this, SLOT(acceptConnection()));


    ui->imageLabel->show();
```
其余函数的定义:

```cpp
void Server::start()
{


    if (!tcpServer.listen(QHostAddress::LocalHost, ui->portLineEdit->text().toInt())) {
        qDebug() << tcpServer.errorString();
        close();
        return;
    }

    totalBytes = 0;
    bytesReceived = 0;
    imageSize = 0;
    ui->serverStatusLabel->setText(tr("正在监听"));

}

void Server::acceptConnection()
{
    //获得链接套接字
    tcpServerConnection = tcpServer.nextPendingConnection();

    //接收数据
    //readyRead()当网络套接字上有新的网络数据有效负载时
    connect(tcpServerConnection, SIGNAL(readyRead()),
            this, SLOT(updateServerProgress()));
    //处理异常
    connect(tcpServerConnection, SIGNAL(error(QAbstractSocket::SocketError)),
            this, SLOT(displayError(QAbstractSocket::SocketError)));

    ui->serverStatusLabel->setText(tr("接受连接"));
    // 关闭服务器,不再进行监听
//    tcpServer.close();
}


void Server::updateServerProgress()
{
    QDataStream in(tcpServerConnection);
    in.setVersion(QDataStream::Qt_5_6);

    // 如果接收到的数据小于16个字节,保存到来的文件头结构
    if (bytesReceived <= sizeof(qint64)*2) {
        if((tcpServerConnection->bytesAvailable() >= sizeof(qint64)*2)
                && (imageSize == 0)) {
            // 接收数据总大小信息和文件名大小信息
            in >> totalBytes  >> imageSize;
            bytesReceived += sizeof(qint64) * 2;

            if(imageSize == 0){
                  ui->serverStatusLabel->setText(tr("显示的图片为空!"));
            }
              qDebug() <<"定位点0" << endl;
        }
        if((tcpServerConnection->bytesAvailable() >= imageSize)
                && (imageSize != 0)) {

            // 接收文件名,并建立文件
            in >> imageContent;

//            qDebug() << imageContent << endl;

            ui->serverStatusLabel->setText(tr("接收文件 …"));

            QImage imageData = getImage(imageContent);

            QPixmap resImage = QPixmap::fromImage(imageData);
            QPixmap* imgPointer = &resImage;
            imgPointer->scaled(ui->imageLabel->size(), Qt::IgnoreAspectRatio);//重新调整图像大小以适应窗口
           // imgPointer->scaled(ui->imageLabel->size(), Qt::KeepAspectRatio);//设置pixmap缩放的尺寸

            ui->imageLabel->setScaledContents(true);//设置label的属性,能够缩放pixmap充满整个可用的空间。
            ui->imageLabel->setPixmap(*imgPointer);

            bytesReceived += imageSize;

            qDebug() << "定位1  bytesReceived: " << bytesReceived << endl;

            if(bytesReceived == totalBytes){
                 ui->serverStatusLabel->setText(tr("接收文件成功"));
                 totalBytes = 0;
                 bytesReceived = 0;
                 imageSize = 0;
            }

         }
     }
}

void Server::displayError(QAbstractSocket::SocketError socketError)
{
    qDebug() <<"errorString()" <<tcpServerConnection->errorString();
    tcpServerConnection->close();

    ui->serverStatusLabel->setText(tr("服务端就绪"));

}

QImage Server::getImage(const QString &data)
{
    QByteArray imageData = QByteArray::fromBase64(data.toLatin1());
    QImage image;
    image.loadFromData(imageData);
    return image;
}

// 开始监听按钮

void Server::on_startButton_clicked()
{
    if(ui->startButton->text() == tr("监听")){
        ui->startButton->setText(tr("断开"));
        start();
    }else{
        ui->startButton->setText(tr("监听"));
        tcpServer.close();

        tcpServerConnection->disconnectFromHost();
    }
}

```


# 4 不足
*  没有心跳机制来判断离线,进而自动重连服务器,没有设置超时时间;
* 没有使用线程进行图片传输
* 没有实现多个客户端和一个服务端通信
离线九重水

只看该作者 1楼 发表于: 04-11
不明觉厉
离线big_mouse

只看该作者 2楼 发表于: 04-19
      
快速回复
限100 字节
 
上一个 下一个