一、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;
}
那么在什么情况下会监听失败呢?
- 端口号太低导致冲突或者没有权限;
- 监听除了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(); //解除绑定
码字不易,看到这里如果给您带来一丢丢的启发,点个赞再走吧!