在 Qt 网络编程里,
QTcpServer和QTcpSocket是最常用的一组类。单独讲 API 往往比较抽象,而如果把它们放到一个带界面的 TCP Server 小工具里,整个实现思路就会清晰很多。本文就结合一个完整的 Qt TCP 服务端模块,讲清楚一个 TCP Server 是如何完成启动监听、客户端连接、消息收发以及日志保存这些功能的。整个实现包含界面初始化、IP 与端口配置恢复、服务器启动/停止、服务端主动发送消息、日志记录与保存等内容。
1. 项目功能概览:先明确这个 TCP Server 要做什么
一个实用的 TCP Server,一般不只是"能监听端口"这么简单,而是至少要把下面几件事做完整:
- 选择本机 IP 和端口并启动监听
- 接收客户端连接,读取客户端发送的数据
- 服务端可以主动给客户端发送消息
- 在界面中显示运行日志
- 关闭程序前把日志保存到本地文件中
这几个功能在本文这个实现里都是完整具备的。比如程序启动时会自动枚举本机的 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 通信的基本流程:
- 用
sender()找到当前是哪一个客户端触发了这个槽函数 - 用
readAll()把当前可读数据全部取出来 - 转成字符串后写入日志
- 再构造一个响应消息回发给客户端
也就是说,这里实现的是一个很基础但很实用的"回显式服务器":客户端发来什么,服务器就带上前缀再返回去。这种方式很适合前期调试,因为只要客户端能收到回显,就说明整个通信链路是通的。
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 网络编程的人来说,这样的项目特别适合作为练手案例。因为它既包含 QTcpServer 和 QTcpSocket 的基础用法,也加入了界面交互、日志管理、配置持久化这些实用功能。后续如果继续扩展,还可以加入客户端列表展示、指定客户端发送、断线重连提示、消息协议封装等内容,让这个 TCP Server 工具继续完善下去。