fanxinglanyu |
2020-04-11 09:52 |
Qt TCP网络编程——传输图片(附TCP连接逻辑以及完整代码)
@[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 不足 * 没有心跳机制来判断离线,进而自动重连服务器,没有设置超时时间; * 没有使用线程进行图片传输 * 没有实现多个客户端和一个服务端通信
|
|