基于UDP协议的网络通信应用程序(UDP-Socket)
前两篇文章介绍了基于TCP/IP协议的网络通信应用程序。
嵌入式Linux应用程序开发-(7)TCP-IP网络通信应用程序(TCP-Client)
嵌入式Linux应用程序开发-(8)TCP-IP网络通信应用程序(TCP-Server)
相比起TCP/IP协议的可靠,面向连接,基于字节流通信这些特性,UDP协议是一种轻量级,不可靠,基于数据报的传输协议。很多人会问,为什么UDP协议传输不可靠还要继续使用它?那是因为UDP协议使用起来很方便,不需要建立连接,资源消耗少,通信效率高,在线播放音频或者视频的时候,使用UDP协议比使用TCP/IP协议有更高的传输效率,因为在这种使用场景下,即使丢失一两个数据包,对结果都影响不大。在网络质量不佳的情况下,使用UDP协议传输可能丢包严重,开发者应该注意根据不同的场合选择合适的协议。
对于UDP传输协议,数据报有以下三种传输方式:
单播:UDP单播传输,即发送方指定接收方的IP地址和端口号,把数据直接发送到对方。这种属于一对一的数据传输方式。
组播:接收方需要加入组播地址和端口号,发送方往组播的IP地址发送数据,只要是加入了组播地址的设备,都能收到数据,属于一对多的数据传输方式。
广播:接收方不需要加入广播地址,只指定接收的端口号,发送方往广播的IP地址发送数据,则局域网内的所有设备都能收到数据,属于一对多的数据传输方式。
总的来说,不管是单播,组播,还是广播,对于发送方,每一次数据传输都需要指定接收方的IP地址和端口。而对于接收方,单播和广播的接收方式是一样的,都是监听局域网内所有的IP地址,组播则多了一个步骤,需要加入指定的组播地址才能接收组播数据。
使用嵌入式QT进行UDP协议的应用程序开发,QT的network类库提供了QUdpSocket类,这个类继承于QAbstractSocket类,并提供了UDP通信的基本操作方法,如绑定函数bind(),加入组播joinMulticastGroup(),离开组播leaveMulticastGroup(),数据报的读写操作函数,等等。具体可以参阅 QtNetwork/qudpsocket.h 文件的内容。
目标:使用QT提供的UDP网络通信类,实现一个简单的Udp_Socket收发工具
功能:
(1)界面显示开发板本机IP地址。
(2)可手动输入接收端的IP地址和端口。
(3)可选择UDP数据报的发送方式:组播,单播,广播。
(4)界面显示UDP协议的网络收发数据,并提供清屏按钮。
(5)提供手动发送按钮和自动发送按钮。
开发板运行Udp_Socket收发工具后,界面如下图所示:
界面描述:
(1)Udp_Socket收发工具运行后,在界面显示开发板的IP地址。
(2)可输入接收端的IP地址和端口。
(3)选择UDP的发送方式,单播,组播或广播。
(4)点击 [tcp_send_data] ,往指定的IP地址和端口发送数据。
(5)点击 [START_AUTO_SEND] ,以一秒的频率间隔发送数据。
以下是Udp_Socket收发工具的开发过程。
(1)先用Qt Creator构建一个工程,命名为:009_udp_socket,关于如何构建工程,请参考“第一个嵌入式QT应用程序”的具体内容。
(2)创建工程后,修改009_udp_socket.pro里面的内容,添加QT里面的网络通信模块network,使工程支持QT网络类的调用,如下图所示。
(3)双击打开“widget.ui”文件,构建界面,构建后的界面如下图所示:
应用程序界面描述如上面内容所示,这里不作重复。
(4)对于Udp_Socket收发工具,我们使用一个类UdpSocket对其进行封装,这个类定义了UDP数据的收发函数,以及获取自身IP地址的函数,还定义了一个信号,用来通知ui界面接收到数据并显示。UdpSocket类的具体内容如下图所示:
class UdpSocket : public QUdpSocket
{
Q_OBJECT
public:
explicit UdpSocket(QObject *parent = 0,int port = 0);
~UdpSocket();
QUdpSocket *udp_socket;
QString get_local_ipaddr(void); //获取本机的IP地址
int listen_port;
public slots:
void slot_udp_read_data(); //udp读取数据槽函数
//udp发送数据槽函数
void slot_udp_send_data(const QString ip, quint16 port, QByteArray data_send, unsigned char send_type);
signals:
//数据接收信号,用于通知ui线程,让ui显示服务端接收的数据来自哪个客户端
void signals_udp_receive_data(const QString ip, int port, const QByteArray &data);
};
(5)对于UdpSocket类,在其构造函数里,绑定监听的IP地址和端口号,并禁止在组播发送时自己接收数据。绑定QUdpSocket()类的readyRead()信号,一旦发现数据即调用槽函数进行处理。UdpSocket类的构造函数如下图所示。
//UdpSocket的构造函数
UdpSocket::UdpSocket(QObject *parent, int port) : QUdpSocket(parent)
{
this->listen_port = port;
udp_socket = new QUdpSocket();
//绑定监听的端口号,监听所有的IP地址
//udp_socket->bind((QHostAddress)get_local_ipaddr(),listen_port);
udp_socket->bind(QHostAddress::Any, listen_port);
udp_socket->setSocketOption(QAbstractSocket::MulticastLoopbackOption, 0);//禁止本机接收
//绑定数据读取的槽函数
connect(udp_socket, SIGNAL(readyRead()), this, SLOT(slot_udp_read_data()));
}
(6)当接收到UDP的数据报时,底层系统就会触发readRead()信号,进而调用数据接收的槽函数void UdpSocket::slot_udp_read_data()进行处理。这个函数不断读取缓冲区的数据,读取完成后,发送信号通知ui界面进行数据显示。数据接收槽函数如下图所示:
//udp数据读取的槽函数
void UdpSocket::slot_udp_read_data()
{
QByteArray ba;
QHostAddress address;
quint16 port;
//不断读取缓冲区的数据
while(udp_socket->hasPendingDatagrams())
{
ba.resize(udp_socket->pendingDatagramSize());
udp_socket->readDatagram(ba.data(), ba.size(),&address,&port);
}
//发送数据接收信号,告知ui界面显示数据内容
emit signals_udp_receive_data(address.toString(),(int)port,ba);
}
(7)应用程序进行UDP数据发送时,可以调用QUdpSocket::writeDatagram()进行数据发送,对于单播,组播,广播这三种发送方式,需要对应不同的IP地址,UDP数据发送函数的具体实现,如下图所示:
//udp发送数据的槽函数
void UdpSocket::slot_udp_send_data(const QString ip, quint16 port, QByteArray data_send, unsigned char send_type)
{
switch(send_type)
{
case TYPE_UNICAST: //使用单播方式发送
udp_socket->writeDatagram(data_send,QHostAddress(ip),port);
break;
case TYPE_GROUPCAST: //使用组播方式发送
udp_socket->writeDatagram(data_send,QHostAddress(GROUPCAST_ADDR),port);
break;
case TYPE_BROADCAST: //使用组播方式发送
udp_socket->writeDatagram(data_send,QHostAddress::Broadcast,port);
break;
default:break;
}
}
(8)以上就是UdpSocket类的主要函数实现,接下来,分析一下窗体构造Widget类的具体实现过程。
(9)在Widget类的构造函数里,我们使用UdpSocket类定义一个udp_socket对象,并且绑定数据处理的槽函数。构建一个定时器,用于自动发送数据。显示本机的IP地址。构建一个QButtonGroup类对象,并使界面上的radioButton实现互斥。如下图所示:
Widget::Widget(QWidget *parent) :
QWidget(parent),
ui(new Ui::Widget)
{
ui->setupUi(this);
bool ok;
int listen_port = ui->lineEdit_port->text().toInt(&ok,10);
//定义一个udp_socket对象
udp_socket = new UdpSocket(this,listen_port);
//关联数据处理的槽函数
connect( udp_socket, SIGNAL(signals_udp_receive_data(QString,int,QByteArray)), \
this, SLOT(slot_recv_udp_data(QString,int,QByteArray)));
//构建一个UDP自动发送数据的定时器
auto_send_timer = new QTimer();
//关联定时器超时槽函数
connect( auto_send_timer, SIGNAL(timeout()), this, SLOT(slot_auto_send_timer_handler()));
//显示本机的IP地址
ui->label_local_ip_display->setText(udp_socket->get_local_ipaddr());
//把radiobuttons加入buttonGroup
buttongroup = new QButtonGroup(this);
buttongroup->setExclusive(true);// 设置互斥
buttongroup->addButton(ui->radioButton_unicast);
buttongroup->addButton(ui->radioButton_groupcast);
buttongroup->addButton(ui->radioButton_broadcast);
//连接信号槽,radiobutton改变时作出处理
connect(buttongroup, SIGNAL(buttonClicked(QAbstractButton*)), \
this, SLOT(slot_radiobutton_clicked(QAbstractButton*)));
//默认是单播方式
ui->radioButton_unicast->click();
}
(10)应用程序可以自由选择(单播,组播,广播)这三种发送方式的其中一种,可以通过使用radioButton来实现互斥,当发送方式发生改变时,调用以下槽函数进行处理。
//radiobutton改变时的处理槽函数
void Widget::slot_radiobutton_clicked(QAbstractButton* button)
{
(void)button;
if(ui->radioButton_unicast->isChecked())
{
send_type = TYPE_UNICAST;
ui->label_local_ip_display->setText(udp_socket->get_local_ipaddr()); //显示本机的IP地址
udp_socket->udp_socket->leaveMulticastGroup(QHostAddress(GROUPCAST_ADDR));
}
else if(ui->radioButton_groupcast->isChecked())
{
send_type = TYPE_GROUPCAST;
ui->label_local_ip_display->setText(GROUPCAST_ADDR); //显示组播的IP地址
udp_socket->udp_socket->joinMulticastGroup(QHostAddress(GROUPCAST_ADDR));
}
else if(ui->radioButton_broadcast->isChecked())
{
send_type = TYPE_BROADCAST;
ui->label_local_ip_display->setText(BROADCAST_ADDR); //显示广播的IP地址
udp_socket->udp_socket->leaveMulticastGroup(QHostAddress(GROUPCAST_ADDR));
}
else
{
send_type = TYPE_UNICAST;
udp_socket->udp_socket->bind(QHostAddress(udp_socket->get_local_ipaddr()),udp_socket->listen_port);
ui->label_local_ip_display->setText(udp_socket->get_local_ipaddr()); //显示本机的IP地址
udp_socket->udp_socket->leaveMulticastGroup(QHostAddress(GROUPCAST_ADDR));
}
}
(11)当应用程序接收到UDP数据时,会调用void Widget::slot_recv_udp_data()函数进行处理,在这个函数里面,把接收到的数据以及数据来自哪个IP地址,都显示出来。
//处理UPD数据的槽函数
void Widget::slot_recv_udp_data(const QString ip, int port, const QByteArray &data)
{
(void)port;
QString data_recv;
data_recv = QString(data);
//显示接收到的数据
text_browser_append(1,ip+">>>"+data_recv);
}
(12)以上就是整个Udp_Socket应用程序的开发过程,相比起TCP/IP,使用UDP协议进行数据传输是比较方便快捷的,开发者应该根据不同的应用场景选择不同的数据传输协议,每种协议都各有利弊,合适的就是最好的。
(13)把应用程序下载到开发板上运行,如下图所示: