(495) 925-0049, ITShop интернет-магазин 229-0436, Учебный Центр 925-0049
  Главная страница Карта сайта Контакты
Поиск
Вход
Регистрация
Рассылки сайта
 
 
 
 
 

Клиент-серверный чат, используя сокеты Qt/C++

Статья ориентирована в основном на новичков. Целью ее написания является быстрое и максимально подробное описание сокетов, для начального понимания сети и сокетов. В свое время искал подобную, но нужны были подробные примеры. В стандартном примере 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; //флаг авторизации
};

 

Ссылки по теме


 Распечатать »
 Правила публикации »
  Обсудить материал в конференции Embarcadero »
Написать редактору 
 Рекомендовать » Дата публикации: 01.11.2011 
 

Магазин программного обеспечения   WWW.ITSHOP.RU
Delphi Professional Named User
Enterprise Connectors (1 Year term)
IBM DOMINO ENTERPRISE CLIENT ACCESS LICENSE AUTHORIZED USER LICENSE + SW SUBSCRIPTION & SUPPORT 12 MONTHS
SAP CRYSTAL Reports 2013 WIN INTL NUL
EMS Data Export for PostgreSQL (Business) + 1 Year Maintenance
 
Другие предложения...
 
Курсы обучения   WWW.ITSHOP.RU
 
Другие предложения...
 
Магазин сертификационных экзаменов   WWW.ITSHOP.RU
 
Другие предложения...
 
3D Принтеры | 3D Печать   WWW.ITSHOP.RU
 
Другие предложения...
 
Новости по теме
 
Рассылки Subscribe.ru
Информационные технологии: CASE, RAD, ERP, OLAP
Новости ITShop.ru - ПО, книги, документация, курсы обучения
Программирование на Microsoft Access
CASE-технологии
Программирование в AutoCAD
Компьютерный дизайн - Все графические редакторы
СУБД Oracle "с нуля"
 
Статьи по теме
 
Новинки каталога Download
 
Исходники
 
Документация
 
Обсуждения в форумах
Слот Биг Бэнг (6)
Большой взрыв как правящая теория сотворения мира. Теперь вы можете поиграть слот Big Bang в...
 
Отличается ли ДрифтКазино от беттинга? (57)
Друзья, давно заметил, что на Дрифте уже несколько месяцев во всю рекламируется и предлагается...
 
Подскажите лучшее онлайн казино (8)
Вот нашёл интересную игровую площадку, и это онлайн казино Вавада. Может кто что подсказать по...
 
Помощь по MS Access (345)
Доброе время суток. Случайно оказался на этом сайте, искал статьи по OLAP. Вижу, что...
 
Требуется Краснодар: Java -разрабочик (2)
Обязанности: Разработка корпоративных backend-приложений для автоматизации розничных продаж...
 
 
 



    
rambler's top100 Rambler's Top100