• Qt - 聊天室发送图片/文件


    Qt - 聊天室发送图片/文件

    简介

    好久没有发博客,上一篇的博客还是在上一份工作离职前整理的一篇博客。大半年没有发,一是工作繁忙,转成了音视频方向,新的工作内容暂时还不便发出来,二是不知道发什么内容,也没有整理。考虑了一下是python调用C库,但是整理起来比较费劲,想想就整理这个了 内容还相对少一点,比较有意思。
    
    这个聊天室是我上一次的一个小项目,头像,签名,群聊,登录,图片发送等等相关功能,这次就单独说一下图片发送了。
    
    • 1
    • 2
    • 3

    思路

    版本信息

    1.Qt version: 5.12.2
    2.没有使用第三库
    
    • 1
    • 2

    关键点

    CSDN不支持plantuml,贴了一下图

    聊天室收发图片时序图

    @startuml
    title 时序图
    entity clientA as clientA
    entity clientB as clientB
    == 初始化 ==
    clientA -> clientA : tcp-socket初始化
    clientB -> clientB : tcp-socket初始化
    clientA <-> clientB : tcp连接建立
    == 图片收发 ==
    clientA -> clientA : 选择图片
    clientA -> clientB : 发送图片
    clientB -> clientB : 接收图片
    clientB -> clientB : 保存图片
    clientB -> clientB : 显示图片
    == end ==
    @enduml
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    在这里插入图片描述

    关键点的选择

    1.建立TCP连接 : QTcpSocket ,不做说明
    2.选择图片 : QFileDialog 实现图片选择
    3.发送图片 :  消息拼装,QTcpSocket -> write(QByteArray)
    4.接收图片 :  QTcpSocket -> readyRead(),消息解析
    5.显示图片 : QWidget->show()
    
    • 1
    • 2
    • 3
    • 4
    • 5

    其中,关键点为,tcp在实际的场景中,会遇到拆包,丢包,沾包等一些意外的情况,当图片文件比较大的时候,tcp的单帧数据有限,必然会发生拆包现象,所以我们在接收时需要考虑从组包的情况,把完整的图片数据提取出来。

    TCP连接建立

        socket = new QTcpSocket;
        socketState = false;
    	//ipAddressStr ip地址
    	//port 端口号
        if(!socketState)
        {
            socket->connectToHost(ipAddressStr, port);
            if(socket->waitForConnected(3000))
            {
                qDebug() << "Connect2Server OK";
                ui->pushButtonConnect->setText("连接成功");
                socketState = true;
            }
            else
            {
                qDebug() << socket->errorString();
                return;
            }
        }
        else
        {
            socket->close();    //触发disconnected()信号
            ui->pushButtonConnect->setText("断开连接");
            socketState = false;
        }	
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    TCP接收数据

        connect(socket, SIGNAL(readyRead()),this, SLOT(readyReadSlot()));          //接收消息
    	//接收数据槽函数
        void Widget::readyReadSlot()
        {
            QByteArray data = socket->readAll();
            byteArray += data; //当前socket接收数据缓冲区,将新来的数据添加到数据缓冲区末尾
            emit sign_recvData(); //触发数据解析事件
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    TCP发送数据

    void Widget::sendMsg(QString msg)
    {
        if(socket->isOpen() && socket->isValid())
        {
            QByteArray _bufByteArry;
            //msg -> _bufByteArry : QString 转为 QByteArray
            socket->write(_bufByteArry);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    图片选择

    void Widget::on_pushButtonSend_img_clicked()
    {
        QString fileName = QFileDialog::getOpenFileName(this,
                                                         tr("图片选择对话框"),
                                                         "F:",
                                                         tr("*png *jpg;"));
        QImage image(fileName);
        QByteArray imgBy;
        QBuffer imgBuf(&imgBy);
        image.save(&imgBuf, "png");
    
        emit chartMsg(ui->groupBox->title(), true, QString::fromLocal8Bit(imgBy.toBase64())); //送入到发送区
        //图片显示
        QString str = QString(QDateTime::currentDateTime().toString("yyyy.MM.dd hh:mm:ss ddd")) + selfName + ":\n";
        ui->textBrowserRecv->append(QString(str));
        ui->textBrowserRecv->insertHtml(imgPathToHtml(fileName));
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    图片保存

    void UserChart::setRecvMsg(bool msgType, QString msgData)
    {
        QString str = QString(QDateTime::currentDateTime().toString("yyyy.MM.dd hh:mm:ss ddd")) + ui->groupBox->title() + ":\n";
        if(!msgType)
        {
            str += msgData;
            ui->textBrowserRecv->append(QString(str));
        }
        //如果消息类型为图片消息
        else
        {
            QImage image;
            image.loadFromData(QByteArray::fromBase64(msgData.toLocal8Bit()));
            image.save(QString("./" + QDateTime::currentDateTime().toString("yyyyMMddhhmmsddd") + ".png"), "png");
            ui->textBrowserRecv->append(QString(str));
            ui->textBrowserRecv->insertHtml(imgPathToHtml(QString("./" + QDateTime::currentDateTime().toString("yyyyMMddhhmmsddd") + ".png")));
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    数据发送与数据解析

    在上述的内容中,给出了一些的基础写法。还剩在发送的前的数据组包,接收数据后的拆包,组包等一些处理。在这些处理中,有一些关键问题。

    在实际的通信过程,数据类型与内容时很复杂的,怎么确认数据是点对点的聊天数据,还是群聊的聊天数据,数据的发送人是谁,数据的接收人是谁,这些都是需要在业务过程实际的处理的一些问题。

    其中涉及到的是通信数据包数据结构的定义,以及实际的拆包组包逻辑两个关键点的解决。

    包结构

    为了减少开发的成本以及高效的阅读性,序列化与反序列化的成本。选择通用json,来处理实际的有效用户数据。

    数据结构如下所示:

    {
        "sendname" : "username",
        "recvname" : "username",
        "msgtype" : 0, //在实际的业务处理中,消息类型只包含两种数据, 文本数据,图片数据
        "msgdata" : "data"
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    {
        "type" : "", //消息类型
        "length" : "", //数据长度
        "data" : ""	//数据内容
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    user_msg为例全部的数据包如下:

    {
        "type" : "user_msg", 
        "length" : "",
        "data" : "
        {
        \"sendname\" : \"username\",
        \"recvname\" : \"username\",
        \"msgtype\" : 0, 
        \"msgdata\" : \"data\"}
    	"
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    结构体内容如下所示:

    struct UserMsg
    {
        QString sendName;
        QString recvName;
        bool msgType;
        QString msgData;
        QString parseJson()
        {
            QJsonObject jsonObj;
            jsonObj.insert("sendname", sendName);
            jsonObj.insert("recvname", recvName);
            jsonObj.insert("msgtype", msgType);
            jsonObj.insert("msgdata",msgData);
            QJsonDocument jsonDoc;
            jsonDoc.setObject(jsonObj);
            return QString::fromUtf8(jsonDoc.toJson(QJsonDocument::JsonFormat::Compact));
        }
        int parseJsonObject(QString data)
        {
            try
            {
                QJsonObject j = parse(data.toLocal8Bit(), err);
                if(err == QString(ERROR_UNSTR))
                    return KERROR;
                sendName = get_value(j, "sendname").toString();
                recvName = get_value(j, "recvname").toString();
                msgType = get_value(j, "msgtype").toBool();
                msgData = get_value(j, "msgdata").toString();
                return KSUCCESS;
            } catch (const std::exception) {
                return KFAIL;
            }
        }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    校验数据

    在包数据完成之后,就涉及到实际的 沾包,组包,拆包的实际处理,怎样保证或者说判断你接收的数据是一个完整的数据包,就涉及到包的校验。就是传统的 包头,包长度,包数据,包尾。

    //下边的数据结构就是类似的抽象概念
    struct NetMsgHeader
    {
        int startID;
        int length;
    };
    
    struct NetMsgEnd
    {
        int endID;
    };
    
    struct NetMsg
    {
        NetMsgHeader header;
        QString msg;
        NetMsgEnd end;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    #define MSG_HEAD_ID             123456 //定义包头
    #define MSG_END_ID              654321 //定义包尾
    
    • 1
    • 2
    组包数据
    void Widget::sendMsg(QString msg)
    {
        if(socket->isOpen() && socket->isValid())
        {
            NetMsg netMsg;
            netMsg.header.startID = 123456; 								//包头赋值
            netMsg.end.endID = 654321;      								//包尾赋值
            netMsg.msg = msg;												//用户数据
            netMsg.header.length = sizeof(int) * 3 + netMsg.msg.length();	//数据长度
    
            qDebug() << "SendMsg:" << msg;
    
            QByteArray _bufByteArry;
            //append 方式尾插插入数据,注意数据的转换
            _bufByteArry.append((const char*)&netMsg.header.startID, sizeof(int));	//包头转为字节数组
            _bufByteArry.append((const char*)&netMsg.header.length, sizeof(int));	//包长度转为字节数组
            _bufByteArry.append(msg.toStdString().c_str(), msg.length());			//数据转为字节数组
            _bufByteArry.append((const char*)&netMsg.end.endID, sizeof(int));		//包尾转为字节数据
    //        qDebug() << _bufByteArry << byteArrayToInt(_bufByteArry.mid(0, 4)) << byteArrayToInt(_bufByteArry.mid(4, 4))
    //                 << QString::fromLocal8Bit(_bufByteArry.mid(8, (netMsg.header.length - 12))) << byteArrayToInt(_bufByteArry.mid((netMsg.header.length - 4), 4));
            socket->write(_bufByteArry); //写入数据到socket
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    数据组包
    QByteArray byteArray; 	//声明字节型数组缓冲区,将所有接收的数据,全量保存的数据缓冲区
    void Widget::readyReadSlot()
    {
        QByteArray data = socket->readAll(); 	//读取IO口缓冲区的所有数据
        byteArray += data; 						//采用尾插的方法将数据写入数据缓冲区
        emit sign_recvData();					//触发接收信号,进行数据解析
    }
    
    connect(this, &Widget::sign_recvData, this, &Widget::slt_packagetHandle); //信号槽
    
    //数据解析
    void Widget::slt_packagetHandle()
    {
        NetMsg netMsg;
        //判断数据缓冲区的数据是否大于消息头,如果小于包头(haed + length),判断数据无效,跳出解析,继续等待下次数据到来
        if(byteArray.length() >= sizeof(NetMsgHeader))
        {
            //取出包头
            //注意提取方式
            netMsg.header.startID = byteArrayToInt(byteArray.mid(MSG_DEFAULT_POSTION, sizeof(int)));
            //取出包长度
            netMsg.header.length = byteArrayToInt(byteArray.mid(sizeof(int), sizeof(int)));
            //如果缓冲区长度大于包长度,进去数据解析
            if(byteArray.length() >= netMsg.header.length)
            {   
                //取出包尾
                netMsg.end.endID = byteArrayToInt(byteArray.mid(netMsg.header.length - sizeof(int), sizeof(int)));
                //校验包头包尾
                if(netMsg.end.endID == MSG_END_ID && netMsg.header.startID == MSG_HEAD_ID)
                {
                    //触发用户消息,发送到主线程进行对应的消息处理
                    emit sign_recvMsg(QString::fromLocal8Bit(byteArray.mid(8, netMsg.header.length - sizeof(int) * 3)));
                    //数据缓冲区,移除已经处理的数据
                    byteArray = byteArray.remove(MSG_DEFAULT_POSTION, netMsg.header.length);
                    //如果数据不为空,继续进行下一次解析
                    if(!byteArray.isEmpty())
                    {
                        emit sign_recvData();
                    }
                }
            }
            else if(byteArray.length() < netMsg.header.length)
            {
                return;
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47

    扩张

    上述的消息结构只满足图片发送与文本发送,在发送文件的时候,文件格式以及文件名称的确实导致文件无法保存。所以需要将消息结构进行扩张。

    {
        "sendname" : "username",
        "recvname" : "username",
        "msgtype" : 0, //在实际的业务处理中,消息类型只包含两种数据, 0:文本数据 1:图片数据 2:文件数据
        "msgname" : "name", //消息名称 -0:text 1: image 2: filename.fmt
        "msgdata" : "data"
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    结构体改为如下:

    struct UserMsg
    {
        QString sendName;
        QString recvName;
        int msgType;
        QString msgName;
        QString msgData;
        QString parseJson()
        {
            QJsonObject jsonObj;
            jsonObj.insert("sendname", sendName);
            jsonObj.insert("recvname", recvName);
            jsonObj.insert("msgtype", msgType);
            jsonObj.insert("msgname", msgName);
            jsonObj.insert("msgdata",msgData);
            QJsonDocument jsonDoc;
            jsonDoc.setObject(jsonObj);
            return QString::fromUtf8(jsonDoc.toJson(QJsonDocument::JsonFormat::Compact));
        }
        int parseJsonObject(QString data)
        {
            try
            {
                QJsonObject j = parse(data.toLocal8Bit(), err);
                if(err == QString(ERROR_UNSTR))
                    return KERROR;
                sendName = get_value(j, "sendname").toString();
                recvName = get_value(j, "recvname").toString();
                msgType = get_value(j, "msgtype").toInt();
                msgName = get_value(j, "msgname").toString();
                msgData = get_value(j, "msgdata").toString();
                return KSUCCESS;
            } catch (const std::exception) {
                return KFAIL;
            }
        }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    针对不同类型的文件保存,则需要一个 QFile 文件句柄,来保存文件。

    备注

    文档只写了关键内容以及关键思路,如有错误或者说更好的思路,欢迎指正,以及交流。

  • 相关阅读:
    Godot 官方2D C#重构(3):TileMap使用
    SpringBoot中读取配置文件的几个注解
    【Python】模块
    为什么要写单测
    (SpringBoot)第七章:SpringBoot日志文件
    禅道如何编辑项目模块
    Python 序列
    搭建单机版FastDFS分布式文件存储系统
    扬帆起航:许战海方法论日文版正式发布
    七夕力扣刷不停,343. 整数拆分(剑指 Offer 14- I. 剪绳子、剑指 Offer 14- II. 剪绳子 II)
  • 原文地址:https://blog.csdn.net/u011218356/article/details/126324888