Qt网络通信——TCP和UDP

一、TCP通信

TCP通信必须先建立 TCP 连接,通信端分为客户端和服务器端。

Qt 为服务器端提供了 QTcpServer 类用于实现端口监听,QTcpSocket 类则用于服务器和客户端之间建立连接。大致流程如下图所示:

1. 服务器端建立

1.1 监听------listen()

服务器端程序首先需要用函数 listen() 开始服务器监听,可以设置监听的IP地址和端口,一般一个服务器端程序只监听某个端口的网络连接。函数原型定义如下:

cpp 复制代码
bool QTcpServer::listen(const QHostAddress &address = QHostAddress::Any, quint16 port = 0)

函数返回 true 时,表示监听成功。此时服务器会持续监听来自客户端的连接请求。举例:

cpp 复制代码
if (!tcpServer->listen(QHostAddress::LocalHost, 8080)) {    
    QMessageBox::information(this, "Error", tcpServer->errorString());
    return;
}

那么在什么情况下会监听失败呢?

  1. 端口号太低导致冲突或者没有权限;
  2. 监听除了127.0.0.1和0.0.0.0以外的端口,可能需要管理员权限。
1.2 接受连接------nextPendingConnection()

当有新的客户端接入时,QTcpServer的内部有一个受保护函数 incomingConnection(),它会创建一个与客户端连接的QTcpSocket对象,然后发射 newConnection() 信号。

此时,可以建立自定义槽函数对该信号进行处理,使用 nextPendingConnection() 建立socket连接。

cpp 复制代码
MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);
    
    ...

    tcpServer = new QTcpServer(this);
    connect(tcpServer, SIGNAL(newConnection()), this, SLOT(do_newConnection()));
}

void MainWindow::do_newConnection()
{
    tcpSocket = tcpServer->nextPendingConnection(); //创建socket接收客户端连接   

    ...
}

2. 客户端建立

客户端的 QTcpSocket 对象首先通过 connectToHost() 尝试连接到服务器,该函数需要指定服务器的IP地址和端口。值得注意的是,该函数是以异步方式连接到服务器,并不会阻塞整个程序的运行,只有成功连接后 QTcpSocket 对象才会发射 connected() 信号表示已经成功连接。

cpp 复制代码
MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);
    tcpClient = new QTcpSocket(this);    //创建socket变量
    connect(tcpClient, SIGNAL(connected()), this, SLOT(do_connected()));
}

// 尝试连接并发射connected()信号
void MainWindow::on_actConnect_triggered()
{
    tcpClient->connectToHost(QHostAddress::LocalHost, 8080);
}

// connected()信号的自定义槽函数
void MainWindow::do_connected()
{ 
    QMessageBox::information(this, "Success", "已成功连接到服务器!");
    ...
}

如果真的需要以阻塞方式连接到服务器,则可以使用函数 waitForConnected(),用法大差不差。

3. 通信

当 QTcpSocket 对象接收到服务器或客户端数据后会发射**readyRead()**信号。或者可以说,当缓冲区有新数据就会发射此信号。我们可以设计相应的槽函数来接收此信号。举例:

cpp 复制代码
// 客户端发送消息
void MainWindow::on_btnSend_clicked()
{
    QString msg = ui->editMsg->text();
    ui->textEdit->appendPlainText("客户端说:" + msg);

    tcpClient->write(msg.toUtf8() + '\n');
}

// 客户端接收消息
void MainWindow::do_socketReadyRead()
{
    while(tcpClient->canReadLine())
        ui->textEdit->appendPlainText("收到数据:" + tcpClient->readLine());
}

二、UDP

与TCP通信不同,UDP通信不区分客户端和服务器。而且UDP是不可靠、无连接的协议,因此UDP客户端每次发送数据都需要指定目标ip地址和端口。

QUdpSocket 和 QTcpSocket 有着相同的父类 QAbstractSocket ,因此这两个类的大部分接口函数也会大差不差。要说区别,那就应该是传输数据上,QTcpSocket 使用 write() 函数发送数据流(字节),而 QTcpSocket 使用 writeDatagram() 函数发送数据报。

UDP发送消息采用单播、广播、组播(多播)3种方式。

1. 单播

1.1 绑定端口------bind()

因为UDP是无连接的,所以在收发数据前,不需要像TCP那样建立连接。只需要绑定本机的任意一个端口即可,保证对方可以给这个端口发送消息。

cpp 复制代码
udpSocket->bind(1200);  // 绑定
udpSocket->abort();  // 解绑
1.2 发送数据------writeDatagram()

上面已经讲过,发送消息需要用到 writeDatagram() 这个函数。函数原型如下:

cpp 复制代码
qint64 QUdpSocket::writeDatagram(const QbyteArray &datagram, const QHostAddress &host, quint16 port)
  • datagram:要发出的数据报
  • host:目标主机ip
  • port:目标主机端口
  • 返回值:已经成功发送的字节数,若 <0 则表示发送失败

举个例子:

cpp 复制代码
void MainWindow::on_btnSend_clicked()
{
    QHostAddress targetAddr(ui->comboTargetIP->currentText());  //目标IP
    quint16 targetPort = ui->spinTargetPort->value();     //目标port
    QString msg = ui->editMsg->text();       //发送的消息内容

    udpSocket->writeDatagram(msg.toUtf8(), targetAddr, targetPort); //发出数据报
    ui->textEdit->appendPlainText("[单播消息] 自己:" + msg);
}
1.3 接收数据------readDatagram()

与 QTcpSocket 类似,在 QUdpSocket 接收到数据报后也发射 readReady() 信号。只要有等待读取的数据报,hasPendingDatagrams() 函数就会返回 true,然后利用 readDatagram() 函数读取到数据报信息。readDatagram() 函数原型如下:

cpp 复制代码
qint64 QUdpSocket::readDatagram(char *data, qint64 maxSize, QHostAddress *address = nullptr, quint16 *port = nullptr)
  • data:数据报的数据缓冲区
  • maxSize:接收获取多少数据报到缓冲区data里

其中 data 和 maxSize 是必须要有的,而ip地址和端口是可以选择不要的。

举例:

cpp 复制代码
void MainWindow::do_socketReadyRead()
{
    while(udpSocket->hasPendingDatagrams())
    {
        QByteArray datagram;
        QHostAddress peerAddr;  //格式为:QHostAddress("::ffff:127.0.0.1")
        quint16 peerPort;
        
        // 确保 datagram 能够存储来自 udpSocket 的完整数据报,而不会截断数据或导致内存分配错误。
        datagram.resize(udpSocket->pendingDatagramSize());
 
        udpSocket->readDatagram(datagram.data(),datagram.size(), &peerAddr, &peerPort);

        QString str = datagram.data();
        QString peer = "[来自 " + peerAddr.toString() + ":" + QString::number(peerPort) + "] 说:";
        ui->textEdit->appendPlainText(peer + str);
    }
}

注:这里的ip地址类型与 TCP 有区别,为 QHostAddress("::ffff:127.0.0.1") 。因为 UDP 是无连接的协议,系统可能会选择将IPv4地址映射为IPv6地址来处理。

2. 广播

广播与单播类似。只需要注意发送数据时把目标ip改为 QHostAddress::Broadcast 即可。

3. 组播

QUdpSocket 支持 UDP 组播,joinMulticastGroup() 函数使主机加入多播组,leaveMulticastGroup() 函数使主机离开多播组。UDP 组播的特点就是使用组播地址(D类地址),其他的端口绑定、数据收发等功能的实现与 UDP 单播完全相同。

3.1 设置udp组播的生存周期------MulticastTtlOption
cpp 复制代码
udpSocket = new QUdpSocket(this);
udpSocket->setSocketOption(QAbstractSocket::MulticastTtlOption, 1);
  • 参数1:QAbstractSocket::MulticastTtlOption:udp组播的生存周期,每跨一个路由值-1
  • 参数2:默认值是1,表示只能在同一路由的局域网传播
3.2 绑定端口------bind()

与单播的绑定端口不同,这里的函数原型如下:

cpp 复制代码
bool QAbstractSocket::bind(QHostAddress::SpecialAddress addr, quint16 port = 0, BindMode mode = DefaultForPlatform)
  • addr:特殊的主机IP地址,如Broadcast,LocalHost,AnyIPv4等。
  • port:绑定的端口
  • mode:绑定模式,如 ShareAddress 允许其他服务使用这个地址和端口,ReuseAddressHint 允许多个套接字绑定到相同的地址和端口。

举例:

cpp 复制代码
quint16 groupPort = 35320;    //组播端口
udpSocket->bind(QHostAddress::AnyIPv4, groupPort, QUdpSocket::ShareAddress);
3.3 加入组播------ joinMulticastGroup()

加入多播组只需要指定一个组播地址(239.0.0.0~239.255.255.255)即可。修改后的代码如下:

cpp 复制代码
QHostAddress groupAddress = QHostAddress("239.255.43.21");      //D类地址
if (udpSocket->bind(QHostAddress::AnyIPv4, 35320, QUdpSocket::ShareAddress)) {
    udpSocket->joinMulticastGroup(groupAddress);  //加入多播组
    ui->textEdit->appendPlainText("**加入组播成功");
    ui->textEdit->appendPlainText("**组播地址IP:"+IP);
    ui->textEdit->appendPlainText("**绑定端口:"+QString::number(groupPort));
}
else
    ui->textEdit->appendPlainText("**绑定端口失败");

组播类似于QQ群,在加入组播之后, 就可以看到所有人发的消息,包括自己发的消息。

3.4 退出组播------leaveMulticastGroup()

在退出指定的组播后,记得还要解除绑定。

cpp 复制代码
udpSocket->leaveMulticastGroup(groupAddress);   //退出组播
udpSocket->abort();     //解除绑定

码字不易,看到这里如果给您带来一丢丢的启发,点个赞再走吧!

相关推荐
Estar.Lee43 分钟前
查手机号归属地免费API接口教程
android·网络·后端·网络协议·tcp/ip·oneapi
傻啦嘿哟2 小时前
代理IP在后端开发中的应用与后端工程师的角色
网络·网络协议·tcp/ip
Estar.Lee4 小时前
时间操作[计算时间差]免费API接口教程
android·网络·后端·网络协议·tcp/ip
友友马4 小时前
『 Linux 』网络层 - IP协议(一)
linux·网络·tcp/ip
徐霞客3205 小时前
Qt入门1——认识Qt的几个常用头文件和常用函数
开发语言·c++·笔记·qt
姆路6 小时前
QT Designer内存飙升
qt
Bruce小鬼8 小时前
QT文件基本操作
开发语言·qt
懷淰メ8 小时前
PyQt飞机大战游戏(附下载地址)
开发语言·python·qt·游戏·pyqt·游戏开发·pyqt5
hgdlip9 小时前
主IP地址与从IP地址:深入解析与应用探讨
网络·网络协议·tcp/ip
今天我刷leetcode了吗9 小时前
docker 配置同宿主机共同网段的IP 同时通过通网段的另一个电脑实现远程连接docker
tcp/ip·docker·电脑