【C++/Qt】C++/Qt 实现 TCP Server:支持启动监听、消息收发、日志保存

在 Qt 网络编程里,QTcpServerQTcpSocket 是最常用的一组类。单独讲 API 往往比较抽象,而如果把它们放到一个带界面的 TCP Server 小工具里,整个实现思路就会清晰很多。本文就结合一个完整的 Qt TCP 服务端模块,讲清楚一个 TCP Server 是如何完成启动监听、客户端连接、消息收发以及日志保存这些功能的。整个实现包含界面初始化、IP 与端口配置恢复、服务器启动/停止、服务端主动发送消息、日志记录与保存等内容。

1. 项目功能概览:先明确这个 TCP Server 要做什么

一个实用的 TCP Server,一般不只是"能监听端口"这么简单,而是至少要把下面几件事做完整:

  1. 选择本机 IP 和端口并启动监听
  2. 接收客户端连接,读取客户端发送的数据
  3. 服务端可以主动给客户端发送消息
  4. 在界面中显示运行日志
  5. 关闭程序前把日志保存到本地文件中

这几个功能在本文这个实现里都是完整具备的。比如程序启动时会自动枚举本机的 IPv4 地址加入下拉框,同时还会通过 QSettings 恢复上次使用的 IP 和端口;点击启动按钮后,服务器开始监听;有客户端接入后会建立 socket 连接,并在有数据到达时读取消息;界面上的日志列表会持续显示运行状态,退出前再统一写入日志文件。

从学习角度看,这样的实现比单纯写一个控制台 TCP 服务器更有价值,因为它把"网络通信"和"界面交互"结合到一起了,更接近实际项目中的写法。

2. 服务器初始化:先把界面、服务器对象和本机网络信息准备好

要让 TCP Server 正常工作,第一步不是直接调用 listen(),而是先把基础环境初始化好。这里最关键的有三件事:

  • 创建 QTcpServer 对象
  • 连接 newConnection 信号
  • 枚举本机 IPv4 地址并恢复上次配置

先看初始化服务器对象和连接信号的代码:

cpp 复制代码
tcpServer = new QTcpServer(this);   // 创建服务器对象,父对象负责自动回收

// 监听新连接信号
connect(tcpServer, &QTcpServer::newConnection,
        this, &FormTCPServer::TcpServerConnectedFunc);

ui->pushButton_TCPServerStop->setEnabled(false); // 初始时禁止"停止"按钮

这段代码很重要。QTcpServer 的职责是"监听",一旦有新的客户端连接进来,就会发出 newConnection 信号。这里把它连接到 TcpServerConnectedFunc(),就意味着后面所有"客户端接入"的处理,都会在这个槽函数里完成。

接着是获取本机可用 IP 地址:

cpp 复制代码
QList<QHostAddress> addrlist = QNetworkInterface::allAddresses();
foreach(const QHostAddress &address, addrlist)
{
    if(address.protocol() == QAbstractSocket::IPv4Protocol)
    {
        ui->comboBox_TCPServerIP->addItem(address.toString());
    }
}

这段逻辑的作用很直接:遍历本机所有地址,只把 IPv4 地址加入到下拉框中。这样用户在启动服务器时,就可以直接选择一个本机地址作为监听地址,而不需要手动输入。对于初学者来说,这种方式比写死 IP 更直观,也更不容易出错。

另外,这个实现里还用了 QSettings 来保存上次使用的 IP 和端口:

cpp 复制代码
QSettings settings;  // 使用默认组织/应用名保存配置
const QString lastIp = settings.value("TCPServer/lastIp").toString();
const int lastPort = settings.value("TCPServer/lastPort", 12345).toInt();

if(!lastIp.isEmpty()){
    int index = ui->comboBox_TCPServerIP->findText(lastIp);
    if(index >= 0)
        ui->comboBox_TCPServerIP->setCurrentIndex(index);
}

if(lastPort >= ui->spinBox_TCPServerPort->minimum() &&
   lastPort <= ui->spinBox_TCPServerPort->maximum())
{
    ui->spinBox_TCPServerPort->setValue(lastPort);
}

程序再次打开时,不需要重新选择 IP 和端口,直接恢复上次使用的配置,更像一个真正可用的小工具。

3. 启动与停止监听:核心就是 listen 和 close

TCP Server 的核心动作是"开始监听"。在 Qt 中,这一步主要依赖 QTcpServer::listen() 完成。本文这个实现里,还额外加入了 IP 和端口的合法性校验,以及重复启动判断,这样逻辑会更完整。

先看启动监听的关键代码:

cpp 复制代码
void FormTCPServer::on_pushButton_TCPServerStart_clicked()
{
    QHostAddress address(ui->comboBox_TCPServerIP->currentText());  // 读取选中IP
    int port = ui->spinBox_TCPServerPort->value();

    if(serverRunning){
        appendColorLog("[Server already running]", QColor("#666666"));
        return;
    }

    if(!CheckIpAddrIsValid(address.toString())){
        return;
    }

    if(port < 1 || port > 65535) {
        return;
    }

    if(tcpServer->listen(address, port)){
        appendColorLog("[Server started successfully]", QColor("#666666"));
        ui->pushButton_TCPServerStart->setEnabled(false);
        ui->pushButton_TCPServerStop->setEnabled(true);
        serverRunning = true;

        QSettings settings;
        settings.setValue("TCPServer/lastIp", address.toString());
        settings.setValue("TCPServer/lastPort", port);
    }
    else
    {
        appendColorLog(QString("[Failed to start server]:%1")
                       .arg(tcpServer->errorString()),
                       QColor("#CC0000"));
    }
}

这段代码可以拆成四步理解:

第一步,读取界面中当前选择的 IP 和端口。

第二步,判断服务器是不是已经在运行,防止重复启动。

第三步,校验 IP 和端口是否合法。

第四步,调用 listen(address, port) 开始监听。

如果监听成功,就更新按钮状态、记录日志,并且把当前配置保存到 QSettings 中。这样一来,"启动服务器"就不只是一个简单的函数调用,而是一套比较完整的交互流程。

对应地,停止监听的逻辑也不能只关服务器本身,还要把已有客户端连接一起清理掉:

cpp 复制代码
void FormTCPServer::on_pushButton_TCPServerStop_clicked()
{
    if(!serverRunning){
        appendColorLog("[Server not running]", QColor("#666666"));
        return;
    }

    tcpServer->close();     // 停止监听

    for(auto client : tcpServerSocketList){
        if(client){
            client->disconnect();
            client->close();
        }
    }

    tcpServerSocketList.clear();
    appendColorLog("[Prompt:Disconnect to client connections.]\n",
                   QColor("#666666"));

    ui->pushButton_TCPServerStart->setEnabled(true);
    ui->pushButton_TCPServerStop->setEnabled(false);
    serverRunning = false;
}

这里要注意一个很容易忽略的问题:服务器停止监听,并不等于已经连接进来的客户端会自动全部断开。所以这里额外遍历了 tcpServerSocketList,把每个客户端 socket 都关闭,再清空列表。这样停止服务器才算真正"停干净了"。

4. 客户端连接与消息收发:TCP Server 真正工作的地方

如果说 listen() 只是把门打开,那么真正开始"干活"的地方,其实是客户端连接进来之后的处理逻辑。本文这个实现里,核心分为两部分:

  • 有新客户端接入时,创建并保存 socket
  • 客户端发送数据时,读取消息并返回响应

4.1 处理新连接

来看连接建立时的槽函数:

cpp 复制代码
void FormTCPServer::TcpServerConnectedFunc()
{
    tcpServerSocket = tcpServer->nextPendingConnection(); // 获取新接入的客户端socket
    if(!tcpServerSocketList.contains(tcpServerSocket))
        tcpServerSocketList.append(tcpServerSocket);      // 保存到客户端列表中

    connect(tcpServerSocket, &QTcpSocket::readyRead,
            this, &FormTCPServer::ReadAllDataFunc);       // 有数据可读时触发
    connect(tcpServerSocket, &QTcpSocket::disconnected,
            this, &FormTCPServer::ClientDisconnectedFunc);// 客户端断开时触发

    appendColorLog("\n[Prompt:New client connection.]\n",
                   QColor("#666666"));
}

这段逻辑里最关键的一句是:

cpp 复制代码
tcpServer->nextPendingConnection();

它会取出当前等待处理的客户端连接,并返回一个 QTcpSocket*。有了这个 socket,服务端才能和这个客户端进行后续通信。拿到 socket 后,再连接两个很重要的信号:

  • readyRead:客户端发来数据时触发
  • disconnected:客户端断开时触发

这种写法是 Qt 网络编程里非常典型的模式:服务器负责监听,真正和客户端通信的是 QTcpSocket

4.2 读取客户端发送的数据

有了 readyRead 之后,客户端一发数据,槽函数就会被调用:

cpp 复制代码
void FormTCPServer::ReadAllDataFunc()
{
    if(QTcpSocket *client = qobject_cast<QTcpSocket*>(sender())){
        QByteArray data = client->readAll();              // 读取全部可用数据
        QString message = QString::fromUtf8(data);        // 按 UTF-8 转成字符串

        QString timestamp = QDateTime::currentDateTime()
                            .toString("yyyy/MM/dd hh:mm:ss");
        QString logEntry = QString("\n[%1] Receiced:%2\n")
                           .arg(timestamp, message);

        appendColorLog(logEntry, QColor("#666666"));

        QString response = "Server reponse: " + message;  // 简单回显
        client->write(response.toUtf8());                 // 发回客户端
    }
}

这段代码非常适合初学者理解 TCP 通信的基本流程:

  1. sender() 找到当前是哪一个客户端触发了这个槽函数
  2. readAll() 把当前可读数据全部取出来
  3. 转成字符串后写入日志
  4. 再构造一个响应消息回发给客户端

也就是说,这里实现的是一个很基础但很实用的"回显式服务器":客户端发来什么,服务器就带上前缀再返回去。这种方式很适合前期调试,因为只要客户端能收到回显,就说明整个通信链路是通的。

4.3 服务端主动发送消息

除了"被动接收",这个实现还支持"主动发送",也就是由服务端在界面输入框里输入内容后,点击按钮群发给所有已连接客户端:

cpp 复制代码
void FormTCPServer::on_pushButton_TCPServerSendMsg_clicked()
{
    if(!serverRunning){
        return;
    }

    QString message = ui->plainTextEdit_TCPServerSendData
                      ->toPlainText().trimmed();
    if(message.isEmpty()){
        return;
    }

    if(tcpServerSocketList.isEmpty()){
        return;
    }

    QByteArray data = message.toUtf8();
    for(QTcpSocket *client : qAsConst(tcpServerSocketList)){
        if(client && client->state() == QAbstractSocket::ConnectedState){
            client->write(data);   // 给每个在线客户端发送同一条消息
        }
    }

    QString timestamp = QDateTime::currentDateTime()
                        .toString("yyyy/MM/dd hh:mm:ss");
    QString logEntry = QString("\n[%1] Server send: %2\n")
                       .arg(timestamp, message);

    appendColorLog(logEntry, QColor("#008000"));
}

这一段说明一个问题:服务端并不只是"接收者",只要手里保存着客户端的 socket 指针,就可以主动向客户端发数据。这里通过遍历 tcpServerSocketList 的方式实现了群发,适合做简单测试,也方便后续扩展成"指定客户端发送"。

5. 日志显示与保存:让程序更像一个真正可用的工具

很多入门 TCP 例子写到"能通信"就结束了,但如果希望这个程序真正能拿来调试和观察运行过程,日志功能就很有必要。本文这个实现里,日志处理做了三件事:

  • 把消息实时显示到 QListWidget
  • 控制日志数量,避免界面越跑越卡
  • 程序关闭前保存日志到本地文件

先看日志追加函数:

cpp 复制代码
void FormTCPServer::appendColorLog(const QString &text, const QColor &color)
{
    QString sanitized = text;
    sanitized.replace('\n', ' ');
    sanitized = sanitized.simplified();
    if(sanitized.isEmpty())
        return;

    ui->listWidget_TCPServerListMsg->addItem(sanitized);

    trimLog();  // 日志过长时自动裁剪

    int row = ui->listWidget_TCPServerListMsg->count() - 1;
    if(row >= 0){
        if(QListWidgetItem *item = ui->listWidget_TCPServerListMsg->item(row)){
            item->setForeground(color);                      // 设置颜色区分日志类型
            ui->listWidget_TCPServerListMsg->setCurrentRow(row); // 滚动到最新一行
        }
    }
}

这个函数做得比较细。它不是简单地 addItem(),而是先把换行和多余空格处理掉,再追加到列表中,然后自动滚动到最新行。更关键的是,它还会调用 trimLog() 做日志裁剪,这一点在长时间运行的程序里很重要。

日志裁剪逻辑如下:

cpp 复制代码
void FormTCPServer::trimLog(int keepRows, int trimStep)
{
    static const int kKeepDefault = 1000;
    static const int kTrimDefault = 200;
    const int targetKeep = keepRows > 0 ? keepRows : kKeepDefault;
    const int step = trimStep > 0 ? trimStep : kTrimDefault;

    int count = ui->listWidget_TCPServerListMsg->count();
    if(count <= targetKeep + step)
        return;

    int removeCount = count - targetKeep;
    for(int i = 0; i < removeCount; i++){
        delete ui->listWidget_TCPServerListMsg->takeItem(0); // 删除最早的日志
    }
}

这里的思路很简单:日志太多以后,界面控件会越来越重,所以当日志行数超过阈值时,就把最旧的部分删除,只保留最近的一部分记录。这种做法虽然不复杂,但很实用,属于典型的"工程化细节"。

最后是日志保存功能:

cpp 复制代码
void FormTCPServer::saveListWidgetToFile(QListWidget* listWidget)
{
    QFile file("TCPServerLogFile.txt");
    if(file.open(QIODevice::WriteOnly | QIODevice::Text))
    {
        QTextStream out(&file);
        for(int i = 0; i < listWidget->count(); i++){
            QListWidgetItem *item = listWidget->item(i);
            if(item){
                out << item->text() << Qt::endl;
            }
        }
        file.close();
        QMessageBox::information(this, "成功",
                                 "日志已保存到 TCPServerLogFile.txt");
    }else{
        QMessageBox::critical(this, "失败",
                              "保存失败:" + file.errorString());
    }
}

程序在关闭按钮和窗口关闭事件中都会调用这个函数,把当前日志写入 TCPServerLogFile.txt。这样做的好处是,运行过程不会随着程序退出而丢失,后面排查问题或者整理测试结果时就方便很多。

另外,这个模块在析构函数中还主动关闭服务器、断开所有客户端并释放资源,避免对象悬挂和资源泄漏,这也是一个比较完整的收尾处理。

总结

本文通过一个带图形界面的 Qt TCP Server 模块,把 TCP 服务端开发中最核心的几个环节串了起来:服务器初始化、IP 与端口配置、启动监听、客户端连接、消息接收、服务端主动发送、日志显示以及日志保存。整体上看,这样的实现已经不仅仅是"调用几个网络类",而是一个比较完整的小型 TCP 调试工具。

对于学习 Qt 网络编程的人来说,这样的项目特别适合作为练手案例。因为它既包含 QTcpServerQTcpSocket 的基础用法,也加入了界面交互、日志管理、配置持久化这些实用功能。后续如果继续扩展,还可以加入客户端列表展示、指定客户端发送、断线重连提示、消息协议封装等内容,让这个 TCP Server 工具继续完善下去。

0voice · GitHub

相关推荐
并不喜欢吃鱼2 小时前
从零开始C++----七.继承及相关模型和底层(上篇)
开发语言·c++
tankeven3 小时前
HJ182 画展布置
c++·算法
W23035765733 小时前
【改进版】C++ 固定线程池实现:基于调用者运行的拒绝策略优化
开发语言·c++·线程池
谭欣辰4 小时前
C++ 控制台跑酷小游戏
c++·游戏
周末也要写八哥4 小时前
C++实际开发之泛型编程(模版编程)
java·开发语言·c++
兵哥工控5 小时前
MFC中return和break用法示例
c++·mfc
2401_841495646 小时前
Linux C++ TCP 服务端经典的监听骨架
linux·网络·c++·网络编程·ip·tcp·服务端
春栀怡铃声6 小时前
【C++修仙录02】筑基篇:类和对象(中)
c++
楼田莉子6 小时前
同步/异步日志系统:日志器管理器模块\全局接口\性能测试
linux·服务器·开发语言·c++·后端·设计模式