Can't start server in C++ using boost/asio

79 views Asked by At

I'm trying to write a simple server using boost/asio on windows, but it throws an error:

Exception thrown at 0x00007FF90EE05B0C in Server.exe:
Microsoft C++ exception:
boost::wrapexcept<boost::asio::ip::bad_address_cast> at memory location 0x0000002A5AF2EFF8. Debug Error!

after I entered the port and IP. I don't understand what could be wrong, because I pass the parameter to the function as written in the implementation of this function.

When I use a constructor with one argument the error does not immediately appear, but when I enter something from the keyboard it appears immediately.

I will be glad to any fair criticism.

Server.h simple header file.

#pragma once

#include <iostream>
#include <boost/asio.hpp>
#include <thread>

#include "utillity.hpp"

#ifndef _WIN32_WINNT

#define _WIN32_WINNT 0x0601

#endif


using namespace boost::asio;
using socket_ptr = boost::shared_ptr<ip::tcp::socket>;

class MainServer
{
private:

    ip::tcp::endpoint       ep;
    ip::tcp::acceptor       acc;
    socket_ptr              sock;
    ip::address             ipAddr;

    streambuf               buffer;

public:

    MainServer(boost::asio::io_service& service);
    MainServer(boost::asio::io_service& service, const std::string& ip_address, uint16_t port);
    ~MainServer();

    streambuf& getBuffer() { return buffer; }

    [[noreturn]] void Handler(boost::asio::io_service& service);
    [[noreturn]] void client_session(socket_ptr sock);
    [[noreturn]] void Receive_message(socket_ptr sock, streambuf& buffe) const;
    [[noreturn]] void send_message(socket_ptr sock, std::string mess) ;
    
};

namespace util_common
{

    std::string create_message()
    {
        std::string message;
        std::getline(std::cin, message);
        return message;
    }
}




namespace util_server
{
    std::pair<std::string, uint16_t> create_ip_and_port()
    {
        try
        {
            uint16_t port;
            std::string server_ip_address;

            std::cout << "IP: ";
            std::getline(std::cin, server_ip_address);

            std::cout << "Port: ";
            std::cin >> port;
            std::cin.ignore();
            static_cast<uint16_t>(port);
            static_cast<std::string>(server_ip_address);
            return std::make_pair(server_ip_address, port);
        }
        catch (const std::exception& e)
        {
            std::cerr << "Error create_ip_and_port(): " << e.what() << std::endl;
        }

    }
}

Server.cpp realization

#include "../headers/Server.h"

MainServer::MainServer(boost::asio::io_service& service, const std::string& ip_address, uint16_t port) :
    ipAddr  (ip::make_address(ip_address)),
    ep      (ipAddr, port),
    acc     (service, ep),
    sock    (new ip::tcp::socket(service))
{
    try
    {
        std::cout << "Server starting in ip: " << ip_address << "and port:" << port << std::endl;
        Handler(service);
    }
    catch (const std::exception& e)
    {
        std::cerr << "Error constructor MainServer(): " << e.what() << std::endl;
    }
}

MainServer::MainServer(boost::asio::io_service& service) :
    ipAddr(ip::make_address("0.0.0.0")),
    ep(ipAddr, 8080),
    acc(service, ep),
    sock(new ip::tcp::socket(service))
{
        try
    {
        std::cout << "Server starting on \"0.0.0.0\" ip and port: 8080\n";
        Handler(service);
    }
    catch (const std::exception& e)
    {
        std::cerr << "Error constructor MainServer(): " << e.what() << std::endl;
    }
}

MainServer::~MainServer() = default;


[[noreturn]] void MainServer::Handler(boost::asio::io_service& service)
{
    acc.async_accept
    (
        *sock, 
        [this, &service](const boost::system::error_code& error)
        {
            if (!error) _LIKELY
            {
                std::cout << "Client Connected!" << std::endl;
                client_session(sock);
            }
            else _UNLIKELY
            {
                std::cout << "Error Handler(): " << error.message() << std::endl;
            }

            Handler(service);
        }
    );

    service.run();
}

[[noreturn]] void MainServer::client_session(socket_ptr sock) 
{
    try
    {
        Receive_message(sock, getBuffer());
        send_message(sock, util_common::create_message());
    }
    catch (const std::exception& e)
    {
        std::cerr << "Error client_session(socket_ptr sock): " << e.what() << std::endl;
    }
}

[[noreturn]] void MainServer::Receive_message(socket_ptr sock, streambuf& buffer) const
{
    async_read(*sock, buffer, [this, sock, &buffer](const boost::system::error_code& error, std::size_t bytes_transferred)
    {
        if (!error) _LIKELY
        {
            std::istream is(&buffer);
            std::string message;
            std::getline(is, message);

            std::cout << "Client message: " << message << std::endl;

            // Consume the data that was read
            buffer.consume(bytes_transferred);

            // Handle the next message
            Receive_message(sock, buffer);
        }
        else _UNLIKELY
        {
            std::cout << "Error Receive_message(socket_ptr sock, streambuf& buffer): " << error.message() << std::endl;
        }
    });
}

[[noreturn]] void MainServer::send_message(socket_ptr sock, std::string mess) 
{
    async_write
    (
        *sock, 
        boost::asio::buffer(mess),
        [this, sock, mess](const boost::system::error_code& error, std::size_t bytes_transferred)
        {
            if (!error) _LIKELY
            {
                std::cout << "Send message: " << mess << std::endl;
                client_session(sock);
            }
            else _UNLIKELY
            {
                std::cout << "Error send_message(socket_ptr sock, std::string mess): " << error.message() << std::endl;
            }
        }   
    );
}

int main()
{
    setlocale(LC_ALL, "Rus");
    auto result = util_server::create_ip_and_port();
    try
    {
        io_service service;
        MainServer server(service, result.first, result.second);
        service.run();
    }
    catch (const std::exception& e)
    {
        std::cerr << "Exception in main(): " << e.what() << std::endl;
    }
    return 0;
}

Full message in console:

'Server.exe' (Win32): Loaded 'D:\C++\Server\out\build\x64-debug\Server.exe'. Symbols loaded.
'Server.exe' (Win32): Loaded 'C:\Windows\System32\ntdll.dll'. 
'Server.exe' (Win32): Loaded 'C:\Windows\System32\kernel32.dll'. 
'Server.exe' (Win32): Loaded 'C:\Windows\System32\KernelBase.dll'. 
'Server.exe' (Win32): Loaded 'C:\Windows\System32\ws2_32.dll'. 
'Server.exe' (Win32): Loaded 'C:\Windows\System32\rpcrt4.dll'. 
'Server.exe' (Win32): Loaded 'C:\Windows\System32\msvcp140d.dll'. 
'Server.exe' (Win32): Loaded 'C:\Windows\System32\mswsock.dll'. 
'Server.exe' (Win32): Loaded 'C:\Windows\System32\vcruntime140d.dll'. 
'Server.exe' (Win32): Loaded 'C:\Windows\System32\vcruntime140_1d.dll'. 
'Server.exe' (Win32): Loaded 'C:\Windows\System32\ucrtbased.dll'. 
The thread 16212 has exited with code 0 (0x0).
Exception thrown at 0x00007FF90EE05B0C in Server.exe: Microsoft C++ exception: boost::wrapexcept<boost::asio::ip::bad_address_cast> at memory location 0x000000F986B7F298.
Debug Error!

Program: D:\C++\Server\out\build\x64-debug\Server.exe

abort() has been called

(Press Retry to debug the application)
'Server.exe' (Win32): Loaded 'C:\Windows\System32\kernel.appcore.dll'. 
'Server.exe' (Win32): Loaded 'C:\Windows\System32\msvcrt.dll'. 
'Server.exe' (Win32): Loaded 'C:\Windows\System32\user32.dll'. 
'Server.exe' (Win32): Loaded 'C:\Windows\System32\win32u.dll'. 
'Server.exe' (Win32): Loaded 'C:\Windows\System32\gdi32.dll'. 
'Server.exe' (Win32): Loaded 'C:\Windows\System32\gdi32full.dll'. 
'Server.exe' (Win32): Loaded 'C:\Windows\System32\msvcp_win.dll'. 
'Server.exe' (Win32): Loaded 'C:\Windows\System32\ucrtbase.dll'. 
'Server.exe' (Win32): Loaded 'C:\Windows\System32\imm32.dll'. 
'Server.exe' (Win32): Loaded 'C:\Windows\System32\TextShaping.dll'. 
'Server.exe' (Win32): Loaded 'C:\Windows\System32\uxtheme.dll'. 
'Server.exe' (Win32): Loaded 'C:\Windows\System32\combase.dll'. 
'Server.exe' (Win32): Loaded 'C:\Windows\System32\msctf.dll'. 
'Server.exe' (Win32): Loaded 'C:\Windows\System32\bcryptprimitives.dll'. 
'Server.exe' (Win32): Loaded 'C:\Windows\System32\sechost.dll'. 
'Server.exe' (Win32): Loaded 'C:\Windows\System32\bcrypt.dll'. 
'Server.exe' (Win32): Loaded 'C:\Windows\System32\TextInputFramework.dll'. 
'Server.exe' (Win32): Loaded 'C:\Windows\System32\oleaut32.dll'. 
'Server.exe' (Win32): Loaded 'C:\Windows\System32\CoreMessaging.dll'. 
'Server.exe' (Win32): Loaded 'C:\Windows\System32\CoreUIComponents.dll'. 
'Server.exe' (Win32): Loaded 'C:\Windows\System32\WinTypes.dll'. 
'Server.exe' (Win32): Loaded 'C:\Windows\System32\advapi32.dll'. 
'Server.exe' (Win32): Loaded 'C:\Windows\System32\cryptbase.dll'. 
'Server.exe' (Win32): Loaded 'C:\Windows\System32\ole32.dll'. 
The thread 36256 has exited with code 3 (0x3).
The thread 15152 has exited with code 3 (0x3).
The thread 19884 has exited with code 3 (0x3).
The thread 32800 has exited with code 3 (0x3).
The thread 4196 has exited with code 3 (0x3).
The program '[25276] Server.exe' has exited with code 3 (0x3).

Since the error says that the problem is in the conversion, I tried in every possible way to additionally convert the IP to a string, although I already pass the string as a const parameter.

boost::asio::ip::make_address realization

1

There are 1 answers

2
sehe On

Your ipAddr is initialized AFTER your ep, which depends on it (in both constructors).

This means that ep is never valid. So at least flip the order in which members are declared around e.g.

tcp::endpoint ep;
tcp::acceptor acc;
socket_ptr        sock;
asio::ip::address ipAddr;

To

asio::ip::address ipAddr;
tcp::endpoint ep;
tcp::acceptor acc;
socket_ptr        sock;

Better yet, a quick scan shows that ipAddr is literally never used except to initialize ep, so, I'd drop it entirely:

MainServer::MainServer(asio::io_service& service, std::string const& ip_address,
                       uint16_t port)
    : ep(asio::ip::make_address(ip_address), port)
    , acc(service, ep)
    , sock(new tcp::socket(service)) {

Review

There are MANY other things alarming about the code. I'll draw up a quick list reviewing under a coffee soon:

  1. all the [[noreturn]] attributes are lies. In fact, Asio async initiation function all guarantee immediate return. The operation itself will complete on the service.

  2. there is a missing return in create_id_and_port (which should really have a better name, and just return tcp::endpoint). This invokes UB.

  3. your message reading never deals with end-of-file, so it will likely stick in an infinite loop at the end

  4. you are passing a temporary value as a buffer to send_message. The buffer will be destroyed before the async operation completes, again invoking UB. The buffer needs to be kept alive until the operation completes.

  5. send_message also needs to guard against overlapping writes. This is unlikely to happen if the input is actually from the console, but with input redirection I wouldn't rely on chance (and Nagle's algorithm) to prevent it.

  6. Combine both constructors using default arguments. This reduces code duplication:

    MainServer(asio::io_service& service, std::string const& ip_address = "0.0.0.0",
               uint16_t port = 8080);
    
  7. You appear to expect line-wise input, yet your async_read is unbounded. Change it to async_read_until with a delimiter (e.g. '\n'). Then, you don't need istream and streambuf complexxities, instead just use something like std::string or std::deque<char> (the latter being more efficient for consuming from the front end), e.g.:

  8. Many functions take socket_ptr and streambuf. This is a code smell. It would be more effective to group these methods in their own class (session). This would also allow you to use std::enable_shared_from_this to avoid the need for shared_ptr in the first place.

    Instead of rewriting it all, I'll point to this recent question where I showed the exact same thing (separating concerns between server and session): async_write only sends after the server is closed

  9. You end up doing BLOCKING I/O in client_session:

    send_message(sock, util_common::readMessage());
    

    This blocks the IO service, and refutes the purpose of using Asio.

  10. There's a conceptual problem with accepting many (concurrent) connections. All of them read from std::cin creating ... a mess at best, and more [UB] if you add multi-threading to the mix.

  11. Some aspects of the code seem to "optimize" (e.g. branch-predicting hints). That is contradicted by other signs, such as redundant dynamic allocation.

  12. the useless static_cast expressions have already been mentioned

SIMPLIFY YOUR LIFE

Frankly there end up too many problems in the code which aren't really solvable (without changing the behaviour, like reading from std::cin), I would consider embrace the non-asynchrony (blocking functions instead of async_XXXX). You can still benefit from all the networking primitives in Asio.

In fact you can have extremely simple, very similar, service in a few lines, e.g. using the example from the comments here:

Live On Coliru

#include <boost/asio.hpp>
#include <iostream>
namespace asio = boost::asio;
using boost::system::error_code;
using boost::asio::ip::tcp;

void ReverseEcho(tcp::iostream conn) {
    std::cout << "ReverseEcho session from " << conn.socket().remote_endpoint() << std::endl;
    conn << "Hello from server\n";

    for (std::string line; getline(conn, line);) {
        reverse(begin(line), end(line));
        conn << "Echo reversed: " << quoted(line) << std::endl;
    }
    std::cout << "ReverseEcho session closed" << std::endl;
}

struct listener {
    using Handler = std::function<void(tcp::iostream)>;
    listener(tcp::endpoint ep, Handler handler) : acc_(ioc_, ep), handler_(std::move(handler)) {
        accept_loop();
    }

    void cancel() {
        post(acc_.get_executor(), [this] { acc_.cancel(); });
    }

    ~listener() { ioc_.join(); }

  private:
    asio::thread_pool ioc_{1};
    tcp::acceptor     acc_;
    Handler           handler_;

    void accept_loop() {
        acc_.async_accept(make_strand(acc_.get_executor()), [this](error_code ec, tcp::socket s) {
            if (!ec) {
                std::thread([s = std::move(s), h = handler_]() mutable {
                    h(tcp::iostream(std::move(s)));
                }).detach();
                accept_loop();
            } else
                std::cout << std::endl;
        });
    }
};

int main() {
    listener server({{}, 1900}, ReverseEcho);
}

With some demos for illustrative purpose:

Of course, tcp::iostream does not allow Full Duplex IO (Does Boost asio ip tcp iostream support asynch?).

You can hack it using some threads very carefully: How to avoid data race with `asio::ip::tcp::iostream`?. However, that answer also says: don't do that. If you need full-duplex, bite the bullet and go async. However, I can only help when you have some requirements that allow this to be demonstrated in the first place.

BONUS: Thread Safe And Async

Here's a best effort implementation of the original "requirements". It uses threads for the console IO and uses mutual exclusion to avoid concurrent access to std::cin. That's pretty arbitrary, but at least it is safe and allows you to see what techniques I had in mind:

Live On Coliru

#include <boost/asio.hpp>
#include <deque>
#include <iostream>

namespace asio = boost::asio;
using asio::ip::tcp;
using error_code = boost::system::error_code;

namespace util_server {
    static std::mutex console_mx;

    std::string readMessage() {
        std::lock_guard lock(console_mx);

        if (std::string message; getline(std::cin, message))
            return message;

        throw std::runtime_error("readMessage(): IO Error");
    }

    tcp::endpoint promptEndpoint() {
        uint16_t    port;
        std::string ipAddress;

        if (std::cout << "IP: " && getline(std::cin, ipAddress) //
            && std::cout << "Port: " && (std::cin >> port).ignore()) {
            if (ipAddress.empty())
                return {{}, port};
            else
                return {asio::ip::make_address(ipAddress), port};
        } else {
            throw std::runtime_error("readEndpoint(): Invalid input");
        }
    }

} // namespace util_server

struct ClientSession : std::enable_shared_from_this<ClientSession> {
    ClientSession(tcp::socket s) : socket_(std::move(s)) {
        std::cout << "ClientSession created for " << socket_.remote_endpoint() << std::endl;
    }

    ~ClientSession() {
        post(socket_.get_executor(), [self = shared_from_this(), this] { socket_.cancel(); });
        std::cout << "ClientSession closing" << std::endl;
        console_thread.join();
    }

    void start() {
        assert(!console_thread.joinable());

        do_read_loop();
        console_thread = std::thread([self = shared_from_this()] {
            for (;;) {
                post(self->socket_.get_executor(),
                     [self, m = util_server::readMessage()]() mutable { self->do_send(std::move(m)); });
            }
        });
    }

  private:
    tcp::socket             socket_;
    std::string             buffer_;
    std::thread             console_thread;
    std::deque<std::string> outbox_;

    void do_read_loop() {
        async_read_until( //
            socket_, asio::dynamic_buffer(buffer_), "\n",
            [this, self = shared_from_this()](error_code ec, size_t bytes) {
                if (!ec) {
                    auto message = std::string_view(buffer_).substr(0, bytes - 1); // exclude the delimiter
                    std::cout << "Client message: " << std::quoted(message) << std::endl;

                    // Consume the data that was read
                    buffer_.erase(0, bytes);

                    // Handle the next message
                    do_read_loop();
                } else {
                    std::cout << "do_read_loop: " << ec.message() << std::endl;
                }
            });
    }

    void do_write_loop() {
        if (outbox_.empty())
            return;

        async_write(socket_, asio::buffer(outbox_.front()),
                    [this, self = shared_from_this()](error_code ec, size_t /*bytes*/) {
                        if (!ec) {
                            outbox_.pop_front();
                            do_write_loop();
                        } else {
                            std::cout << "do_write_loop: " << ec.message() << std::endl;
                        }
                    });
    }

    void do_send(std::string mess) {
        if (!mess.ends_with('\n'))
            mess += '\n';
        outbox_.push_back(std::move(mess));
        if (outbox_.size() == 1) {
            do_write_loop();
        }
    }
};

struct Server {
    Server(asio::any_io_executor ex, tcp::endpoint ep) : acc(ex, ep) {
        try {
            std::cout << "Server starting at " << ep << std::endl;
            acceptLoop();
        } catch (std::exception const& e) {
            std::cerr << "Error constructor MainServer(): " << e.what() << std::endl;
        }
    }

  private:
    tcp::acceptor acc;

    void acceptLoop() {
        acc.async_accept(make_strand(acc.get_executor()), [this](error_code ec, tcp::socket s) {
            if (!ec) {
                std::make_shared<ClientSession>(std::move(s))->start();
                acceptLoop();
            } else {
                std::cout << "acceptLoop: " << ec.message() << std::endl;
            }
        });
    }
};

int main() try {
    setlocale(LC_ALL, "Rus");
    auto ep = util_server::promptEndpoint();

#ifdef MULTI_THREADING
    asio::thread_pool ioc(1);
    Server            server(ioc.get_executor(), result);
    ioc.join();
#else
    asio::io_context ioc(1);
    Server           server(ioc.get_executor(), ep);
    ioc.run();
#endif
} catch (std::exception const& e) {
    std::cerr << "main(): " << e.what() << std::endl;
}

With a live interactive demonstration:

enter image description here