Предисловие
Статья ориентирована в основном на новичков. Целью ее написания является быстрое и максимально подробное описание сокетов, для начального понимания сети и сокетов. В свое время искал подобную, но нужны были подробные примеры. В стандартном примере fortune server/client, который идет с qt очень плохо показывают возможности сокетов.
Итак, сервер умеет:
- «Слушать» произвольный адрес, порт
- Авторизовать клиента по имени
- Отправлять общие, приватные, серверные сообщения
- Отправлять список пользователей
Для понимания это будут Гуи-приложения:

В qt существуют классы QTcpSocket и QTcpServer для работы с сокетами. Используя сигналы и слоты, с ними можно работать в неблокирующем (асинхронном режиме). Это значит, если подключение к серверу занимает заметное количество времени, гуи не блокируется, а продолжает обрабатывать события, а когда произойдет подключение (либо ошибка), вызовется определенный слот (в текущем случае подключенный к сигналу connected()).
Клиент
Начнем с простого — не будем наследовать классы, а воспользуемся QTcpSocket:
Вообще при работе с сокетами нужно смотреть на данные как набор байтов, иначе могут быть проблемы с отображением информации лишь частично (пришло не полное сообщение, а следующее отображается с куском предыдущего). Чтобы избежать этих неприятностей будем использовать потоки данных (QDataStream) и предавать между сокетами блоки, в которых первые 2 байта это размер текущего блока, 3-й байт это команда клиента серверу (или ответ сервера), а остальное — данные в зависимости от команды. Стоит сказать, что протокол tcp гарантирует доставку всех пакетов, поэтому можно смело ждать полный размер блока, прежде чем его обрабатывать.//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;//имя клиента };
На этом с клиентом все — основные моменты описаны, остальное все довольно просто.//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; //флаг авторизации };
Upd 2017:
Увы, но исходники уже утеряны навсегда
Upd 2018:
Как известно, все что когда либо было загружено в интернет остается там навсегда. Благодаря vladpower исходники снова доступны
