Статья ориентирована в основном на новичков. Целью ее написания является быстрое и максимально подробное описание сокетов, для начального понимания сети и сокетов. В свое время искал подобную, но нужны были подробные примеры. В стандартном примере fortune server/client, который идет с qt очень плохо показывают возможности сокетов.
Итак, сервер умеет:
"Слушать" произвольный адрес, порт
Авторизовать клиента по имени
Отправлять общие, приватные, серверные сообщения
Отправлять список пользователей
Клиент умеет подавать соответствующие запросы серверу.
Для понимания это будут Гуи-приложения:
В qt существуют классы QTcpSocket и QTcpServer для работы с сокетами. Используя сигналы и слоты, с ними можно работать в неблокирующем (асинхронном режиме). Это значит, если подключение к серверу занимает заметное количество времени, гуи не блокируется, а продолжает обрабатывать события, а когда произойдет подключение (либо ошибка), вызовется определенный слот (в текущем случае подключенный к сигналу connected()).
Клиент
Начнем с простого - не будем наследовать классы, а воспользуемся QTcpSocket:
//dialog.h
class Dialog : public QDialog
{
private slots:
//определим слоты для обработки сигналов сокета
void onSokConnected();
void onSokDisconnected();
//сигнал readyRead вызывается, когда сокет получает пакет (который может быть лишь частью отправленых данных) байтов
void onSokReadyRead();
void onSokDisplayError(QAbstractSocket::SocketError socketError);
private:
QTcpSocket *_sok; //сокет
quint16 _blockSize;//текущий размер блока данных
QString _name;//имя клиента
};
Вообще при работе с сокетами нужно смотреть на данные как набор байтов, иначе могут быть проблемы с отображением информации лишь частично (пришло не полное сообщение, а следующее отображается с куском предыдущего). Чтобы избежать этих неприятностей будем использовать потоки данных (QDataStream) и предавать между сокетами блоки, в которых первые 2 байта это размер текущего блока, 3-й байт это команда клиента серверу (или ответ сервера), а остальное - данные в зависимости от команды. Стоит сказать, что протокол tcp гарантирует доставку всех пакетов, поэтому можно смело ждать полный размер блока, прежде чем его обрабатывать.
//dialog.cpp
Dialog::Dialog(QWidget *parent) :QDialog(parent),ui(new Ui::Dialog)
{
//создаем сокет
_sok = new QTcpSocket(this);
//подключаем сигналы
connect(_sok, SIGNAL(readyRead()), this, SLOT(onSokReadyRead()));
connect(_sok, SIGNAL(connected()), this, SLOT(onSokConnected()));
connect(_sok, SIGNAL(disconnected()), this, SLOT(onSokDisconnected()));
connect(_sok, SIGNAL(error(QAbstractSocket::SocketError)),this, SLOT(onSokDisplayError(QAbstractSocket::SocketError)));
}
//по нажатию кнопки подключаемся к северу, отметим, что connectToHost() возвращает тип void, потому, что это асинхронный вызов и в случае ошибки будет вызван слот onSokDisplayError
void Dialog::on_pbConnect_clicked()
{
_sok->connectToHost(ui->leHost->text(), ui->sbPort->value());
}
void Dialog::onSokConnected()
{
//после подключения следует отправить запрос на авторизацию
QByteArray block;
QDataStream out(&block, QIODevice::WriteOnly);
//резервируем 2 байта для размера блока. Класс MyClient используется в реализации сервера, но тут используем статические члены этого класса - константы команд
//третий байт - команда
out << (quint16)0 << (quint8)MyClient::comAutchReq << ui->leName->text();
_name = ui->leName->text();
//возваращаемся в начало
out.device()->seek(0);
//вписываем размер блока на зарезервированное место
out << (quint16)(block.size() - sizeof(quint16));
_sok->write(block);
}
void Dialog::onSokReadyRead()
{
//тут обрабатываются данные от сервера
QDataStream in(_sok);
//если считываем новый блок первые 2 байта это его размер
if (_blockSize == 0) {
//если пришло меньше 2 байт ждем пока будет 2 байта
if (_sok->bytesAvailable() < (int)sizeof(quint16))
return;
//считываем размер (2 байта)
in >> _blockSize;
}
//ждем пока блок прийдет полностью
if (_sok->bytesAvailable() < _blockSize)
return;
else
//можно принимать новый блок
_blockSize = 0;
//3 байт - команда серверу
quint8 command;
in >> command;
switch (command)
{
//сервер отправит список пользователей, если авторизация пройдена, в таком случае третий байт равен константе MyClient::comUsersOnline
case MyClient::comUsersOnline:
{
QString users;
in >> users;
if (users == "")
return;
//сервер передает имена через запятую, как строку (хотя можно писать в поток и объект QStringList)
QStringList l = users.split(",");
//обновляем гуи
ui->lwUsers->addItems(l);
}
break;
//общее сообщение от сервера
case MyClient::comPublicServerMessage:
{
//считываем и добавляем в лог
QString message;
in >> message;
AddToLog("[PublicServerMessage]: "+message, Qt::red);
}
...
}
}
На этом с клиентом все - основные моменты описаны, остальное все довольно просто.
Сервер
В сервере будет все посложнее - отделим гуи (dialog), сервер (myserver) и клиент (myclient). Кто малознаком с сокетами может не понять какой клиент может быть в сервере? Так вот, при подключении сокета к серверу, на сервере, как привило, создается "клиентский" сокет, который добавляется в массив (напрашивается мысль использовать ассоциативный массив, но для простоты возьмем QList).
//dialog.cpp
Dialog::Dialog(QWidget *parent) :QDialog(parent), ui(new Ui::Dialog)
{
//создаем сервер. первый параметр стандартный - parent, второй - передадим ссылку на объект виджета, для подключения сигналов от myclient к нему
_serv = new MyServer(this, this);
//подключаем сигналы от виджета к серверу
connect(this, SIGNAL(messageFromGui(QString,QStringList)), _serv, SLOT(onMessageFromGui(QString,QStringList)));
...
//по умолчанию запускаем сервер на 127.0.0.1:1234
if (_serv->doStartServer(QHostAddress::LocalHost, 1234))
{...}
else
{...}
}
Унаследуем класс от QTcpServer, это нужно для переопределения виртуальной функции incomingConnection, в которой перехватывается входящее соединение (входной параметр - дескриптор сокета)
//myserver.h
class MyServer : public QTcpServer
{
public:
bool doStartServer(QHostAddress addr, qint16 port);
void doSendToAllUserJoin(QString name); //уведомить о новом пользователе
void doSendToAllUserLeft(QString name);
void doSendToAllMessage(QString message, QString fromUsername); //разослать сообщение
void doSendToAllServerMessage(QString message);//серверное сообщение
void doSendServerMessageToUsers(QString message, const QStringList &users); //приватное серверное сообщение
void doSendMessageToUsers(QString message, const QStringList &users, QString fromUsername);
QStringList getUsersOnline() const; //узнать список пользователей
bool isNameValid(QString name) const; //проверить имя
bool isNameUsed(QString name) const; //проверить используется ли имя
protected:
void incomingConnection(int handle);
private:
QList<MyClient *> _clients; //список пользователей
QWidget *_widget; //ссылка на виджет для подключения к нему сигналов от myclient
};
//myserver.cpp
void MyServer::incomingConnection(int handle)
{
//передаем дескрпитор сокета, указатель на сервер (для вызова его методов), и стандартный параметр - parent
MyClient *client = new MyClient(handle, this, this);
//подключаем сигналы напрямую к виджету, если его передали в конструктор сервера
if (_widget != 0)
{
connect(client, SIGNAL(addUserToGui(QString)), _widget, SLOT(onAddUserToGui(QString)));
connect(client, SIGNAL(removeUserFromGui(QString)), _widget, SLOT(onRemoveUserFromGui(QString)));
...
}
_clients.append(client);
}
/*
При рассылке сообщения всем нужно делать проверку авторизован ли текущий пользователь, ибо в массиве _clients, возможно, находятся не авторизованные
*/
void MyServer::doSendServerMessageToUsers(QString message, const QStringList &users)
{
//знакомые по клиенту действия
QByteArray block;
QDataStream out(&block, QIODevice::WriteOnly);
out << (quint16)0 << MyClient::comPrivateServerMessage << message;
out.device()->seek(0);
out << (quint16)(block.size() - sizeof(quint16));
//отправка сообщения всем (тут отсутствует проверка, ибо все пользователи в users гарантированно авторизованы)
for (int j = 0; j < _clients.length(); ++j)
if (users.contains(_clients.at(j)->getName()))
_clients.at(j)->_sok->write(block);
}
Для класса-клиента, пожалуй, стоит описать только интерфейс, тут все понятно по аналогии. MyClient не обязательно наследовать от QTcpSocket, можно сделать иначе:
//myclient.h
class MyClient : public QObject
{
//откроем доступ классу MyServer к приватному члену _sok
friend class MyServer;
public:
static const QString constNameUnknown;
static const quint8 comAutchReq = 1;
static const quint8 comUsersOnline = 2;
...
static const quint8 comErrNameUsed = 202;
void setName(QString name) {_name = name;}
QString getName() const {return _name;}
bool getAutched() const {return _isAutched;}
void doSendCommand(quint8 comm) const;
void doSendUsersOnline() const;
signals:
//сигналы для обновления гуи
void addUserToGui(QString name);
void removeUserFromGui(QString name);
void messageToGui(QString message, QString from, const QStringList &users);
//сигнал удаления пользователя из QList
void removeUser(MyClient *client);
//знакомые слоты для работы с сокетом
private slots:
void onConnect();
void onDisconnect();
void onReadyRead();
void onError(QAbstractSocket::SocketError socketError) const;
private:
QTcpSocket *_sok; //сокет
MyServer *_serv; //ссылка на сервер
quint16 _blockSize; //текущий размер блока
QString _name; //имя
bool _isAutched; //флаг авторизации
};