Qt網(wǎng)絡(luò)編程之TCP通信及常見問題
本文為作者在開發(fā)項(xiàng)目時(shí)對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的目的是實(shí)現(xiàn)快速、安全的信息傳遞,因此在協(xié)議中針對針對數(shù)據(jù)安全做了很多處理,很適合應(yīng)用在一些對安全性要求高的場合。而UDP是非連接的協(xié)議,在形式上有點(diǎn)類似串口,適合傳輸語音、視頻等流量大的任務(wù)。
一、Qt中TCP通信基本用法
TCP 通信必須先建立 TCP 連接,通信端分為客戶端和服務(wù)端。服務(wù)端通過監(jiān)聽某個(gè)端口來監(jiān)聽是否有客戶端連接到來,如果有連接到來,則建立新的 socket 連接;客戶端通過 ip 和port 連接服務(wù)端,當(dāng)成功建立連接之后,就可進(jìn)行數(shù)據(jù)的收發(fā)了。
由于這一塊網(wǎng)上的資料很豐富,我就不做過多介紹,我是參考的正點(diǎn)原子 Qt 開發(fā)教程,這里將我的TCP服務(wù)器源碼貼出來,大家有需要的在此基礎(chǔ)上進(jìn)行修改。后面我主要介紹一下我在開發(fā)過程中實(shí)際遇到的問題與解決方案,僅供參考。
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_H3. 封裝好的 mytcpserver.cpp
#include "mytcpserver.h"
#include <QDebug>
TcpServer::TcpServer(QObject *parent) : QObject(parent)
{
? ? tcpServer_6010 = new QTcpServer(this); ? ? ?//實(shí)例化TCP服務(wù)器(6010端口)
? ? tcpSocket_6010 = new QTcpSocket(this); ? ? ?//實(shí)例化TCP服務(wù)器(6010端口)
? ? tcpServer_6030 = new QTcpServer(this); ? ? ?//實(shí)例化TCP服務(wù)器(6030端口)
? ? tcpSocket_6030 = new QTcpSocket(this); ? ? ?//實(shí)例化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; ? ? ? ? ? ? ? ? ? ? ? //計(jì)算一包數(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會(huì)將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ā)送消息
}這里我需要使用了兩個(gè)端口,6030端口用作發(fā)送指令,6030端口用作接收數(shù)據(jù)。我的項(xiàng)目中傳輸?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ù)時(shí),為了達(dá)到最佳傳輸效能,數(shù)據(jù)包的最大長度需要由MSS限定(MSS就是TCP數(shù)據(jù)包每次能夠傳輸?shù)淖畲髷?shù)據(jù)分段),超過這個(gè)長度會(huì)進(jìn)行自動(dòng)拆包。也就是說雖然客戶端一次發(fā)送16006字節(jié)數(shù)據(jù),但是實(shí)際TCP傳輸時(shí)會(huì)將16006字節(jié)劃分為若干小包。我使用wireshark軟件抓包時(shí)可以看到,數(shù)據(jù)被拆分成長度為1440的數(shù)據(jù)包(不滿1440則單獨(dú)發(fā)送)。
2. TCP拆包和黏包現(xiàn)象
我們來看一下數(shù)據(jù)經(jīng)過TCP傳輸時(shí)可能出現(xiàn)的幾種情況:
接收端正常收到兩個(gè)數(shù)據(jù)包,即沒有發(fā)生拆包和粘包的現(xiàn)象。

接收端只收到一個(gè)數(shù)據(jù)包,由于TCP是不會(huì)出現(xiàn)丟包的,所以這一個(gè)數(shù)據(jù)包中包含了發(fā)送端發(fā)送的兩個(gè)數(shù)據(jù)包的信息,這種現(xiàn)象即為粘包。這種情況由于接收端不知道這兩個(gè)數(shù)據(jù)包的界限,所以對于接收端來說很難處理。

這種情況有兩種表現(xiàn)形式,如下圖。接收端收到了兩個(gè)數(shù)據(jù)包,但是這兩個(gè)數(shù)據(jù)包要么是不完整的,要么就是多出來一塊,這種情況即發(fā)生了拆包和粘包。這兩種情況如果不加特殊處理,對于接收端同樣是不好處理的。

3. 解決方法
在使用Qt編寫TCP服務(wù)器端程序時(shí),Qt提供的TCP接收函數(shù) readAll() 并非一次讀取客戶端全部數(shù)據(jù),也不是讀取客戶端的每小包數(shù)據(jù),而是讀取TCP服務(wù)器的接收緩沖區(qū)的全部數(shù)據(jù),這里算是Qt的一個(gè)坑,因?yàn)檎б豢?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;?? ?//此時(shí)打印結(jié)果為0
其實(shí)仔細(xì)想一下,被拆包的每包數(shù)據(jù)都被封裝成相同的格式進(jìn)行傳輸,TCP協(xié)議并沒有提供任何標(biāo)識,接收端也壓根無法自動(dòng)判別哪些包屬于完整的一包數(shù)據(jù)。
知道了接收函數(shù) readAll() 的原理,再加上我們已知客戶端發(fā)送的每包數(shù)據(jù)長度為 16006 字節(jié),那么我們不就可以手動(dòng)計(jì)算接收數(shù)據(jù)的長度,然后將這些數(shù)據(jù)拼接合成嘛。確實(shí)應(yīng)該這么做,但是別忘了TCP還有黏包的問題,也就是TCP傳輸?shù)臄?shù)據(jù)包可能出現(xiàn)粘合在一起的現(xiàn)象,本次要傳輸?shù)臄?shù)據(jù)和下一次傳輸?shù)臄?shù)據(jù)被粘合在一起,那么我們按長度累加計(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; ? ? ? ? ? ? ? ? ? ? ? //計(jì)算一包數(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會(huì)將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ù)時(shí),TCP拆包和黏包導(dǎo)致的數(shù)據(jù)無法解析的問題,讀者可參考此方法自行修改。
三、TCP接收到的QByteArray類型數(shù)據(jù)的轉(zhuǎn)換
上述通過 readAll() 函數(shù)接收到的數(shù)據(jù)為 QByteArray 類型,這是一個(gè)Qt 自己定義的一種類似于 String 的處理字符串的類,這個(gè)類也提供了很多成員函數(shù),方便我們對數(shù)據(jù)進(jìn)行轉(zhuǎn)化。

如果你不需要對接收到的數(shù)據(jù)進(jìn)行運(yùn)算,只是想打印數(shù)據(jù),那么可以直接使用 QByteArray 類型。但是如果你需要對數(shù)據(jù)進(jìn)做加減乘除,那使用 QByteArray 就不合適了,需要轉(zhuǎn)換成基本數(shù)據(jù)類型。需要注意上圖中 QByteArray 類提供的幾個(gè)成員函數(shù),比如 toHex() ,它的返回值依然是 QByteArray,也就是說它是將原始的 QByteArray 轉(zhuǎn)換成十六進(jìn)制的 QByteArray,比如 “255”->“FF”,本質(zhì)上還是字符串。大家在使用官方提供的成員函數(shù)時(shí),一定要看一下函數(shù)的返回值,不要想當(dāng)然了。
我在客戶端發(fā)送的每個(gè)數(shù)據(jù)為兩個(gè)字節(jié),如 0xFFFF,使用 readAll() 接收到的 QByteArray 類型的數(shù)據(jù)也只是按字節(jié)接收,它并不知道我們一個(gè)數(shù)據(jù)占幾個(gè)字節(jié),所以實(shí)際上 receiveData[0] = 255, receiveData[1] = 255。由于我沒有找到現(xiàn)成的可供直接使用的處理函數(shù),所以就手動(dòng)實(shí)現(xiàn)了一下:
/* cacheBuf為合成后的uint數(shù)組, msg為待處理的QByteArray數(shù)據(jù) */
for(uint i=0; i<1000; i++) ?//高低位兩字節(jié)合成為一個(gè)uint
{
? ? ? cacheBuf[i] = msg[16*i+4] & 0x000000FF; ? ? ? ? ? //低位
? ? ? cacheBuf[i] |= ((msg[16*i+5] << 8) & 0x0000FF00);?? ?//高位
}總結(jié)
即使我在stm32單片機(jī)上發(fā)送的是int類型的數(shù)據(jù),但是在Qt上通過 toInt() 函數(shù)時(shí)接收不到我想要的數(shù)據(jù)的,因?yàn)?2位的單片機(jī)中int占兩個(gè)字節(jié),而Qt中的C++的int類型占4個(gè)字節(jié),那么我使用 toInt() 函數(shù)來接收數(shù)據(jù)時(shí),程序就會(huì)以4個(gè)字節(jié)為一個(gè)數(shù)來接收。這告訴我們,在不同平臺(tái)之間傳輸數(shù)據(jù)的時(shí)候,要考慮同種類型的數(shù)據(jù),它們的寬度是否一致。
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
- QT實(shí)現(xiàn)簡單TCP通信
- Qt實(shí)現(xiàn)簡單的TCP通信
- QT編寫tcp通信工具(Client篇)
- QT編寫tcp通信工具(Server端)
- QT網(wǎng)絡(luò)通信TCP客戶端實(shí)現(xiàn)詳解
- Qt?TCP網(wǎng)絡(luò)通信學(xué)習(xí)
- Qt網(wǎng)絡(luò)編程實(shí)現(xiàn)TCP通信
- QT5實(shí)現(xiàn)簡單的TCP通信的實(shí)現(xiàn)
- 基于QT的TCP通信服務(wù)的實(shí)現(xiàn)
- QT網(wǎng)絡(luò)編程Tcp下C/S架構(gòu)的即時(shí)通信實(shí)例
相關(guān)文章
C語言中strcpy()函數(shù)的具體實(shí)現(xiàn)及注意事項(xiàng)
C語言庫函數(shù)char *strcpy(char *dest, const char *src)把src所指向的字符串復(fù)制到dest,下面這篇文章主要給大家介紹了關(guān)于C語言中strcpy()函數(shù)的具體實(shí)現(xiàn)及注意事項(xiàng)的相關(guān)資料,需要的朋友可以參考下2022-11-11
C++通過msxml調(diào)用webservice示例分享
這篇文章主要介紹了C++通過msxml調(diào)用webservice示例分享,需要的朋友可以參考下2014-03-03
linux下實(shí)現(xiàn)的2048游戲示例分享
這篇文章主要介紹了linux下實(shí)現(xiàn)的2048游戲示例,需要的朋友可以參考下2014-04-04
判斷一個(gè)數(shù)是不是素?cái)?shù)的方法
判斷一個(gè)數(shù)是不是素?cái)?shù)的方法,需要的朋友可以參考一下2013-03-03
C和MFC巧妙獲取外網(wǎng)IP的兩種實(shí)現(xiàn)方法
這篇文章主要介紹了C和MFC巧妙獲取外網(wǎng)IP的兩種實(shí)現(xiàn)方法,功能非常的實(shí)用,需要的朋友可以參考下2014-07-07

