commit 3f705ebff87549eeb2654777dc40d7cd737fc448 Author: ApoG Date: Thu Mar 26 20:36:05 2026 +0200 Upload files to "/" diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..b3309f2 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,15 @@ +cmake_minimum_required(VERSION 3.16) +project(ChatServer LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_AUTOMOC ON) + +find_package(Qt6 REQUIRED COMPONENTS Core Network) + +add_executable(ChatServer + main.cpp + ChatServer.h + ChatServer.cpp +) + +target_link_libraries(ChatServer PRIVATE Qt6::Core Qt6::Network) diff --git a/ChatServer.cpp b/ChatServer.cpp new file mode 100644 index 0000000..eabf7cd --- /dev/null +++ b/ChatServer.cpp @@ -0,0 +1,201 @@ +#include "ChatServer.h" + +#include +#include +#include +#include +#include + +// ───────────────────────────────────────────────────────────────────────────── +// Constructor +// ───────────────────────────────────────────────────────────────────────────── +ChatServer::ChatServer(QObject *parent) : QTcpServer(parent) +{ +} + +// ───────────────────────────────────────────────────────────────────────────── +// Qt calls this automatically when a new client connects +// ───────────────────────────────────────────────────────────────────────────── +void ChatServer::incomingConnection(qintptr socketDescriptor) +{ + auto *socket = new QTcpSocket(this); + + // Qt docs recommend checking setSocketDescriptor 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())); +} + +// ───────────────────────────────────────────────────────────────────────────── +// Fires when data arrives from any client +// ───────────────────────────────────────────────────────────────────────────── +void ChatServer::onReadyRead() +{ + auto *socket = qobject_cast(sender()); + if (!socket) return; + + // append whatever arrived to this client's buffer + m_buffers[socket] += socket->readAll(); + + // guard against a client sending huge amounts of data with no newlines + if (m_buffers[socket].size() > MAX_BUFFER_SIZE) { + qWarning() << "[Server] Buffer overflow from" + << m_clients.value(socket, "unknown") << "— disconnecting."; + socket->disconnectFromHost(); + return; + } + + // extract and process 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 ChatServer::onDisconnected() +{ + // Qt docs: use deleteLater() when deleting sender() of disconnected signal + auto *socket = qobject_cast(sender()); + if (!socket) return; + + 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.", "")); + broadcastUserList(); +} + +// ───────────────────────────────────────────────────────────────────────────── +// Handle one complete JSON message from a client +// ───────────────────────────────────────────────────────────────────────────── +void ChatServer::processFrame(QTcpSocket *socket, const QByteArray &data) +{ + // parse 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; + } + } + + m_clients[socket] = username; + qInfo() << "[Server]" << username << "joined."; + + sendSystemMessage(socket, "Welcome, " + username + "!"); + broadcast(socket, buildFrame("system", "Server", + username + " joined the chat.", "")); + broadcastUserList(); + return; + } + + // ── GUARD: must have a username 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(nullptr, buildFrame("message", username, text, ts)); + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// Package data into a JSON frame ready to send +// ───────────────────────────────────────────────────────────────────────────── +QByteArray ChatServer::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 ChatServer::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 ChatServer::sendSystemMessage(QTcpSocket *socket, const QString &text) +{ + socket->write(buildFrame("system", "Server", text, "")); +} + +// ───────────────────────────────────────────────────────────────────────────── +// Send the current online user list to all clients +// ───────────────────────────────────────────────────────────────────────────── +void ChatServer::broadcastUserList() +{ + // QJsonArray is explicitly included — don't rely on indirect includes + 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); +} diff --git a/ChatServer.h b/ChatServer.h new file mode 100644 index 0000000..1b6043f --- /dev/null +++ b/ChatServer.h @@ -0,0 +1,36 @@ +#pragma once + +#include +#include +#include +#include +#include + +class ChatServer : public QTcpServer +{ + Q_OBJECT + +public: + explicit ChatServer(QObject *parent = nullptr); + +protected: + void incomingConnection(qintptr socketDescriptor) override; + +private slots: + void onReadyRead(); + void onDisconnected(); + +private: + void processFrame(QTcpSocket *socket, const QByteArray &data); + void broadcast(QTcpSocket *exclude, const QByteArray &frame); + void sendSystemMessage(QTcpSocket *socket, const QString &text); + void broadcastUserList(); + QByteArray buildFrame(const QString &type, const QString &from, + const QString &text, const QString &ts); + + // max bytes we buffer per client before dropping them (prevents memory abuse) + static constexpr int MAX_BUFFER_SIZE = 1024 * 64; // 64 KB + + QMap m_clients; // socket → username + QMap m_buffers; // socket → incoming data buffer +}; diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..39c02f6 --- /dev/null +++ b/main.cpp @@ -0,0 +1,213 @@ +#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"