Upload files to "/"
This commit is contained in:
commit
3f705ebff8
4 changed files with 465 additions and 0 deletions
15
CMakeLists.txt
Normal file
15
CMakeLists.txt
Normal file
|
|
@ -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)
|
||||||
201
ChatServer.cpp
Normal file
201
ChatServer.cpp
Normal file
|
|
@ -0,0 +1,201 @@
|
||||||
|
#include "ChatServer.h"
|
||||||
|
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QDateTime>
|
||||||
|
#include <QDebug>
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// 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<QTcpSocket *>(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<QTcpSocket *>(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);
|
||||||
|
}
|
||||||
36
ChatServer.h
Normal file
36
ChatServer.h
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QTcpServer>
|
||||||
|
#include <QTcpSocket>
|
||||||
|
#include <QMap>
|
||||||
|
#include <QByteArray>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
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<QTcpSocket *, QString> m_clients; // socket → username
|
||||||
|
QMap<QTcpSocket *, QByteArray> m_buffers; // socket → incoming data buffer
|
||||||
|
};
|
||||||
213
main.cpp
Normal file
213
main.cpp
Normal file
|
|
@ -0,0 +1,213 @@
|
||||||
|
#include <QCoreApplication>
|
||||||
|
#include <QDebug>
|
||||||
|
#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<QTcpSocket *>(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<QTcpSocket *>(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<QTcpSocket *, QString> m_clients;
|
||||||
|
|
||||||
|
// socket → incoming data buffer
|
||||||
|
QMap<QTcpSocket *, QByteArray> 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"
|
||||||
Loading…
Add table
Reference in a new issue