一、TCP通信

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

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

1. 服务器端建立

1.1 监听——listen()

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

bool QTcpServer::listen(const QHostAddress &address = QHostAddress::Any, quint16 port = 0)

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

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连接。

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() 信号表示已经成功连接。

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() 信号。或者可以说,当缓冲区有新数据就会发射此信号。我们可以设计相应的槽函数来接收此信号。举例:

// 客户端发送消息
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那样建立连接。只需要绑定本机的任意一个端口即可,保证对方可以给这个端口发送消息。

udpSocket->bind(1200);  // 绑定
udpSocket->abort();  // 解绑
1.2 发送数据——writeDatagram()

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

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

举个例子: 

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() 函数原型如下:

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

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

举例:

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
udpSocket = new QUdpSocket(this);
udpSocket->setSocketOption(QAbstractSocket::MulticastTtlOption, 1);
  • 参数1:QAbstractSocket::MulticastTtlOption:udp组播的生存周期,每跨一个路由值-1
  • 参数2:默认值是1,表示只能在同一路由的局域网传播
3.2  绑定端口——bind()

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

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

举例:

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

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

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()

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

udpSocket->leaveMulticastGroup(groupAddress);   //退出组播
udpSocket->abort();     //解除绑定

 

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

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐