UDP通信是无连接的,与TCP通信相比,少了一步建立连接的过程,只要经过绑定,就可以直接进行数据的发送和接收。
在Qt的UDP通信中,由于少了连接这一个步骤,客户端和服务端没有太大区别,所以也可以看作只有发送端和接收端。无论是发送端还是接收端,都只有一个套接字,也就是QUdpSocket。此外,UDP通信中没有监听listen(),只有绑定bind()
,往套接字中读写数据用的是readDatagram()
和writeDatafram()
,关闭套接字时同样是调用close()
。
P.S:datagram是数据报 / 数据包 / 数据报文的意思
和TCP相同的地方是,发送端向接收端发送数据时,会触发接收端的readyRead()
信号。
UDP收发消息的实现
首先,为了方便测试需要绘制两个窗口,在它们之间互相通信:
//WidgetA.h
#ifndef WIDGETA_H
#define WIDGETA_H
#include <QWidget>
#include <QLineEdit>
#include <QPushButton>
#include <QTextEdit>
class WidgetA : public QWidget
{
Q_OBJECT
public:
WidgetA(QWidget *parent = 0);
~WidgetA();
private:
QLineEdit *p_ipEdit;
QLineEdit *p_portEdit;
QPushButton *p_connectButton;
QTextEdit *p_sendBox;
QPushButton *p_sendButton;
QPushButton *p_closeButton;
};
#endif // WIDGETA_H
//WidgetA.cpp
#include "WidgetA.h"
#include <QGridLayout>
WidgetA::WidgetA(QWidget *parent)
: QWidget(parent)
{
this->resize(640,480);
this->move(300,300);
this->setWindowTitle("Port 9999");
QGridLayout *p_layout=new QGridLayout(this);
p_ipEdit=new QLineEdit(this);
p_portEdit=new QLineEdit(this);
p_sendBox=new QTextEdit(this);
p_sendButton=new QPushButton("send",this);
p_closeButton=new QPushButton("close",this);
p_layout->addWidget(p_ipEdit,0,0,1,10);
p_layout->addWidget(p_portEdit,1,0,1,10);
p_layout->addWidget(p_sendBox,2,0,8,10);
p_layout->addWidget(p_sendButton,10,0,1,2);
p_layout->addWidget(p_closeButton,10,9,1,1);
}
WidgetA::~WidgetA()
{
}
另一个窗口可以增加一个C++文件,用相同的代码实现,也可以直接new一个WidgetA对象,在main.cpp中为不同的对象绑定不同的端口。
//main.cpp
#include "WidgetA.h"
#include "WidgetB.h"
#include <QApplication>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
WidgetA wa;
WidgetB wb;
wa.show();
wb.show();
return a.exec();
}
实现效果:
接下来实现消息的收发功能,在开始之前,记得在.pro文件中加上QT += network
,然后进行构建。
bind()
接着,创建通信套接字QUdpSocket:
//绑定端口
p_udpSocket=new QUdpSocket(this);
p_udpSocket->bind(9999);
readDatagram()
当接收方接收到数据时,会触发readyRead()
信号,在与之对应的槽函数中,我们需要调用readDatagram()
方法来获取发送方发送过来的数据报内容以及发送方的主机地址和端口号。
P.S:
readDatagram()
方法的有四个参数:
第一个参数是存储数据报内容的char型数组;
第二个参数是数据报的最大长度,超过这个长度的数据会丢失;
第三个参数是发送者的主机地址,类型为QHostAddress;
第四个参数是发送者的端口号,类型为qint16。
这个方法的返回值是成功读取的字符数。
void WidgetB::readData(){
//获取发送者的IP和端口号以及数据报内容
char array[1024]; //用于接收数据报内容的char型数据
QHostAddress m_ip; //用于接收发送方IP地址的变量
quint16 m_port; //用于接收发送方端口号的变量,注意是quint16类型
qint64 m_len=p_udpSocket->readDatagram(array,sizeof(array),&m_ip,&m_port);
//组包
if(m_len>0){
QString str=QString("[%1:%2]:%3").arg(m_ip.toString()).arg(m_port).arg(array);
//设置文本区内容
p_sendBox->setText(str);
}
}
P.S:端口号这里的类型要用quint16而不是qint16,否则会报错。
writeDatagram()
当作为发送方发送数据时,需要从窗口中获取目标主机地址和端口号,然后调用writeDatagram()
将数据写到通信套接字中:
void WidgetB::sendData(){
//获取接收方的IP端口号
QString m_ip=p_ipEdit->text();
quint16 m_port=p_portEdit->text().toInt();
//获取文本区内
QString str=p_sendBox->toPlainText();
//往通信套接字中写数据
p_udpSocket->writeDatagram(str.toUtf8(),QHostAddress(m_ip),m_port);
}
P.S:端口号的类型是quint16
,而主机地址的类型是QHostAddress
。
实现效果:
close()
若想关闭UDP套接字,直接调用close()
即可:
void WidgetB::closeConnection(){
p_udpSocket->close();
}
实现效果:
结论:当A关闭通信套接字时,它依然可以向B发送消息,B在没有关闭通信套接字的情况下也依然可以接收到A发送过来的消息,但A接收不到B发送过来的消息。
UDP广播和组播
UDP广播
UDP进行广播时,同一个局域网中的所有主机都能接收到数据报。但哪些应用程序会收到消息取决于端口号。
UDP的广播地址为255.255.255.255
。
实现效果:
虽然IP设置为255.255.255.255,但IP地址为127.0.0.1且端口号为8888的通信套接字还是收到了消息。当然,同一局域网下其他主机端口号为8888的进程也能收到消息。
UDP组播
我们在使用广播发送消息的时候会发送给所有用户,但是有些用户是不想接受消息的,这时候我们就应该使用组播,接收方只有先注册到组播地址中才能收到组播消息,否则则接受不到消息。
UDP中的组播地址必须是D类地址。D类地址有:
- 224.0.0.0~224.0.0.255为预留的组播地址(永久组地址),地址224.0.0.0保留不做分配,其它地址供路由协议使用;
- 224.0.1.0~224.0.1.255是公用组播地址,可以用于Internet;
- 224.0.2.0~238.255.255.255为用户可用的组播地址(临时组地址),全网范围内有效;
- 239.0.0.0~239.255.255.255为本地管理组播地址,仅在特定的本地范围内有效。
可以调用joinMultiGroup()
方法来加入一个组播。但是加入组播之后,bind()中就不能再使用默认的主机地址了,必须指定QHostAddress,否则会有如下警告:
可供选择的QHostAddress有:
- QHostAddress::Null
- QHostAddress::LocalHost
- QHostAddress::LocalHostIPv6
- QHostAddress::Broadcast
- QHostAddress::AnyIPv4
- QHostAddress::AnyIPv6
- QHostAddress::Any
p_udpSocket->bind(QHostAddress::AnyIPv4,8888);
p_udpSocket->joinMulticastGroup(QHostAddress("244.0.0.2"));
实现效果:
P.S:
接收到的数据在末尾多了一个“1”,是因为我们准备用来接收数据的数组array没有被数据填满,也没有遇到结束符,因此会产生意想不到的结果,比如数据是中文时,会出现乱码。
要解决这个问题,可以利用获取到的数据长度m_len,置array[m_len]=’\0’。
qint64 m_len=p_udpSocket->readDatagram(array,sizeof(array),&m_ip,&m_port); //获取成功读取的字符数
array[m_len]='\0'; //加入结束符
也可以使用pendingDatagramSize()
方法来获取数据报的大小,根据返回值来准备对应大小的内存空间存放将要接收的数据。
完整代码:
//WidgetA.h
#ifndef WIDGETA_H
#define WIDGETA_H
#include <QWidget>
#include <QLineEdit>
#include <QPushButton>
#include <QTextEdit>
#include <QUdpSocket>
class WidgetA : public QWidget
{
Q_OBJECT
public:
WidgetA(QWidget *parent = 0);
~WidgetA();
private:
QLineEdit *p_ipEdit;
QLineEdit *p_portEdit;
QPushButton *p_connectButton;
QTextEdit *p_sendBox;
QPushButton *p_sendButton;
QPushButton *p_closeButton;
QUdpSocket *p_udpSocket;
protected:
void WidgetA::readData();
void WidgetA::sendData();
void WidgetA::closeConnection();
};
#endif // WIDGETA_H
//WidgetA.cpp
#include "WidgetA.h"
#include <QGridLayout>
#include <QHostAddress>
WidgetA::WidgetA(QWidget *parent)
: QWidget(parent)
{
//绘制界面
this->resize(640,480);
this->move(200,300);
this->setWindowTitle("Port 9999");
QGridLayout *p_layout=new QGridLayout(this);
p_ipEdit=new QLineEdit(this);
p_portEdit=new QLineEdit(this);
p_sendBox=new QTextEdit(this);
p_sendButton=new QPushButton("send",this);
p_closeButton=new QPushButton("close",this);
p_layout->addWidget(p_ipEdit,0,0,1,10);
p_layout->addWidget(p_portEdit,1,0,1,10);
p_layout->addWidget(p_sendBox,2,0,8,10);
p_layout->addWidget(p_sendButton,10,0,1,2);
p_layout->addWidget(p_closeButton,10,9,1,1);
//绑定端口
p_udpSocket=new QUdpSocket(this);
p_udpSocket->bind(QHostAddress::AnyIPv4,9999);
p_udpSocket->joinMulticastGroup(QHostAddress("224.0.0.2"));
connect(p_udpSocket,&QUdpSocket::readyRead,this,&WidgetA::readData);
connect(p_sendButton,&QPushButton::clicked,this,&WidgetA::sendData);
connect(p_closeButton,&QPushButton::clicked,this,&WidgetA::closeConnection);
}
WidgetA::~WidgetA()
{
}
void WidgetA::readData(){
//获取发送者的IP和端口号以及数据报内容
char array[1024];
QHostAddress m_ip;
quint16 m_port;
qint64 m_len=p_udpSocket->readDatagram(array,sizeof(array),&m_ip,&m_port);
//组包
if(m_len>0){
QString str=QString("[%1:%2]:%3").arg(m_ip.toString()).arg(m_port).arg(array);
array[m_len]='\0';
//设置文本区内容
p_sendBox->append(str);
}
}
void WidgetA::sendData(){
//获取接收方的IP端口号
QString m_ip=p_ipEdit->text();
quint16 m_port=p_portEdit->text().toInt();
//获取文本区内
QString str=p_sendBox->toPlainText();
//往通信套接字中写数据
p_udpSocket->writeDatagram(str.toUtf8(),QHostAddress(m_ip),m_port);
}
void WidgetA::closeConnection(){
p_udpSocket->close();
}
另外两个窗口实现的方法类似。
//main.cpp
#include "WidgetA.h"
#include "WidgetB.h"
#include "WidgetC.h"
#include <QApplication>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
WidgetA wa;
WidgetB wb;
WidgetC wc;
wa.show();
wb.show();
wc.show();
return a.exec();
}
实现效果:
如果想要退出组播,调用通信套接字的leaveMultiGroup()
方法即可。