Qt編寫圖片及視頻TCP/UDP網絡傳輸


一、前言

很多年前就做過類似的項目,無非就是將本地的圖片上傳到服務器,就這么簡單,其實用http的post上傳比較簡單容易,無需自定義協議,直接設置好二進制數據即可,而采用TCP或者UDP通信的話,必須自定義協議,因為不知道什么時候數據接收完了是完整的圖片數據,可能同時在發送很多圖片數據,而且還不能區分收到的圖片是哪個客戶端發來的,TCP長連接的話,還需要有心跳來檢測連接,所以必須自定義一套協議來支撐通信,這套協議采用的是上海監管平台的通信協議格式,拓展性比較強,其中頭部信息包括了類型+當前完整包的數據長度,這個類型就是通信協議的標識,這樣下次來一個其他類型的比如樓宇對講可以叫IDOOR,服務端根據這個標識就能知道采用何種解析算法來處理后面的數據,而當前完整包的數據長度可以用來處理收到的數據,只有該長度的數據才表示接收完成一個完整的圖片數據,再去解碼處理。當傳輸的圖片到了一定速度的時候比如一秒鍾傳輸20張圖片,其實就相當於傳輸視頻了,一般人的肉眼看到一秒鍾20張圖片基本上認識就是視頻了。

TCP理論上是穩定的連接,不會丟包,也不會隨便一個包插入到一個包的中間,肯定能保證一個數據包的完整性,TCP連接也分兩種,一種是長連接,一旦連接了就一直通信,主要用在頻繁通信的場景中比如實時上傳,還有一種叫短連接,客戶端發完數據或者服務端接收完數據就立即斷開連接,主要用在不頻繁的通信場景中比如報警上傳,畢竟報警的情況在一天中很少發生,采用短連接為佳,可以省去很多系統的開銷,Qt對TCP的通信也是封裝的很好用,在一些小並發的就幾個幾十個連接的項目中,效率還是可以的,據說Qt5的QNetwork組件底層重新改寫了,效率比Qt4更高一些,本人也沒用去詳細的查看對應的源碼,只是聽說。

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的,但是應付一些簡單的應用場景還是如魚得水毫無壓力。

體驗地址:https://pan.baidu.com/s/1bbL2ZughZAgfIGrexyN-9g 提取碼:zkeh 文件名:bin_video_image.zip。

二、功能特點

  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. 采用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>

四、效果圖


五、核心代碼

//圖片轉base64字符串
QByteArray DeviceFun::getImageData2(const QImage &image)
{
    QByteArray imageData;
    QBuffer buffer(&imageData);
    image.save(&buffer, "jpg");
    imageData = imageData.toBase64();
    return imageData;
}

//base64字符串轉圖片
QImage DeviceFun::getImage(const QString &data)
{
    QByteArray imageData = QByteArray::fromBase64(data.toLatin1());
    QImage image;
    image.loadFromData(imageData);
    return image;
}

//客戶端線程發送圖片
void TcpImageClient::run()
{
    while(!stopped) {
        //這里采用線程去處理,其實完全可以用定時器搞定,畢竟tcp的write是異步的,操作系統自動調度
        //為了后期的拓展性,比如需要判斷是否發送成功之類的,需要同步處理,所以改成的線程去處理
        //圖片數據轉成base64編碼的數據也需要時間的,主要的耗時在轉碼
        //取出數據發送,這里需要加鎖,避免正在插入數據
        if (isOk && images.count() > 0) {
            QMutexLocker locker(&mutexImage);
            QImage image = images.takeFirst();
            QString imageData = DeviceFun::getImageData(image);
            emit readyWrite(imageData);
        }

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

    stopped = false;
}

//服務端線程解析圖片
void TcpImageSocket::run()
{
    while(!stopped) {
        //這里采用線程去處理,其實完全可以用定時器搞定,畢竟tcp的write是異步的,操作系統自動調度
        //為了后期的拓展性,比如需要判斷是否發送成功之類的,需要同步處理,所以改成的線程去處理
        //base64編碼數據轉圖片數據也需要時間的,主要的耗時在轉碼
        //取出數據發送,這里需要加鎖,避免正在插入數據
        if (imageFlags.count() > 0) {
            QMutexLocker locker(&mutexImage);
            QString imageFlag = imageFlags.takeFirst();
            QString imageData = imageDatas.takeFirst();
            QImage image = DeviceFun::getImage(imageData);
            emit receiveImage(imageFlag, image);
        }

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

    stopped = false;
}

//客戶端類使用方法
TcpImageClient *client = new TcpImageClient(this);
UdpImageClient *client = new UdpImageClient(this);
connect(widget, SIGNAL(receiveImage(QImage)), client, SLOT(append(QImage)));                
client->setServerIP("127.0.0.1");
client->setServerPort(6000);
client->start();

//服務端類使用方法
TcpImageServer *tcpServer = new TcpImageServer(this);
UdpImageServer *tcpServer = new UdpImageServer(this);
connect(tcpServer, SIGNAL(receiveImage(QString, QImage)), this, SLOT(receiveImage(QString, QImage)));


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM