3. QTcpServer服务器:从零搭建你的网络服务端
聊到网络编程,服务端永远是绕不开的核心。QTcpServer这个类,说白了就是Qt给我们准备好的「接客员」——它负责在指定端口上等着,一旦有客户端来敲门,它就帮你把连接接进来。
我个人习惯把QTcpServer比作一个前台接待。它不负责具体聊天,只负责「谁来了,我帮你引见一下」。真正干活的,是后面要讲的QTcpSocket。
3.1 QTcpServer的核心职责
先看看这个类到底能干啥。我列几个最常用的功能:
- 监听端口:告诉系统「我要在这个端口上等别人连我」
- 接受连接:有客户端连上来时,自动创建一个QTcpSocket
- 管理连接:通过信号告诉你「有新连接啦」,你自己决定怎么处理
嗯,这里要注意:QTcpServer本身不负责收发数据。它只负责「接进来」,数据读写是QTcpSocket的事。这个分工要搞清楚。
核心原则:QTcpServer = 接线员,QTcpSocket = 话务员。各司其职,别搞混了。
3.2 监听与接受连接
写一个最简单的服务端,其实就三步。我在项目里第一次写的时候,还觉得「这也太简单了吧?」后来才发现,坑都在后面。
// 第一步:创建服务器对象
QTcpServer *server = new QTcpServer(this);
// 第二步:开始监听
if (!server->listen(QHostAddress::Any, 8888)) {
qDebug() << "监听失败:" << server->errorString();
return;
}
qDebug() << "服务器已启动,端口:" << 8888;
// 第三步:连接新连接信号
connect(server, &QTcpServer::newConnection, this, [=]() {
QTcpSocket *socket = server->nextPendingConnection();
qDebug() << "新客户端连进来了:" << socket->peerAddress().toString();
});
看到没?核心就三个动作:创建、监听、接信号。QHostAddress::Any表示监听所有网卡地址,你想想看,如果你的服务器有多个IP,这个设置就能全部覆盖到。
小技巧:端口号建议选1024以上的。1024以下是系统保留端口,我曾经图省事用了80端口,结果权限不够,折腾了半天才发现问题。
3.3 管理多个客户端连接
一个客户端好办,但真实场景下,往往是几十上百个客户端同时连着你。这时候怎么管?
我的做法是:用一个QList或者QMap把所有的socket存起来。每个socket就是一个独立的连接通道。
// 用列表管理所有客户端
QList<QTcpSocket*> clientList;
connect(server, &QTcpServer::newConnection, this, [&]() {
QTcpSocket *socket = server->nextPendingConnection();
clientList.append(socket);
// 每个客户端断开时,从列表移除
connect(socket, &QTcpSocket::disconnected, this, [&, socket]() {
clientList.removeOne(socket);
socket->deleteLater();
qDebug() << "客户端断开,当前在线:" << clientList.size();
});
});
为什么用QMap?如果你需要根据客户端ID查找,QMap更合适。我一般用QMap<QString, QTcpSocket*>,key存用户名或者设备ID,value存socket指针。这样查找起来特别快。
注意:客户端断开后,socket对象不会自动销毁。一定要调用deleteLater(),否则内存泄漏等着你。我曾经在一个项目里忘了这茬,跑了三天服务器内存爆了,查了半天才发现是socket没清理。
3.4 简易聊天室服务器实现
光说不练假把式。咱们写一个真正的聊天室服务器。核心逻辑就一条:谁发了消息,我就转发给所有人。
先画个图,看看整体流程:
好,图看明白了,咱们上代码。这个服务器我写过很多次,每次结构都差不多:
class ChatServer : public QObject {
Q_OBJECT
public:
ChatServer(QObject *parent = nullptr) : QObject(parent) {
server = new QTcpServer(this);
connect(server, &QTcpServer::newConnection, this, &ChatServer::onNewConnection);
}
bool start(quint16 port) {
return server->listen(QHostAddress::Any, port);
}
private slots:
void onNewConnection() {
QTcpSocket *socket = server->nextPendingConnection();
clients.append(socket);
// 收到消息时转发
connect(socket, &QTcpSocket::readyRead, this, [this, socket]() {
QByteArray data = socket->readAll();
QString msg = QString("[%1]: %2")
.arg(socket->peerAddress().toString())
.arg(QString::fromUtf8(data));
// 转发给其他客户端
for (QTcpSocket *client : clients) {
if (client != socket) { // 不发给自己
client->write(msg.toUtf8());
}
}
});
// 断开时清理
connect(socket, &QTcpSocket::disconnected, this, [this, socket]() {
clients.removeOne(socket);
socket->deleteLater();
});
}
private:
QTcpServer *server;
QList<QTcpSocket*> clients;
};
这段代码看着简单,但有几个关键点我得提醒你:
- readyRead信号:只要有数据来,这个信号就会触发。但要注意,它可能分多次触发,所以别假设一次readAll就能读完所有数据。我遇到过粘包问题,后来加了消息头才解决。
- 转发时排除自己:这个逻辑很重要。不然A发一句「大家好」,自己又收到一句「大家好」,体验很差。
- 断开清理:removeOne和deleteLater缺一不可。前者从列表移除,后者释放内存。
避坑指南:我曾经在转发消息时直接用了socket->write(),但没检查socket是否有效。结果客户端断开了,socket变成悬空指针,一写就崩溃。后来我加了个判断:if (client->state() == QAbstractSocket::ConnectedState),稳了。
3.5 关于并发的一点思考
有人可能会问:Qt的QTcpServer是单线程的,能处理高并发吗?
嗯,这个问题问得好。Qt的事件循环机制决定了它确实是单线程处理。但别担心,对于几百个连接以内的场景,完全够用。为什么?因为网络I/O是异步非阻塞的,不会卡住主线程。
如果你真的需要处理上万连接,那就得考虑QTcpServer的线程池方案了。不过那是进阶内容,咱们先把基础打牢。
| 场景 | 推荐方案 | 说明 |
|---|---|---|
| 连接数 < 100 | 单线程QTcpServer | 简单够用,开发快 |
| 连接数 100-1000 | 单线程+异步处理 | 注意不要阻塞事件循环 |
| 连接数 > 1000 | 线程池+多QTcpServer | 需要更复杂的架构设计 |
说白了,技术选型要看场景。别一上来就整高大上的架构,先跑起来再说。我见过太多人一开始就想着「高并发」「分布式」,结果连基本的收发都没调通。
好了,这一章的内容就到这。QTcpServer的核心用法你心里应该有数了:监听、接受、管理、转发。下一章咱们聊聊QTcpSocket,看看数据到底是怎么收发的。
课后练习:试着把上面的聊天室代码跑起来,用两个telnet客户端连上去,互相发消息看看效果。telnet localhost 8888,就这么简单。