Qt網(wǎng)絡(luò)編程之TCP通信及常見問題
本文為作者在開發(fā)項目時對Qt的TCP通信部分的總結(jié),主要包含TCP服務(wù)器收發(fā)數(shù)據(jù)的demo,解決TCP拆包和黏包問題的解決方案,以及對接收到的QByteArray數(shù)據(jù)的轉(zhuǎn)換。
簡介
TCP(Transmission Control Protocol,傳輸控制協(xié)議)是面向連接的協(xié)議,也就是說,在收發(fā)數(shù)據(jù)前,必須和對方建立可靠的連接,也就是我們常聽到的三次握手。TCP的目的是實現(xiàn)快速、安全的信息傳遞,因此在協(xié)議中針對針對數(shù)據(jù)安全做了很多處理,很適合應(yīng)用在一些對安全性要求高的場合。而UDP是非連接的協(xié)議,在形式上有點類似串口,適合傳輸語音、視頻等流量大的任務(wù)。
一、Qt中TCP通信基本用法
TCP 通信必須先建立 TCP 連接,通信端分為客戶端和服務(wù)端。服務(wù)端通過監(jiān)聽某個端口來監(jiān)聽是否有客戶端連接到來,如果有連接到來,則建立新的 socket 連接;客戶端通過 ip 和port 連接服務(wù)端,當(dāng)成功建立連接之后,就可進(jìn)行數(shù)據(jù)的收發(fā)了。
由于這一塊網(wǎng)上的資料很豐富,我就不做過多介紹,我是參考的正點原子 Qt 開發(fā)教程,這里將我的TCP服務(wù)器源碼貼出來,大家有需要的在此基礎(chǔ)上進(jìn)行修改。后面我主要介紹一下我在開發(fā)過程中實際遇到的問題與解決方案,僅供參考。
1. 在 .pro文件中添加 network
QT += core gui network
2. 封裝好的 mytcpserver.h
#ifndef MYTCPSERVER_H #define MYTCPSERVER_H #include <QTcpServer> #include <QTcpSocket> #include <QObject> class TcpServer : public QObject { ? ? Q_OBJECT public: ? ? explicit TcpServer(QObject *parent = nullptr); private: ? ? QTcpServer *tcpServer_6010; //TCP服務(wù)器(6010端口) ? ? QTcpSocket *tcpSocket_6010; //通信套接字(6010端口) ? ? QTcpServer *tcpServer_6030; //TCP服務(wù)器(6030端口) ? ? QTcpSocket *tcpSocket_6030; //通信套接字(6030端口) public slots: ? ? void startListen(); ? ? ? ? ? ? ? ? ? ? ? ? //開始監(jiān)聽槽函數(shù) ? ? void stopListen(); ? ? ? ? ? ? ? ? ? ? ? ? ?//停止監(jiān)聽槽函數(shù) ? ? void clientConnected_6010(); ? ? ? ? ? ? ? ?//客戶端連接處理槽函數(shù) ? ? void clientConnected_6030(); ? ? ? ? ? ? ? ?//客戶端連接處理槽函數(shù) ? ? void receiveMessages_6010(); ? ? ? ? ? ? ? ?//接收消息(6010端口) ? ? void sendMessages_6030(QByteArray); ? ? ? ? //發(fā)送消息(6030端口) signals: ? ? void signal_clientConnected_6010(); ? ? ? ? //客戶端連接成功信號(6010端口) ? ? void signal_clientConnected_6030(); ? ? ? ? //客戶端連接成功信號(6030端口) ? ? void signal_receiveMsg_6010(QByteArray); ? ?//傳輸TCP接收數(shù)據(jù)的信號 }; #endif // MYTCPSERVER_H
3. 封裝好的 mytcpserver.cpp
#include "mytcpserver.h" #include <QDebug> TcpServer::TcpServer(QObject *parent) : QObject(parent) { ? ? tcpServer_6010 = new QTcpServer(this); ? ? ?//實例化TCP服務(wù)器(6010端口) ? ? tcpSocket_6010 = new QTcpSocket(this); ? ? ?//實例化TCP服務(wù)器(6010端口) ? ? tcpServer_6030 = new QTcpServer(this); ? ? ?//實例化TCP服務(wù)器(6030端口) ? ? tcpSocket_6030 = new QTcpSocket(this); ? ? ?//實例化TCP套接字(6030端口) ? ? connect(tcpServer_6010, SIGNAL(newConnection()), this, SLOT(clientConnected_6010())); ? ? connect(tcpServer_6030, SIGNAL(newConnection()), this, SLOT(clientConnected_6030())); } void TcpServer::clientConnected_6010() { ? ? tcpSocket_6010 = tcpServer_6010->nextPendingConnection(); ? //獲取客戶套接字 ? ? emit signal_clientConnected_6010(); ? ? ? ? ? ? ? ? ? ? ? ? //端口6010連接成功信號 ? ? connect(tcpSocket_6010, SIGNAL(readyRead()), this, SLOT(receiveMessages_6010())); } void TcpServer::clientConnected_6030() { ? ? tcpSocket_6030 = tcpServer_6030->nextPendingConnection(); ? //獲取客戶套接字 ? ? emit signal_clientConnected_6030(); ? ? ? ? ? ? ? ? ? ? ? ? //端口6030連接成功信號 } void TcpServer::startListen() { ? ? tcpServer_6030->listen(QHostAddress("192.168.116.250"), 6030); ? ? tcpServer_6010->listen(QHostAddress("192.168.116.250"), 6010); } void TcpServer::stopListen() { ? ? tcpServer_6010->close(); ? ? ? ? ? ? ? ? ? ?//關(guān)閉監(jiān)聽(6010) ? ? tcpServer_6030->close(); ? ? ? ? ? ? ? ? ? ?//關(guān)閉監(jiān)聽(6030) ? ? if(tcpSocket_6010->state() == tcpSocket_6010->ConnectedState) ? ? ? ? tcpSocket_6010->disconnectFromHost(); ? ?//斷開連接(6010) ? ? if(tcpSocket_6030->state() == tcpSocket_6030->ConnectedState) ? ? ? ? tcpSocket_6030->disconnectFromHost(); ? ?//斷開連接(6030) } /* 分包接收數(shù)據(jù),合成發(fā)送*/ void TcpServer::receiveMessages_6010() { ? ? static uint receiveLen=0; ? ? static QByteArray receiveData; ? ? ?//TCP接收到的完整數(shù)據(jù) ? ? QByteArray receiveBuf = tcpSocket_6010->readAll(); ? ? ? ? ? ? ?//讀取TCP接收緩沖區(qū)的所有數(shù)據(jù)(不定長) ? ? uint messageLen = receiveBuf.size(); ? ? receiveLen += messageLen; ? ? ? ? ? ? ? ? ? ? ? //計算一包數(shù)據(jù)的長度(16006) ? ? if(receiveLen < 16006) ? ? ? ? ? ? ? ? ? ? ? ? ?//還沒收滿 ? ? { ? ? ? ? receiveData.append(receiveBuf); ? ? ? ? ? ? //每接收一次數(shù)據(jù)就追加到接收數(shù)組中 ? ? } ? ? else if(receiveLen == 16006) ? ? ? ? ? ? ? ? ? ?//剛好收滿 ? ? { ? ? ? ? receiveData.append(receiveBuf); ? ? ? ? ? ? //每接收一次數(shù)據(jù)就追加到接收數(shù)組中 ? ? ? ? emit signal_receiveMsg_6010(receiveData); ? //發(fā)送傳輸數(shù)據(jù)的信號 ? ? ? ? receiveLen=0; ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? //清空數(shù)據(jù)長度 ? ? ? ? receiveData.clear(); ? ? ? ? ? ? ? ? ? ? ? ?//清空數(shù)據(jù)(clear會將receiveData長度變?yōu)?) ? ? } ? ? else if(receiveLen > 16006) ? ? ? ? ? ? ? ? ? ? //長度超過16006發(fā)生粘包 ? ? { ? ? ? ? while(receiveLen > 16006) ? ? ? ? { ? ? ? ? ? ? qDebug()<<receiveBuf.size()<<endl; ? ? ? ? ? ? receiveData.append(receiveBuf); ? ? ? ? ? ? //每接收一次數(shù)據(jù)就追加到接收數(shù)組中 ? ? ? ? ? ? receiveBuf = receiveData.right(16007); ? ? ?//將超出16006范圍的數(shù)據(jù)放入receiveBuf數(shù)組中 ? ? ? ? ? ? receiveData.truncate(16006); ? ? ? ? ? ? ? ?//將接收數(shù)組大于16006部分刪除 ? ? ? ? ? ? emit signal_receiveMsg_6010(receiveData); ? //發(fā)送傳輸數(shù)據(jù)的信號 ? ? ? ? ? ? receiveLen = receiveLen-16006; ? ? ? ? ? ? ?//更新接收數(shù)組長度 ? ? ? ? } ? ? } } /* 服務(wù)端發(fā)送消息 */ void TcpServer::sendMessages_6030(QByteArray sendData) { ? ? if(NULL == tcpSocket_6030) ? //TCP未連接,退出 ? ? ? ? return; ? ? if(tcpSocket_6030->state() == tcpSocket_6030->ConnectedState) ? //TCP建立連接 ? ? ? ? tcpSocket_6030->write(sendData); ? ? ? ? ? ? ? ? ? ? ? ? ? ?//發(fā)送消息 }
這里我需要使用了兩個端口,6030端口用作發(fā)送指令,6030端口用作接收數(shù)據(jù)。我的項目中傳輸?shù)臄?shù)據(jù)量較大,一包幾萬字節(jié),所以接收數(shù)據(jù)的 receiveMessages_6010() 函數(shù)已做了對黏包問題的處理。大家可以根據(jù)自己的需求做相應(yīng)的修改。
二、TCP黏包解決方法
1. 問題描述
TCP客戶端使用的是STM32開發(fā)的8通道高速數(shù)據(jù)采集卡,客戶端每100ms發(fā)送一次數(shù)據(jù),每次為16006字節(jié)的數(shù)據(jù)長度。由于TCP傳輸數(shù)據(jù)時,為了達(dá)到最佳傳輸效能,數(shù)據(jù)包的最大長度需要由MSS限定(MSS就是TCP數(shù)據(jù)包每次能夠傳輸?shù)淖畲髷?shù)據(jù)分段),超過這個長度會進(jìn)行自動拆包。也就是說雖然客戶端一次發(fā)送16006字節(jié)數(shù)據(jù),但是實際TCP傳輸時會將16006字節(jié)劃分為若干小包。我使用wireshark軟件抓包時可以看到,數(shù)據(jù)被拆分成長度為1440的數(shù)據(jù)包(不滿1440則單獨發(fā)送)。
2. TCP拆包和黏包現(xiàn)象
我們來看一下數(shù)據(jù)經(jīng)過TCP傳輸時可能出現(xiàn)的幾種情況:
接收端正常收到兩個數(shù)據(jù)包,即沒有發(fā)生拆包和粘包的現(xiàn)象。
接收端只收到一個數(shù)據(jù)包,由于TCP是不會出現(xiàn)丟包的,所以這一個數(shù)據(jù)包中包含了發(fā)送端發(fā)送的兩個數(shù)據(jù)包的信息,這種現(xiàn)象即為粘包。這種情況由于接收端不知道這兩個數(shù)據(jù)包的界限,所以對于接收端來說很難處理。
這種情況有兩種表現(xiàn)形式,如下圖。接收端收到了兩個數(shù)據(jù)包,但是這兩個數(shù)據(jù)包要么是不完整的,要么就是多出來一塊,這種情況即發(fā)生了拆包和粘包。這兩種情況如果不加特殊處理,對于接收端同樣是不好處理的。
3. 解決方法
在使用Qt編寫TCP服務(wù)器端程序時,Qt提供的TCP接收函數(shù) readAll() 并非一次讀取客戶端全部數(shù)據(jù),也不是讀取客戶端的每小包數(shù)據(jù),而是讀取TCP服務(wù)器的接收緩沖區(qū)的全部數(shù)據(jù),這里算是Qt的一個坑,因為乍一看 readAll() 不就是讀取全部數(shù)據(jù)嘛,而官方文檔又沒有給出具體解釋。
qDebug()<<tcpSocket_6010->byteAvailable()<<endl;?? ?//打印當(dāng)前緩沖區(qū)中的數(shù)據(jù)長度 QByteArray receiveBuf = tcpSocket_6010->readAll();?? ?//讀取緩沖區(qū)中的所有數(shù)據(jù) qDebug()<<tcpSocket_6010->byteAvailable()<<endl;?? ?//此時打印結(jié)果為0
其實仔細(xì)想一下,被拆包的每包數(shù)據(jù)都被封裝成相同的格式進(jìn)行傳輸,TCP協(xié)議并沒有提供任何標(biāo)識,接收端也壓根無法自動判別哪些包屬于完整的一包數(shù)據(jù)。
知道了接收函數(shù) readAll() 的原理,再加上我們已知客戶端發(fā)送的每包數(shù)據(jù)長度為 16006 字節(jié),那么我們不就可以手動計算接收數(shù)據(jù)的長度,然后將這些數(shù)據(jù)拼接合成嘛。確實應(yīng)該這么做,但是別忘了TCP還有黏包的問題,也就是TCP傳輸?shù)臄?shù)據(jù)包可能出現(xiàn)粘合在一起的現(xiàn)象,本次要傳輸?shù)臄?shù)據(jù)和下一次傳輸?shù)臄?shù)據(jù)被粘合在一起,那么我們按長度累加計算接收到的數(shù)據(jù)長度可能無法獲取我們想要的結(jié)果。
我的解決方法如下:
/* 分包接收數(shù)據(jù),合成發(fā)送*/ void TcpServer::receiveMessages_6010() { ? ? static uint receiveLen=0;?? ? ? ??? ?//累加接收數(shù)據(jù)的長度 ? ? static QByteArray receiveData; ? ? ?//TCP接收到的完整數(shù)據(jù) ? ? QByteArray receiveBuf = tcpSocket_6010->readAll();//讀取TCP接收緩沖區(qū)的所有數(shù)據(jù)(不定長) ? ? uint messageLen = receiveBuf.size();?? ??? ?//每次從緩沖區(qū)讀取的數(shù)據(jù)長度 ? ? receiveLen += messageLen; ? ? ? ? ? ? ? ? ? ? ? //計算一包數(shù)據(jù)的長度(16006) ? ? if(receiveLen < 16006) ? ? ? ? ? ? ? ? ? ? ? ? ?//還沒收滿 ? ? { ? ? ? ? receiveData.append(receiveBuf); ? ? ? ? ? ? //每接收一次數(shù)據(jù)就追加到接收數(shù)組中 ? ? } ? ? else if(receiveLen == 16006) ? ? ? ? ? ? ? ? ? ?//剛好收滿 ? ? { ? ? ? ? receiveData.append(receiveBuf); ? ? ? ? ? ? //每接收一次數(shù)據(jù)就追加到接收數(shù)組中 ? ? ? ? emit signal_receiveMsg_6010(receiveData); ? //發(fā)送傳輸數(shù)據(jù)的信號 ? ? ? ? receiveLen=0; ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? //清空數(shù)據(jù)長度 ? ? ? ? receiveData.clear(); ? ? ? ? ? ? ? ? ? ? ? ?//清空數(shù)據(jù)(clear會將receiveData長度變?yōu)?) ? ? } ? ? else if(receiveLen > 16006) ? ? ? ? ? ? ? ? ? ? //長度超過16006發(fā)生粘包 ? ? { ? ? ? ? while(receiveLen > 16006) ? ? ? ? { ? ? ? ? ? ? qDebug()<<receiveBuf.size()<<endl; ? ? ? ? ? ? receiveData.append(receiveBuf); ? ? ? ? ? ? //每接收一次數(shù)據(jù)就追加到接收數(shù)組中 ? ? ? ? ? ? receiveBuf = receiveData.right(16007); ? ? ?//將超出16006范圍的數(shù)據(jù)放入receiveBuf數(shù)組中 ? ? ? ? ? ? receiveData.truncate(16006); ? ? ? ? ? ? ? ?//將接收數(shù)組大于16006部分刪除 ? ? ? ? ? ? emit signal_receiveMsg_6010(receiveData); ? //發(fā)送傳輸數(shù)據(jù)的信號 ? ? ? ? ? ? receiveLen = receiveLen-16006; ? ? ? ? ? ? ?//更新接收數(shù)組長度 ? ? ? ? } ? ? } }
大體思路就是收滿16006字節(jié)的數(shù)據(jù)就將數(shù)據(jù)發(fā)送出去,如果發(fā)生黏包,數(shù)據(jù)長度超過16006就對數(shù)據(jù)進(jìn)行裁剪,多出來的部分作為下一包數(shù)據(jù)的開頭。經(jīng)過測試,該方法能夠完美解決在傳輸大量數(shù)據(jù)時,TCP拆包和黏包導(dǎo)致的數(shù)據(jù)無法解析的問題,讀者可參考此方法自行修改。
三、TCP接收到的QByteArray類型數(shù)據(jù)的轉(zhuǎn)換
上述通過 readAll() 函數(shù)接收到的數(shù)據(jù)為 QByteArray 類型,這是一個Qt 自己定義的一種類似于 String 的處理字符串的類,這個類也提供了很多成員函數(shù),方便我們對數(shù)據(jù)進(jìn)行轉(zhuǎn)化。
如果你不需要對接收到的數(shù)據(jù)進(jìn)行運算,只是想打印數(shù)據(jù),那么可以直接使用 QByteArray 類型。但是如果你需要對數(shù)據(jù)進(jìn)做加減乘除,那使用 QByteArray 就不合適了,需要轉(zhuǎn)換成基本數(shù)據(jù)類型。需要注意上圖中 QByteArray 類提供的幾個成員函數(shù),比如 toHex() ,它的返回值依然是 QByteArray,也就是說它是將原始的 QByteArray 轉(zhuǎn)換成十六進(jìn)制的 QByteArray,比如 “255”->“FF”,本質(zhì)上還是字符串。大家在使用官方提供的成員函數(shù)時,一定要看一下函數(shù)的返回值,不要想當(dāng)然了。
我在客戶端發(fā)送的每個數(shù)據(jù)為兩個字節(jié),如 0xFFFF,使用 readAll() 接收到的 QByteArray 類型的數(shù)據(jù)也只是按字節(jié)接收,它并不知道我們一個數(shù)據(jù)占幾個字節(jié),所以實際上 receiveData[0] = 255, receiveData[1] = 255。由于我沒有找到現(xiàn)成的可供直接使用的處理函數(shù),所以就手動實現(xiàn)了一下:
/* cacheBuf為合成后的uint數(shù)組, msg為待處理的QByteArray數(shù)據(jù) */ for(uint i=0; i<1000; i++) ?//高低位兩字節(jié)合成為一個uint { ? ? ? cacheBuf[i] = msg[16*i+4] & 0x000000FF; ? ? ? ? ? //低位 ? ? ? cacheBuf[i] |= ((msg[16*i+5] << 8) & 0x0000FF00);?? ?//高位 }
總結(jié)
即使我在stm32單片機上發(fā)送的是int類型的數(shù)據(jù),但是在Qt上通過 toInt() 函數(shù)時接收不到我想要的數(shù)據(jù)的,因為32位的單片機中int占兩個字節(jié),而Qt中的C++的int類型占4個字節(jié),那么我使用 toInt() 函數(shù)來接收數(shù)據(jù)時,程序就會以4個字節(jié)為一個數(shù)來接收。這告訴我們,在不同平臺之間傳輸數(shù)據(jù)的時候,要考慮同種類型的數(shù)據(jù),它們的寬度是否一致。
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
OpenCV透視變換應(yīng)用之書本視圖矯正+廣告屏幕切換
透視變換是指利用透視中心、像點、目標(biāo)點三點共線的條件,按透視旋轉(zhuǎn)定律使承影面繞跡線旋轉(zhuǎn)某一角度,破壞原有的投影光線束,仍能保持承影面上投影幾何圖形不變的變換。本文將為大家介紹兩個OpenCV透視變換應(yīng)用,需要的可以參考一下2022-08-08