網站首頁 編程語言 正文
本文為作者在開發項目時對Qt的TCP通信部分的總結,主要包含TCP服務器收發數據的demo,解決TCP拆包和黏包問題的解決方案,以及對接收到的QByteArray數據的轉換。
簡介
TCP(Transmission Control Protocol,傳輸控制協議)是面向連接的協議,也就是說,在收發數據前,必須和對方建立可靠的連接,也就是我們常聽到的三次握手。TCP的目的是實現快速、安全的信息傳遞,因此在協議中針對針對數據安全做了很多處理,很適合應用在一些對安全性要求高的場合。而UDP是非連接的協議,在形式上有點類似串口,適合傳輸語音、視頻等流量大的任務。
一、Qt中TCP通信基本用法
TCP 通信必須先建立 TCP 連接,通信端分為客戶端和服務端。服務端通過監聽某個端口來監聽是否有客戶端連接到來,如果有連接到來,則建立新的 socket 連接;客戶端通過 ip 和port 連接服務端,當成功建立連接之后,就可進行數據的收發了。
由于這一塊網上的資料很豐富,我就不做過多介紹,我是參考的正點原子 Qt 開發教程,這里將我的TCP服務器源碼貼出來,大家有需要的在此基礎上進行修改。后面我主要介紹一下我在開發過程中實際遇到的問題與解決方案,僅供參考。
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服務器(6010端口)
? ? QTcpSocket *tcpSocket_6010; //通信套接字(6010端口)
? ? QTcpServer *tcpServer_6030; //TCP服務器(6030端口)
? ? QTcpSocket *tcpSocket_6030; //通信套接字(6030端口)
public slots:
? ? void startListen(); ? ? ? ? ? ? ? ? ? ? ? ? //開始監聽槽函數
? ? void stopListen(); ? ? ? ? ? ? ? ? ? ? ? ? ?//停止監聽槽函數
? ? void clientConnected_6010(); ? ? ? ? ? ? ? ?//客戶端連接處理槽函數
? ? void clientConnected_6030(); ? ? ? ? ? ? ? ?//客戶端連接處理槽函數
? ? void receiveMessages_6010(); ? ? ? ? ? ? ? ?//接收消息(6010端口)
? ? void sendMessages_6030(QByteArray); ? ? ? ? //發送消息(6030端口)
signals:
? ? void signal_clientConnected_6010(); ? ? ? ? //客戶端連接成功信號(6010端口)
? ? void signal_clientConnected_6030(); ? ? ? ? //客戶端連接成功信號(6030端口)
? ? void signal_receiveMsg_6010(QByteArray); ? ?//傳輸TCP接收數據的信號
};
#endif // MYTCPSERVER_H
3. 封裝好的 mytcpserver.cpp
#include "mytcpserver.h"
#include <QDebug>
TcpServer::TcpServer(QObject *parent) : QObject(parent)
{
? ? tcpServer_6010 = new QTcpServer(this); ? ? ?//實例化TCP服務器(6010端口)
? ? tcpSocket_6010 = new QTcpSocket(this); ? ? ?//實例化TCP服務器(6010端口)
? ? tcpServer_6030 = new QTcpServer(this); ? ? ?//實例化TCP服務器(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(); ? ? ? ? ? ? ? ? ? ?//關閉監聽(6010)
? ? tcpServer_6030->close(); ? ? ? ? ? ? ? ? ? ?//關閉監聽(6030)
? ? if(tcpSocket_6010->state() == tcpSocket_6010->ConnectedState)
? ? ? ? tcpSocket_6010->disconnectFromHost(); ? ?//斷開連接(6010)
? ? if(tcpSocket_6030->state() == tcpSocket_6030->ConnectedState)
? ? ? ? tcpSocket_6030->disconnectFromHost(); ? ?//斷開連接(6030)
}
/* 分包接收數據,合成發送*/
void TcpServer::receiveMessages_6010()
{
? ? static uint receiveLen=0;
? ? static QByteArray receiveData; ? ? ?//TCP接收到的完整數據
? ? QByteArray receiveBuf = tcpSocket_6010->readAll(); ? ? ? ? ? ? ?//讀取TCP接收緩沖區的所有數據(不定長)
? ? uint messageLen = receiveBuf.size();
? ? receiveLen += messageLen; ? ? ? ? ? ? ? ? ? ? ? //計算一包數據的長度(16006)
? ? if(receiveLen < 16006) ? ? ? ? ? ? ? ? ? ? ? ? ?//還沒收滿
? ? {
? ? ? ? receiveData.append(receiveBuf); ? ? ? ? ? ? //每接收一次數據就追加到接收數組中
? ? }
? ? else if(receiveLen == 16006) ? ? ? ? ? ? ? ? ? ?//剛好收滿
? ? {
? ? ? ? receiveData.append(receiveBuf); ? ? ? ? ? ? //每接收一次數據就追加到接收數組中
? ? ? ? emit signal_receiveMsg_6010(receiveData); ? //發送傳輸數據的信號
? ? ? ? receiveLen=0; ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? //清空數據長度
? ? ? ? receiveData.clear(); ? ? ? ? ? ? ? ? ? ? ? ?//清空數據(clear會將receiveData長度變為0)
? ? }
? ? else if(receiveLen > 16006) ? ? ? ? ? ? ? ? ? ? //長度超過16006發生粘包
? ? {
? ? ? ? while(receiveLen > 16006)
? ? ? ? {
? ? ? ? ? ? qDebug()<<receiveBuf.size()<<endl;
? ? ? ? ? ? receiveData.append(receiveBuf); ? ? ? ? ? ? //每接收一次數據就追加到接收數組中
? ? ? ? ? ? receiveBuf = receiveData.right(16007); ? ? ?//將超出16006范圍的數據放入receiveBuf數組中
? ? ? ? ? ? receiveData.truncate(16006); ? ? ? ? ? ? ? ?//將接收數組大于16006部分刪除
? ? ? ? ? ? emit signal_receiveMsg_6010(receiveData); ? //發送傳輸數據的信號
? ? ? ? ? ? receiveLen = receiveLen-16006; ? ? ? ? ? ? ?//更新接收數組長度
? ? ? ? }
? ? }
}
/* 服務端發送消息 */
void TcpServer::sendMessages_6030(QByteArray sendData)
{
? ? if(NULL == tcpSocket_6030) ? //TCP未連接,退出
? ? ? ? return;
? ? if(tcpSocket_6030->state() == tcpSocket_6030->ConnectedState) ? //TCP建立連接
? ? ? ? tcpSocket_6030->write(sendData); ? ? ? ? ? ? ? ? ? ? ? ? ? ?//發送消息
}
這里我需要使用了兩個端口,6030端口用作發送指令,6030端口用作接收數據。我的項目中傳輸的數據量較大,一包幾萬字節,所以接收數據的 receiveMessages_6010() 函數已做了對黏包問題的處理。大家可以根據自己的需求做相應的修改。
二、TCP黏包解決方法
1. 問題描述
TCP客戶端使用的是STM32開發的8通道高速數據采集卡,客戶端每100ms發送一次數據,每次為16006字節的數據長度。由于TCP傳輸數據時,為了達到最佳傳輸效能,數據包的最大長度需要由MSS限定(MSS就是TCP數據包每次能夠傳輸的最大數據分段),超過這個長度會進行自動拆包。也就是說雖然客戶端一次發送16006字節數據,但是實際TCP傳輸時會將16006字節劃分為若干小包。我使用wireshark軟件抓包時可以看到,數據被拆分成長度為1440的數據包(不滿1440則單獨發送)。
2. TCP拆包和黏包現象
我們來看一下數據經過TCP傳輸時可能出現的幾種情況:
接收端正常收到兩個數據包,即沒有發生拆包和粘包的現象。
接收端只收到一個數據包,由于TCP是不會出現丟包的,所以這一個數據包中包含了發送端發送的兩個數據包的信息,這種現象即為粘包。這種情況由于接收端不知道這兩個數據包的界限,所以對于接收端來說很難處理。
這種情況有兩種表現形式,如下圖。接收端收到了兩個數據包,但是這兩個數據包要么是不完整的,要么就是多出來一塊,這種情況即發生了拆包和粘包。這兩種情況如果不加特殊處理,對于接收端同樣是不好處理的。
3. 解決方法
在使用Qt編寫TCP服務器端程序時,Qt提供的TCP接收函數 readAll() 并非一次讀取客戶端全部數據,也不是讀取客戶端的每小包數據,而是讀取TCP服務器的接收緩沖區的全部數據,這里算是Qt的一個坑,因為乍一看 readAll() 不就是讀取全部數據嘛,而官方文檔又沒有給出具體解釋。
qDebug()<<tcpSocket_6010->byteAvailable()<<endl;?? ?//打印當前緩沖區中的數據長度
QByteArray receiveBuf = tcpSocket_6010->readAll();?? ?//讀取緩沖區中的所有數據
qDebug()<<tcpSocket_6010->byteAvailable()<<endl;?? ?//此時打印結果為0
其實仔細想一下,被拆包的每包數據都被封裝成相同的格式進行傳輸,TCP協議并沒有提供任何標識,接收端也壓根無法自動判別哪些包屬于完整的一包數據。
知道了接收函數 readAll() 的原理,再加上我們已知客戶端發送的每包數據長度為 16006 字節,那么我們不就可以手動計算接收數據的長度,然后將這些數據拼接合成嘛。確實應該這么做,但是別忘了TCP還有黏包的問題,也就是TCP傳輸的數據包可能出現粘合在一起的現象,本次要傳輸的數據和下一次傳輸的數據被粘合在一起,那么我們按長度累加計算接收到的數據長度可能無法獲取我們想要的結果。
我的解決方法如下:
/* 分包接收數據,合成發送*/
void TcpServer::receiveMessages_6010()
{
? ? static uint receiveLen=0;?? ? ? ??? ?//累加接收數據的長度
? ? static QByteArray receiveData; ? ? ?//TCP接收到的完整數據
? ? QByteArray receiveBuf = tcpSocket_6010->readAll();//讀取TCP接收緩沖區的所有數據(不定長)
? ? uint messageLen = receiveBuf.size();?? ??? ?//每次從緩沖區讀取的數據長度
? ? receiveLen += messageLen; ? ? ? ? ? ? ? ? ? ? ? //計算一包數據的長度(16006)
? ? if(receiveLen < 16006) ? ? ? ? ? ? ? ? ? ? ? ? ?//還沒收滿
? ? {
? ? ? ? receiveData.append(receiveBuf); ? ? ? ? ? ? //每接收一次數據就追加到接收數組中
? ? }
? ? else if(receiveLen == 16006) ? ? ? ? ? ? ? ? ? ?//剛好收滿
? ? {
? ? ? ? receiveData.append(receiveBuf); ? ? ? ? ? ? //每接收一次數據就追加到接收數組中
? ? ? ? emit signal_receiveMsg_6010(receiveData); ? //發送傳輸數據的信號
? ? ? ? receiveLen=0; ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? //清空數據長度
? ? ? ? receiveData.clear(); ? ? ? ? ? ? ? ? ? ? ? ?//清空數據(clear會將receiveData長度變為0)
? ? }
? ? else if(receiveLen > 16006) ? ? ? ? ? ? ? ? ? ? //長度超過16006發生粘包
? ? {
? ? ? ? while(receiveLen > 16006)
? ? ? ? {
? ? ? ? ? ? qDebug()<<receiveBuf.size()<<endl;
? ? ? ? ? ? receiveData.append(receiveBuf); ? ? ? ? ? ? //每接收一次數據就追加到接收數組中
? ? ? ? ? ? receiveBuf = receiveData.right(16007); ? ? ?//將超出16006范圍的數據放入receiveBuf數組中
? ? ? ? ? ? receiveData.truncate(16006); ? ? ? ? ? ? ? ?//將接收數組大于16006部分刪除
? ? ? ? ? ? emit signal_receiveMsg_6010(receiveData); ? //發送傳輸數據的信號
? ? ? ? ? ? receiveLen = receiveLen-16006; ? ? ? ? ? ? ?//更新接收數組長度
? ? ? ? }
? ? }
}
大體思路就是收滿16006字節的數據就將數據發送出去,如果發生黏包,數據長度超過16006就對數據進行裁剪,多出來的部分作為下一包數據的開頭。經過測試,該方法能夠完美解決在傳輸大量數據時,TCP拆包和黏包導致的數據無法解析的問題,讀者可參考此方法自行修改。
三、TCP接收到的QByteArray類型數據的轉換
上述通過 readAll() 函數接收到的數據為 QByteArray 類型,這是一個Qt 自己定義的一種類似于 String 的處理字符串的類,這個類也提供了很多成員函數,方便我們對數據進行轉化。
如果你不需要對接收到的數據進行運算,只是想打印數據,那么可以直接使用 QByteArray 類型。但是如果你需要對數據進做加減乘除,那使用 QByteArray 就不合適了,需要轉換成基本數據類型。需要注意上圖中 QByteArray 類提供的幾個成員函數,比如 toHex() ,它的返回值依然是 QByteArray,也就是說它是將原始的 QByteArray 轉換成十六進制的 QByteArray,比如 “255”->“FF”,本質上還是字符串。大家在使用官方提供的成員函數時,一定要看一下函數的返回值,不要想當然了。
我在客戶端發送的每個數據為兩個字節,如 0xFFFF,使用 readAll() 接收到的 QByteArray 類型的數據也只是按字節接收,它并不知道我們一個數據占幾個字節,所以實際上 receiveData[0] = 255, receiveData[1] = 255。由于我沒有找到現成的可供直接使用的處理函數,所以就手動實現了一下:
/* cacheBuf為合成后的uint數組, msg為待處理的QByteArray數據 */
for(uint i=0; i<1000; i++) ?//高低位兩字節合成為一個uint
{
? ? ? cacheBuf[i] = msg[16*i+4] & 0x000000FF; ? ? ? ? ? //低位
? ? ? cacheBuf[i] |= ((msg[16*i+5] << 8) & 0x0000FF00);?? ?//高位
}
總結
即使我在stm32單片機上發送的是int類型的數據,但是在Qt上通過 toInt() 函數時接收不到我想要的數據的,因為32位的單片機中int占兩個字節,而Qt中的C++的int類型占4個字節,那么我使用 toInt() 函數來接收數據時,程序就會以4個字節為一個數來接收。這告訴我們,在不同平臺之間傳輸數據的時候,要考慮同種類型的數據,它們的寬度是否一致。
原文鏈接:https://blog.csdn.net/qq_40380893/article/details/119965007
相關推薦
- 2022-10-28 Swift類和對象的底層探索分析_Swift
- 2021-12-07 詳解C語言編程之thread多線程_C 語言
- 2022-10-03 python中pandas操作apply返回多列的實現_python
- 2022-02-03 ionic4 ngFor中使用ngIf
- 2022-09-21 Python實現斐波那契數列的多種寫法總結_python
- 2021-12-04 C#獲取Windows10屏幕縮放比例的操作方法_C#教程
- 2022-07-21 element 中loading顏色的修改
- 2022-08-16 python+pytest接口自動化參數關聯_python
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支