> 文章列表 > 嵌入式Linux应用程序开发-TCP-IP网络通信应用程序

嵌入式Linux应用程序开发-TCP-IP网络通信应用程序

嵌入式Linux应用程序开发-TCP-IP网络通信应用程序

作为全世界最优秀的开源操作系统,Linux内部已经集成了强大的网络协议栈,并向应用层提供丰富的系统调用,开发者可以基于通用的系统调用接口,使用Linux内核提供的网络功能。
如果要分析Linux内部的网络通信机制以及实现原理,相信不是一时半刻或片文只字能描述清楚,一般的应用开发者可以通过网上搜索资料去了解一下,但在初学阶段,不建议去深入研究。
因此,本章节只是站在应用开发的角度,描述如何开发嵌入式QT的网络应用程序。
        TCP/IP协议模型(Transmission Control Protocol/Internet Protocol),包含了一系列构成互联网基础的网络协议,是Internet的核心协议。
它是一种面向连接的、可靠的、基于字节流的传输协议。关于TCP/IP协议的具体实现原理,本文不进行描述(关于TCP/IP的实现原理描述,其复杂度已经可以用一本书来具体阐述了)。
本文着重描述在嵌入式QT环境下,如何使用TCP/IP进行数据通信。
对于TCP/IP的客户端(TCP-Client)角色,在进行数据通信之前,一般会经历以下过程:
(1)调用socket套接字函数,创建套接字的文件描述符。
(2)配置连接参数(需要连接的服务器IP,端口,连接协议,等等)。
(3)基于套接字的文件描述符,调用connect函数,连接指定的服务器。
(4)处理connect过程中可能出现的情况。(如连接成功,连接出错,找不到服务器等)
(5)connect成功后,调用数据发送接口,进行数据发送。
(6)调用数据接收接口,并处理接收到的网络数据。(Linux C语言开发,可以阻塞接收或使用select/poll机制接收。QT开发,可以使用信号槽机制进行接收。)
        使用嵌入式QT进行TCP/IP的网络通信应用程序开发,对于TCP客户端,只需要关注QT提供的QTcpSocket类,这个类继承于QAbstractSocket类,在QAbstractSocket类里面提供了一系列的网络操作接口函数,
如:连接服务器connectToHost()、断开与服务器的连接disconnectFromHost(),等等。提供各种信号(connected()、disconnected()、stateChanged())与槽函数,方便应用开发者调用。具体可以参阅 QtNetwork/qabstractsocket.h 文件的内容。

目标:使用QT提供的TCP/IP网络通信类,实现一个简单的TCP客户端(TCP-Client)
功能:
(1)开发板界面显示开发板的网络IP地址。
(2)可手动输入需要连接的服务器IP和端口。
(3)界面显示TCP客户端的连接状态。(连接成功,断开连接,连接出错)
(4)界面显示TCP客户端的收发数据,并提供清屏按钮。
(5)提供手动发送按钮和自动发送按钮。

创建一个tcp_client.h文件,编写一个TCP_Client类继承于QTcpSocket,TCP_Client类提供了一些客户端经常用的操作函数,如连接服务器,获取本机IP,处理连接状态,等等。类的具体实现如下图所示:

  1. #define TCP_CONNECTED        "CONNECTED"
  2. #define TCP_DISCONNECTED     "DISCONNECTED"
  3. #define TCP_CONNECT_ERROR    "CONNECT_ERROR"
  4. class TCP_Client : public QTcpSocket
  5. {
  6.     Q_OBJECT
  7. public:
  8.     TCP_Client(QObject* parent=0);
  9.     ~TCP_Client();
  10.     QString get_local_ipaddr(void);   //获取本机的IP地址
  11.     void connect_server(QString server_ip, int server_port);   //连接服务器函数
  12.     void disconnect_server(void);  //断开与服务器的连接
  13.     void tcp_client_send_data(QByteArray arr_data);    //客户端向服务器发送数据
  14.     void tcp_client_send_data(QString str_data);    //客户端向服务器发送数据
  15. public slots:
  16.     void slot_tcp_client_connected(void);  //客户端连接成功的槽函数
  17.     void slot_tcp_client_disconnected(void);  //客户端断开连接的槽函数
  18.     void slot_tcp_client_connect_error(QAbstractSocket::SocketError);   //客户端连接出错的槽函数
  19.     void slot_tcp_client_recv_data();   //客户端从服务器接收数据
  20. signals:
  21.     void signal_tcp_client_recv_data(QByteArray arr_data);  //发送此信号,通知widget类处理接收到的服务端数据
  22.     void signal_tcp_client_connect_state(QString state);   //发送此信号,通知widget类关于TCP-Client的连接状态
  23. private:
  24.     QTcpSocket *m_tcp_socket;  //QTcpSocket对象,用来进行网络操作
  25. };

复制代码

创建一个tcp_client.cpp文件,这个文件内包含了TCP_Client类里面的所有函数实现。有关tcp_client.cpp的具体内容,请参阅源码。

Qt应用程序启动时,跟C语言一样,都是以main函数作为入口。(当然了,真正的程序启动入口并不是main函数,这里忽略了main函数之前的一系列复杂过程,应用程序呈现给开发者的,一般都是以main函数作为入口)。
以下是Qt应用程序的main函数入口。

  1. int main(int argc, char *argv[])
  2. {
  3.     QApplication a(argc, argv);
  4.     Widget w;
  5.     w.show();
  6.     return a.exec();
  7. }

复制代码

这个main函数比较简单,里面定义了一个QApplication和Widget对象,通过Widget::show()函数显示窗体,然后执行QApplication::exec()函数把整个应用程序加入Qt的事件队列,不断循环。

在Qt的界面应用程序中,对于Widget类型的窗体,其ui都是通过QWidget类进行构建,在widget.cpp文件中,针对本工程,Widget类的构造函数如下图所示:

  1. Widget::Widget(QWidget *parent) :
  2.     QWidget(parent),
  3.     ui(new Ui::Widget)
  4. {
  5.     ui->setupUi(this);
  6.     ui->pushButton_connect->setCheckable(false);
  7.     ui->pushButton_auto_send_data->setCheckable(false);
  8.     ui->pushButton_send_data->setEnabled(false);         //手动发送数据按钮不可用
  9.     ui->pushButton_auto_send_data->setEnabled(false);    //自动发送数据按钮不可用
  10.     ui->lineEdit_server_ip->setEnabled(true);  //服务器IP可以被输入
  11.     ui->lineEdit_server_port->setEnabled(true); //服务器端口可以被输入
  12.     tcp_client = new TCP_Client();  //创建一个TCP_Client类对象
  13.     ui->label_local_ip_display->setText(tcp_client->get_local_ipaddr());   //显示本机的IP地址
  14.     //连接信号槽,用来处理TCP客户端的连接状态
  15.     connect(tcp_client,SIGNAL(signal_tcp_client_connect_state(QString)),this,SLOT(slot_tcp_client_connect_state(QString)));
  16.     //连接信号槽,用来处理TCP客户端接收到的数据
  17.     connect(tcp_client,SIGNAL(signal_tcp_client_recv_data(QByteArray)),this,SLOT(slot_tcp_client_recv_data(QByteArray)));
  18.     auto_send_timer = new QTimer();     //构建一个TCP自动发送数据的定时器
  19.     connect( auto_send_timer, SIGNAL(timeout()), this, SLOT(slot_auto_send_timer_handler()));  //关联定时器超时槽函数
  20. }

在Widget类的构造函数里面,先对ui里面的某些控件进行配置,比如:在连接按钮点击之前,手动和自动发送数据的按钮不可用,服务器IP和端口可被编辑。
然后创建一个TCP_Client类对象,TCP-Client的一系列操作,如连接服务器,收发数据等等,都是基于这个类对象进行。
程序开始运行的时候,在界面上显示本机的IP地址,并连接tcp-client对象提供的信号槽。最后,构建一个定时器对象用于数据的自动发送,定时器对象在构建时,并没有启动计时。

先在windows7上运行服务器端的程序,设置好监听的IP和端口。imx6ul板卡上的客户端应用程序启动后,设置好需要连接的服务器参数,点击 [CONNECT] 按钮,程序就会调用该按钮的槽函数void Widget::on_pushButton_connect_clicked(),函数的实现内容如下图所示:

  1. //点击此按钮,连接服务器
  2. void Widget::on_pushButton_connect_clicked()
  3. {
  4.     QString server_ip;
  5.     int server_port;
  6.     bool ok;
  7.     if(ui->pushButton_connect->isCheckable())      //客户端处于连接状态
  8.     {
  9.         tcp_client->disconnect_server();
  10.     }
  11.     else     //客户端处于断开状态
  12.     {
  13.         //检查服务器IP和服务器端口是否参数有效
  14.         if(ui->lineEdit_server_ip->text().isEmpty() || ui->lineEdit_server_port->text().isEmpty())
  15.         {
  16.             ui->label_connect_status->setText(QString("params error!"));
  17.             QMessageBox::information(this,"Server Params","Server Params Error!");
  18.             return;
  19.         }
  20.         else
  21.         {
  22.             server_ip = ui->lineEdit_server_ip->text();
  23.             server_port = ui->lineEdit_server_port->text().toInt(&ok,10);
  24.             tcp_client->connect_server(server_ip,server_port);   //根据指定的ip和端口,连接服务器
  25.         }
  26.     }
  27. }

点击按钮后,分两种情况,需要判断当前的客户端网络状态是连接还是断开。如果客户端处于连接状态,则点击按钮后,断开客户端与服务器的连接。
如果客户端处于断开状态,则点击按钮后,先检查输入的参数是否有效,如果参数有效,则启动客户端与服务器的连接。客户端与服务器的连接函数connect_server(),如下图所示:

  1. //连接服务器函数
  2. void TCP_Client::connect_server(QString server_ip,int server_port)
  3. {
  4.     if(m_tcp_socket)
  5.     {
  6.         m_tcp_socket->connectToHost(server_ip,server_port,QTcpSocket::ReadWrite); //尝试连接服务器
  7.         //建立信号槽,接收连接成功的信号
  8.         connect(m_tcp_socket,SIGNAL(connected()),this,SLOT(slot_tcp_client_connected()));
  9.         //建立信号槽,接收连接失败的信号
  10.         connect(m_tcp_socket,SIGNAL(error(QAbstractSocket::SocketError)),this,SLOT(slot_tcp_client_connect_error(QAbstractSocket::SocketError)));
  11.     }
  12. }
  13. //断开与服务器的连接
  14. void TCP_Client::disconnect_server(void)
  15. {
  16.     if(m_tcp_socket)
  17.         m_tcp_socket->disconnectFromHost();   //断开与服务器的连接
  18. }

客户端与服务器连接过程中,由于在TCP-Client类里面绑定了连接状态的信号槽,因此,连接状态改变后,系统会发送相应的信号,然后调用对应的槽函数进行处理。
因为需要把连接状态显示在界面上,因此,需要在Widget类中,编写一个处理连接状态的槽函数,这个槽函数处理了CONNECTED,DISCONNECTED和CONNECT_ERROR这三种状态。如下图所示:

  1. //客户端连接状态的信号槽
  2. void Widget::slot_tcp_client_connect_state(QString state)
  3. {
  4.     if(state == TCP_CONNECTED)    //TCP客户端处于连接状态
  5.     {
  6.         ui->pushButton_connect->setCheckable(true);
  7.         ui->pushButton_connect->setText(trUtf8("DISCONNECT"));
  8.         //连接成功后,服务器的IP和端口不可以再被编辑
  9.         ui->lineEdit_server_ip->setEnabled(false);
  10.         ui->lineEdit_server_port->setEnabled(false);
  11.         //连接成功后,客户端的发送数据按钮和自动发送按钮可被使用
  12.         ui->pushButton_send_data->setEnabled(true);
  13.         ui->pushButton_auto_send_data->setEnabled(true);
  14.         ui->label_connect_status->setText("connect success");
  15.     }
  16.     else if(state == TCP_DISCONNECTED)   //TCP客户端处于断开状态
  17.     {
  18.         ui->pushButton_connect->setCheckable(false);
  19.         ui->pushButton_connect->setText(trUtf8("CONNECT"));
  20.         //连接断开后,服务器的IP和端口可以再被编辑
  21.         ui->lineEdit_server_ip->setEnabled(true);
  22.         ui->lineEdit_server_port->setEnabled(true);
  23.         //连接断开后,客户端的发送数据按钮和自动发送按钮不可被使用
  24.         ui->pushButton_send_data->setEnabled(false);
  25.         ui->pushButton_auto_send_data->setEnabled(false);
  26.         //自动发送定时器要关闭
  27.         ui->pushButton_auto_send_data->setCheckable(false);
  28.         ui->pushButton_auto_send_data->setText(trUtf8("START_AUTO_SEND"));
  29.         auto_send_timer->stop();
  30.         ui->label_connect_status->setText("disconnect success");
  31.     }
  32.     else if(state == TCP_CONNECT_ERROR)   //TCP客户端连接出错
  33.     {
  34.         ui->pushButton_connect->setCheckable(false);
  35.         ui->pushButton_connect->setText(trUtf8("CONNECT"));
  36.         ui->lineEdit_server_ip->setEnabled(true);
  37.         ui->lineEdit_server_port->setEnabled(true);
  38.         ui->pushButton_send_data->setEnabled(false);
  39.         ui->pushButton_auto_send_data->setEnabled(false);
  40.         ui->label_connect_status->setText("connect error");
  41.     }
  42. }

每点击一次手动发送按钮 [tcp_send_data],客户端将会发送一包数据到服务端。点击自动发送按钮 [START_AUTO_SEND] 按钮,客户端将启动定时器,并以1秒的间隔向服务端发送数据。
手动发送函数和自动发送函数,如下图所示:

  1. //手动发送数据按钮
  2. void Widget::on_pushButton_send_data_clicked()
  3. {
  4.     tcp_client->tcp_client_send_data(QString("helloworld"));
  5.     display_tcp_client_tx_data(QString("helloworld"));
  6. }
  7. //自动发送数据按钮
  8. void Widget::on_pushButton_auto_send_data_clicked()
  9. {
  10.     if(ui->pushButton_auto_send_data->isCheckable())
  11.     {
  12.         ui->pushButton_auto_send_data->setCheckable(false);
  13.         ui->pushButton_auto_send_data->setText(trUtf8("START_AUTO_SEND"));
  14.         auto_send_timer->stop();   //关停定时器
  15.     }
  16.     else
  17.     {
  18.         ui->pushButton_auto_send_data->setCheckable(true);
  19.         ui->pushButton_auto_send_data->setText(trUtf8("STOP_AUTO_SEND"));
  20.         auto_send_timer->start(1000);   //启动定时器,以1秒的频率自动发送数据
  21.     }
  22. }

客户端的发送数据函数tcp_client_send_data(QString),是直接调用了QIODevice::write()进行发送的。我们用面向对象多态的思维,编写两个发送函数,可以分别以QByteArry或QString类型的参数进行数据发送,如下图所示:

  1. //客户端向服务器发送数据,以QByteArray发送
  2. void TCP_Client::tcp_client_send_data(QByteArray arr_data)
  3. {
  4.     if(m_tcp_socket)
  5.         m_tcp_socket->write(arr_data);
  6. }
  7. //客户端向服务器发送数据,以QString格式发送
  8. void TCP_Client::tcp_client_send_data(QString str_data)
  9. {
  10.     if(m_tcp_socket)
  11.         m_tcp_socket->write(str_data.toLatin1().data(),str_data.length());
  12. }

客户端的接收数据函数,是通过信号槽机制来实现的,当Widget类对象接收到TCP-Client类发送的信号signal_tcp_client_recv_data(QByteArray),就调用槽函数处理接收到的数据,如下图所示:

  1. //处理TCP客户端接收到的数据
  2. void Widget::slot_tcp_client_recv_data(QByteArray data_recv)
  3. {
  4.     QString str_display;
  5.     str_display.prepend(data_recv);
  6.     display_tcp_client_rx_data(str_display);   //显示TCP客户端接收到的数据
  7. }

在TCP-Client类对象中,客户端是通过槽函数slot_tcp_client_recv_data()进行网络数据接收的,这个槽函数绑定了QIODevice::readyRead()信号,
一旦底层的IO有网络数据接收,则会调用该槽函数处理,然后这个槽函数会把数据从底层驱动的缓冲区中把数据全部读出,再发送signal_tcp_client_recv_data(QByteArray),通知Widget类进行处理。
在这里,可能大家会有一个疑惑,为什么不在这个槽函数直接处理数据呢?那是因为考虑到了软件的封装性。TCP-Client类只负责接收数据,然后再把接收到的数据通过信号槽传递出去,至于数据怎样处理,则应该在其他的数据handle类里面进行。
比如,这个客户端只进行数据显示,则可以在Widget类里面处理数据,只需要把Widget类里面的数据处理槽函数与TCP-Client类里面的信号绑定就行了。TCP-Client类里面的槽函数slot_tcp_client_recv_data()如下图所示:

  1. //客户端从服务器接收数据
  2. void TCP_Client::slot_tcp_client_recv_data()
  3. {
  4.     //qDebug() << ">>> void TCP_Client::slot_tcp_client_recv_data() >>>";
  5.     QByteArray arr_data_recv;
  6.     arr_data_recv.resize(m_tcp_socket->bytesAvailable());
  7.     arr_data_recv = m_tcp_socket->readAll();
  8.     emit signal_tcp_client_recv_data(arr_data_recv);
  9. }

至此,整个TCP-Client连接服务器以及数据收发过程已经描述完成。这个工程只是简单地描述了TCP-Client建立通信和数据收发的简单过程。在真正的TCP-Client网络应用程序中,还需要处理很多突发的网络情况。
如:连接过程中的错误处理,心跳包机制,服务端强行断开连接后客户端的处理,数据粘包与断包,数据接收队列,等等。开发者应在工程开发中不断积累经验,才能开发出稳定可靠的网络应用程序。

题外知识:
      很多初学者可能会对服务器和客户端没有什么概念,不知道怎样理解服务端/客户端这两个角色,个人觉得,要记住关键的一点:客户端是请求服务的,服务器是提供服务的。
举一个简单而通俗的例子:汽车去加油站加油。
把汽车比喻为TCP-Client客户端,把加油站的加油机比喻为TCP-Server服务器。当汽车(TCP-Client)需要向加油机(TCP-Server)请求加油服务时,
则需要先知道加油站在哪(服务器的IP地址),在哪台加油机(服务器端口)上加油,当指定好加油站和具体的加油机后,就可以向加油机请求连接(向汽车插上加油枪)了。
But,总会有特殊情况的时候,比如,当该加油机没有油而导致加不了油(连接不上)了,那么,加油机(服务器)就会通知请求加油的汽车(客户端)进行处理,(这里就涉及到连接不上的情况了)。
当汽车加完油之后,就像是服务端已经提供完服务,那么,汽车(客户端)就可以主动端开与加油机(服务端)的连接了。