1. 程式人生 > 實用技巧 >Qt音視訊開發46-視訊傳輸UDP版

Qt音視訊開發46-視訊傳輸UDP版

一、前言

上篇文章寫道採用的TCP傳輸視訊,優缺點很明顯,優點就是不丟包,缺點就是速度慢,後面換成UDP通訊,速度快了很多,少了3次握手,而且在區域網中基本上不丟包,就算偶爾丟包,對於一秒鐘25-30張圖片來說,偶爾一張圖片丟失,基本上看不出來,所以忽略,但是放到廣域網或者網際網路比如阿里雲平臺上測試的話,UDP慘不忍睹,丟包蠻多的,畢竟包資料特別多。

Qt的網路通訊類,我們平時常用的就是三個:QTcpSocket客戶端類、QTcpServer服務端類、QUdpSocket通訊類,為啥沒有QUdpServer類?其實UDP是無連線的通訊,佔用資源很小,他既可以是客戶端也可以是服務端,如果要作為服務端則指定埠呼叫bind方法即可。本程式同時支援了TCP模式和UDP模式,實際測試下來,還是建議使用TCP模式,UDP模式由於無連線在短時間內傳送大量的資料包發現會丟包,而且包的大小有限制,是65507位元組,大約64K,所以UDP模式下實時傳輸的圖片解析度不能太大,實測640*480的視訊檔案還是挺好的,720P基本上有點慘,丟包好多,可能後期還需要從協議上改進處理。

本程式和協議約定的圖片採用base64編碼傳輸,接收到以後將base64字串解碼出來生成圖片,QByteArray內建類toBase64方法轉成base64編碼的字串,QByteArray::fromBase64方法將base64字串還原成資料。在經過多次的實驗以後統計的資料顯示,編碼解碼的速度還可以,其中720P圖片編碼25ms-30ms、解碼15ms-20ms,1080P圖片編碼35ms-40ms、解碼25ms-30ms。總體上來說一秒鐘傳輸25-30張圖片和解碼25-30張圖片,還是沒有什麼問題的,只是走的CPU編碼解碼,如果開的通道數比較多的話,還是很耗CPU的,但是應付一些簡單的應用場景還是如魚得水毫無壓力。

通訊協議:

  1. 採用TCP長連線和UDP協議可選,預設通訊埠6000。
  2. 採用自定義的xml通訊協議。
  3. 所有傳輸加20個位元組頭部:IIMAGE:0000000000000,IIMAGE:為固定頭部,後面接13個位元組的 內容的長度(含20個頭部長度) 字串。
  4. 下面協議部分省略了頭部位元組。
  5. 服務端返回的資料中的uuid是對應接收到的訊息的uuid。
  6. 服務端每次返回的時候都帶了當前時間,可用於客戶端校時。
客戶端傳送心跳
<?xml version="1.0" encoding="UTF-8"?>
<ImageClient Uuid="8AF12208-0356-434C-8A49-69A2170D9B5A" Flag="SHJC00000001">
    <ClientHeart/>
</ImageClient>

伺服器收到心跳返回
<?xml version="1.0" encoding="UTF-8"?>
<ImageServer Uuid="8AF12208-0356-434C-8A49-69A2170D9B5A" NowTime="2019-12-05 16:37:47">
    Ok
</ImageServer>

客戶端傳送圖片
<?xml version="1.0" encoding="UTF-8"?>
<ImageClient Uuid="66BCB44A-B567-48ED-8889-36B8FA6C4363" Flag="SHJC00000001">
    <ClientImage>圖片base64編碼後的字串/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAJAAtADASIAAhEBAxEB/8QAHwAAAQUBAQEB...nvWsQRlXA61mTjmtWcdazLgcmrQ0U2plSMKjpDE7UtFFAwxRRRQAUuKWigQlFFFLcD//2Q==</ClientImage>
</ImageClient>

服務端收到圖片返回
<?xml version="1.0" encoding="UTF-8"?>
<ImageServer Uuid="66BCB44A-B567-48ED-8889-36B8FA6C4363" NowTime="2019-12-05 16:38:47">
    Ack
</ImageServer>

二、功能特點

  1. 多執行緒收發圖片資料和解析圖片資料,不卡主介面。
  2. 同時支援TCP和UDP兩種模式,封裝了TCP模式以及UDP模式的客戶端類和服務端類。
  3. 圖片傳輸客戶端同時支援傳送到多個服務端,可以作為一個教師機同屏傳送到多個學生機的應用場景。
  4. 同時支援多個客戶端同時往服務端傳送圖片,服務端每個連線都會自動開闢執行緒收發和解析圖片資料。
  5. 自定義label控制元件訊號槽機制繪製圖片,不卡主介面。
  6. 自帶心跳機制判斷離線,自動重連伺服器,可設定超時時間。
  7. 每個訊息都有唯一的訊息標識uuid,服務端收到以後會返回對應的uuid訊息表示收到,客戶端可以根據此返回訊息判斷服務端解析成功,不用再發,這樣可以確保發出去的資料伺服器接收到了並解析成功。
  8. 每個訊息都有唯一的圖片標識flag,相當於ID號,根據此標識判斷需要解析顯示到哪個介面。
  9. 圖片以base64的字串格式傳送,接收端接收到base64字串的圖片資料解碼後重新生成圖片。
  10. 所有資料的收發都有訊號發出去,方便輸出檢視。
  11. 都提供單例類,方便只有一個的時候直接使用無需new。
  12. 採用自定義的xml協議,可以自由拓展其他屬性欄位比如帶上圖片內容等。

三、效果圖

四、相關站點

  1. 國內站點:https://gitee.com/feiyangqingyun/QWidgetDemo
  2. 國際站點:https://github.com/feiyangqingyun/QWidgetDemo
  3. 個人主頁:https://blog.csdn.net/feiyangqingyun
  4. 知乎主頁:https://www.zhihu.com/people/feiyangqingyun/
  5. 體驗地址:https://blog.csdn.net/feiyangqingyun/article/details/97565652

五、核心程式碼

#include "udpimageclient.h"
#include "devicefun.h"

QScopedPointer<UdpImageClient> UdpImageClient::self;
UdpImageClient *UdpImageClient::Instance()
{
    if (self.isNull()) {
        static QMutex mutex;
        QMutexLocker locker(&mutex);
        if (self.isNull()) {
            self.reset(new UdpImageClient);
        }
    }

    return self.data();
}

UdpImageClient::UdpImageClient(QObject *parent) : QThread(parent)
{
    //如果是外網請自行調整這個值的大小,外網需要調小
    packageSize = 10000;
    flag = "SHJC00000001";
    serverIP = "127.0.0.1";
    serverPort = 6000;

    stopped = false;

    //UDP通訊物件
    udpSocket = new QUdpSocket(this);
    connect(udpSocket, SIGNAL(readyRead()), this, SLOT(readData()));

    //定時器解析收到的資料,可以自行調整間隔
    timerData = new QTimer(this);
    connect(timerData, SIGNAL(timeout()), this, SLOT(checkData()));
    timerData->setInterval(100);

    //繫結訊號啟動後啟動定時器
    connect(this, SIGNAL(started()), this, SLOT(started()));
    //繫結傳送資料訊號槽
    connect(this, SIGNAL(readyWrite(QString)), this, SLOT(sendImage(QString)));
}

UdpImageClient::~UdpImageClient()
{
    this->stop();
}

void UdpImageClient::run()
{
    while (!stopped) {
        //這裡採用執行緒去處理,其實完全可以用定時器搞定,畢竟tcp的write是非同步的,作業系統自動排程
        //為了後期的拓展性,比如需要判斷是否傳送成功之類的,需要同步處理,所以改成的執行緒去處理
        //圖片資料轉成base64編碼的資料也需要時間的,主要的耗時在轉碼
        //取出資料傳送,這裡需要加鎖,避免正在插入資料
        if (images.count() > 0) {
            QMutexLocker locker(&mutexImage);
            QImage image = images.takeFirst();
            QString imageData = DeviceFun::getImageData(image);
            emit readyWrite(imageData);
        }

        //要稍微休息下,否則CPU會被一直佔用
        msleep(1);
    }

    stopped = false;
}

void UdpImageClient::readData()
{
    QHostAddress host;
    quint16 port;
    QByteArray data;

    while (udpSocket->hasPendingDatagrams()) {
        data.resize(udpSocket->pendingDatagramSize());
        udpSocket->readDatagram(data.data(), data.size(), &host, &port);

        //接收的資料存入buffer需要加鎖
        QMutexLocker locker(&mutexData);
        buffer.append(data);
        emit receiveData(data);
    }
}

void UdpImageClient::checkData()
{
    if (buffer.length() == 0) {
        return;
    }

    //取出資料處理需要加鎖,防止此時正在插入資料
    QMutexLocker locker(&mutexData);
    QDomDocument dom;
    if (!DeviceFun::getReceiveXmlData(buffer, dom, "IIMAGE:", 11, true)) {
        return;
    }

    //逐個取出節點判斷資料
    QDomElement element = dom.documentElement();
    if (element.tagName() == "ImageServer") {
        QString uuid = element.attribute("Uuid");
        QDomNode childNode = element.firstChild();
        QString name = childNode.nodeName();
        QString value = element.text();
        //qDebug() << uuid << name << value;
        //這裡可以根據收到的資料自行增加自己的處理
    }
}

void UdpImageClient::started()
{
    if (!timerData->isActive()) {
        timerData->start();
    }
}

void UdpImageClient::stop()
{
    buffer.clear();
    images.clear();
    stopped = true;
    this->wait();
    udpSocket->disconnectFromHost();

    if (timerData->isActive()) {
        timerData->stop();
    }
}

void UdpImageClient::setPackageSize(int packageSize)
{
    if (packageSize <= 65507) {
        this->packageSize = packageSize;
    }
}

void UdpImageClient::setFlag(const QString &flag)
{
    this->flag = flag;
}

void UdpImageClient::setServerIP(const QString &serverIP)
{
    this->serverIP = serverIP;
}

void UdpImageClient::setServerPort(int serverPort)
{
    this->serverPort = serverPort;
}

void UdpImageClient::writeData(const QString &body)
{
    //構建xml字串
    QStringList list;
    list.append(QString("<ImageClient Uuid=\"%1\" Flag=\"%2\">").arg(DeviceFun::getUuid()).arg(flag));
    list.append(body);
    list.append("</ImageClient>");

    //呼叫通用方法根據協議組成完整資料
    QString data = DeviceFun::getSendXmlData(list.join(""), "IIMAGE:");
    QByteArray buffer = data.toUtf8();

    //udp最大隻能傳送65507位元組的資料=64K 超過的話都會發送失敗
    //所以這裡需要手動分包,外網的話包還要小一點
    if (packageSize == 65500) {
        udpSocket->writeDatagram(buffer, QHostAddress(serverIP), serverPort);
    } else {
        int len = buffer.length();
        int count = len / packageSize + 1;
        for (int i = 0; i < count; i++) {
            QByteArray temp = buffer.mid(i * packageSize, packageSize);
            udpSocket->writeDatagram(temp, QHostAddress(serverIP), serverPort);
        }
    }

    emit sendData(buffer);
}

void UdpImageClient::sendImage(const QString &body)
{
    writeData(QString("<ClientImage>%1</ClientImage>").arg(body));
}

void UdpImageClient::append(const QImage &image)
{
    //這裡需要加鎖,避免正在取出資料
    QMutexLocker locker(&mutexImage);
    //限制佇列中最大訊息數,避免離線的時候瘋狂插入
    if (this->isRunning() && images.count() < 10) {
        images << image;
    }
}

void UdpImageClient::clear()
{
    QMutexLocker locker(&mutexImage);
    images.clear();
}