目录
1.传输流程
2.服务器端实现代码
3.客户端代码:
4.界面
每次发送都在图像数据(经过压缩-编码后的图像数据)的最前面设置一64bit的帧头标志位,正常图像传输时,该帧头标志位为本次图像数据(经过压缩-编码后的图像数据)的大小(字节数);当图像序列传输完成时,该标志位为64bit的全1:
(quint64)0xFFFFFFFFFFFFFFFF
以此来判断视频传输的结束;
内存中的图像数据(3个通道的二维数组->以jpeg格式压缩后得到压缩的图像码流->再经过qCompress压缩->转换成Base64编码方式->发送;
接收到的图像数据->从Base64编码还原成字节数组QByteArray->经过qUncompress解压缩->将数据以jpeg的方式解释成QImage->显示
C-S 通讯,传输的过程就是一个同步解释数据的过程。
从代码部分解释清楚一个收发过程当时的 对数据存取的逻辑控制 很重要!
套接字这个管道像流水的水管一样的,一端一直在输入,另一端一直在取;(这个管道的容量本身是有限的(套接字缓存区的大小)
同步好这个关系;
Server.h 头文件
#pragma once
#include
#include "ui_Server.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace cv;
class Server : public QMainWindow
{
Q_OBJECT
public:
Server(QWidget *parent = Q_NULLPTR);
private:
Ui::ServerClass ui;
QTcpServer* ServerListenSocket;
QTcpSocket* ClientConnectedSocket;
VideoCapture cap;
QTimer* timer;
quint16 clientRequestSize;
private slots:
void clientConnection();
void readClientRequest();
void SendData();
};
Server.cpp
#include "Server.h"
Server::Server(QWidget *parent)
: QMainWindow(parent)
{
ui.setupUi(this);
ServerListenSocket = new QTcpServer(this); //服务器端监听套接字 ServerListenSocket
if (!ServerListenSocket->listen(QHostAddress::Any, 8888)) //监听本机8888端口上的任意IP地址的连入
{
QMessageBox::critical(this, tr("Fortune Server"), tr("Unable to start the server:%l.").arg(ServerListenSocket->errorString()));
ServerListenSocket->close();
return;
}
connect(ServerListenSocket, SIGNAL(newConnection()), this, SLOT(clientConnection())); //将监听套接字有新客户端的接入信号newConnection() 的与处理的槽函数clientConnection() 连接
clientRequestSize = 0; //客户端发送过来的第一段数据块的大小(字节数)
}
//有新客户端连接的处理函数
void Server::clientConnection()
{
ui.info->addItem("An new client is connected!");
ClientConnectedSocket = ServerListenSocket->nextPendingConnection(); //返回已连接套接字对象
connect(ClientConnectedSocket, SIGNAL(readyRead()), this, SLOT(readClientRequest())); //将已连接套接字对象的准备好可读信号readyRead与 readClientRequest()槽函数连接
connect(ClientConnectedSocket, SIGNAL(disconnected()), ClientConnectedSocket,SLOT(deleterLater())); //已连接套接字的断开信号与自身的稍后删除信号相连接
}
//读取客户端发送过来的 请求 (发送任意字节流,只是将其按收发双方约定的规则解释为 第一次交互的请求)
void Server::readClientRequest()
{
QDataStream in(ClientConnectedSocket);
in.setVersion(QDataStream::Qt_5_10);
//如果客户端发送过来的第一段数据块的大小为0,说明确实是第一次交互
if (clientRequestSize == 0)
{
//客户端发送过来的第一段数据块的大小如果小于 64bit ,则说明:还未收到客户端发送过来的前64bit的数据,这64bit的数据存储了客户端第一次请求包的大小(字节数)
if (ClientConnectedSocket->bytesAvailable() < sizeof(quint16))
{
return; //返回,继续等待 接收数据,数据还在套接字缓存当中
}
else//如果 客户端发送过来的第一段数据块的大小>= 64bit 了
{
in >> clientRequestSize;//将数据的前64bit提取出来,存储到quint64 类型的clientRequestSize
}
}
if (ClientConnectedSocket->bytesAvailable() < clientRequestSize)//当前套接字缓冲区中存储的数据如果小于clientRequestSize个字节
{
return;//返回,继续等待 接收数据,数据还在套接字缓存当中
}
quint8 requestType;
in >> requestType;//从套接字缓冲区中读取 8bit的数据解释为quint8类型,存储到requestType中
if (requestType == 'R') //如果requestType是 'R' 字符R的ASCII值
{
timer = new QTimer(this);//启动定时器
timer->start(20);//设置30ms的定时周期
connect(timer, SIGNAL(timeout()), this, SLOT(SendData()));//将30ms时间到与发送数据的 SendData() 连接
cap.open("drop.mp4"); //用Opencv 的VideoCapture对象打开视频文件
if (!cap.isOpened()) {
QMessageBox::information(this, tr("warrning"), tr("can't open video!"));
}
}
}
void Server::SendData()
{
Mat frame;
cap >> frame; //从视频中提取一帧图像
if (frame.empty())//图像为空:视频播放结束或是空视频
{
//图像流传输结束:
QByteArray finishFlag;
QDataStream out(&finishFlag, QIODevice::WriteOnly); //二进制只写输出流
out.setVersion(QDataStream::Qt_5_10); //输出流的版本
out << (quint64)0xFFFFFFFFFFFFFFFF;
ClientConnectedSocket->write(finishFlag); //将整块数据写入套接字
timer->stop();
ClientConnectedSocket->close();
ServerListenSocket->close();
QMessageBox::information(this, tr("warning"), tr("the video is end!"));
return;
}
cvtColor(frame, frame, COLOR_BGR2RGB);//颜色转换
QByteArray byte; //The QByteArray class provides an array of bytes.
QBuffer buf(&byte); //缓存区域
QImage image((unsigned const char*)frame.data, frame.cols, frame.rows, QImage::Format_RGB888);//Mat图像矩阵转为QImage
//QString imageSize = "image size is:" + QString::number(frame.cols*frame.rows * 3) + " Bytes";
//ui.info->addItem(imageSize);//图像的大小(字节数)
image.save(&buf, "JPEG"); //将图像以jpeg的压缩方式压缩了以后保存在 buf当中
//QString jpegImageSize = "jpeg image size is " + QString::number(buf.size()) + " Bytes";
//ui.info->addItem(jpegImageSize); //压缩后的jpg图像的大小(字节数)
QByteArray ss = qCompress(byte, 1);//将压缩后的jpg图像 再用qCompress 压缩 ,第二个参数1-9,9是最大压缩率
//QString ssSize="ss's size is "+ QString::number(ss.size()) + " Bytes";
//ui.info->addItem(ssSize);//用qCompress 压缩后的数据大小(字节数)
//将压缩后的字节串数据编码成Base64方式,字节数会比压缩前稍微变多一些
QByteArray vv = ss.toBase64(); // QByteArray QByteArray::toBase64() const : Returns a copy of the byte array, encoded as Base64.
//QString vvSize = "vv's size is " + QString::number(vv.size()) + " Bytes";
//ui.info->addItem(vvSize); //编码后的数据的大小
QByteArray ba;
QDataStream out(&ba, QIODevice::WriteOnly); //二进制只写输出流
out.setVersion(QDataStream::Qt_5_10); //输出流的版本
/*当操作复杂数据类型时,我们就要确保读取和写入时的QDataStream版本是一样的,简单类型,比如char,short,int,char* 等不需要指定版本也行*/
/*上面这些编解码的过程肯定是会影响 时效性的,可以考虑只使用jpeg 压缩后就进行发送 。*/
out << (quint64)0; //写入套接字的经压缩-编码后的图像数据的大小
out << vv; //写入套接字的经压缩-编码后的图像数据
out.device()->seek(0);
out << (quint64)(ba.size() - sizeof(quint64));//写入套接字的经压缩-编码后的图像数据的大小
ClientConnectedSocket->write(ba); //将整块数据写入套接字
ui.image_label->setPixmap(QPixmap::fromImage(image)); //再界面上显示该图像
ui.image_label->resize(image.size());
update(); //更新界面
}
Client.h
#pragma once
#include
#include "ui_Client.h"
#include
#include
#include
#include
#include
#include
class Client : public QMainWindow
{
Q_OBJECT
public:
Client(QWidget *parent = Q_NULLPTR);
private:
Ui::ClientClass ui;
QTcpSocket tcpSocket;
QImage *img;
qint64 imageBlockSize;
protected:
void ShowImage(QByteArray ba);
private slots:
void connectToServer();
void sendRequest();
void ReceiveData();
void connectionCloseByServer();
void error();
void tcpConnected();
void on_connectToServer_clicked();
void on_requestVideo_clicked();
};
Client.cpp
#include "Client.h"
Client::Client(QWidget *parent)
: QMainWindow(parent)
{
ui.setupUi(this);
ui.requestVideo->setEnabled(false);
imageBlockSize = 0; //一次接收的图像数据的大小(字节数)
connect(&tcpSocket, SIGNAL(disconnect()), this, SLOT(connectionCloseByServer()));//套接字的断开信号
connect(&tcpSocket, SIGNAL(readyRead()), this, SLOT(ReceiveData()));//套接字一次可读的触发信号
connect(&tcpSocket, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(error()));//套接字的错误消息信号
}
void Client::on_connectToServer_clicked()//点击该按钮,连接到服务器主机
{
connectToServer();
bool k=connect(&tcpSocket, SIGNAL(connected()), this, SLOT(tcpConnected())); //将连接以完成信号与 tcpConnected()槽函数绑定
}
void Client::on_requestVideo_clicked()//点击该按钮,发送请求,请求视频图像序列
{
sendRequest();
}
void Client::connectToServer()
{
//连接到本机的8888端口
tcpSocket.connectToHost(QHostAddress::LocalHost, 8888);//connectToHost是异步连接函数(不阻塞),一经调用结束立刻返回
ui.info->addItem("connecting to LocalHost port 8888...");
ui.connectToServer->setEnabled(false);
}
void Client::sendRequest()//发送请求,请求视频图像序列
{
QByteArray requestMessage; //请求消息(字节数组)
QDataStream out(&requestMessage, QIODevice::WriteOnly);//只读输出流
out.setVersion(QDataStream::Qt_5_10);
out << quint16(0) << quint8('R');//将请求消息的大小、长度(字节数)与请求消息 'R'写入 输出数据流out
out.device()->seek(0);
out << quint16(requestMessage.size() - sizeof(quint16));
tcpSocket.write(requestMessage);//将输出数据流中的数据写入套接字
ui.info->addItem("Sending request...");
ui.requestVideo->setEnabled(false);
}
static int receiveCount = 0;//接收 readyRead()信号的触发计数
static int imageCount = 0; //接收到的图像数据的计数
void Client::ReceiveData()
{
receiveCount++;
QString rCount = QString::number(receiveCount);
ui.receiveCount->setText(rCount);
QByteArray message;//存放从服务器接收到的字节流数据
QDataStream in(&tcpSocket); //将客户端套接字与输入数据流对象in绑定
in.setVersion(QDataStream::Qt_5_10);//设置数据流的版本
/*接收端的 这部分控制逻辑很重要*/
if (imageBlockSize == 0)
{
//如果imageBlockSize == 0 则说明,一幅图像的大小信息还未传输过来
//uint64是8字节的8 Bytes 64bit
//判断接收的数据是否有8字节(文件大小信息)
//如果有则保存到basize变量中,没有则返回,继续接收数据
if (tcpSocket.bytesAvailable() < (int)sizeof(quint64))
{//一幅图像的大小信息还未传输过来
return;
}
in >> imageBlockSize;//一幅图像的大小信息
if (imageBlockSize == (quint64)0xFFFFFFFFFFFFFFFF)//视频结束的标注符
{
tcpSocket.close();
QMessageBox::information(this, tr("warning"), tr("the video is end!"));
return;
}
qDebug() << "imageBlockSize is " << imageBlockSize;
QString imageBlockS = "imageBlockSize is " + QString::number(imageBlockSize) + "Bytes!";
ui.info->addItem(imageBlockS);
message.resize(imageBlockSize);
}
//如果没有得到一幅图像的全部数据,则返回继续接收数据
if (tcpSocket.bytesAvailable() < imageBlockSize)
{
return;
}
in >> message;//一幅图像所有像素的完整字节流
imageBlockSize = 0;//已经收到一幅完整的图像,将imageBlockSize置0,等待接收下一幅图像
imageCount++; //已接收的图像计数
QString iCount = QString::number(imageCount);
ui.imageCount->setText(iCount);
ShowImage(message); //显示当前接收到的这一幅图像
}
void Client::connectionCloseByServer()//服务端主动断开了已连接套接字
{
ui.info->addItem("Error:Connection closed by server!");
tcpSocket.close();//关闭客户端套接字
ui.connectToServer->setEnabled(true);
}
void Client::error()
{
ui.info->addItem(tcpSocket.errorString());
tcpSocket.close();
ui.connectToServer->setEnabled(true);
}
void Client::tcpConnected()//套接字已经建立连接信号的处理槽函数
{
ui.requestVideo->setEnabled(true);
}
void Client::ShowImage(QByteArray ba) //从接收到了字节流中,执行与服务器断相反的操作:解压缩、解释为图像数据
{
QString ss = QString::fromLatin1(ba.data(), ba.size());
QByteArray rc;
rc = QByteArray::fromBase64(ss.toLatin1());
QByteArray rdc = qUncompress(rc);
QImage img;
//img.loadFromData(rdc,"JPEG");//解释为jpg格式的图像
img.loadFromData(rdc);//解释为jpg格式的图像
ui.label_imageShow->setPixmap(QPixmap::fromImage(img));
ui.label_imageShow->resize(img.size());
update();
}
1.客户端:
2.服务器端:
3.显示界面:
Qt TCP Opencv 图像传输
参考:https://blog.csdn.net/u013812682/article/details/52185540