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

基于TCP/IP的网络通信应用程序(TCP-Server)

上一篇文章讲述了在i.MX6UL开发板中,以客户端的角色,使用TCP/IP协议进行网络通信。

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

本章节,将以服务端的角色进行讲解,如何开发一个TCP服务端(TCP-Server)。

目标:使用QT提供的TCP/IP网络通信类,实现一个简单的TCP服务端(TCP-Server)

功能:

(1)开发板界面显示开发板服务端的网络IP地址。

(2)可手动输入需要监听的网络端口。

(3)提供按钮,可手动开启/停止服务端监听。

(4)界面显示TCP客户端的收发数据,并提供清屏按钮。

(5)提供服务端手动发送按钮和自动发送按钮。

开发板运行TCP服务端(TCP-Server)后,界面如下图所示:

服务端界面描述:

1、服务端程序启动后,先获取本机IP地址作为服务器的IP地址。程序默认监听4418端口,用户可自定义修改需要监听的端口

2、点击[LISTEN]按钮,开始服务端监听,等待客户端连接。

3、客户端连接成功后,会在数据显示窗口提示客户端上线,并在客户端列表显示每个客户端的IP地址和连接端口。

4、用户点击[START_AUTO_SEND]按钮,服务端以1秒的频率,自动对所有客户端发送固定数据。再次点击该按钮,停止自动发送数据。

5、用户每点击一次[tcp_send_data]按钮,服务端对指定的客户端发送一帧固定数据。

6、用户点击[CLEAR]按钮,清空数据显示窗口的内容。

对于TCP/IP的服务端(TCP-Server)角色,在进行数据通信之前,一般会经历以下过程:

(1)调用socket套接字函数,创建套接字的文件描述符(这个套接字是用来监听客户端连接请求的)。

(2)调用bind()绑定函数,将创建成功的套接字与需要监听的IP地址和Port绑定。

(3)绑定成功后,就可以调用listen()函数进行监听,等待客户端的连接请求。(服务端需要成功调用listen()函数,客户端才可以发起连接请求,否则,客户端的连接会出错)

(4)调用listen()函数成功后,若此时有客户端申请建立连接,服务端则调用accept()函数,接收客户端的连接,并自动产生用于网络I/O通信的套接字,作为accept()函数的返回值。(accept()函数自动产生的套接字是用来进行网络I/O数据收发的,与socket()套接字不同)

(5)当客户端连接成功后,服务端就可以基于accept()返回的套接字,使用系统调用的读写函数read()/write()进行数据收发了。

使用嵌入式QT进行TCP/IP的网络通信应用程序开发,对于TCP服务端,QT的network类库提供的QTcpServer类,这个类继提供了一系列的服务端网络操作接口函数,如:监听函数listen(),阻塞等待客户端连接waitForNewConnection(),虚函数incomingConnection()用来处理客户端的连接请求。更多接口函数,具体可以参阅 QtNetwork/qtcpserver.h 文件的内容。在服务端应用程序里面,我们通常建立一个继承于QTcpSocket的类,来描述每一个连接成功的客户端,每一个客户端的具体信息,可以通过这个类来获取。关于这个类的使用方法,请参考上一章节的内容。

 

以下是TCP服务端(TCP-Server)应用程序的开发过程。

1、先用Qt Creator构建一个工程,命名为:008_tcp_server,关于如何构建工程,请参考“第一个嵌入式QT应用程序”的具体内容。

2、创建工程后,修改008_tcp_server.pro里面的内容,添加QT里面的网络通信模块network,使工程支持QT网络类的调用,如下图所示。

3、双击打开“widget.ui”文件,构建界面,构建后的界面如下图所示:

服务端界面描述如上面内容所示,这里不作重复。

4、对于一个合格的TCP服务端,由于要给多个TCP客户端提供服务,因此,难以避免处理多个客户端连接,以及对指定的客户端进行数据收发。因此,TCP服务端需要一个TcpClient类来描述每个客户端的信息,这个TcpClient类继承于QTcpSocket,注意,这个TcpClient类与上一章节的TcpClient类有些差异。它是用来描述客户端,而非创建一个客户端,如下图所示:

//定义一个TcpClient类,这个类用于TcpServer的基础通信
class TcpClient : public QTcpSocket
{
    Q_OBJECT
public:
    explicit TcpClient(QObject *parent = 0);

private:
    QString client_ip;     //连接上服务器的客户端IP
    int client_port;       //连接上服务器的客户端端口

private slots:
    void slot_read_data();     //读取每个客户端发送上来的数据

signals:
    //数据发送信号,用于通知ui线程,让ui显示哪个客户端发送了哪些数据
    void signals_send_data(const QString &ip, int port, const QString &data);
    //数据接收信号,用于通知ui线程,让ui显示服务端接收的数据来自哪个客户端
    void signals_receive_data(const QString &ip, int port, const QString &data);

public slots:
    void slot_set_client_ip(const QString &ip);   //用于设置client_ip这个变量的值
    void slot_set_client_port(int port);   //用于设置client_port这个变量的值
    void slot_send_data(const QString &data);   //用于服务端向指定的客户端发送数据
};

5、创建好TcpClient类之后,对于服务端,我们还需要创建一个TcpServer类,用来描述整个服务端的操作,这个TcpServer类包含了服务端的数据收发函数,连接成功的客户端列表QList<TcpClient *> tcp_clients,启动和停止服务端的监听,重载void incomingConnection(int handle);虚函数,等等。TcpServer类的具体定义,如下图所示:

//定义一个TcpServer类
class TcpServer : public QTcpServer
{
    Q_OBJECT
public:
    explicit TcpServer(QObject *parent = 0,int port = 0);

private:
    QList<TcpClient *> tcp_clients;    //客户端列表,用于保存已经连接成功的客户端

    int listen_port;    //服务器监听的端口
protected:
    //重载此函数,这个函数主要用来通知TcpServer有新的连接准备建立
    void incomingConnection(int handle);

private slots:
    void slot_disconnected();   //关闭服务器,断开与客户端的所有连接

signals:
    //数据发送信号,用于通知ui线程,让ui显示哪个客户端发送了哪些数据
    void signal_send_data(const QString &ip, int port, const QString &data);
    //数据接收信号,用于通知ui线程,让ui显示服务端接收的数据来自哪个客户端
    void signal_receive_data(const QString &ip, int port, const QString &data);
    //连接成功信号,用于通知连接服务器成功
    void signal_client_connected(const QString &ip, int port);
    //断开成功信号,用于通知断开服务器成功
    void signal_client_disconnected(const QString &ip, int port);

public slots:
    bool slot_server_start();//启动服务器监听
    void slot_server_stop();//停止服务器监听
    void slot_send_data(const QString &ip, int port, const QString &data);//指定连接的客户端发送数据
    void slot_send_data(const QString &data);//对所有连接的客户端发送数据
    QString get_local_ipaddr(void); //获取本机的IP地址
};

6、以上的两个类创建完成后,我们创建一个tcp_server.cpp文件,这个文件主要是用来编写Tcp-Client类和Tcp-Server类里面各个函数的具体实现,关于整个tcp_server.cpp文件的具体内容,请参阅源码,里面有详细的注释。以下列出关键的函数进行讲解

7、Tcp-Client类是用来描述每个连接服务端成功的客户端的,这个类的构造函数,如下图所示:

//TcpServer的基础通信类Tcp_Client的构造函数
TcpClient::TcpClient(QObject *parent) :  QTcpSocket(parent)
{
    client_ip = "127.0.0.1";
    client_port = 4418;

    //绑定通信出错的槽函数
    connect(this, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(deleteLater()));
    //绑定通信断开的槽函数
    connect(this, SIGNAL(disconnected()), this, SLOT(deleteLater()));
    //绑定数据读取的槽函数
    connect(this, SIGNAL(readyRead()), this, SLOT(slot_read_data()));
}

构造函数主要绑定了错误相关的槽函数,通信断开槽函数,以及数据接收的槽函数。当服务端底层I/O接收到客户端发送上来的数据的时候,就会调用slot_read_data()进行处理。

8、对于每个连接服务端成功的客户端,服务端可以使用以下的函数,对其进行数据的接收和发送,函数实现如下图所示:

//用于服务端向指定的客户端发送数据
void TcpClient::slot_send_data(const QString &data)
{
    QByteArray buffer;

    buffer = data.toLatin1();

    this->write(buffer); //发送数据

    //通过此信号,告诉ui是发送给哪个客户端,以及数据的具体内容
    emit signals_send_data(client_ip, client_port, data);
}

//读取客户端数据的槽函数
void TcpClient::slot_read_data()
{
    QByteArray data = this->readAll();   //读取所有接收到的数据

    if (data.length() <= 0)return;   //如果数据长度为0,返回

    QString buffer;
    buffer = QString(data);   //把接收到的数据转化为QString类型

    //通过信号,把接收到的数据,以及来自哪个客户端的IP和端口,都发送出去
    emit signals_receive_data(client_ip, client_port, buffer);
}

对于数据发送函数void TcpClient::slot_send_data(const QString &data),主要是把需要发送的数据转为QByteArray,然后调用QIODevice::write()函数进行发送,发送完成后,调用信号函数signals_send_data(client_ip, client_port, data); 通知widget类,是向哪个客户端发送了哪些数据。

对于数据接收函数void TcpClient::slot_read_data(),主要是跟底层I/O的readyRead()信号进行绑定,当接收到服务端的数据时,会调用QIODevice::readAll()函数,把缓冲区的数据全部读出,然后通过signals_receive_data(client_ip, client_port, buffer);信号,把客户端的IP地址,端口,数据内容都发送出去。

9、Tcp-Server类主要用来描述Tcp服务端的,里面实现了服务端的开始监听与停止监听的函数,如下图所示:

//启动服务器监听
bool TcpServer::slot_server_start()
{
#if (QT_VERSION > QT_VERSION_CHECK(5,0,0))
    bool ok = listen(QHostAddress::AnyIPv4, listen_port);
#else
    bool ok = listen(QHostAddress::Any, listen_port);
#endif

    return ok;
}

//停止服务器监听
void TcpServer::slot_server_stop()
{
    foreach (TcpClient *client, tcp_clients) {
        client->disconnectFromHost();
    }

    this->close();
}

主要调用listen函数,启动服务器监听所有地址的客户端连接。对于停止服务器监听,则先断开所有的客户端连接后,调用close()函数关闭服务端监听。

10、服务器的监听函数成功启动后,对于客户端发起的连接请求,服务端会调用一个虚函数void TcpServer::incomingConnection(int handle)进行处理,这个虚函数的具体实现,如下图所示:

//重载此函数,这个函数主要用来通知TcpServer有新的连接准备建立
void TcpServer::incomingConnection(int handle)
{
    //构建一个TcpClient对象,用来描述连接上的Tcp客户端
    TcpClient *client = new TcpClient(this);

    client->setSocketDescriptor(handle);  //保存客户端的设备描述符

    //绑定客户端断开连接的槽函数
    connect(client, SIGNAL(disconnected()), this, SLOT(slot_disconnected()));
    //绑定客户端发送数据的信号,与服务端的发送数据信号绑定
    connect(client, SIGNAL(signals_send_data(QString, int, QString)), this, SIGNAL(signal_send_data(QString, int, QString)));
    //绑定客户端接收数据的信号,与服务端的接收数据信号绑定
    connect(client, SIGNAL(signals_receive_data(QString, int, QString)), this, SIGNAL(signal_receive_data(QString, int, QString)));

    QString ip = client->peerAddress().toString();//获取连接成功的客户端的IP地址
    int port = client->peerPort();  //获取连接成功的客户端的端口
    client->slot_set_client_ip(ip);
    client->slot_set_client_port(port);

    emit signal_client_connected(ip, port);  //发送信号,通知ui客户端连接成功
    emit signal_send_data(ip, port, trUtf8("客户端上线"));

    tcp_clients.append(client);   //把这个客户端加入客户端列表
}

当有客户端的连接请求,这个函数就会被调用,在这个函数里面,保存客户端的设备描述符,这个描述符是服务端和客户端通信的基础。然后绑定各个槽函数,分别是断开连接槽函数,数据收发槽函数,然后把连接上来的客户端IP和端口保存下来。在发送信号通知ui界面,有客户端上线了。最后,把这个客户端对象保存在服务端的tcp_clients列表中。

11、对于客户端主动断开连接,调用以下函数进行处理:

//客户端断开槽函数
void TcpServer::slot_disconnected()
{
    //断开连接后从链表中移除
    TcpClient *client = (TcpClient *)sender();
    QString ip = client->peerAddress().toString();

    int port = client->peerPort();

    emit signal_client_disconnected(ip, port);
    emit signal_send_data(ip, port, trUtf8("客户端下线"));

    tcp_clients.removeOne(client); //把这个客户端从客户端列表中移除
}

这个槽函数是在void TcpServer::incomingConnection(int handle)里面,与TcpClient类的disconnected()进行绑定的,当客户端连接断开后,函数会发送信号给ui界面,告知客户端下线,然后把客户端从tcp_clients列表中移除。

12、服务端可以对指定的客户端发送数据,也可以对所有的客户端发送数据,具体的函数实现,如下图所示:

//对指定连接的客户端发送数据
void TcpServer::slot_send_data(const QString &ip, int port, const QString &data)
{
    foreach (TcpClient *client, tcp_clients)   //从客户端列表中取出客户端
    {
        //如果IP地址和端口匹配
        if (client->peerAddress().toString() == ip && client->peerPort() == port)
        {
            client->slot_send_data(data);   //发送数据
            break;
        }
    }
}

//对所有连接的客户端发送数据
void TcpServer::slot_send_data(const QString &data)
{
    foreach (TcpClient *client, tcp_clients)
    {
        client->slot_send_data(data);
    }
}

对指定的客户端发送数据,则先从客户端列表中获取匹配的IP地址和端口,获取成功后,调用TcpClient::slot_send_data()函数发送数据。

对所有的客户端发送数据,则不用匹配IP地址和端口,直接对列表中所有的客户端发送数据。

13、TcpClient类和TcpServer类的具体实现已经介绍完毕,在这里,我们就可以在界面的构造类Class Widget里面,基于TcpServer构建服务端应用。Widget类是ui的界面类,关于ui界面的操作,都在该类实现,Widget类的构造函数,如下图所示:

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

    tcp_server = new TcpServer(this,listen_port);   //定义一个tcp_server对象

    //绑定服务端数据发送的信号槽,用于显示服务端的数据发送
    connect(tcp_server, SIGNAL(signal_send_data(QString, int, QString)), this, SLOT(slot_send_data(QString, int, QString)));
    //绑定服务端数据接收的信号槽,用于显示服务端的数据接收
    connect(tcp_server, SIGNAL(signal_receive_data(QString, int, QString)), this, SLOT(slot_receive_data(QString, int, QString)));
    //绑定客户端连接成功的信号槽,用来显示客户端上线
    connect(tcp_server, SIGNAL(signal_client_connected(QString, int)), this, SLOT(slot_client_connected(QString, int)));
    //绑定客户端断开连接的信号槽,用来显示客户端下线
    connect(tcp_server, SIGNAL(signal_client_disconnected(QString, int)), this, SLOT(slot_client_disconnected(QString, int)));

    ui->pushButton_listen->setCheckable(false);
    ui->pushButton_send_data->setEnabled(false);   //发送数据按钮不可用
    ui->pushButton_auto_send_data->setEnabled(false); //自动发送数据按钮不可用

    auto_send_timer = new QTimer();     //构建一个TCP自动发送数据的定时器
    connect( auto_send_timer, SIGNAL(timeout()), this, SLOT(slot_auto_send_timer_handler()));  //关联定时器超时槽函数

    ui->label_local_ip_display->setText(tcp_server->get_local_ipaddr());   //显示本机的IP地址
}

在这个构造函数里面,主要定义了一个tcp_server对象,然后把这个tcp_server对象里面的各种信号与相关的槽函数进行绑定,对界面的按钮控件进行初始化,定义一个定时器,用来自动发送数据,最后,在界面显示本机的IP地址。

14、widget.cpp还实现了按钮控件和显示控件的相关功能,具体的实现函数,请参考widget.cpp的源码文件,部分源码如下图所示:

/*************************************************
>>>>>>>>>>>>  源码篇幅太长,此处适当省略  >>>>>>>>>>
*************************************************/

//服务端发送数据的槽函数,用于ui显示
void Widget::slot_send_data(const QString &ip, int port, const QString &data)
{
    QString str = QString("[%1:%2] %3").arg(ip).arg(port).arg(data);
    text_browser_append(0, str);
}

//服务端接收数据的槽函数,用于ui显示
void Widget::slot_receive_data(const QString &ip, int port, const QString &data)
{
    QString str = QString("[%1:%2] %3").arg(ip).arg(port).arg(data);
    text_browser_append(1, str);
}

//客户端上线的槽函数,加到客户端列表进行显示
void Widget::slot_client_connected(const QString &ip, int port)
{
    QString str = QString("%1:%2").arg(ip).arg(port);
    ui->listWidget_client_list->addItem(str);
}

//客户端下线的槽函数,从客户端列表进行移除
void Widget::slot_client_disconnected(const QString &ip, int port)
{
    int row = -1;
    QString str = QString("%1:%2").arg(ip).arg(port);

    for (int i = 0; i < ui->listWidget_client_list->count(); i++)
    {
        if (ui->listWidget_client_list->item(i)->text() == str)
        {
            row = i;
            break;
        }
    }

    ui->listWidget_client_list->takeItem(row);
}

/*************************************************
>>>>>>>>>>>>  源码篇幅太长,此处适当省略  >>>>>>>>>>
*************************************************/

15、至此,整个TCP服务端已经开发完毕,编译成功后,下载到开发板运行,实验现象如下图所示:

16、以上只是实现了一个简单的TCP服务端应用程序,并在单个线程里面处理了少量的TCP客户端连接,对于大规模的TCP服务端应用程序,还需要考虑高并发,数据低延迟,如何管理大规模的客户端数量,保证服务端7*24小时运行不宕机,等等,这些大规模的服务端程序,都是运行在性能较高的硬件上。在硬件性能满足的前提下,如果嵌入式设备需要管理比较多的客户端连接,建议采用线程池的管理方式,对客户端分批采用线程池管理,即可达到多客户端管理,更多的网络服务应用技术,需要开发者不断在工程应用中不断优化,不断积累经验,才能不断进步。

 

 

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

工程师进阶笔记

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值