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

举报
何其不顾四月天 发表于 2022/08/17 22:43:45 2022/08/17
【摘要】 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 文件句柄,来保存文件。

备注

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

文章来源: blog.csdn.net,作者:何其不顾四月天,版权归原作者所有,如需转载,请联系作者。

原文链接:blog.csdn.net/u011218356/article/details/126324888

【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。