Qt网络编程之TCP通信及常见问题

本文为作者在开发项目时对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个字节为一个数来接收。这告诉我们,在不同平台之间传输数据的时候,要考虑同种类型的数据,它们的宽度是否一致。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • QT编写tcp通信工具(Server端)

    本文实例为大家分享了QT编写Server端的tcp通信工具的具体代码,供大家参考,具体内容如下 1.说明 使用qt写一个类似网上常见的网络调试工具.此篇为Server端.Client端在上一篇. 2.基本流程 新建QTcpServer对象,为其newConnection信号写槽函数.此为新的Client连接信号,在其对应槽函数里使用nextPendingConnection方法获取Client对象,并为Client添加readyRead(读数据),disconnected(断开连接)两个信号写槽

  • Qt TCP网络通信学习

    TCP简介: Transmission Control Protocol,传输控制协议 .用于数据传输的低层网络协议,多个物联网协议都是基于TCP协议的.它是一个面向数据流和连接的可靠传输协议. TCP头部格式: QTcpSocket类为TCP提供了一个接口,继承自QAbstractSocket.可实现POP3.SMTP.NNTP等标准的网络协议,也可以实现自定义的网络协议.异步工作,依靠事件循环来检测到来的数据,并且自动刷新输出的数据.而QAbstractSocket继承自QIODevice,

  • Qt实现简单的TCP通信

    这段时间用到了QT的TCP通信,做了初步的学习与尝试,编写了一个客户端和服务器基于窗口通信的小例程. 使用QT的网络套接字需要.pro文件中加入一句: QT += network 一.客户端 1.客户端的代码比服务器稍简单,总的来说,使用QT中的QTcpSocket类与服务器进行通信只需要以下5步: (1)创建QTcpSocket套接字对象 socket = new QTcpSocket(); (2)使用这个对象连接服务器 socket->connectToHost(IP, port); (3)

  • QT网络编程Tcp下C/S架构的即时通信实例

    先写一个客户端,实现简单的,能加入聊天,以及加入服务器的界面. #ifndef TCPCLIENT_H #define TCPCLIENT_H #include <QDialog> #include <QListWidget> #include <QLineEdit> #include <QPushButton> #include <QLabel> #include <QGridLayout> #include <QtNetWo

  • Qt网络编程实现TCP通信

    Qt网络编程实现TCP通信,供大家参考,具体内容如下 标签(空格分隔): Tcp通信 一.Tcp简介 (1)TCP(Transmission Control Protocol,传输控制协议)TCP是一个用于数据传输的传输层网络协议,多个网络协议包括(HTTP和FTP都是基于TCP协议),TCP是面向数据流和连接的可靠的传输协议,它区别于传输层的另外一个协议UDP(具体可看—Qt简单实现UDP通信) . (2)QTcpSocket继承自QAbstractSocket,与QUdpSocket传输的数

  • QT5实现简单的TCP通信的实现

    目录 一.客户端 二.服务器 三.运行结果 这段时间用到了QT的TCP通信,做了初步的学习与尝试,编写了一个客户端和服务器基于窗口通信的小例程. 使用QT的网络套接字需要.pro文件中加入一句: QT += network 一.客户端 1.客户端的代码比服务器稍简单,总的来说,使用QT中的QTcpSocket类与服务器进行通信只需要以下5步: (1)创建QTcpSocket套接字对象 socket = new QTcpSocket(); (2)使用这个对象连接服务器 socket->connec

  • QT编写tcp通信工具(Client篇)

    本文实例为大家分享了QT编写tcp通信工具的具体实现代码,Client篇,供大家参考,具体内容如下 1.说明 使用qt写一个类似网上常见的网络调试工具.此篇为Client端.下一遍再写Server端. 2.基本流程 Client端相对简单:创建QTcpSocket对象,为对象的readyRead,error,connected(可选)分别写槽函数,以处理读数据,错误,连接成功三个事件. 连接使用对象的connectToHost方法,断开使用disconnectFromHost方法. 程序不做编码

  • 基于QT的TCP通信服务的实现

    目录 一.结构 1.1 套接字 1.2 socket通信流程 1.3 QTcpsocket 1.4 QTcpServer 二.设计UI 2.1 客户端UI 2.2 服务器端UI 三.核心代码 四.效果图 一.结构 1.1 套接字 应用层通过传输层进行数据通信时,TCP和UDP会遇到同时为多个应用程序进程提供并发服务的问题.多个TCP连接或多个应用程序进程可能需要 通过同一个TCP协议端口传输数据.为了区别不同的应用程序进程和连接,许多计算机操作系统为应用程序与TCP/IP协议交互提供了称为套接字

  • QT网络通信TCP客户端实现详解

    本文实例为大家分享了QT网络通信TCP客户端实现的具体代码,供大家参考,具体内容如下 QT中基于TCP套接字的网络通信需要用到两个类 QTcpServer:服务器类,用于监听客户端连接和客户端建立连接 QTcpSocket:通信套接字类,客户端和服务端都需要使用* 这两个类都属于网络通信的network需要在工程路径下添加network QT += core gui network 服务器 mainwindow.cpp #include "mainwindow.h" #include

  • QT实现简单TCP通信

    本文实例为大家分享了QT实现简单TCP通信的具体代码,供大家参考,具体内容如下 开发环境:win7开发软件:Qt5.5.0下图是实现效果 首先点击客户端的connect与服务器连接,连接成功后再服务器会显示“成功连接”,然后在客户端会显示“成功与服务器建立连接”,然后我们就可以分别在服务器和客户端向对方发送数据了. 1 TCP介绍 传输控制协议(TCP,Transmission Control Protocol)是一种面向连接的.可靠的.基于字节流的传输层通信协议. 在QT中的网络编程的过程如下

随机推荐