Portál AbcLinuxu, 30. října 2025 20:58
V tomto díle si ukážeme, jak za použití modulu QtNetwork naprogramovat jednoduchý TCP server.
Tento díl seriálu úzce souvisí s článkem Grafické programy v Qt 4 – 8 (TCP klient), jelikož je o server, jehož klienta jsem napsal s grafickým rozhraním. Základní informace o TCP síťování, socketech v Qt a ukázkovém programu se dočtete tam, zatímco tento článek popisuje čistě serverovou část, takže doporučuji začít čtení odkazovaným článkem.
Teď, když chápeme komunikaci přes QTcpSocket, můžeme se vrhnout na serverovou část. Základem je třída QTcpServer. Hned po vytvoření instance je vhodné nastavit nějaké základní parametry serveru, například takto:
QTcpServer* server = new QTcpServer(this); // explicitně zakážeme použití proxy server->setProxy(QNetworkProxy::NoProxy); // nastavíme max. počet spojení server->setMaxPendingConnections(50);
Ve většině případů je dobré zpracovat signál newConnection(), kterým nás server informuje o novém příchozím spojení. Slot, který na tento signál napojíme, bude chtít nejspíš získat socket tohoto nového spojení. K tomu využijeme metodu nextPendingConnection(), a to následovně:
// získáme socket QTcpSocket* socket = server->nextPendingConnection(); // dobré je hned ověřit, zda ještě existuje (tj. zda metoda nevrátila 0) if(!socket) return;
Existují dvě alternativy k tomuto přístupu zpracování nových příchozích spojení a jejich použití záleží na tom, čeho přesně chcete dosáhnout. Pokud potřebujete provádět něco velmi specifického a vyžadujete vysokou flexibilitu, potom si celou událost můžete zpracovat sami, a to tak, že si vytvoříte vlastní třídu založenou na QTcpServer a reimplementujete její chráněnou virtuální metodu incomingConnection(int socketDescriptor). Integer, který dostanete jako argument, je popisovač (descriptor) socketu a pokud nepoužíváte QNetworkProxy, lze s ním pracovat pomocí nativních socketových funkcí.
Druhý alternativní způsob je podstatně jednodušší. Nejdřív je třeba spustit server (viz níže) a potom lze použít blokující metodu waitForNewConnection(), které lze zadat čas v milisekundách (jak dlouho má čekat na příchozí připojení) a ukazatel na bool, do kterého se uloží informace o tom, zda došlo k vypršení času (tedy false znamená, že máte nové spojení).
Když máme připravené zpracování nových připojení, můžeme spustit server, tedy naslouchání na určité adrese (nebo více adresách) a portu. Třeba takto:
QString ip("127.0.0.1");
QHostAddress addr;
if(!addr.setAddress(ip))
{ // zadaná IP adresa není ve správném formátu
return 1;
}
// port=0 zajistí automatický výběr portu
server->listen(addr, 0);
Ještě než server spustíte, je třeba vytvořit mu konfigurační soubor, kam zapíšete platná uživatelská jména (bez mezer) a přiřadíte jim hesla (resp. jejich SHA1 hash). Chcete-li vytvořit uživatele user s heslem pass, tak si nejdřív zjistěte SHA1 hash řetězce „pass“:
$ echo -n "pass" | sha1sum 9d4e1e23bd5b727046a9e3b4b7db57bd8d6ee684
Když máte hash, vytvořte si konfigurační soubor v textovém editoru (na UNIXech ~/.config/watzke.cz/todoserver.conf) a přidejte do něj sekci [logins] a vaším uživatelem.
[logins] user=9d4e1e23bd5b727046a9e3b4b7db57bd8d6ee684
Server komunikuje jednoduchým protokolem, který je zdokumentovaný ve výše odkazovaném článku. Obsahuje jednoduchou obranu před bruteforce přihlašováním – povolí maximálně 10 chybných přihlášení za minutu. Jde spíše jen o ukázku implementace takové věci, spíš než cokoliv jiného, nicméně crackera by to mělo zpomalit :-)
Samozřejmě teď lze namítnout, že zmiňovat crackery je výsměch, když nepoužívám SSL sockety (nebo jiný způsob šifrování), ale o to mi v této ukázce nejde. Nicméně, když najdete bezpečnostní chybu, která s tímto nesouvisí, určitě se ozvěte v diskusi. Ještě dodám, že o chybějícím limitu délky jednotlivých záznamů vím, může tam být (teoreticky) libovolně dlouhý řetězec, i když můj klient umožní max. 32767 (2^15-1) znaků. Sám jsem několik bezpečnostních chyb odhalil a opravil při testování. Zvlášť při psaní síťového serveru, kde se řeší přihlašování, je třeba korektně ošetřovat nejrůznější výjimky. Nikdy nespoléhejte pouze na kontroly ve vašem klientovi.
tcpserver.h: API.
#ifndef TCPSERVER_H
#define TCPSERVER_H
#include <QObject>
#include <QTextStream>
#include <QList>
#include <QPair>
#include <QHostAddress>
#include <QHash>
#include <QSettings>
class QTcpServer;
class QTcpSocket;
class TcpServer : public QObject
{
Q_OBJECT
public:
TcpServer(QHostAddress addr = QHostAddress(QHostAddress::Any),
int port = 2266);
private:
QTcpServer* server;
QTextStream cerr;
QHash<QTcpSocket*, QString> logins;
QHash<QString, int> log;
QSettings settings;
void login(QTcpSocket* socket, const QString& user, const QString& pass,
bool force = false);
QString getKey(QString user, int index);
void sortKeys(QString user);
void listItems(QTcpSocket* socket);
void addItem(QTcpSocket* socket, QString item);
void removeItems(QTcpSocket* socket, QList<QByteArray> keys);
void socketWrite(QTcpSocket* socket, QByteArray data);
private slots:
void purgeLog();
void handleNewConnection();
void reply();
void gotDisconnected();
};
#endif // TCPSERVER_H
tcpserver.cpp:
#include "tcpserver.h"
#include <QtNetwork>
#include <cstdio>
TcpServer::TcpServer(QHostAddress addr, int port)
: cerr(stderr, QIODevice::WriteOnly) // poslouží pro výpis na std. chybový výstup,
// podobně jako std::cerr v C++
{
// vytvoříme TCP server
server = new QTcpServer(this);
// zajistíme zpracování požadavků vlastním slotem
connect(server, SIGNAL(newConnection()), this, SLOT(handleNewConnection()));
// každou minutu pročistíme záznam přihlašování
QTimer::singleShot(60*1000, this, SLOT(purgeLog()));
// explicitně zakážeme použití proxy
server->setProxy(QNetworkProxy::NoProxy);
// nastavíme max. počet spojení
server->setMaxPendingConnections(50);
// nastavíme IP a port pro naslouchání
bool listening = server->listen(addr, port);
// uložíme si IP adresu s portem do řetězce ve tvaru IP:port (jen pro výpis)
QString ipport = addr.toString() + ":" + QString::number(port);
// pokud se TCP server nepodařilo spustit na dané IP a portu
if(not listening)
{
// vypíšeme chybovou hlášku
cerr << tr("couldn't bind to %1, quitting…").arg(ipport) + "\n";
// zajistíme okamžitý zápis na stderr
cerr.flush();
// tímto zajistíme ukončení programu po provedení konstruktoru
QTimer::singleShot(0, qApp, SLOT(quit()));
} else
{
cerr << tr("listening on %1").arg(ipport) + "\n";
cerr.flush();
}
}
// (ihned) zapíše data do socketu
void TcpServer::socketWrite(QTcpSocket* socket, QByteArray data)
{
// zapíše data
socket->write(data);
// vynutí okamžitý zápis čekajících dat
socket->flush();
}
// vyprázdní záznam o přihlašování
void TcpServer::purgeLog()
{
log.clear();
}
// zpracuje nové příchozí spojení
void TcpServer::handleNewConnection()
{
// získáme socket
QTcpSocket* socket = server->nextPendingConnection();
if(!socket)
return;
cerr << tr("incoming connection from ") << socket->peerAddress().toString() << "\n";
cerr.flush();
// komunikace s klientem
connect(socket, SIGNAL(readyRead()), SLOT(reply()));
// šetříme paměť a zajistíme smazání socketu po jeho odpojení
connect(socket, SIGNAL(disconnected()), SLOT(gotDisconnected()));
// požádáme klienta, aby se přihlásil
this->socketWrite(socket, "LOGIN");
}
// vyřizuje odpověď na příchozí požadavky
void TcpServer::reply()
{
// získáme socket (skrz objekt, který vyslal signál)
QTcpSocket* socket = qobject_cast<QTcpSocket*>(this->sender());
if(log.value(socket->peerAddress().toString(), 0) >= 10)
this->socketWrite(socket, "TOOMANYWRONGLOGINS");
// požadavek
QByteArray rawdata = socket->readAll();
// argumenty požadavku
QList<QByteArray> args = rawdata.split(' ');
// samotný příkaz
QString command = args.takeFirst();
// přihlašovací/uživatelské jméno
QString user = logins.value(socket);
qDebug() << rawdata;
if(command == "LOGIN" || command == "FORCELOGIN")
{ // klient se chce přihlásit
// klient je již přihlášen (na tomto socketu)
if(!user.isEmpty())
{
this->socketWrite(socket, "ALREADYLOGGED");
return;
}
// vynucené přihlášení?
bool force = (command == "FORCELOGIN");
// heslo
QString pass;
// špatný počet argumentů nebudeme tolerovat
if(args.size() == 2)
{
user = args.at(0);
pass = args.at(1);
}
// zahájíme přihlašování
this->login(socket, user, pass, force);
return;
}
if(user.isEmpty())
{
socket->disconnectFromHost();
QString ip(socket->peerAddress().toString());
cerr << tr("a client (IP=%1) has requested something without being logged in,").arg(ip)
<< "\n" << tr("which is suspicious – closing the connection.") << "\n";
cerr.flush();
}
if(command == "LIST")
{ // klient žádá o seznam svých TODO položek
this->listItems(socket);
} else
if(command == "ADD")
{ // klient si přeje přidat novou TODO položku
this->addItem(socket, rawdata.right( rawdata.size()-command.size()-1 ));
} else
if(command == "REMOVE")
{ // klient si přeje odstranit položky
this->removeItems(socket, rawdata.right( rawdata.size()-command.size()-1 )
.split(' '));
} else
{ // klient poslal neplatný příkaz
this->socketWrite(socket, "INVALIDCMD");
cerr << "invalid command: " << command << "\n";
cerr.flush();
return;
}
}
// přihlásí uživatele
void TcpServer::login(QTcpSocket* socket, const QString& user,
const QString& pass, bool force)
{
//qDebug() << "user" << user << "is trying to log in with pass" << pass;
// načteme správné heslo
QString realpass = settings.value(QString("logins/%1").arg(user)).toString();
// a porovnáme jej se zadaným (navíc se ujistíme, zda uživatel vůbec existuje,
// abychom ošetřili přihlášení bez hesla)
if(!user.isEmpty() && !realpass.isEmpty() && pass == realpass)
{ // ok
// zjistíme, zda je uživatel již přihlášen odjinud
bool alreadyLogged = logins.values().contains(user);
// pokud ano a nejde o vynucený login…
if(alreadyLogged and not force)
{
// informujeme o tom klienta
this->socketWrite(socket, "ALREADYLOGGED");
return;
}
// pokud ano a jde o vynucený login…
if(alreadyLogged)
{
// získáme seznam všech socketů
QList<QTcpSocket*> keys = logins.keys();
// a odpojíme ten, který je přihlášený pod daným jménem
for(int i=0; i<keys.size(); i++)
if(logins.value(keys.at(i)) == user)
{
QTcpSocket* oldSocket = keys.at(i);
oldSocket->disconnectFromHost();
logins.remove(oldSocket);
break;
}
}
// pošleme odpověď
this->socketWrite(socket, "LOGGED");
// přiřadíme si socket k uživatelskému jménu do seznamu přihlášení
logins.insert(socket, user);
cerr << tr("user %1 successfully logged in").arg(user) << "\n";
cerr.flush();
} else
{ // špatný login
this->socketWrite(socket, "WRONGLOGIN");
// odpojíme klienta
socket->disconnectFromHost();
// zaznamenáme chybný login
QString ip(socket->peerAddress().toString());
log.insert(ip, log.value(ip, 0)+1);
cerr << tr("user %1 tried to log in with a wrong password, "
"closing the connection").arg(user) << "\n";
cerr.flush();
}
}
// opraví klíče (indexy) položek v nastavení tak, aby šly po sobě (000 až 999)
void TcpServer::sortKeys(QString user)
{
// získáme seznam klíčů
QStringList keys = settings.allKeys();
// seznam je skutečně plný, nedá se nic dělat
if(keys.size() > 999)
return;
cerr << tr("re-sorting %1's todo list").arg(user) + "\n";
cerr.flush();
// získáme seznam položek
QStringList items;
foreach(QString key, keys)
items << settings.value(key).toString();
//qDebug() << "items:" << items;
// odstraníme všechny položky
settings.remove("");
// a přidáme je zpátky se správnými klíči
for(int key = 0; key < keys.size(); key++)
settings.setValue(this->getKey(user, key), items.at(key));
settings.sync();
}
// vrátí klíč (index), pod kterým se položka uloží
QString TcpServer::getKey(QString user, int index)
{
QString key = QString::number(index);
if(index < 10) // pokud je index menší než 10
key.prepend("00"); // přidáme dvě nuly (5 -> 005), atd.
else if(index < 100)
key.prepend("0");
else if(index < 1000)
Q_UNUSED(key)
else
{ // index je vyšší než 999, zkusíme to ještě zachránit
// zkusíme opravit indexy
this->sortKeys(user);
// načteme poslední index a přičteme k němu 1
int nIndex = settings.allKeys().last().toInt() + 1;
// pokud je menší než 1000, povedlo se…
if(nIndex < 1000)
key = this->getKey(user, nIndex);
else // prázdný klíč = plný seznam
key.clear();
}
return key;
}
// odešle klientovi seznam jeho položek
void TcpServer::listItems(QTcpSocket* socket)
{
// načteme si uživatelské jméno
QString user = logins.value(socket);
// odpověď
QByteArray reply;
settings.beginGroup("todolist_" + user);
QStringList keys = settings.allKeys();
// přidáme do odpovědi seznam všech položek
foreach(QString key, keys)
reply += settings.value(key).toString() + '\n';
settings.endGroup();
// ořízneme poslední \n
reply.chop(1);
// přidáme odpovídající příkaz
if(reply.isEmpty())
reply = "LISTEMPTY";
else
reply.prepend("LISTING ");
//qDebug() << reply;
// a odpovíme
this->socketWrite(socket, reply);
}
// přidá položku do seznamu uživatele
void TcpServer::addItem(QTcpSocket* socket, QString item)
{
QString user = logins.value(socket);
settings.beginGroup("todolist_" + user);
// zjistíme, pod kterým klíčem/indexem položku uložit
int index;
if(settings.allKeys().size() > 0)
// k poslednímu uloženému indexu přičteme 1
index = settings.allKeys().last().toInt() + 1;
else // první index bude 0
index = 0;
//qDebug() << "index:" << index;
// získáme klíč ve správném formátu
QString key = this->getKey(user, index);
// prázdný klíč = plný seznam
if(key.isEmpty())
{
// informujeme klienta, že má plný seznam TODO
this->socketWrite(socket, "LISTFULL");
// nezapomeneme ukončit skupinu!
settings.endGroup();
cerr << tr("user %1 has got a full todo list").arg(user) << "\n";
cerr.flush();
return;
}
// uložíme položku
settings.setValue(key, item);
// sestrojíme odpověď
QByteArray reply = "ADDED ";
reply += settings.value(key).toString();
settings.endGroup();
// odešleme odpověď
this->socketWrite(socket, reply);
// zapíšeme změny v TODO
settings.sync();
}
// odstraní vybrané položky ze seznamu
void TcpServer::removeItems(QTcpSocket* socket, QList<QByteArray> keys)
{
QString user = logins.value(socket);
QByteArray reply = "REMOVED";
settings.beginGroup("todolist_" + user);
QStringList savedKeys = settings.allKeys();
foreach(QByteArray key, keys)
{
int index = key.toInt();
settings.remove(savedKeys.at(index));
reply += " " + key;
}
settings.endGroup();
settings.sync();
this->socketWrite(socket, reply);
}
// klient se odpojil
void TcpServer::gotDisconnected()
{
// získáme ukazatel na objekt (socket), který vyslal signál
QTcpSocket* socket = qobject_cast<QTcpSocket*>( this->sender() );
// získáme přihlašovací jméno
QString user = logins.value(socket);
if(user.isEmpty())
user = "*not logged*";
else // odstraníme ze seznamu aktivních připojení
logins.remove(socket);
// smažeme socket
socket->deleteLater();
cerr << tr("a client (%1) has disconnected").arg(user) << "\n";
cerr.flush();
}
main.cpp:
#include <QCoreApplication>
#include <QDebug>
#include <QHostAddress>
#include <QStringList>
#include <QTextCodec>
#include "tcpserver.h"
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
// výchozí port
int port = 2266;
// výchozí adresa pro naslouchání
QHostAddress addr(QHostAddress::Any);
// je vše v pořádku pro spuštění programu?
bool ok = true;
// kontrola argumentů na příkazovém řádku
if(argc > 3)
{
ok = false;
} else
{
if(argc == 3)
port = a.arguments().at(2).toInt(&ok);
if(argc > 1)
ok = addr.setAddress(a.arguments().at(1));
}
if(not ok)
{
qDebug() << "usage:" << argv[0] << "[IP(0.0.0.0)] [port(2266)]";
return 1;
}
a.setApplicationName("todoserver");
a.setOrganizationDomain("watzke.cz");
// nastavíme kódování C řetězců
QTextCodec::setCodecForCStrings( QTextCodec::codecForName("UTF-8") );
TcpServer s(addr, port);
return a.exec();
}
Zdrojáky si můžete stáhnout v archívu tcpserver.tar.bz2.
Ale jinak skvely pocteni Davide
Odkaz v uvodu clanku odkazuje asi jinam nez by melJo, díky. To je tím, že ten článek měl původně č. 9, ale nakonec předběhl původní osmičku
To pak musí opravit někdo z vyšších adminů.
Ale jinak skvely pocteni DavideTo jsem rád, díky.
a potom lze použít blokovací metodu waitForNewConnection()spíš bych napsal blokující. Blokovací vyznívá tak, že účelem té metody je blokovat.
ISSN 1214-1267, (c) 1999-2007 Stickfish s.r.o.