news 2026/4/20 14:08:50

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【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 地址并恢复上次配置

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

tcpServer = new QTcpServer(this); // 创建服务器对象,父对象负责自动回收 // 监听新连接信号 connect(tcpServer, &QTcpServer::newConnection, this, &FormTCPServer::TcpServerConnectedFunc); ui->pushButton_TCPServerStop->setEnabled(false); // 初始时禁止“停止”按钮

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

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

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 和端口:

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 和端口的合法性校验,以及重复启动判断,这样逻辑会更完整。

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

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中。这样一来,“启动服务器”就不只是一个简单的函数调用,而是一套比较完整的交互流程。

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

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 处理新连接

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

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

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

tcpServer->nextPendingConnection();

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

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

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

4.2 读取客户端发送的数据

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

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 服务端主动发送消息

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

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
  • 控制日志数量,避免界面越跑越卡
  • 程序关闭前保存日志到本地文件

先看日志追加函数:

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()做日志裁剪,这一点在长时间运行的程序里很重要。

日志裁剪逻辑如下:

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); // 删除最早的日志 } }

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

最后是日志保存功能:

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/20 14:08:25

Magpie:5大核心功能深度解析,打造Windows窗口缩放终极方案

Magpie&#xff1a;5大核心功能深度解析&#xff0c;打造Windows窗口缩放终极方案 【免费下载链接】Magpie A general-purpose window upscaler for Windows 10/11. 项目地址: https://gitcode.com/gh_mirrors/mag/Magpie Magpie是一款专为Windows 10/11设计的窗口缩放工…

作者头像 李华
网站建设 2026/4/20 14:07:21

Redis - Docker环境下的持久化、主从复制、哨兵、集群、淘汰策略

安装 拉取redis镜像 docker pull redis:latest创建文件夹 mkdir /usr/local/docker/redis并进入到该文件夹 cd /usr/local/docker/redis将redis.conf文件上传到该文件夹下&#xff0c;并修改以下内容 69 #bind 127.0.0.1 #注释掉这行&#xff0c;让其它网络设备可以访问到re…

作者头像 李华
网站建设 2026/4/20 14:00:16

漫画翻译革命:如何用BallonsTranslator让外文漫画阅读零门槛?

漫画翻译革命&#xff1a;如何用BallonsTranslator让外文漫画阅读零门槛&#xff1f; 【免费下载链接】BallonsTranslator 深度学习辅助漫画翻译工具, 支持一键机翻和简单的图像/文本编辑 | Yet another computer-aided comic/manga translation tool powered by deeplearning …

作者头像 李华