В этой статье мы рассмотрим написание своего простейшего сервера на базе Qt5, которые использует ssl сертификаты для авторизации. В процессе создания статьи мы рассмотрим базовые понятия к подходу организации сервера и напишем небольшой пример для передачи сообщений по сети.
Для работы нам понадобится Qt версии не ниже 5.3 так, так начиная с этой версии были исправлены некоторые баги при работе с сокетами
В качестве платформы для разработки используется 64x разрядная Ubuntu 14.04 LTS с установленным openssl версией 1.0.1f .
И так приступим. Создадим консольное приложение и назовем его к примеру SslServer. Для работы нам понадобится модуль network и его необходимо подключить в фале проекта, заодно укажем в конфигурации, что мы будем использовать c++11.
Открываем фал проекта SslServer.pro и дописываем туда 2 строчки:
QT += core
QT += network
QT -= gui
CONFIG += c++11
TARGET = SslServer
CONFIG += console
CONFIG -= app_bundle
TEMPLATE = app
SOURCES += main.cpp
Теперь необходимо создать класс который будет обрабатывать соединения, которые поступают на наш сервер по указанно порту. Создадим свой класс , который наследуется от QTcpServer, незабываем про Q_OBJECT для работы с сигналами и слотами.
namespace server {
class SslServer : public QTcpServer
{
Q_OBJECT
public:
SslServer(int serverPort);
private:
int serverPort;
void incomingConnection(qintptr handle);
};
}
Класс QTcpServer предоставляет метод
void incomingConnection(qintptr handle);
который нам необходимо переопределить, что бы мы могли принимать соединения.
Создадим еще один метод для запуска нашего сервера:
bool startServer();
данный метод будет запускать наш сервер и в случае ошибки будет выдавать сигнал ошибки сервера с кодом ошибки, так же, нам необходимо создать дополнительно сигнал для старта для сервера.
Для начала определим тип ошибок которыми оперирует наш сервер. Для этого создадим отдельно заголовочный фал type.h в котором будем прописывать наши возможные варианты.
В файл type.h пишем:
namespace server {
enum class Error: int {
SERVER_START_ERROR = 1
};
}
Дописав наши методы и сигналы в класс мы получим слудующее:
class SslServer : public QTcpServer
{
Q_OBJECT
public:
SslServer(int serverPort);
bool startServer();
signals:
void error(Error errorCode);
void started();
public slots:
private:
int serverPort;
void incomingConnection(qintptr handle);
};
Метод startServer() должен указать, что мы будем слушать заданный порт а также какой интерфейс слушать. Для этого есть метод
bool QTcpServer::listen(const QHostAddress & address = QHostAddress::Any, quint16 port = 0)
иректива QhostAddress указывает какой интерфейс слушать, если мы укажем QhostAddress::Any то мы будем слушать любой интерфейс.
Наш метод выглядит следующим образом:
bool SslServer::startServer()
{
//указуем что мы слушаем заданный порт на любом интерфейсе
if (listen(QHostAddress::Any,this->serverPort)) {
qDebug()<<"Server start on port:" << this->serverPort;
//посылаем сигнал, что наш сервер запущен
emit started();
} else {
qDebug()<<"Cant satrt server on port:"<< this->serverPort;
//посылаем сигнал ошибки, в параметре указываем код ошибки — ошибка запуска сервера
emit error(Error::SERVER_START_ERROR);
return false;
}
return true;
}
Осталось определить метод который принимает наши соединения. На данный момент оставим в нем только сообщения что к нам пришло соединения, но пока его не обрабатываем.
void SslServer::incomingConnection(qintptr handle)
{
qDebug()<<"incomingConnection on socket:"<< handle;
}
И конструктор:
SslServer::SslServer(int serverPort)
{
this->serverPort = serverPort;
}
Если мы скомпилируем и запустим то, что мы написали, мы увидим следующее:
Теперь необходимо проверить что наш сервер принимает соединения на указанный нами порт. Для этого откроем терминал и напишем
telnet 127.0.0.1 1234
мы увидим что наш сервер принимает соединения
и если мы еще параллельно откроем терминал и подключимся еще, то мы увидим что изменился дескриптор сокета, который обслуживает данное соединения.
Теперь нам необходимо написать обработчик соединения. Для этого создадим класс ClientConnection, который будет обрабатывать новое соединения. Данный класс должен наследоваться от QObject.
Данный класс будет работать через дескриптор сокета, который мы получили в методе incomingConnection. Для передачи данных мы будем использовать QsslSocket. Если открыть документацию по сокету, то мы увидим список сигналов которые он использует в своей работе. Некоторые из них нам пригодятся и по этому добавим слоты в класс ClientConnection для обработки этих сигналов.
Для работы создадим метод bool setSocket(int socketID), который свяжет наш обработчик соединения с сокетом.
bool ClientConnection::setSocket(int socketID)
{
//Создаем новый сокет
this->socket = new QSslSocket(this);
//указываем дескриптор сокета
if (this->socket->setSocketDescriptor(socketID)) {
this->soketID = socketID;
//соединяем сигнали сокета с нашим обработчиком
connect(this->socket, SIGNAL(encrypted()), this, SLOT(encrypted()));
connect(this->socket, SIGNAL(readyRead()), this, SLOT(readyRead()));
connect(this->socket, SIGNAL(disconnected()), this, SLOT(disconnected());
connect(this->socket, SIGNAL(connected()),this, SLOT(connected()));
connect(this->socket, SIGNAL(sslErrors(QList)), this, SLOT(socketSslErrors(QList));
connect(this->socket, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(socketError(QAbstractSocket::SocketError)));
//указываем ключи, которые мы сгенерировали
this->socket->setLocalCertificate("key.pem");
this->socket->setPrivateKey("key.key");
//указываем версию протокола
this->socket->setProtocol(QSsl::TlsV1_2);
//устанвливаем наше соединение по сокету
this->socket->startServerEncryption();
return true;
} else {
delete this->socket;
return false;
}
}
Для слота socketSslErrors и socketError выведем сообщение об ошибке что мы понимали состояния сокета.
void ClientConnection::socketSslErrors(const QList & list)
{
qDebug()<<"soket ssl error";
/* выводим все полученные ошибки */
foreach (QSslError item, list) {
qDebug()<< item.errorString();
}
}
void ClientConnection::socketError(QAbstractSocket::SocketError error)
{
qDebug() << "socketError: "<< error;
}
Для примера, в методе setSocket мы просто указали, без никаких проверок, что наши ключи находятся в том же каталоге что и исполняемый файл.
Для генерации данных ключей необходимо использовать утилиту openssl
openssl req -x509 -newkey rsa:1024 -keyout key.key -out key.pem -days 365 -nodes
При генерации ключа, важным моментом будет при заполнение поле
Common Name (e.g. server FQDN or YOUR name) []:
Вам необходимо будет указать ip адрес сервера на котором будет запущен сервер, иначе сертификаты не будут работать.
Комментарий (3)
Константин
24 Марта 2020 в 14:30А можете выложит исходники данной программы. Просто непонятно содержимое класса ClientConnection, какой у него заголовочный файл и где создаётся объект класса ClientConnection.
Vin
10 Сентября 2015 в 12:26Из названия не понятно, какого рода сервер? HTTP или другой? Посмотрел статью по диагонали, походу имеется ввиду простой tcp сервер с поддержкой ssl, используя класс QTcpServer
admin
Vin, да именно так, Вы правы. В статье описано поcтроения простого tcp сервера с поддержкой сертификатов для шифрования данных