1

I want to create a C++ multi-client server (asio) application that creates a server-side Qt GUI instance for every established connection to a client. The GUI will be associated with the connection.

Problem: The created GUI instances remain unresponsive. Removing a recursive call (shown below) in the asio::async_accept handler solves that, but obviously doesn't allow another client to connect.

The design is based on OneLoneCoder's Networking tutorial

The MWE does not include error handling, resource management etc...

MWE

Header file communication.h:

#ifndef COMMUNICATION_H
#define COMMUNICATION_H

#include <iostream>
#include <string>

#include "dep/3rdparty/asio/asio.hpp"

#include <QMainWindow>


// Provides read/write methods and message handling (removed for brevity).
// Associated with a Server/Client's asio::io_context, owning an individual socket.
class Connection : public std::enable_shared_from_this<Connection> {
public:
    typedef std::unique_ptr<Connection> Unique;
    typedef std::shared_ptr<Connection> Shared;

    explicit Connection(asio::io_context& context, asio::ip::tcp::socket&& socket);

    void connect(const std::string& remoteIp, const uint16_t remotePort);

    // contains methods for synchronous/asynchronous read/write,
    // but omitted for brevity.

private:
    asio::io_context& mContext;
    asio::ip::tcp::socket mSocket;
};


class Client : public std::enable_shared_from_this<Client> {
public:
    void connect(const std::string& serverIp, const uint16_t serverPort);

    void loop();

private:
    asio::io_context mContext;
    Connection::Unique mConnection;
};


class TestWindow : public QMainWindow {
Q_OBJECT
public:
    TestWindow(Connection& connectionRef, QWidget* parent) : mConnectionRef(connectionRef), QMainWindow(parent) {}

private:
    Connection& mConnectionRef;  // Used to interact with the client.
};


// Resembles a session that consists of a connection to a client, a GUI (and other things hidden for brevity). Used by the server to keep track of all associated GUIs and to communicate with them (e.g. close all, ...).
class Session {
public:
    typedef std::unique_ptr<Session> Unique;
    Session(Connection::Unique&& connection);

private:
    Connection::Unique mConnection;
    TestWindow* mTestWindow = nullptr;
    void setupAndShowGui();
};


class Server {
public:
    static const std::string localhost;
    explicit Server(std::string ip, uint16_t port);

    bool start();

private:
    asio::io_context mContext;
    asio::ip::tcp::acceptor mAcceptor;

    void waitForConnections();
    std::vector<Session::Unique> mSessions;
};


#endif //SAMPLEEDITOR_COMMUNICATION_H

Implementation file communication.cpp

#include "communication.h"


Connection::Connection(asio::io_context& context, asio::ip::tcp::socket&& socket) : mContext(context), mSocket(std::move(socket)) {}

void Connection::connect(const std::string& remoteIp, const uint16_t remotePort) {
    asio::ip::tcp::resolver::results_type endpoints = asio::ip::tcp::resolver(mContext).resolve(remoteIp, std::to_string(remotePort));
    asio::async_connect(mSocket, endpoints, [this](std::error_code ec, asio::ip::tcp::endpoint endpoint) {
        if (ec) {
            std::cout << "Connection error.\n";
        }
        std::cout << "Connected to " << mSocket.remote_endpoint() << "\n";
    });
}

void Client::connect(const std::string& serverIp, const uint16_t serverPort) {
    mConnection = std::make_unique<Connection>( Connection(mContext, asio::ip::tcp::socket(mContext)) );
    mConnection->connect(serverIp, serverPort);
    mContext.run();
}

void Client::loop() {
    for (;;) {
        // client would read/write messages and react
    }
}


Session::Session(Connection::Unique&& connection) : mConnection(std::move(connection)) {
    setupAndShowGui();
}

void Session::setupAndShowGui() {
    if (mTestWindow == nullptr) {
        mTestWindow = new TestWindow(*mConnection, nullptr);
        mTestWindow->show();
        std::cout << "Test Window created, associated with connection at address " << &mConnection << ".\n";
    }
}


const std::string Server::localhost = "127.0.0.1";

Server::Server(std::string ip, uint16_t port)
: mAcceptor(mContext, asio::ip::tcp::endpoint(asio::ip::make_address_v4(ip), port))
{
    asio::socket_base::keep_alive option(true);
    mAcceptor.set_option(option);
}

bool Server::start() {
    std::cout << "Starting server (" << mAcceptor.local_endpoint() << ").\n";
    waitForConnections();
    mContext.run();
    return true;
}

void Server::waitForConnections() {
    auto acceptHandler = [this](std::error_code ec, asio::ip::tcp::socket socket) {
        if (ec) {
            throw std::system_error(ec);
        }
        asio::socket_base::keep_alive option(true);
        socket.set_option(option);
        std::cout << "Received connection request from " << socket.remote_endpoint() << ".\n";
        Connection::Unique newConnection = std::make_unique<Connection>( Connection(mContext, std::move(socket)) );
        Session::Unique session = std::make_unique<Session>(Session(std::move(newConnection)));
        mSessions.push_back(std::move(session));
        waitForConnections();  // <--!-- remove this line and GUI becomes responsive
    };
    std::cout << "Waiting for connections...\n";
    mAcceptor.async_accept(acceptHandler);
}

Client application:

#include "communication.h"


int main(int argc, char* argv[]) {
    Client client;
    client.connect(Server::localhost, 60000);
    client.loop();
    return 0;
}

Server application:

#include <QApplication>

#include "communication.h"

class ServerGui : public QApplication {
public:
    ServerGui(int argc, char **argv) : QApplication(argc, argv), mServer(Server(Server::localhost, 60000)) {
        mServer.start();
    }

private:
    Server mServer;
};


int main(int argc, char* argv[]) {
    ServerGui gui(argc, argv);
    gui.exec();
    return 0;
}

Here is also a CMakeLists.txt file to build. We assume CMakeLists.txt and all source files to be in src/ and all asio files in src/dep/3rdparty/asio/.

cmake_minimum_required(VERSION 3.14)

project(mwe)

set(DEBUGGING_FLAGS false)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_BUILD_TYPE Release)

if(${DEBUGGING_FLAGS})
    set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /Od /Zi")
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /Od /Zi")
    set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /DEBUG")
endif()


set(DEP_PATH "${CMAKE_SOURCE_DIR}/dep")
set(QT_BASE_PATH "C:/Qt/6.3.2/msvc2019_64")

set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTORCC ON)

set(CMAKE_PREFIX_PATH "${QT_BASE_PATH}/lib/cmake/")
find_package(Qt6 REQUIRED COMPONENTS Core Widgets Concurrent)


set(HEADERS
    communication.h
)

set(SOURCES
    communication.cpp
)

set(FORMS
)

set(RESOURCES
)

set(INCLUDE_DIRS
    ${QT_BASE_PATH}/include/
)

add_executable(${PROJECT_NAME}_Server
    main_server.cpp
    ${HEADERS}
    ${SOURCES}
)
target_link_libraries(${PROJECT_NAME}_Server PRIVATE
        Qt6::Core Qt6::Widgets Qt6::Concurrent
)
target_include_directories(${PROJECT_NAME}_Server PUBLIC ${INCLUDE_DIRS})


add_executable(${PROJECT_NAME}_MockClient
        main_mock_client.cpp
        ${HEADERS}
        ${SOURCES}
        )
target_link_libraries(${PROJECT_NAME}_MockClient PRIVATE
        Qt6::Core Qt6::Widgets Qt6::Concurrent
        )
target_include_directories(${PROJECT_NAME}_MockClient PUBLIC ${INCLUDE_DIRS})
LCsa
  • 607
  • 9
  • 30
  • Why do you want a GUI on the server side? That seems really odd. – Jesper Juhl Oct 19 '22 at 10:02
  • @JesperJuhl This will run on localhost and allow GUI-based communication with a connected client. – LCsa Oct 19 '22 at 10:42
  • The problem is that `gui.exec` (which starts the Qt event loop) isn't executed, due to `asio::io_context::run` (which blocks the thread) being executed inside the `ServerGui` constructor. – m7913d Oct 19 '22 at 11:26
  • 2
    Not sure what the best solution is, but using different threads for Qt and asio may be a solution. – m7913d Oct 19 '22 at 11:27
  • @m7913d I had one approach with a thread that does the listening, but then I couldn't create GUI elements (because I was in another thread). The solution must be something that listens for a new connections in a new thread, but moves the new connection/socket object into the parent thread (where the "business logic" then works with the socket)... – LCsa Oct 19 '22 at 12:14
  • 3
    ***but then I couldn't create GUI elements (because I was in another thread)*** Keep the GUI in the main thread and use signals and slots to communicate between the background thread and the GUI. That is what I do in my applications at work that use Qt socket communications. – drescherjm Oct 19 '22 at 12:25
  • @drescherjm Yep, this and/or the `QMetaObject::invokeMethod()` function... Do you see a solution in which the TCP part can remain non-Qt? Also, is it a problem that the `context` object of the Server is actually passed by reference to all created connections? – LCsa Oct 19 '22 at 13:03

1 Answers1

0

Your problem is in Server::start:

waitForConnections();
mContext.run();

mContext.run() blocks. This means nothing else executes. The simplest "quick fix" is to replace io_context with a asio::thread_pool, but it may not work well with Qt's assumptions on threading.

Instead you'd want to integrate the event loops of the io context with Qt's. See e.g. How to integrate Boost.Asio main loop in GUI framework like Qt4 or GTK

I've previously done a quick-and-dirty appraoch of the same for a Gkt GUI: How to communicate with child process asynchronously?

I recommend using a ready-made implementation.

sehe
  • 374,641
  • 47
  • 450
  • 633