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})