前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Qt - 聊天室发送图片/文件

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

作者头像
何其不顾四月天
发布2023-03-10 12:52:20
8630
发布2023-03-10 12:52:20
举报
文章被收录于专栏:Qt&PyQt四月天的专栏

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

简介

代码语言:javascript
复制
好久没有发博客,上一篇的博客还是在上一份工作离职前整理的一篇博客。大半年没有发,一是工作繁忙,转成了音视频方向,新的工作内容暂时还不便发出来,二是不知道发什么内容,也没有整理。考虑了一下是python调用C库,但是整理起来比较费劲,想想就整理这个了 内容还相对少一点,比较有意思。

这个聊天室是我上一次的一个小项目,头像,签名,群聊,登录,图片发送等等相关功能,这次就单独说一下图片发送了。

思路

版本信息

代码语言:javascript
复制
1.Qt version: 5.12.2
2.没有使用第三库

关键点

CSDN不支持plantuml,贴了一下图

聊天室收发图片时序图

代码语言:javascript
复制
@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

关键点的选择

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

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

TCP连接建立
代码语言:javascript
复制
    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;
    }	
TCP接收数据
代码语言:javascript
复制
    connect(socket, SIGNAL(readyRead()),this, SLOT(readyReadSlot()));          //接收消息
	//接收数据槽函数
    void Widget::readyReadSlot()
    {
        QByteArray data = socket->readAll();
        byteArray += data; //当前socket接收数据缓冲区,将新来的数据添加到数据缓冲区末尾
        emit sign_recvData(); //触发数据解析事件
    }
TCP发送数据
代码语言:javascript
复制
void Widget::sendMsg(QString msg)
{
    if(socket->isOpen() && socket->isValid())
    {
        QByteArray _bufByteArry;
        //msg -> _bufByteArry : QString 转为 QByteArray
        socket->write(_bufByteArry);
    }
}
图片选择
代码语言:javascript
复制
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));
}
图片保存
代码语言:javascript
复制
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")));
    }
}
数据发送与数据解析

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

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

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

包结构

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

数据结构如下所示:

代码语言:javascript
复制
{
    "sendname" : "username",
    "recvname" : "username",
    "msgtype" : 0, //在实际的业务处理中,消息类型只包含两种数据, 文本数据,图片数据
    "msgdata" : "data"
}
代码语言:javascript
复制
{
    "type" : "", //消息类型
    "length" : "", //数据长度
    "data" : ""	//数据内容
}

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

代码语言:javascript
复制
{
    "type" : "user_msg", 
    "length" : "",
    "data" : "
    {
    \"sendname\" : \"username\",
    \"recvname\" : \"username\",
    \"msgtype\" : 0, 
    \"msgdata\" : \"data\"}
	"
}

结构体内容如下所示:

代码语言:javascript
复制
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;
        }
    }
};
校验数据

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

代码语言:javascript
复制
//下边的数据结构就是类似的抽象概念
struct NetMsgHeader
{
    int startID;
    int length;
};

struct NetMsgEnd
{
    int endID;
};

struct NetMsg
{
    NetMsgHeader header;
    QString msg;
    NetMsgEnd end;
};
代码语言:javascript
复制
#define MSG_HEAD_ID             123456 //定义包头
#define MSG_END_ID              654321 //定义包尾
组包数据
代码语言:javascript
复制
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
    }
}
数据组包
代码语言:javascript
复制
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;
        }
    }
}

扩张

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

代码语言:javascript
复制
{
    "sendname" : "username",
    "recvname" : "username",
    "msgtype" : 0, //在实际的业务处理中,消息类型只包含两种数据, 0:文本数据 1:图片数据 2:文件数据
    "msgname" : "name", //消息名称 -0:text 1: image 2: filename.fmt
    "msgdata" : "data"
}

结构体改为如下:

代码语言:javascript
复制
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;
        }
    }
};

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

备注

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

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022-08-14,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Qt - 聊天室发送图片/文件
    • 简介
      • 思路
        • 版本信息
        • 关键点
      • 扩张
        • 备注
        相关产品与服务
        文件存储
        文件存储(Cloud File Storage,CFS)为您提供安全可靠、可扩展的共享文件存储服务。文件存储可与腾讯云服务器、容器服务、批量计算等服务搭配使用,为多个计算节点提供容量和性能可弹性扩展的高性能共享存储。腾讯云文件存储的管理界面简单、易使用,可实现对现有应用的无缝集成;按实际用量付费,为您节约成本,简化 IT 运维工作。
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档