[Qt] Prosta aplikacja klient-serwer na wątkach

0

Witam.
Piszę prostą aplikację klient-serwer, gdzie do serwera może podłączyć się kilku klientów. Każde połączenie klienta do serwera tworzy nowy wątek, w którym tworzony jest obiekt QTcpSocket. Później będę chciał co jakiś czas wysyłać z serwera jakiś string do wszystkich klientów, co ma być zsynchronizowane. Aktualnie tkwię na etapie, że nie wysyła mi 10 znaków "X" co sekundę do jednego klienta, tylko po 10 sekundach wyświetla wszystkie. Poniżej zamieszczam kody źródłowe. Proszę o jakieś podpowiedzi.

plik Server.cpp z aplikacji serwera:

#include "Server.h"

Server::Server(QObject *parent):QTcpServer(parent)
{
	connect(this,SIGNAL(newConnection()),this,SLOT(addConnection()));
}

Server::~Server(void)
{
}

void Server::addConnection()
{
	QTcpSocket *client = this->nextPendingConnection();
	ClientThread *thread = new ClientThread(client->socketDescriptor(),text,this);
	threadsList.push_back(thread);
	connect(thread, SIGNAL(finished()), thread, SLOT(deleteLater()));
    thread->start();
	clientsList.push_back(client);
	connect(client, SIGNAL(disconnected()), this, SLOT(removeConnection()));
	connect(client, SIGNAL(disconnected()), client, SLOT(deleteLater()));
	emit added(client->localAddress().toString());
}

void Server::removeConnection()
{
	QTcpSocket* client = (QTcpSocket*)sender();
	int index = clientsList.indexOf(client);
	clientsList.removeAt(index);
	threadsList.at(index)->terminate();
	threadsList.removeAt(index);
	emit removed(client->localAddress().toString());
	client->close();
}

void Server::broadcast()
{
	for(int i=0;i<clientsList.size();i++)
	{
		clientsList.at(i)->write("Twoja stara szybko idzie");
	}
}

bool Server::start(int port)
{
	if(!this->listen(QHostAddress::Any,port))
		return false;

	QString ipAddress;
	QList<QHostAddress> ipAddressesList = QNetworkInterface::allAddresses();

	for(int i=0;i<ipAddressesList.size();i++)
	{
		if(ipAddressesList.at(i)!=QHostAddress::LocalHost && ipAddressesList.at(i).toIPv4Address())
		{
			ipAddress = ipAddressesList.at(i).toString();
			break;
		}
	}

	if (ipAddress.isEmpty())
	{
		ipAddress = QHostAddress(QHostAddress::LocalHost).toString();
	}
	return true;
}

void Server::stop()
{
	while(!clientsList.isEmpty())
	{
		clientsList.last()->close();
	}
	this->close();
}

int Server::countClients()
{
	return clientsList.size();
}

int Server::countThreads()
{
	return threadsList.size();
}

plik ClientThread.cpp z aplikacji serwera:

#include "ClientThread.h"

ClientThread::ClientThread(int socketDescriptor,QString &str,QObject *parent):socketDescriptor(socketDescriptor),text(str),QThread(parent)
{
}

ClientThread::~ClientThread(void)
{
}

void ClientThread::run()
{
	QTcpSocket tcpSocket;

	if (!tcpSocket.setSocketDescriptor(socketDescriptor)) {
		emit error(tcpSocket.error());
		return;
	}
	for(int i=0;i<10;i++)
	{
		tcpSocket.write("X ");
		sleep(1);
	}
}

plik ClientApp.cpp z aplikacji klienta:

#include "clientapp.h"

ClientApp::ClientApp(QWidget *parent, Qt::WFlags flags)
	: QMainWindow(parent, flags)
{
	ui.setupUi(this);
	ui.portEdit->setValidator(new QIntValidator(1, 65535, this));
	tcpSocket = new QTcpSocket(this);
	connect(ui.connectButton,SIGNAL(clicked()),this,SLOT(connectTo()));
	connect(ui.disconnectButton,SIGNAL(clicked()),this,SLOT(disconnectFrom()));
	connect(tcpSocket, SIGNAL(readyRead()), this, SLOT(onReadyRead()));
    connect(tcpSocket, SIGNAL(stateChanged(QAbstractSocket::SocketState)), this, SLOT(onStateChanged(QAbstractSocket::SocketState)));
}

ClientApp::~ClientApp()
{
}

void ClientApp::connectTo()
{
	tcpSocket->abort();
    tcpSocket->connectToHost("localhost", ui.portEdit->text().toInt());
}

void ClientApp::disconnectFrom()
{
	//tcpSocket->disconnectFromHost();
	tcpSocket->close();
}

void ClientApp::onReadyRead()
{
	while(!tcpSocket->atEnd())
	{
		QString data(tcpSocket->read(2));
		ui.textEdit->append("Serwer: " + data);
	}
}

void ClientApp::onStateChanged(QAbstractSocket::SocketState socketState)
{
	switch (socketState)
	{
    case QAbstractSocket::ConnectedState:
		ui.connectButton->setEnabled(false);
		ui.disconnectButton->setEnabled(true);
		ui.textEdit->append("Nawiazano polaczenie z serwerem.");
        break;
    case QAbstractSocket::UnconnectedState:
		ui.connectButton->setEnabled(true);
		ui.disconnectButton->setEnabled(false);
        ui.textEdit->append("Rozlaczono.");
        break;
    case QAbstractSocket::HostLookupState:
        ui.textEdit->append("Szukanie serwera.");
        break;
    case QAbstractSocket::ConnectingState:
        ui.textEdit->append("Laczenie z serwerem.");
		break;
    }
}
0

Pod Deusa to też trzeba umieć się podszywać. Np jak jest niezalogowany to daje kropkę na końcu.

deus napisał(a)

plik ClientThread.cpp z aplikacji serwera:

[..]

void ClientThread::run()
{
[...]
	for(int i=0;i<10;i++)
	{
		tcpSocket.write("X ");
		sleep(1);
	}
}

Nie powinieneś czasem w tej pętli flushować? http://doc.qt.nokia.com/4.0/qabstractsocket.html#flush

0

Przepraszam jeśli kogoś uraziłem ale nie miałem zamiaru pod nikogo sie podszywać. Użycie flush() pomogło, dzięki za pomoc. Po wyświetleniu tych 10 znaków X klient rozłącza się jednak serwer tego nie odnotowuje. Mam wątpliwości co do mojego rozwiązania związanego z tworzeniem wskaźników na QTcpSocket w metodzie Server::addConnection() i później przekazywania ich socketDescription do oddzielnie tworzonych obiektów QTcpSocket w wątkach klasy ClientThread. Czy dobrze to robię, a jeśli nie to jak mogę do tego inaczej podejść? Z góry, dziękuję za odpowiedź.

0

Po wyświetleniu tych 10 znaków X klient rozłącza się jednak serwer tego nie odnotowuje.

To niech server odnotowuje rozłączenie jakiegoś połączenia poprzez sygnał finished z threada. Bo ten disconnected z tcpsocketa wydaje się podejrzany, szczególnie jeśli współdzieli deskryptor socketa z lokalnym w threadzie, a w dokumentacji piszą:

Note: It is not possible to initialize two abstract sockets with the same native socket descriptor.

http://doc.trolltech.com/4.7/qabstractsocket.html#setSocketDescriptor

Mam wątpliwości co do mojego rozwiązania związanego z tworzeniem wskaźników na QTcpSocket w metodzie Server::addConnection() i później przekazywania ich socketDescription do oddzielnie tworzonych obiektów QTcpSocket w wątkach klasy ClientThread. Czy dobrze to robię, a jeśli nie to jak mogę do tego inaczej podejść? Z góry, dziękuję za odpowiedź.

Tu też dokumentacja mądrości prawi

Note: If you want to handle an incoming connection as a new QTcpSocket object in another thread you have to pass the socketDescriptor to the other thread and create the QTcpSocket object there and use its setSocketDescriptor() method.

http://doc.trolltech.com/4.7/qtcpserver.html#incomingConnection

A Threaded Fortune Server Example http://doc.trolltech.com/4.7/network-threadedfortuneserver.html niech Twoim przewodnikiem będzie.

0

Dzięki za odpowiedź. Zacząłem się wzorować na Threaded Fortune Server Example. Teraz w funkcji run() mojej klasy wątku mam coś takiego:

void ClientThread::run()
{
	QTcpSocket tcpSocket;

	if (!tcpSocket.setSocketDescriptor(socketDescriptor)) {
		emit error(tcpSocket.error());
		return;
	}

	while(!quit)
	{
		tcpSocket.write("X ");
		tcpSocket.flush();
		sleep(5);
	}
	tcpSocket.disconnectFromHost();
	tcpSocket.waitForDisconnected();
}

Jak wyłączam aplikację klienta to mój serwer tego nie odnotowuje. W jaki sposób ustawić quit na true w momencie rozłączenia klienta? Próbowałem dodać w metodzie run():

connect(&tcpSocket,SIGNAL(disconnected()),this,SLOT(setQuit()));

Niestety to nie działa, poza tym tak chyba nie wolno. Proszę o podpowiedź. Poniżej zamieszczam jeszcze moją metodę incomingConnection() mojej klasy serwera dziedziczącej po QTcpServer:

void Server::incomingConnection(int socketDescriptor)
{
	ClientThread *thread = new ClientThread(socketDescriptor,text,this);
	connect(thread,SIGNAL(finished()),this,SLOT(removeConnection()));
	connect(thread,SIGNAL(finished()),thread,SLOT(deleteLater()));
	thread->start();
	clients++;
	emit addedConnection();
}
0
deus napisał(a)

Jak wyłączam aplikację klienta to mój serwer tego nie odnotowuje. W jaki sposób ustawić quit na true w momencie rozłączenia klienta? Próbowałem dodać w metodzie run():

connect(&tcpSocket,SIGNAL(disconnected()),this,SLOT(setQuit()));

No, nie powinnien tego odnotować natychmiast, pamiętaj, że masz tam sleep(5), po około 5 sekundach(albo więcej) server może odnotować rozłączenie.

Jeśli w setQuit masz tylko instrukcję quit=true; a samą deklarację w dziale public slots:, to powinno to działać.

0

W setQuit() mam tylko quit=true; a deklarację miałem w private slots i zarówno w private jak i public, nie działa. Oprócz tego zauważyłem, że mając w metodzie run() nawet pustą pętlę while(!quit){} i po podłączeniu już jednego klienta, zużycie procesora podnosi się do 50%. Jeśli dam pętlę for która robi cokolwiek to zużycie procesora jest znikome. Dzięki za pomoc.

0

To sprawdź czy metoda removeConnection z Servera jest w sekcji dla slotów.

W ostateczności sprawdzaj czy metody się wykonują prz pomocy qDebug http://doc.qt.nokia.com/4.6/qdebug.html np w setQuit:

     qDebug() <<"Slot setQuit()";

i potem sprawdzaj w konsoli(servera) czy masz ten tekst po wyłaczeniiu klienta(oczywiście nie natychmiast, poczekaj 10-15 sekund).

0
Bury pajac napisał(a)

To sprawdź czy metoda removeConnection z Servera jest w sekcji dla slotów.

W ostateczności sprawdzaj czy metody się wykonują prz pomocy qDebug http://doc.qt.nokia.com/4.6/qdebug.html np w setQuit:

     qDebug() <<"Slot setQuit()";

i potem sprawdzaj w konsoli(servera) czy masz ten tekst po wyłaczeniiu klienta(oczywiście nie natychmiast, poczekaj 10-15 sekund).

Metoda removeConnection() jest w sekcji dla slotów. Gdy usune z metody run() pętlę while, tak żeby wątek nic nie zrobił tylko zaraz się rozłączył to wszystko działa poprawnie. qDebug wykazuje, że gdy mam pętlę while(!quit){} to slot setQuit() się nie wykonuje, natomiast bez tej pętli jest OK - setQuit i removeConnection są wykonywane.

0

Wrzuć gdzieś kody z obiema aplikacjami, wszystkie, abym mógł sam skompilować, bo nie będę zgadywać czego, gdzie nie ma.
Albo do pastebina, albo spakuj do rar, zip, etc. i na jakiś hosting.

0

Próbowałem z innym połączeniem:

connect(&tcpSocket,SIGNAL(stateChanged(QAbstractSocket::SocketState socketState)),this,SLOT(setQuit()));

żeby niekoniecznie reagował na zakończenie finished() ale na jakąkolwiek zmianę stanu ale qDebug pisze:

Object::connect: No such signal QTcpSocket::stateChanged(QAbstractSocket::SocketState socketState) in .\ClientThread.cpp:16
0

Połączenia wykonywane są asynchroniczne. Już była o tym mowa, nawet nie tak dawno temu ( max miesiąc ). Przeszukaj sobie tematy z wątkami Qt a na pewno znajdziesz, tam było podane rozwiązanie bardzo podobnego problemu.

0

http://realmadrid.pl/~deo/pro.rar

Troszke dużo zajmuje bo wrzuciłem z dll'ami, żeby mieć pewność, że to odpalisz.

0

W metodzie run miałeś

connect(&tcpSocket,SIGNAL(finished()),this,SLOT(setQuit())); 

poprawiłem na

connect(&tcpSocket,SIGNAL(disconnected()),this,SLOT(setQuit())); 

w while'a wstawiłem:

            tcpSocket.write("X ");
            tcpSocket.flush();
            sleep(1); 

aby tcpSocket miał jakieś cykle procesora i mógł się zorientować że jest rozłączony.

Teraz kompiluje i uruchamiam obie aplikacje, w serverze "uruchom server", w kliencie "połącz", klient otrzymuje iXy, klikam rozłącz, klient pisze że rozłączony, po chwili server pisze że klient się rozłączył.

Sprawdź czy jesteś w stanie powtórzyć te kroki.

0

Rzeczywiście działa. Dzięki wielkie. Tyle, że jak chcę wysłać jakieś dane tylko od czasu do czasu w poniższy sposób, to wtedy rozłączanie nie jest odnotowywane. Nie rozumiem po co tcpSocket ma mieć te cykle, czy da się to inaczej rozwiązać?

void ClientThread::run()
{
	QTcpSocket tcpSocket;
	connect(&tcpSocket,SIGNAL(disconnected()),this,SLOT(setQuit()));

	if (!tcpSocket.setSocketDescriptor(socketDescriptor)) {
		emit error(tcpSocket.error());
		return;
	}

	while(!quit)
	{
		if(valueChanged)
		{
			tcpSocket.write(text);
			tcpSocket.flush();
			valueChanged = false;
		}
	}
	tcpSocket.disconnectFromHost();
	tcpSocket.waitForDisconnected();
}

valueChanged jest ustawiany na true po wcisnieciu przycisku w serwerze co działa poprawnie ale wątek wykorzystuje 50% procesora. Po dodaniu na końcu pętli while nawet msleep(1) problem wykorzystania procesora znika i program oczywiście nadal działa poprawnie jednak w obu przypadkach już te rozłączanie nie jest odnotowywane.

0

Takie mnożenie zmiennych stanowych do niczego dobrego nie prowadzi.

Widzę że server posiada sygnał send(char*), dlaczego go nie wykorzystasz?
Wywal te zmienne quit, valueChanged, oraz slot setQuit. Zrób slot send(char* msg) w którym dasz

       tcpSocket.write(msg);
       tcpSocket.flush(); 

Deklarację tcpSocketa z run(), przenieś do prywatnych klasy.
W run() połącz sygnał disconnected z tcpSocket z quit() threada, a zamiast pętli while daj this->exec().

Teraz w incommingConnection Servera, połącz sygnał send(char*) z slotem send(char*) utworzonego ClientThreada.

Skompiluj i uruchom obie aplikacje, server->uruchom, klient->połacz, server->wyślij wektor parę razy, klient->rozłącz, powinno wszystko działać, tak jak chcesz, o ile dobrze zrozumiałem Twoje intencje.

Na serverze możesz też modyfikować priorytety jakie mają mieć wątki, aby kontrolować obciążenie procesora. Wszystko jest opisane w dokumentacji http://doc.trolltech.com/4.7/qthread.html

0

Masz rację, działa, tylko po wysłaniu przynajmniej jednego wektora jak wcisnę Rozłącz w kliencie to serwer nie odnotowuje rozłączenia. Nie wysyłając wektory wszystko jest OK.

0

Pokaż metody incommingConnection() servera i run() ClientThreada, oraz zawartość pliku ClientThread.h

0

Server::incomingConnection()

void Server::incomingConnection(int socketDescriptor)
{
	ClientThread *thread = new ClientThread(socketDescriptor,this);
	connect(this,SIGNAL(send(char*)),thread,SLOT(sendData(char*)));
	connect(thread,SIGNAL(finished()),this,SLOT(removeConnection()));
	connect(thread,SIGNAL(finished()),thread,SLOT(deleteLater()));
	thread->start();
	clients++;
	emit addedConnection();
	qDebug() <<"incomingConnection()";
}

ClientThread::run()

void ClientThread::run()
{
	connect(&tcpSocket,SIGNAL(disconnected()),this,SLOT(quit()));

	if (!tcpSocket.setSocketDescriptor(socketDescriptor)) {
		emit error(tcpSocket.error());
		return;
	}

	this->exec();
	
	tcpSocket.disconnectFromHost();
	tcpSocket.waitForDisconnected();
}

ClientThread::sendData(char* str)

void ClientThread::sendData(char* str)
{
	tcpSocket.write(str);
	tcpSocket.flush();
}

ClientThread.h

class ClientThread : public QThread
{
	Q_OBJECT

public:
	ClientThread(int socketDescriptor,QObject *parent);
	~ClientThread(void);
	void run();
signals:
	void error(QTcpSocket::SocketError socketError);
private:
	QTcpSocket tcpSocket;
	int socketDescriptor;
	char* text;
private slots:
	void sendData(char*);
};
0

Masz jakieś warningi podczas budowania? Jeśli tak, to popraw kod, aby się nie pojawiały. Zwiększ level ostrzeżeń w Visual Studio, jeśli dalej nic.

A jak to nie pomoże to prawdopodobnie winny jest kompilator, dlatego dla testu zainstaluj qtcreator z mingw i tam to skompiluj, jeśli aplikacja dalej będzie tak działać jak mówisz to winny może być system operacyjny, bądź port biblioteki qt pod ten system, a to z kolei oznaczałoby że znalazłeś buga.

Z kolei u mnie działa poprawnie, system Ubuntu 10.10,
gcc version 4.4.5 (Ubuntu/Linaro 4.4.4-14ubuntu5)
qt 4.7.0

0

Jeszcze jeden strzał:

deus napisał(a)

Masz rację, działa, tylko po wysłaniu przynajmniej jednego wektora jak wcisnę Rozłącz w kliencie to serwer nie odnotowuje rozłączenia.

A odnotuje jeśli po wykonaniu tych kroków(przy rozłączonym kliencie) wciśniesz wyślij wektor na serverze?

0

Nie odnotowuje wtedy też. Po dwóch wciśnięciach wyślij wektor następuje Debug error i sie wysypuje serwer.

Po wciśnięciu Połącz w kliencie qDebug pisze:

QObject: Cannot create children for a parent that is in a different thread.
(Parent is QTcpSocket(0xadf9b0), parent's thread is QThread(0x948708), current thread is ClientThread(0xadf9a8)
incomingConnection() 

Po wciśnięciu Rozłącz w kliencie bez wysyłania wektora qDebug pisze:

QAbstractSocket::waitForDisconnected() is not allowed in UnconnectedState
The thread 'ClientThread' (0x490) has exited with code 0 (0x0).
Slot removeConnection()

... i po pewnym czasie dochodzi:

The thread 'Win32 Thread' (0xde0) has exited with code 0 (0x0).

... i znowu:

The thread 'Win32 Thread' (0x368) has exited with code 0 (0x0).

Po wyłączeniu aplikacji doszło jeszcze pare tych linijek z Win32 Thread.

Teraz nowe podejście. Wciskam Połącz w kliencie, qDebug pisze:

QObject: Cannot create children for a parent that is in a different thread.
(Parent is QTcpSocket(0xad3f60), parent's thread is QThread(0x948708), current thread is ClientThread(0xad3f58)
incomingConnection() 

Wciskam Wyślij wektor w kliencie, qDebug pisze:

QObject: Cannot create children for a parent that is in a different thread.
(Parent is QNativeSocketEngine(0xacd6e8), parent's thread is ClientThread(0xad3f58), current thread is QThread(0x948708)

Naciskam Rozłącz w kliencie, w qDebug cisza i dopiero po jakimś czasie:

The thread 'Win32 Thread' (0x568) has exited with code 0 (0x0).

Po wyłączeniu serwera całkowicie, w qDebug znów doszło pare linijek z Win32 Thread.

Jak wyślę wektor, rozłączę klienta i spróbuję znów wysłać wektor to przy pierwszej próbie nic sie nie dzieje natomiast przy drugiej jak już wyżej pisałem serwer sie wysypuje, jest Debug error i w qDebug pisze:

QSocketNotifier: socket notifiers cannot be disabled from another thread
ASSERT failure in QCoreApplication::sendEvent: "Cannot send events to objects owned by a different thread. Current thread 948708. Receiver '' (of type 'QNativeSocketEngine') was created in thread a94278", file kernel\qcoreapplication.cpp, line 347
0

Zrób tak:

W ClientThread.h zamień:

QTcpSocket tcpSocket;

na

QTcpSocket *tcpSocket;

Natomiast metody run i sendData na odpowiednio:

void ClientThread::run()
{
    tcpSocket = new QTcpSocket();

    connect(tcpSocket,SIGNAL(disconnected()),this,SLOT(quit()));

    if (!tcpSocket->setSocketDescriptor(socketDescriptor)) {
            emit error(tcpSocket->error());
            return;
    }

    tcpSocket->moveToThread(this->thread());

    this->exec();

    if(tcpSocket->state()==QAbstractSocket::ConnectedState){
        tcpSocket->disconnectFromHost();
        tcpSocket->waitForDisconnected();
    }
    delete tcpSocket;
    
    disconnect(this->parent(),SIGNAL(send(char*)),this,SLOT(sendData(char*)));
}
void ClientThread::sendData(char* str)
{
        tcpSocket->write(str);
        tcpSocket->flush();
}

I sprawdź czy dalej będzie się wysypywać bądź czy wypisze jeszcze jakieś errory.

0

Aplikacja już działa tak jak należy, wszystko jest odnotowywane. Jednak w trybie debugowania przy wciśnięciu w kliencie Rozłącz pojawia się okienko z warningiem (w qDebug pisze to samo), normalnie odpalając nie ma żadnych śladów złego działania:

First-chance exception at 0x64072c2a (QtNetworkd4.dll) in ServerApp.exe: 0xC0000005: Access violation writing location 0xfeeefeee.
HEAP[ServerApp.exe]: HEAP: Free Heap block ae0578 modified at ae0644 after it was freed
Windows has triggered a breakpoint in ServerApp.exe.

This may be due to a corruption of the heap, which indicates a bug in ServerApp.exe or any of the DLLs it has loaded.

This may also be due to the user pressing F12 while ServerApp.exe has focus.

Jeszcze mi wytłumacz dlaczego socket ma się rozłączać z hostem gdy będzie miał stan ConnectedState? Bo chyba jak klient się podłączył to wtedy jest ConnectedState i według tego co jest w kodzie powinien się od razu rozłączyć :)

0

Przenieś delete tcpSocket; do destruktora, a w konstruktorze dodaj w liście inicjalizacyjnej tcpSocket(NULL), bo z run() jest jeszcze jedno wyjście, które spowodowałoby wycieki.

deus napisał(a)

Jeszcze mi wytłumacz dlaczego socket ma się rozłączać z hostem gdy będzie miał stan ConnectedState? Bo chyba jak klient się podłączył to wtedy jest ConnectedState i według tego co jest w kodzie powinien się od razu rozłączyć :)

To wywal to, delete tcpSocket chyba zrobi to samo.

Poeksperymentuj też z zakomentowanym disconnectem pod koniec run() i odkomentowanym, jeśli dalej problemy to daj callstacka.

0

Działa idealnie, bez warunku sprawdzającego stan socketu też. Jesteś moim guru :) Teraz chcę zrobić synchronizację wątków, żeby każdy klient dokładnie w tym samym czasie otrzymał dane, ale mam dziwne wrażenie, że jest to niemożliwe z socketami. Może chociaż żeby dane zostały wysłane w tym samym momencie, da się?

0
deus napisał(a)

Teraz chcę zrobić synchronizację wątków, żeby każdy klient dokładnie w tym samym czasie otrzymał dane, ale mam dziwne wrażenie, że jest to niemożliwe z socketami.

Tak, tego nie da się zrobić w realnym świecie. Technologia + teoria chaosu + relatywistyka i te sprawy.

deus napisał(a)

Może chociaż żeby dane zostały wysłane w tym samym momencie, da się?

Wydaje mi się że poprzez zastosowanie sygnałów/slotów (send(char*) , sendData(char*)), masz już to zrobione.

1 użytkowników online, w tym zalogowanych: 0, gości: 1