Implementiere Benutzerverbindungskontrolle und verbessere Nachrichtenverwaltung

- Füge die Methode `removeUserDisconnected` in der ChatRoom-Klasse hinzu, um Benutzer bei Verbindungsabbrüchen zu entfernen und entsprechende Nachrichten zu senden.
- Aktualisiere die ChatUser-Klasse, um einen Token-Getter bereitzustellen und die Verbindungsprüfung zu optimieren.
- Ändere die Server-Klasse, um die Benutzerverwaltung bei Raumwechseln zu verbessern und Debug-Informationen hinzuzufügen.
- Optimiere die Socket-Optionen für eine schnellere Fehlererkennung und verbessere die Handhabung von Anfragen.
This commit is contained in:
Torsten Schulz (local)
2025-09-01 15:30:19 +02:00
parent 7338f1a740
commit d92c40748e
12 changed files with 868 additions and 33 deletions

View File

@@ -94,13 +94,7 @@ namespace Yc
newUser->start();
Json::Value roomList = _parent->jsonRoomList();
newUser->sendMsg(ChatUser::roomList, roomList, "", "");
// Private Rückmeldung an den User: In welchem Raum befindet er/sie sich jetzt?
{
Json::Value msg = Json::objectValue;
msg["tr"] = "room_entered";
msg["to"] = _name;
newUser->sendMsg(ChatUser::system, msg, "", "");
}
// Sende aktuelle Userliste an den neuen User
Json::Value currentUserList = userList();
newUser->sendMsg(ChatUser::userListe, currentUserList, "", "");
@@ -112,8 +106,11 @@ namespace Yc
}
}
// Broadcast an andere Nutzer: Benutzer X hat den Raum betreten (mit Farbinfo)
addMessage(ChatUser::system, "user_entered_room", newUser->name(), newUser->color());
// Broadcast an andere Nutzer: Benutzer X hat den Chat betreten (mit Farbinfo)
#ifdef YC_DEBUG
std::cout << "[Debug] addUser: Sending 'user_entered_chat' message for user: " << newUser->name() << std::endl;
#endif
addMessage(ChatUser::system, "user_entered_chat", newUser->name(), newUser->color());
_initRound();
return true;
}
@@ -174,6 +171,20 @@ namespace Yc
}
}
void ChatRoom::removeUserDisconnected(std::shared_ptr<ChatUser> userToRemove)
{
for (auto it = _users.begin(); it != _users.end(); ++it)
{
if (*it == userToRemove)
{
// Spezielle Nachricht für Verbindungsabbrüche
addMessage(ChatUser::system, std::string("user_disconnected"), (*it)->name(), (*it)->color());
_users.erase(it);
break;
}
}
}
void ChatRoom::setStop()
{
_stop = true;

View File

@@ -45,6 +45,7 @@ namespace Yc
bool userNameExists(std::string userName);
void removeUser(std::string _token, bool silent = false);
void removeUser(std::shared_ptr<ChatUser> user, bool silent = false);
void removeUserDisconnected(std::shared_ptr<ChatUser> userToRemove);
void setStop();
void addMessage(ChatUser::MsgType type, const char *messageText, std::string userName = "", std::string color = "");
void addMessage(ChatUser::MsgType type, std::string messageText, std::string userName = "", std::string color = "");

View File

@@ -111,6 +111,11 @@ namespace Yc
return _name;
}
std::string ChatUser::getToken() const
{
return _token;
}
bool ChatUser::validateToken(std::string token)
{
return (token == _token);
@@ -155,16 +160,57 @@ namespace Yc
void ChatUser::checkerTask()
{
try {
// Heartbeat-Intervall: Alle 10 Sekunden Verbindung prüfen
const int HEARTBEAT_INTERVAL = 2;
int heartbeatCounter = 0;
while (!_stop)
{
fd_set readSd;
FD_ZERO(&readSd);
FD_SET(_socket, &readSd);
timeval tv;
tv.tv_sec = 0;
tv.tv_usec = 500;
int selectResult = select(_socket + 1, &readSd, NULL, NULL, &tv);
if (selectResult == 1 && FD_ISSET(_socket, &readSd) == 1)
std::this_thread::sleep_for(std::chrono::seconds(1));
// Heartbeat-Check alle 10 Sekunden
heartbeatCounter++;
if (heartbeatCounter >= HEARTBEAT_INTERVAL) {
heartbeatCounter = 0;
// Prüfe Verbindung mit MSG_PEEK (nicht-blockierend)
char peek;
ssize_t r = recv(_socket, &peek, 1, MSG_PEEK | MSG_DONTWAIT);
if (r == 0) {
#ifdef YC_DEBUG
std::cout << "[Debug] Verbindung zum Client abgebrochen (Token: <hidden>)" << std::endl;
#endif
_parent->removeUserDisconnected(shared_from_this());
_stop = true;
if (thread.joinable() && std::this_thread::get_id() == thread.get_id()) {
thread.detach();
}
return;
} else if (r < 0) {
// EINTR = Interrupted system call (normal), EAGAIN/EWOULDBLOCK = No data available (normal)
// Andere Fehler bedeuten Verbindungsabbruch
if (errno != EINTR && errno != EAGAIN && errno != EWOULDBLOCK) {
#ifdef YC_DEBUG
std::cout << "[Debug] Socket-Fehler: " << strerror(errno) << " (Token: <hidden>)" << std::endl;
#endif
_parent->removeUserDisconnected(shared_from_this());
_stop = true;
if (thread.joinable() && std::this_thread::get_id() == thread.get_id()) {
thread.detach();
}
return;
}
}
// Optional: Sende Heartbeat-Ping an Client
// (kann helfen, NAT/Firewall-Verbindungen aktiv zu halten)
#ifdef YC_DEBUG
std::cout << "[Debug] Heartbeat check passed for user: " << _name << std::endl;
#endif
}
// Ursprüngliche Verbindungsprüfung (alle 1 Sekunde)
{
char peek;
ssize_t r = recv(_socket, &peek, 1, MSG_PEEK);
@@ -172,7 +218,7 @@ namespace Yc
#ifdef YC_DEBUG
std::cout << "[Debug] Verbindung zum Client abgebrochen (Token: <hidden>)" << std::endl;
#endif
_parent->removeUser(_token);
_parent->removeUserDisconnected(shared_from_this());
_stop = true;
if (thread.joinable() && std::this_thread::get_id() == thread.get_id()) {
thread.detach();
@@ -180,6 +226,7 @@ namespace Yc
return;
}
}
std::string msg = readSocket(_socket);
if (msg == "")
{
@@ -347,7 +394,7 @@ namespace Yc
}
else if (jsonTree["type"].asString() == "join")
{
changeRoom(jsonTree["newroom"].asString(), jsonTree["password"].asString());
changeRoom(jsonTree["room"].asString(), jsonTree["password"].asString());
}
else if (jsonTree["type"].asString() == "userlist")
{

View File

@@ -36,6 +36,7 @@ namespace Yc
ChatUser(std::shared_ptr<ChatRoom> parent, std::string name, std::string color, int socket);
~ChatUser();
std::string name() const;
std::string getToken() const;
bool validateToken(std::string token);
bool isUser(std::shared_ptr<ChatUser> toValidate);
void sendMsg(MsgType type, std::string message, std::string userName, std::string color);

View File

@@ -147,11 +147,29 @@ namespace Yc {
}
bool Server::roomAllowed(std::string roomName, std::string userName, std::string password){
#ifdef YC_DEBUG
std::cout << "[Debug] roomAllowed called with roomName: '" << roomName << "', userName: '" << userName << "'" << std::endl;
std::cout << "[Debug] Available rooms: ";
for (auto &room: _rooms) {
if (room->name() == roomName && room->accessAllowed(userName, password)) {
std::cout << "'" << room->name() << "' ";
}
std::cout << std::endl;
#endif
for (auto &room: _rooms) {
#ifdef YC_DEBUG
std::cout << "[Debug] Checking room: '" << room->name() << "' against requested: '" << roomName << "'" << std::endl;
#endif
if (room->name() == roomName && room->accessAllowed(userName, password)) {
#ifdef YC_DEBUG
std::cout << "[Debug] Room found and access allowed!" << std::endl;
#endif
return true;
}
}
#ifdef YC_DEBUG
std::cout << "[Debug] Room not found or access denied" << std::endl;
#endif
return false;
}
@@ -159,42 +177,97 @@ namespace Yc {
if (!roomAllowed(newRoom, user->name(), password)) {
return false;
}
std::string oldRoomName = "";
// Finde den aktuellen Raum des Users
for (auto &room: _rooms) {
if (room->userIsInRoom(user->name())) {
oldRoomName = room->name();
break;
}
}
Json::Value userMsg = Json::objectValue;
userMsg["tr"] = "room_change_user";
userMsg["to"] = newRoom;
// Nur Nachrichten senden, wenn der User bereits in einem Raum ist (Raumwechsel)
if (!oldRoomName.empty()) {
for (auto &room: _rooms) {
if (room->name() == oldRoomName) {
// Sende Nachricht an alle User im alten Raum, dass der User den Raum verlassen hat
Json::Value leaveMsg = Json::objectValue;
leaveMsg["tr"] = "user_left_room";
leaveMsg["userName"] = user->name();
leaveMsg["userColor"] = user->color();
leaveMsg["destination"] = newRoom;
room->addMessage(ChatUser::system, leaveMsg, "", "");
}
}
}
// Entferne User aus dem alten Raum
for (auto &room: _rooms) {
if (room->userIsInRoom(user->name())) {
room->removeUser(user);
Json::Value msg = Json::objectValue;
msg["tr"] = "room_change_to";
msg["to"] = newRoom;
userMsg["from"] = room->name();
room->addMessage(ChatUser::system, msg, user->name(), user->color());
room->removeUser(user->getToken(), true); // silent = true, da wir eigene Nachrichten senden
break;
}
}
user->sendMsg(ChatUser::system, userMsg, "", "");
// Füge User zum neuen Raum hinzu
for (auto &room: _rooms) {
if (room->name() == newRoom) {
Json::Value msg = Json::objectValue;
msg["tr"] = "room_change_to";
msg["from"] = userMsg["from"];
room->addMessage(ChatUser::system, msg, user->name(), user->color());
#ifdef YC_DEBUG
std::cout << "[Debug] changeRoom: Adding user '" << user->name() << "' to room '" << newRoom << "'" << std::endl;
std::cout << "[Debug] changeRoom: oldRoomName = '" << (oldRoomName.empty() ? "EMPTY" : oldRoomName) << "'" << std::endl;
#endif
room->addUserWhenQueueEmpty(user);
// Nur bei Raumwechsel (nicht beim ersten Beitritt) die Nachricht senden
if (!oldRoomName.empty()) {
#ifdef YC_DEBUG
std::cout << "[Debug] changeRoom: Sending 'user_entered_room' message (room change)" << std::endl;
#endif
Json::Value joinMsg = Json::objectValue;
joinMsg["tr"] = "user_entered_room";
joinMsg["userName"] = user->name();
joinMsg["userColor"] = user->color();
joinMsg["origin"] = oldRoomName;
room->addMessage(ChatUser::system, joinMsg, "", "");
} else {
#ifdef YC_DEBUG
std::cout << "[Debug] changeRoom: NOT sending 'user_entered_room' message (first join)" << std::endl;
#endif
}
}
}
return true;
}
void Server::createRooms(Json::Value roomList) {
auto self = shared_from_this();
bool created = false;
#ifdef YC_DEBUG
std::cout << "[Debug] createRooms called with roomList size: " << roomList.size() << std::endl;
std::cout << "[Debug] roomList content: " << roomList << std::endl;
#endif
try {
#ifdef YC_DEBUG
std::cout << "[Debug] Attempting to load rooms from database..." << std::endl;
#endif
std::string query = R"(
SELECT r.id, r.title, r.password_hash, r.room_type_id, r.is_public, r.owner_id, r.min_age, r.max_age, r.created_at, r.updated_at, rt.tr as room_type
FROM chat.room r
LEFT JOIN chat.room_type rt ON r.room_type_id = rt.id
)";
auto result = _database->exec(query);
#ifdef YC_DEBUG
std::cout << "[Debug] Database query result size: " << result.size() << std::endl;
#endif
for (const auto& row : result) {
Json::Value room;
room["id"] = row["id"].as<int>();
@@ -214,19 +287,41 @@ namespace Yc {
auto newRoom = std::make_shared<ChatRoom>(self, room);
_rooms.push_back(newRoom);
created = true;
#ifdef YC_DEBUG
std::cout << "[Debug] Created room from database: " << room["name"].asString() << std::endl;
#endif
}
} catch (const std::exception& e) {
#ifdef YC_DEBUG
std::cout << "[Debug] Database error: " << e.what() << std::endl;
#endif
} catch (...) {
// ignore DB errors, fallback below
#ifdef YC_DEBUG
std::cout << "[Debug] Unknown database error occurred" << std::endl;
#endif
}
if (!created) {
#ifdef YC_DEBUG
std::cout << "[Debug] Database loading failed, trying fallback rooms..." << std::endl;
#endif
// fallback to provided JSON room list (if any)
if (roomList.isArray() && roomList.size() > 0) {
#ifdef YC_DEBUG
std::cout << "[Debug] Loading " << roomList.size() << " fallback rooms from config" << std::endl;
#endif
for (const auto& room : roomList) {
#ifdef YC_DEBUG
std::cout << "[Debug] Creating fallback room: " << room["name"].asString() << std::endl;
#endif
auto newRoom = std::make_shared<ChatRoom>(self, room);
_rooms.push_back(newRoom);
created = true;
}
} else {
#ifdef YC_DEBUG
std::cout << "[Debug] No fallback rooms in config, creating default room" << std::endl;
#endif
// final fallback: builtin default room
Json::Value room;
room["name"] = "Halle";
@@ -239,6 +334,13 @@ namespace Yc {
created = true;
}
}
#ifdef YC_DEBUG
std::cout << "[Debug] Total rooms created: " << _rooms.size() << std::endl;
for (const auto& room : _rooms) {
std::cout << "[Debug] Room: " << room->name() << std::endl;
}
#endif
}
void Server::handleRequest() {
@@ -260,10 +362,36 @@ namespace Yc {
std::cout << "[YourChat] Verbindung akzeptiert von " << clientIP << ":" << ntohs(sockAddr.sin_port) << " (fd=" << userSock << ")" << std::endl;
int flags = 1;
setsockopt(userSock, IPPROTO_TCP, TCP_NODELAY, (void *)&flags, sizeof(flags));
int ka2 = 1; setsockopt(userSock, SOL_SOCKET, SO_KEEPALIVE, &ka2, sizeof(ka2));
// Aggressive TCP Keep-Alive für schnelle Verbindungsabbruch-Erkennung
int ka2 = 1;
setsockopt(userSock, SOL_SOCKET, SO_KEEPALIVE, &ka2, sizeof(ka2));
// Keep-Alive Parameter: 5 Sekunden bis zum ersten Probe, dann alle 2 Sekunden
int keepalive_time = 5; // 5 Sekunden bis zum ersten Probe
int keepalive_intvl = 2; // 2 Sekunden zwischen Probes
int keepalive_probes = 3; // 3 Probes bevor Verbindung als tot betrachtet wird
setsockopt(userSock, IPPROTO_TCP, TCP_KEEPIDLE, &keepalive_time, sizeof(keepalive_time));
setsockopt(userSock, IPPROTO_TCP, TCP_KEEPINTVL, &keepalive_intvl, sizeof(keepalive_intvl));
setsockopt(userSock, IPPROTO_TCP, TCP_KEEPCNT, &keepalive_probes, sizeof(keepalive_probes));
// Begrenze Blockierzeit beim Senden, um langsame Clients nicht alle zu verzögern
timeval sendTimeout; sendTimeout.tv_sec = 0; sendTimeout.tv_usec = 500000; // 500ms
setsockopt(userSock, SOL_SOCKET, SO_SNDTIMEO, &sendTimeout, sizeof(sendTimeout));
// Socket-Optionen für schnellere Fehlererkennung
int reuseAddr = 1;
setsockopt(userSock, SOL_SOCKET, SO_REUSEADDR, &reuseAddr, sizeof(reuseAddr));
// LINGER-Option: Sofort schließen bei Verbindungsabbruch
struct linger linger_opt;
linger_opt.l_onoff = 1;
linger_opt.l_linger = 0; // 0 Sekunden = sofort schließen
setsockopt(userSock, SOL_SOCKET, SO_LINGER, &linger_opt, sizeof(linger_opt));
// TCP_NODELAY bereits gesetzt (oben)
// TCP Keep-Alive bereits konfiguriert (oben)
std::string msg = readSocket(userSock);
#ifdef YC_DEBUG
std::cout << "[Debug] Neue Anfrage erhalten: " << msg << std::endl;