QTcpServer: связать сокет с пользовательскими настройками - PullRequest
0 голосов
/ 16 января 2019

Итак, здесь у меня есть какое-то сложное клиент-серверное приложение, и некоторая его часть открывает порт для прослушивания и дальнейшего сетевого взаимодействия.QTcpServer + QTcpSocket (подробности будут описаны ниже).

Этот «сетевой уровень» приложения работал нормально везде, кроме Windows 8.1 Pro.Только не спрашивайте, почему наш клиент решил, что его можно использовать в качестве сервера ... Случилось так, что неправильный перезапуск процесса, который открыл TCP-соединение на определенном порте для прослушивания, иногда приводит к тому, что этот порт не подходит длялюбые последующие попытки связать это.Это выглядит как какая-то магия, но поведение происходило следующим образом:

  • Порт 8554 прослушивался ("соединение установлено") процессом
  • Процесс аварийно завершается или каким-либо образом завершаетсяпри подключении клиентов через этот сокет
  • Процесс перезапускается и снова пытается прослушивать порт.Сбой с «уже используется».
  • Я останавливаю сервер и пытаюсь проверить порт через netstat -an.Это бесплатно.
  • Я жду некоторое время и пытаюсь проверить порт через powershell, например:

    $Listener = [System.Net.Sockets.TcpListener]8554
    $Listener.Start()
    

    Нет, это приводит к той же ошибке, что и "уже используется"Msgstr ""

  • Я могу привязать сокет к другому порту, на них также работает фрагмент powershell.Но аварийный перезапуск нашего сервера также «ломает» их, сценарий тот же.
  • Как только порт «сломан», перезапуск Windows - единственное лекарство от него.
  • Привязка к »"любой адрес", т.е. 0.0.0.0:8554 ведет себя так, как описано.Привязка к точному IP-адресу, например 10.11.12.123:8554, лучше, проверила его при попытке привязать FileZilla к «битому порту».

Теперь проблема кодирования.Предоставление точного IP-адреса для привязки выглядит плохой идеей, хотя бы в нашей архитектуре, поэтому я решил использовать SO_REUSEADDR в Windows.Но, похоже, мне нужно установить эту опцию перед вызовами bind / listen, что приводит к значительной настройке использования QTcpServer.Имейте в виду, что приложение является кроссплатформенным (WinSock + sys / socket + some #defines, если нужно использовать что-то кроме методов Qt ...).Ааа, и вот моя любимая старая версия настройки QTcpServer, посмотрите:

QTcpServer2.h

#pragma once

#include <QTcpServer>
#include <QMutex>

class QTcpServer2 : public QTcpServer
{
  QMutex         mConnectionMutex;
  QList<qintptr> mSocketDescriptors;

private:
  virtual void incomingConnection(qintptr socketDescriptor) override;

public:
  bool TakeIncomingSocketDescription(qintptr& socketDescriptor);

public:
  QTcpServer2();
};

QTcpServer2.cpp

#include <QMutexLocker>

#include "QTcpServer2.h"

void QTcpServer2::incomingConnection(qintptr socketDescriptor)
{
  QMutexLocker lock(&mConnectionMutex);
  mSocketDescriptors.append(socketDescriptor);
}

bool QTcpServer2::TakeIncomingSocketDescription(qintptr& socketDescriptor)
{
  QMutexLocker lock(&mConnectionMutex);
  if (mSocketDescriptors.empty()) {
    return false;
  }
  socketDescriptor = mSocketDescriptors.takeFirst();
  return true;
}

QTcpServer2::QTcpServer2()
{ }

Использование:

bool NetServer::DoInitConnection()
{
  mNetServer = QSharedPointer<QTcpServer2>(new QTcpServer2);
  if (!mNetServer->listen(QHostAddress::AnyIPv4, mPort)) {
    Log.Fatal(QString("Listen port fail (port: %1)").arg(mPort), true);
    return false;
  }
  return true;
}

Вздох ... Да, я знаю о механизме ожидающих соединений, об идее сигнального слота, но это вне кода на данный момент.TakeIncomingSocketDescription используется для передачи дескриптора сокета куда-либо.И многопоточный доступ, да.Во всяком случае, этот кусок ... кода нуждается в рефакторинге, и мне очень нужен ваш совет: каков подходящий способ настройки привязки сокетов здесь? Предположим, что подклассы остаются, список дескрипторов тоже что-то не так-просто избавиться от.

Источники Qt показали мне следующее:

/*! \internal
*/
void QTcpServerPrivate::configureCreatedSocket()
{
#if defined(Q_OS_UNIX)
    // Under Unix, we want to be able to bind to the port, even if a socket on
    // the same address-port is in TIME_WAIT. Under Windows this is possible
    // anyway -- furthermore, the meaning of reusable on Windows is different:
    // it means that you can use the same address-port for multiple listening
    // sockets.
    // Don't abort though if we can't set that option. For example the socks
    // engine doesn't support that option, but that shouldn't prevent us from
    // trying to bind/listen.
    socketEngine->setOption(QAbstractSocketEngine::AddressReusable, 1);
#endif
}

Это именно то, что я хочу, но нет доступа к этим внутренним компонентам из интерфейса классов Qt.QTcpServer выполняет bind вызов неявно, поэтому я не могу передать QAbstractSocket::ReuseAddressHint туда.

Возможно, есть какое-то аккуратное решение, возможно, нет.Буду благодарен за любые идеи.

1 Ответ

0 голосов
/ 23 января 2019

Мне понадобилось время, чтобы рассмотреть все возможные решения, хотя их было совсем немного.

  1. Здесь Я нашел что-то похожее на мою проблему. Сделал адаптацию кода, предоставленного там ... Хорошо, просто взял большую часть этого ответа =)
  2. @G.M. подтолкнул меня к мысли, что сокет api в этом случае должен отличаться только заголовками

В руководствах говорилось, что флаг SO_LINGER часто является результатом плохой архитектуры (в большинстве случаев клиент должен закрывать соединение, а не сервер), после нескольких экспериментов этот флаг не очень пригодился в мое дело.

Привязка к точному IP приводит к нескольким сложностям, скорее костыль, чем решение ИМХО.

Итак, после незначительных адаптаций класс прослушивания сокетов стал примерно таким:

QTcpServer2.h

#pragma once

#include <QTcpServer>
#include <QMutex>

class QTcpServer2 : public QTcpServer
{
  QMutex            mConnectionMutex;
  QList<qintptr>    mSocketDescriptors;
  const int         mPort;
  bool              mConnectionSuccessful;

private:
  /*override */virtual void incomingConnection(qintptr socketDescriptor) Q_DECL_OVERRIDE;

public:
  bool TakeIncomingSocketDescription(qintptr& socketDescriptor);

  bool isConnectionSuccessfull() const
  { return mConnectionSuccessful; }

public:
  QTcpServer2(int port);
};

QTcpServer.cpp

#include "QTcpServer2.h"

#include <QMutexLocker>

#ifdef Q_OS_WIN
    #include <Windows.h>
    #pragma comment(lib, "ws2_32.lib")
#endif

#ifdef Q_OS_LINUX
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <netinet/tcp.h>
#endif

void QTcpServer2::incomingConnection(qintptr socketDescriptor)
{
    QMutexLocker lock(&mConnectionMutex);
    mSocketDescriptors.append(socketDescriptor);
}

bool QTcpServer2::TakeIncomingSocketDescription(qintptr& socketDescriptor)
{
    QMutexLocker lock(&mConnectionMutex);
    if (mSocketDescriptors.empty()) {
        return false;
    }
    socketDescriptor = mSocketDescriptors.takeFirst();
    return true;
}

QTcpServer2::QTcpServer2(int port)
    : QTcpServer()
    , mPort(port)
    , mConnectionSuccessful(false)
{
    // open server and listen on given port
    int sockfd = 0;
    struct sockaddr_in serv_addr;
    memset(&serv_addr, 0, sizeof(serv_addr));

    sockfd = ::socket(AF_INET, SOCK_STREAM, 0);
    if(sockfd < 0)
    {
        qDedug() << "QTcpServer2: socket couldn't be opened successfully!";
        return; //RET
    }

#ifdef Q_OS_WIN
    // Not required in Linux, won't make any good 
    int flag = 1;
    if(::setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, reinterpret_cast<char*>(&flag), sizeof(int)) < 0)
    {
        qDedug() << "QTcpServer2: Can't set SO_REUSEADDR";
        return; //RET
    }
#endif

    //set Address,IFace, Port...
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = INADDR_ANY;
    serv_addr.sin_port = htons(static_cast<ushort>(mPort));


    if (::bind(sockfd, (struct sockaddr *) &serv_addr, sizeof(sockaddr_in)) < 0)
    {
        qDedug() << "QTcpServer2: can't bind socket" ;
        return; //RET
    }

    if(::listen(sockfd, SOMAXCONN) < 0)
    {
        qDedug() << "QTcpServer2: can't listen on port";
        return; //RET
    }

    //forward our descriptor with SO_REUSEPORT to QTcpServer member
    setSocketDescriptor(sockfd);
    mConnectionSuccessful = true;
    qDedug() << "QTcpServer2: socket success =)";
}

Пока «волшебная проблема с сокетами» появилась только в Windows 8.1, #define делает SO_REUSEADDR используемым только в Windows. В Linux Qt уже устанавливает этот флаг самостоятельно (как показано в вопросе), поэтому этот небольшой патч только улучшил поведение Windows, чтобы соответствовать желаемому, предоставленному в Linux без проблем.

Надеюсь, ничего подобного не понадобится на других платформах.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...