文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

Qt实现简单TCP服务器

2024-04-02 19:55

关注

本文实例为大家分享了Qt学习记录之简单的TCP服务器,供大家参考,具体内容如下

简单的多连接TCP服务器​

本节我们使用Qt来编写一个简单的多连接TCP服务器程序,涉及到的功能有监听本地IP、打印上线客户端的IP端口号,接收客户端发来的文字信息并打印其IP端口号、单独或全部地向客户端发送文字信息、显示下线客户端的IP端口号,并具有踢人的功能。

​ 该程序使用正点原子的网络助手来验证功能。Qt基于5.9.9版本。

1、创建工程以及配置工作

创建工程的过程就不再介绍了,这里我选择的是 QWidget ,因为比较简单。

然后我们在 .pro 文件中添加网络的模块,否则待会添加头文件时会提示没有这个头文件。

其他东西不需要看,只要在这后面加上 network 即可。

再然后,在 widget.h 头文件中包含两个头文件:

#include <QTcpServer>
#include <QTcpSocket>

使用Qt搭建TCP服务器需要两个套接字:

这里也插一句,如果使用Qt搭建一个TCP客户端,只需要QTcpSocket套接字。

2、ui界面的设计

1、comboBox。下拉列表框,用来选择监听的IP地址,因为能够被监听的地址肯定是本机拥有的地址,可以是有线、无线网卡的IP地址(局域网),也可以是宽带分配到的IP地址(广域网)。下一节我们再来学习如何查看本机支持的IP。

2、portEdit。旁边的单行文本框是用来输入要监听的端口号。有时候IP的某个端口号已经被某个程序占用,此时再去监听就会监听失败。

3、openbtn。点击开启按钮,监听选定的IP地址及端口号。监听失败会在Qt Creator的控制台打印失败的信息。监听成功,同样也会打印信息,并且IP下拉框、端口号文本框还有本按钮控件都会变成不可选中状态。

4、comboBox_2。选择某个已连接的客户端(如果有的话),或者选择全部。这个可以查看连接上服务器的客户端IP及端口号,单独或全部地向客户端发送信息,或者强制踢下线。

5、kickbtn。点击按钮,强制使选中的客户端下线。

6、closebtn。关闭服务器。停止监听之前选中的IP及端口号,并断开全部的TCP连接。

7、recvEdit。接收文本框。当客户端发来信息,会打印在上面。

8、clearbtn。清除接收文本框内的全部内容。

9、pushButton。在控制台上打印全部连接的客户端IP及端口号。

10、sendEdit。发送文本框。

11、sendbtn。点击发送按钮,会向某个、全部客户端发送文本框内的信息

大体的设计就是这样,目前只满足了基本的功能,后续可以增添更多的功能,并对界面进行美化

3、本地IP的获取

从这一节开始,我们将正式进行代码的编写

首先,打开命令行,输入 ipconfig

这四个IP地址就我我电脑上可以监听的IP地址,我的程序就是以此来搭建TCP服务器。其他的IP地址,目前是监听不了的。

Qt获取本机IP

首先在 widge.cpp 文件中包含一个头文件

#include <QtNetwork>

然后在Widget类的构造函数中添加如下代码:


QString localHostName = QHostInfo::localHostName();
QHostInfo info = QHostInfo::fromName(localHostName);


foreach(QHostAddress ipAddress, info.addresses())
    {
        if(ipAddress.protocol() == QAbstractSocket::IPv4Protocol)
        {
            qDebug() << ipAddress.toString();
            ui->comboBox->addItem(ipAddress.toString());
        }
    }

这里解释几点:

点击运行,控制台就会打印信息,并且下拉列表也会显示信息:

4、开启服务器

首先要有一个 QTcpServer 套接字对象,可以直接在 Widget 类中声明这么一个对象;也可以在类中声明一个对象指针,然后在构造函数中 new 一个对象,让这个指针指向它。这里我选择第二种方法。


QTcpServer *server;

server = new QTcpServer(this);

然后为开启按钮添加槽函数,这里我图方便直接右击控件并点击转到槽。

void Widget::on_openbtn_clicked()
{
    
    if(server->listen(QHostAddress(ui->comboBox->currentText()), ui->portEdit->text().toInt()) == false)
    {
        
        qDebug()<<"listen false";
    }
    else
    {
        
        ui->openbtn->setDisabled(true);
        ui->portEdit->setDisabled(true);
        ui->comboBox->setDisabled(true);
        qDebug()<<"监听成功";
        
        ui->closebtn->setEnabled(true);
    }
}

解释:

对于下拉列表控件(comboBox),使用currentText()成员函数获取选中元素的信息,返回QString型。
对于单行文本控件(portEdit),使用text()成员函数返回文本,并用toInt()转化为int型
对于TCP服务器套接字(server),使用listen(const QHostAddress &address = QHostAddress::Any, quint16 port = 0)成员函数绑定IP及端口。
对于大部分控件,使用setDisabled(true)成员函数可使控件变为不可点击状态

最后,我们就可以开启服务器了。如果开启失败,往往是因为选择的端口号被占用,这里推荐使用8081这个端口号(8080经常会被占用)。开启成功后,我们就可以使用正点原子的网络助手去尝试连接它。

可以看到,右边的网络助手已经处于连接成功的状态,因此可以证明TCP服务器已经被成功开启。其他的东西暂时不用看,后面会一一实现。

5、服务器等待客户端连接

在构造函数中添加如下代码:

connect(server, &QTcpServer::newConnection, this, [=](){
  
        QTcpSocket *socket = server->nextPendingConnection();
        
        
        sockList.append(socket);
        
        
        QString info = socket->peerAddress().toString() \
                + ':' + QString::number(socket->peerPort());
        
        
        ui->recvEdit->append("已连接:"+info);
        
        
        ui->comboBox_2->addItem(info);

        
        connect(socket, &QTcpSocket::readyRead, this, &Widget::on_recv);
        
        connect(socket, &QTcpSocket::disconnected, this, &Widget::on_disconnect);
    });

解释:


QList<QTcpSocket *>sockList;

最后,我们就可以检验一下多连接时的状态了

6、实现接收数据

如果用于通信的socket(QTcpSocket)接收到数据,就会触发 QTcpSocket::readyRead 信号,我这里写了一个槽函数,专门用来接收数据:

void Widget::on_recv()
{
    
    QTcpSocket *sock = qobject_cast<QTcpSocket *>(sender());

    
    QString info = "来自" + sock->peerAddress().toString() \
            + ':' + QString::number(sock->peerPort());
    
    ui->recvEdit->append(info);
    
    ui->recvEdit->append(sock->readAll());
}

解释:

现在也可以解释上一节中的connect(socket, &QTcpSocket::readyRead, this, &Widget::on_recv); 了。可能有人会疑惑,这里的socket是局部变量,调用完会被释放掉,那么这个连接是不是传了个野指针进去?其实并不是的,socket是个指针,指向server->nextPendingConnection() 返回的QTcpSocket对象,真正的对象应该是存在内部堆内存中的,这里只是用了一个局部指针变量去接收它。而connect函数,传进去的是地址,地址自然就是内部QTcpSocket对象的地址了。

最后,演示一下:

注意:Qt使用utf-8编码格式,而原子的软件默认是GBk格式,所以发送中文之前,先把原子的软件全部改成utf-8格式。

实现清除数据

这个非常简单,就没有必要多说了:

void Widget::on_clearbtn_clicked()
{
    
    ui->recvEdit->clear();
}

7、实现客户端的选择

对客户端的选择无非就两种情况:一种情况是选择全部连接上的客户端,一种是选择单独的某个客户端。

我为Widget类添加了一个属性来解决这两类问题:

QTcpSocket *currSock;

如果第二个下拉列表框选择了 All ,那么这个指针就会指向 NULL 。如果选择的是某个特定的IP及端口,那么这个指针就会指向对应的那个QTcpSocket对象了。

那么对象从哪里找?从前几节说过的容器中去找。这个容器用于保存当前连接上的客户端的socket。每当有客户端连上,这个容器就会将它的socket保存进来。每当有客户端下线,这个容器同样会把下线客户端的socket删掉。

QList<QTcpSocket *>sockList;

下拉列表框的信号和槽

这里我同样是以右击控件再点转到槽的方式来自动生成槽函数:

这里的信号不要选错了,当这个控件选择的选项发送变化时,会触发这个信号。下面是槽函数:

void Widget::on_comboBox_2_currentIndexChanged(const QString &arg1)
{
    
    if(arg1 == "All")
    {
        currSock = NULL;
        return;
    }

    
    QStringList info = arg1.split(':');
    QString ip = info[0];
    int port = info[1].toInt();

    
    foreach(QTcpSocket *sock, sockList)
    {
        if(sock->peerAddress().toString() == ip && sock->peerPort() == port)
        {
            
            currSock = sock;
            break;
        }
    }
}

解释:

这个功能暂时不太好演示,大家可以自己弄个按钮控件,点击按钮则打印 currSock 指向 socket 的信息,来检验一下。

8、实现发送功能

到了这一步,程序就开始越写越简单了

为发送按钮添加一个槽函数:

void Widget::on_sendbtn_clicked()
{
    
    if(currSock == NULL)
    {
        foreach(QTcpSocket *sock, sockList)
        {
            sock->write(ui->sendEdit->toPlainText().toUtf8());
        }
    }
    else
    {
        
        currSock->write(ui->sendEdit->toPlainText().toUtf8());
    }
}

解释:

当 currSock 指针指向空的时候,可能有以下几种状态:

程序刚刚初始化完成,此时还没有监听
程序已经开始监听,且有一个或多个客户端上线
程序开始监听,还没有客户端连接,或者之前连接的客户端全部下线

对于1、3两种情况,sockList 容器中是空的,因此即使遍历也遍历不到任何东西,自然也就不会给不存在的socket发送信息。对于第二种情况,也就不用多解释了。当然,这样的程序可能是存在问题的。可以多做一些判断,并且弹出警告提示框,提醒用户做了错误的操作。

最后,演示一下。服务器先给1号客户端发送你好1,接着分别给2、3号客户端发送你好2、3,最后给所有的客户端发送你好123:

9、实现客户端下线

之前我们在构造函数中添加了这一句代码:

connect(socket, &QTcpSocket::disconnected, this, &Widget::on_disconnect);

把套接字的断开连接信号连接到了断开连接槽函数,槽函数内容如下:

void Widget::on_disconnect()
{
    
    QTcpSocket *sock = qobject_cast<QTcpSocket *>(sender());
    
    sockList.removeOne(sock);
    
    
    QString info = sock->peerAddress().toString() \
            + ':' + QString::number(sock->peerPort());
    
    ui->recvEdit->append(info+"已断开");
    
    
    int index = ui->comboBox_2->findText(info);
    
    ui->comboBox_2->removeItem(index);

    
    disconnect(sock, &QTcpSocket::readyRead, this, &Widget::on_recv);
    
    disconnect(sock, &QTcpSocket::disconnected, this, &Widget::on_disconnect);
}

解释:

最后,演示一下客户端主动下线会有什么现象:

10、实现踢人功能

刚才是客户端自己下线,现在是服务器主动踢人下线。但无论是哪一种,在断开连接的时候都会触发QTcpSocket::disconnected 信号

为踢人按钮添加槽函数:

void Widget::on_kickbtn_clicked()
{
    
    if(currSock == NULL)
    {
        foreach(QTcpSocket *sock, sockList)
        {
            sock->close();
        }
    }
    else
    {
        
        currSock->close();
    }
}

到了这里,我感觉已经没有什么好说的了,只需知道调用close() 函数可以断开连接。

这里也不做演示了。

11、实现关闭服务器

关闭服务器主要需要做两件事:

1、停止监听IP及端口。
2、关闭所有与客户端之间的连接。因为停止监听是不够的,之前的连接还是存在的,甚至还能继续收发数据。

void Widget::on_closebtn_clicked()
{
    currSock = NULL;
    
    server->close();
    
    ui->closebtn->setDisabled(true);   
    ui->openbtn->setEnabled(true);
    ui->portEdit->setEnabled(true);
    ui->comboBox->setEnabled(true);
    
    foreach(QTcpSocket *sock, sockList)
    {
        sock->close();
    }
}

演示:

在断开连接后,下拉框会删除掉之前全部的连接信息,被迫选择到“All”,然后 currSock 指针也会自然而然地指向NULL。当然不放心的也可以手动弄一下。

好了,至此全部的功能已经讲解完毕了,多的两个按钮是我自己调试用的。如果绑定的是一个公网IP,时间长了也许会有一些奇怪的信息显示在接收文本框,不用害怕,可能是哪个迷路的孩子找错家了。

总结

1、这个程序总体而言是比较简单的,实现的功能简单、设计的想法也很简单,连注释加空行也不过两百行代码,但我却花了这么长的篇幅去介绍它,可能有些大佬看着会嫌啰嗦,但其实我写这样的一篇文档也是很花费时间的,我自己也觉得很累。我主要是想锻炼一下自己的文档能力,也希望能帮助其他人度过入门的难关,往后的文章可能不会写这么详细了。

2、Qt Creator 好像有什么奇怪的问题。如果用 的方式写注释,编译是不通过的。解决办法是最后一个字用英文字符。还有控件自动生成的槽函数一直有黄色警告,虽然不影响使用,但感觉很变扭,希望有知道原因的大佬能不啬赐教。

3、这个程序只能用于非常简单且频率很低的数据收发。信号和槽的机制虽然很容易使用,但并不好用在并发的场合下。且 foreach 遍历是比较耗费时间的,假如有上万个客户端都连接到这台服务器,并且服务器要给所有客户端都发送一条较长的信息,这个时候就不太好了。所以,后续要升级,肯定要使用线程的方式去解决这些问题。

完整代码

widget.h

#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>
#include <QTcpServer>
#include <QTcpSocket>

QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE

class Widget : public QWidget
{
    Q_OBJECT

public:
    Widget(QWidget *parent = nullptr);
    ~Widget();


private slots:
    void on_openbtn_clicked();

    void on_clearbtn_clicked();

    void on_recv();

    void on_disconnect();

    void on_sendbtn_clicked();

    void on_pushButton_clicked();

    void on_closebtn_clicked();

    void on_comboBox_2_currentIndexChanged(const QString &arg1);

    void on_kickbtn_clicked();

private:
    Ui::Widget *ui;
    QTcpServer *server;
    QList<QTcpSocket *>sockList;
    QTcpSocket *currSock;
};
#endif // WIDGET_H

widget.cpp

#include "widget.h"
#include "ui_widget.h"
#include <QtNetwork>
#include <QDebug>

Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
{
    ui->setupUi(this);

    
    currSock = NULL;                    //当前的socket对象指针指向空
    ui->recvEdit->setReadOnly(true);    //将接受文本框设为只读模式
    ui->closebtn->setEnabled(true);     //将关闭按钮设为不可点击,因为此时服务器还未被开启
    ui->portEdit->setText("8081");      //默认端口号为8081

    
    server = new QTcpServer(this);

    
    QString localHostName = QHostInfo::localHostName();
    QHostInfo info = QHostInfo::fromName(localHostName);

    
    foreach(QHostAddress ipAddress, info.addresses())
    {
        if(ipAddress.protocol() == QAbstractSocket::IPv4Protocol)
        {
            ui->comboBox->addItem(ipAddress.toString());
        }
    }

    
    connect(server, &QTcpServer::newConnection, this, [=](){
        
        QTcpSocket *socket = server->nextPendingConnection();
        
        sockList.append(socket);
        
        QString info = socket->peerAddress().toString() \
                + ':' + QString::number(socket->peerPort());
        
        ui->recvEdit->append("已连接:"+info);
        
        ui->comboBox_2->addItem(info);

        
        connect(socket, &QTcpSocket::readyRead, this, &Widget::on_recv);
        
        connect(socket, &QTcpSocket::disconnected, this, &Widget::on_disconnect);
    });
}

Widget::~Widget()
{
    delete ui;
}

void Widget::on_recv()
{
    
    QTcpSocket *sock = qobject_cast<QTcpSocket *>(sender());

    
    QString info = "来自" + sock->peerAddress().toString() \
            + ':' + QString::number(sock->peerPort());
    
    ui->recvEdit->append(info);
    
    ui->recvEdit->append(sock->readAll());
}

void Widget::on_disconnect()
{
    
    QTcpSocket *sock = qobject_cast<QTcpSocket *>(sender());
    
    sockList.removeOne(sock);
    
    QString info = sock->peerAddress().toString() \
            + ':' + QString::number(sock->peerPort());
    
    ui->recvEdit->append(info+"已断开");
    
    int index = ui->comboBox_2->findText(info);
    
    ui->comboBox_2->removeItem(index);

    
    disconnect(sock, &QTcpSocket::readyRead, this, &Widget::on_recv);
    
    disconnect(sock, &QTcpSocket::disconnected, this, &Widget::on_disconnect);
}

void Widget::on_openbtn_clicked()
{
    
    if(server->listen(QHostAddress(ui->comboBox->currentText()), ui->portEdit->text().toInt()) == false)
    {
        
        qDebug()<<"listen false";
    }
    else
    {
        
        ui->openbtn->setDisabled(true);
        ui->portEdit->setDisabled(true);
        ui->comboBox->setDisabled(true);
        qDebug()<<"监听成功";
        
        ui->closebtn->setEnabled(true);
    }
}

void Widget::on_clearbtn_clicked()
{
    
    ui->recvEdit->clear();
}

void Widget::on_sendbtn_clicked()
{
    
    if(currSock == NULL)
    {
        foreach(QTcpSocket *sock, sockList)
        {
            sock->write(ui->sendEdit->toPlainText().toUtf8());
        }
    }
    else
    {
        
        currSock->write(ui->sendEdit->toPlainText().toUtf8());
    }
}

void Widget::on_pushButton_clicked()
{
    foreach(QTcpSocket *sock, sockList)
    {
        qDebug()<<sock->peerAddress()<<':'<<sock->peerPort();
    }
}

void Widget::on_closebtn_clicked()
{
    currSock = NULL;
    
    server->close();
    
    ui->closebtn->setDisabled(true);   
    ui->openbtn->setEnabled(true);
    ui->portEdit->setEnabled(true);
    ui->comboBox->setEnabled(true);
    
    foreach(QTcpSocket *sock, sockList)
    {
        sock->close();
    }
}

void Widget::on_comboBox_2_currentIndexChanged(const QString &arg1)
{
    
    if(arg1 == "All")
    {
        currSock = NULL;
        return;
    }

    
    QStringList info = arg1.split(':');
    QString ip = info[0];
    int port = info[1].toInt();

    
    foreach(QTcpSocket *sock, sockList)
    {
        if(sock->peerAddress().toString() == ip && sock->peerPort() == port)
        {
            
            currSock = sock;
            break;
        }
    }
}

void Widget::on_kickbtn_clicked()
{
    
    if(currSock == NULL)
    {
        foreach(QTcpSocket *sock, sockList)
        {
            sock->close();
        }
    }
    else
    {
        
        currSock->close();
    }
}

main.cpp

维持原样不需改动

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持编程网。

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     807人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     351人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     314人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     433人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     221人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-服务器
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯