Многопоточный SOCKS 4 сервер на Qt

    Время от времени на форумах рунета в ветке Qt появляются вопросы связанные с программированием сетевых приложений. Одна из проблем которая терзает этих людей, — это подход к организации сервера. Обычно перечисляют три подхода:
    • однопоточный асинхронный;
    • многопоточный, создавать по потоку на соединение;
    • многопоточный, с пулом потоков на QThreadPool и QRunnable.

    Когда говоришь про фиксированное количество рабочих потоков со своим циклом обработки событий, то просят привести пример. Дальше будет приведен пример сервера с пулом потоков, каждый из которых имеет свой цикл обработки событий.

    Действующие лица:
    • класс Server, приминающий соединения и раздающий задачи рабочим;
    • класс Worker, экземпляры которого будут создавать в рабочих потоках экземпляры класса Client;
    • класс Client инкапсулирующий запросы клиента и реализующий SOCKS 4

    Самый простой из этой троицы Worker, он наследуется от QObject и реализует всего одну функцию создания клиента и чисто для «правильного» употребления потоков:
    class Worker: public QObject
    {
    Q_OBJECT
    
    public:
    
        Q_INVOKABLE void addClient(qintptr socketDescriptor);
    };
    

    void Worker::addClient(qintptr socketDescriptor)
    {
        new Client(socketDescriptor, this);
    }
    

    Сервер так же прост:
    class Server: public QTcpServer
    {
    Q_OBJECT
    
    public:
    
        Server(size_t threads = 4, QObject * parent = nullptr);
        ~Server();
    
    protected:
    
        virtual void incomingConnection(qintptr socketDescriptor);
    
    private:
    
        void initThreads();
    
    private:
    
        size_t m_threadCount;
    
        QVector<QThread*> m_threads;
        QVector<Worker*> m_workers;
        size_t m_rrcounter;
    };
    

    Server::Server(size_t threads, QObject * parent) :
            QTcpServer(parent),
            m_threadCount(threads),
            m_rrcounter(0)
    {
        initThreads();
    }
    
    Server::~Server()
    {
        for(QThread* thread: m_threads)
        {
            thread->quit();
            thread->wait();
        }
    }
    
    void Server::initThreads()
    {
        for (size_t i = 0; i < m_threadCount; ++i)
        {
            QThread* thread = new QThread(this);
    
            Worker* worker = new Worker();
            worker->moveToThread(thread);
            connect(thread, &QThread::finished,
                    worker, &QObject::deleteLater);
    
            m_threads.push_back(thread);
            m_workers.push_back(worker);
    
            thread->start();
        }
    }
    
    void Server::incomingConnection(qintptr socketDescriptor)
    {
        Worker* worker = m_workers[m_rrcounter % m_threadCount];
        ++m_rrcounter;
    
        QMetaObject::invokeMethod(worker, "addClient",
                Qt::QueuedConnection,
                Q_ARG(qintptr, socketDescriptor));
    }
    

    Он в конструкторе создает потоки, рабочих и перемещает рабочих в потоки. Каждое новое соединение он передает рабочему. Рабочего он выбирает «почестноку», т. е. по Round-robin.

    SOCKS 4 очень простой протокол, нужно лишь:
    1. прочитать IP-адрес, номер порта;
    2. установить соединение с «миром»;
    3. отправить клиенту сообщение, что запрос подтвержден;
    4. пересылать данные из одного сокета в другой, пока кто-нибудь не закроет соединение.

    class Client: public QObject
    {
    Q_OBJECT
    
    public:
    
        Client(qintptr socketDescriptor, QObject* parent = 0);
    
    public slots:
    
        void onRequest();
    
        void client2world();
        void world2client();
    
        void sendSocksAnsver();
    
        void onClientDisconnected();
        void onWorldDisconnected();
    
    private:
    
        void done();
    
    private:
    
        QTcpSocket m_client;
        QTcpSocket m_world;
    };
    

    namespace
    {
    #pragma pack(push, 1)
        struct socks4request
        {
            uint8_t version;
            uint8_t command;
            uint16_t port;
            uint32_t address;
            uint8_t end;
        };
    
        struct socks4ansver
        {
            uint8_t empty = 0;
            uint8_t status;
            uint16_t field1 = 0;
            uint32_t field2 = 0;
        };
    #pragma pack(pop)
    
        enum SocksStatus
        {
            Granted = 0x5a,
            Failed = 0x5b,
            Failed_no_identd = 0x5c,
            Failed_bad_user_id = 0x5d
        };
    }
    Client::Client(qintptr socketDescriptor, QObject* parent) :
            QObject(parent)
    {
        m_client.setSocketDescriptor(socketDescriptor);
    
        connect(&m_client, &QTcpSocket::readyRead,
                this, &Client::onRequest);
    
        connect(&m_client,&QTcpSocket::disconnected,
                this, &Client::onClientDisconnected);
    
        connect(&m_world, &QTcpSocket::connected,
                this, &Client::sendSocksAnsver);
    
        connect(&m_world, &QTcpSocket::readyRead,
                this, &Client::world2client);
    
        connect(&m_world,&QTcpSocket::disconnected,
                this, &Client::onWorldDisconnected);
    }
    
    void Client::onRequest()
    {
        QByteArray request = m_client.readAll();
    
        socks4request* header = reinterpret_cast<socks4request*>(request.data());
    
    #if Q_BYTE_ORDER == Q_LITTLE_ENDIAN
        const QHostAddress address(qFromBigEndian(header->address));
    #else
        const QHostAddress address(header->address);
    #endif
    
    #if Q_BYTE_ORDER == Q_LITTLE_ENDIAN
        const uint16_t port = qFromBigEndian(header->port);
    #else
        const uint16_t port = header->port;
    #endif
        //qDebug()<<"connection:"<<address<<"port:"<<port;
    
        m_world.connectToHost(address, port);
    
        disconnect(&m_client, &QTcpSocket::readyRead, this,
                &Client::onRequest);
    
        connect(&m_client, &QTcpSocket::readyRead, this,
                &Client::client2world);
    }
    
    void Client::sendSocksAnsver()
    {
        socks4ansver ans;
        ans.status = Granted;
        m_client.write(reinterpret_cast<char*>(&ans), sizeof(ans));
        m_client.flush();
    }
    
    void Client::client2world()
    {
        m_world.write(m_client.readAll());
    }
    
    void Client::world2client()
    {
        m_client.write(m_world.readAll());
    }
    
    void Client::onClientDisconnected()
    {
        m_world.flush();
    
        done();
    }
    
    void Client::onWorldDisconnected()
    {
        m_client.flush();
    
        done();
    }
    
    void Client::done()
    {
        m_client.close();
        m_world.close();
    
        deleteLater();
    }
    

    Epoll и Qt

    Крик подобен грому:
    — Дайте людям рому
    Нужно по любому
    Людям выпить рому!


    Если мы скомпилим предыдущий код и прогоним его через strace -f, то увидим вызовы poll. Обязательно найдется кто-то, кто скажет своё веское «фи», мол с epoll будет «ну ваще ракета».

    В Qt есть класс QAbstractEventDispatcher, позволяющий определять свой диспетчер событий. Естественно нашлись добрые люди которые сделали и выложили диспетчеры с разными бекенд. Вот небольшой их список:

    При использовании своего диспетчера в main.cpp прописываем
    QCoreApplication::setEventDispatcher(new QEventDispatcherEpoll);
    QCoreApplication app(argc, argv)
    

    а метод initThreads у сервера становится таким:
    void Server::initThreads()
    {
        for (size_t i = 0; i < m_threadCount; ++i)
        {
            QThread* thread = new QThread(this);
            thread->setEventDispatcher(new QEventDispatcherEpoll);
            Worker* worker = new Worker();
            worker->moveToThread(thread);
            connect(thread, &QThread::finished,
                    worker, &QObject::deleteLater);
    
            m_threads.push_back(thread);
            m_workers.push_back(worker);
    
            thread->start();
        }
    }
    

    И если мы снова запустим strace, то увидим заветные вызовы функций с префиксом epoll_.

    Выводы

    Выводы сугубо прагматические.

    Если вы прикладной программист и у вас нет задач из разряда «больших» данных или highload по Бунину, то пишите на чем хотите и как можете. Задача прикладного программиста выдать продукт определенного качества, затратив определенное количество ресурсов. В противном случае одними лишь сокетами с epoll не обойдешься.

    P.S.

    Исходные коды доступны на GitHub.
    Поделиться публикацией
    Похожие публикации
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 14
    • +4
      До конца статьи надеялся и верил, что будет про Epoll. И, как оказалось, не зря. Спасибо!
      • 0
        Спасибо за статью!
        Последний абзац заинтриговал. Есть ли какие-то общепринятые практики, куда развивать пример в случае задач из разряда highload? Или это тема для следующего поста? :)
        • +1
          Где-то читал, что qt больше подходит для приложений с gui, а в высоконагруженных проектах показывает плохие результаты из-за использования сигналов и слотов, мол, они медленнее, чем в других языках, где используются обратные вызовы (callbacks).
          На хабре была статья с тестом, но там не было для сравнения веб-сервера на qt.
          В связи с этим вопрос: вы в примере используете SOCKS-сервер, сколько реально он может выдержать соединений по сравнению с остальными?
          Не делали таких тестов?
          Было бы интересно прочесть про highload и qt на примерах.
          • 0
            Поразмышляю. Всегда считал, что серверы пишутся с использованием других инструментов. Плюс Qt в переносимости. Однако, в случае с epoll это будет linux и какой тут разговор о переносимости?
            С другой стороны, если я хорошо знаю Qt и плохо знаю стандартные библиотеки, то мне будет очень комфортно писать приложение с применением всяческих QFile, QQueue, QMap и т.д. пусть даже это приложение и будет серверным.
            По поводу сигналов и слотов, наслышан, что они медленнее. Но у такой системы есть несколько важных свойств, которых нет у callbacks: 1) место, из которого испускается сигнал никак не привязано к месту приема (слоту), то есть сигнал выпустил, а кто его принимает — уже не наше дело; 2) приемников сигнала (слотов) может быть несколько — к приему сообщения готовятся те, кому оно нужно; 3) в Qt можно обмениваться сигналами между thread'ами. Это позволяет строить некие подобия системы акторов.
            • +2
              libevent, libuv переносимые. Вообще удивительно, что существуют столько альтернативных диспетчеров.
            • +2
              Тестирование прокси сервера это отличись тормоза ресурса, от тормозов твоего сервера. Можно пойти по пути описанным в этом письме. Я пробовал словить блокировки внутри malloc, что бы сравнить с jemalloc/tcmalloc.

              Да. Сигналы и слоты медленные, особенно если использовать синтаксис
              QObject::connect(const QObject * sender, const char * signal, const char * method, Qt::ConnectionType type = Qt::AutoConnection) const
              

              Понятно что работа со строками быстрой быть не может. Сравнение между Qt4/Qt5 слотами можно посмотреть Benchmark for conception


              Я естественно не делаю ничего из highload на Qt, потому что уткнемся в память, а именно в операциях malloc/free, фрагментация памяти и т.д. Потом еще обязательно придется оптимизировать парсеры, если они есть, как например в nxweb – HTTP сервер для приложений на Си. Если вы хотите с базой общаться, то скорее всего захочется асинхронно и с ней работать, а интерфейс QSqlQuery не предоставляет этой возможности. Потом еще вдруг заходим размещение ответа от базы за оптимизировать как Побег из темницы типов. Реализуем работу с данными, тип которых определяется динамически. Если мы скажем захотим сделать игровой сервер, то epoll с сокетами будет недостаточно.
              • +3
                Использую Qt в своем проекте iCloud DNS Bypass.
                Сделал HTTP и DNS сервера на QTcpSocket и QUdpSocket.
                Постоянная нагрузка на HTTP 10-100 запросов в секунду и в сутки примерно 1 млн запросов.
                На DNS сервер 20-500 запросов в секунду и в сутки более 5 млн запросов.
                При этом средняя нагрузка обоих серверов 5% а пиковая 10%.
                Так что Qt вполне себя хорошо ведет на сетевых проектах.
                PS: в живую это выглядит так: youtu.be/FUysTl-TsIQ
                • 0
                  Обычно на форумах хотят от 500 запросов в секунду :)

                  Все зависит от задач. PhantomJS — это гибридное приложение написанное на Qt, C++, JavaScript. Его использует другой проект jsreport, где люди радуются 641 pdf станицей в секунду.
                  • +1
                    Я специально оставил % нагрузки чтоб можно было подсчитать максимально возможное количество в секунду.
                    Был такой случай.
                    Происходила атака от 1579 разных IP-адресов и нагрузка на 1 ядерный 3 ггз процессор составляла 80%.
                    А количество запросов в секунду превышало 7000.
                    При этом я и никто из пользователей не заметил сбоев в работе сервера.
                    О том что происходит атака я узнал по пол гигабайтным логам запросов.
                    • 0
                      Мне бы хотелось сравнить с Erlang, задача вроде простая, я смог бы написать. У вас есть какие-то нагрузочные тесты?
                      • 0
                        У меня есть логи за все времена и может каким-то образом получится воспроизвести нагрузку.
                        Но у меня нет возможности достать 1579 IP-адресов только чтоб произвести тест.
                        Если у вас есть чем сделать тесты то можем попробовать на одинаковых условиях.
                        Мой Qt HTTP сервер тут ui.iclouddnsbypass.com, данные доступны только из-под iOS юзер агентов.
                        А Qt DNS сервер запущен тут 78.109.17.60
                        Я использовал только сокеты и треды из Qt, вся реализация HTTP и DNS протоколов своя.
                        • 0
                          У меня получается каша в голове. Да и iOS не имею.
                          Хочу уточнить. У вас есть сервис для людей. Он состоит из: HTTP, DNS и SOCKS4 серверов?
                          • 0
                            Сервис только из HTTP и DNS, общее количество уникальных пользователей уже больше 1,5 млн.
                            А использование QTcpServer для HTTP у меня схоже с тем что в этой статье для SOCKS4.
                            Чтобы получать ответы от ui.iclouddnsbypass.com надо просто задать User-Agent любой iOS браузера Safari.
                • +1
                  А кто запрещает в Qt использовать callback-модель? Более того, ее успешно можно комбинировать с сигнально-слотовой моделью (что в рамках одной нити исполнения тоже callback-модель). Это все-таки C++, просто с хорошими фичами. :-)

                  Инструментов в Qt, для работы с классической callback-моделью в купе с сигналами слотами, полным полно.

              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.