#include #include #include "ChatServer.h" class ChatServer : public QTcpServer { Q_OBJECT public: explicit ChatServer(QObject *parent = nullptr) : QTcpServer(parent) {} protected: // Qt calls this automatically when a new client connects void incomingConnection(qintptr socketDescriptor) override { auto *socket = new QTcpSocket(this); // Qt docs recommend checking this actually succeeded if (!socket->setSocketDescriptor(socketDescriptor)) { qWarning() << "[Server] Failed to set descriptor:" << socket->errorString(); delete socket; return; } // wire up this socket's signals to our slots connect(socket, &QTcpSocket::readyRead, this, &ChatServer::onReadyRead); connect(socket, &QTcpSocket::disconnected, this, &ChatServer::onDisconnected); m_clients[socket] = ""; // socket known, username not yet qInfo() << "[Server] New connection from" << socket->peerAddress().toString(); sendSystemMessage(socket, QString("Connected to server. %1 user(s) online.") .arg(m_clients.size())); } private slots: // fires when data arrives from any client void onReadyRead() { auto *socket = qobject_cast(sender()); if (!socket) return; // append whatever arrived to this client's buffer m_buffers[socket] += socket->readAll(); // extract complete messages (delimited by \n) while (m_buffers[socket].contains('\n')) { int idx = m_buffers[socket].indexOf('\n'); QByteArray line = m_buffers[socket].left(idx).trimmed(); m_buffers[socket] = m_buffers[socket].mid(idx + 1); if (line.isEmpty()) continue; processFrame(socket, line); } } // fires when a client disconnects void onDisconnected() { auto *socket = qobject_cast(sender()); if (!socket) return; // look up their username before removing them QString username = m_clients.value(socket, "Unknown"); m_clients.remove(socket); m_buffers.remove(socket); socket->deleteLater(); qInfo() << "[Server]" << username << "disconnected."; broadcast(nullptr, buildFrame("system", "Server", username + " has left the chat.", "")); } private: // handle one complete JSON message from a client void processFrame(QTcpSocket *socket, const QByteArray &data) { // parse the raw bytes into a JSON document QJsonParseError err; QJsonDocument doc = QJsonDocument::fromJson(data, &err); if (err.error != QJsonParseError::NoError || !doc.isObject()) { qWarning() << "[Server] Bad JSON from client:" << err.errorString(); return; } QJsonObject obj = doc.object(); QString type = obj["type"].toString(); QString username = obj["username"].toString().trimmed(); // ── JOIN ────────────────────────────────────────────────────────── if (type == "join") { if (username.isEmpty()) { sendSystemMessage(socket, "Username cannot be empty."); return; } // check for duplicate username (case insensitive) for (auto it = m_clients.begin(); it != m_clients.end(); ++it) { if (it.value().toLower() == username.toLower() && it.key() != socket) { sendSystemMessage(socket, "Username '" + username + "' is already taken."); return; } } // store the username against this socket m_clients[socket] = username; qInfo() << "[Server]" << username << "joined."; // welcome the new user sendSystemMessage(socket, "Welcome, " + username + "!"); // tell everyone else they joined broadcast(socket, buildFrame("system", "Server", username + " joined the chat.", "")); // send updated user list to all clients broadcastUserList(); return; } // ── GUARD: must have joined before doing anything else ──────────── if (m_clients.value(socket).isEmpty()) { sendSystemMessage(socket, "Please join with a username first."); return; } // ── MESSAGE ─────────────────────────────────────────────────────── if (type == "message") { QString text = obj["text"].toString().trimmed(); if (text.isEmpty()) return; QString ts = QDateTime::currentDateTime().toString("hh:mm"); qInfo() << "[Server]" << username << ":" << text; // broadcast to everyone including the sender broadcast(nullptr, buildFrame("message", username, text, ts)); } } // package data into a JSON frame ready to send QByteArray buildFrame(const QString &type, const QString &from, const QString &text, const QString &ts) { QJsonObject obj; obj["type"] = type; obj["from"] = from; obj["text"] = text; obj["ts"] = ts; // Compact = no spaces, everything on one line, \n is the message delimiter return QJsonDocument(obj).toJson(QJsonDocument::Compact) + "\n"; } // send a frame to all clients, optionally excluding one void broadcast(QTcpSocket *exclude, const QByteArray &frame) { for (auto *s : m_clients.keys()) { if (s != exclude && s->state() == QAbstractSocket::ConnectedState) s->write(frame); } } // send a system notification to one specific client void sendSystemMessage(QTcpSocket *socket, const QString &text) { socket->write(buildFrame("system", "Server", text, "")); } // send the current online user list to all clients void broadcastUserList() { QJsonArray names; for (const QString &name : m_clients.values()) if (!name.isEmpty()) names.append(name); QJsonObject obj; obj["type"] = "userlist"; obj["users"] = names; QByteArray frame = QJsonDocument(obj).toJson(QJsonDocument::Compact) + "\n"; for (auto *s : m_clients.keys()) s->write(frame); } // socket → username ("" if not yet joined) QMap m_clients; // socket → incoming data buffer QMap m_buffers; }; // ───────────────────────────────────────────────────────────────────────────── int main(int argc, char *argv[]) { QCoreApplication app(argc, argv); quint16 port = 54321; ChatServer server; if (!server.listen(QHostAddress::Any, port)) { qCritical() << "[Server] Failed to listen on port" << port << ":" << server.errorString(); return 1; } qInfo() << "╔══════════════════════════════════╗"; qInfo() << "║ Qt Chat Server — port" << port << " ║"; qInfo() << "╚══════════════════════════════════╝"; qInfo() << "Waiting for connections..."; return app.exec(); } #include "main.moc"