2

I am using Boost Asio as a TCP networking solution.

Here is my server Code:

Server.h:

#ifndef VIBRANIUM_CORE_SERVER_H
#define VIBRANIUM_CORE_SERVER_H
#include <cstdlib>
#include <iostream>
#include <memory>
#include <utility>
#include <boost/asio.hpp>
#include <deque>
#include "Logger.h"
#include "Client.h"

using boost::asio::ip::tcp;
namespace Vibranium {
    class Server {
    public:
        Server(boost::asio::io_service &io_service, short port)
                : acceptor_(io_service, tcp::endpoint(tcp::v4(), port)),
                  socket_(io_service) {
            do_accept();
            Logger::Log("Server Started! Listening on Port("+std::to_string(port)+")", Logger::Success, true);
        }
        static std::deque<std::shared_ptr<Client>> Clients;

    private:
        void do_accept();
        int incrementor;
        tcp::acceptor acceptor_;
        tcp::socket socket_;
    };

}


#endif //VIBRANIUM_CORE_SERVER_H

Server.cpp:

#include "Server.h"
#include "Client.h"

using namespace Vibranium;
std::deque<std::shared_ptr<Client>> Server::Clients;
void Server::do_accept()
{
    acceptor_.async_accept(socket_,
   [this](boost::system::error_code ec)
   {
       if (!ec)
       {
           incrementor++;
           Logger::Log("New Connection (ID: " + std::to_string(incrementor) + ")",Logger::Success);
           std::shared_ptr<Client> c = std::make_shared<Client>(std::move(socket_));
           c->start();
           c->connectionId = incrementor;
           Server::Clients.push_back(c);
       }

       do_accept();
   });
}

Client.h:

#ifndef VIBRANIUM_CORE_CLIENT_H
#define VIBRANIUM_CORE_CLIENT_H

#include <cstdlib>
#include <iostream>
#include <memory>
#include <utility>
#include <boost/asio.hpp>

using boost::asio::ip::tcp;

namespace Vibranium{
    class Client: public std::enable_shared_from_this<Client>
    {
    public:
        Client(tcp::socket socket)
        : socket_(std::move(socket))
        {
        }
        void start();
        int connectionId;
    private:
        void do_read();
        void do_write(std::size_t length);
        tcp::socket socket_;
        enum { max_length = 1024 };
        char data_[max_length];
    };
}
#endif //VIBRANIUM_CORE_CLIENT_H

Client.cpp:

#include "Client.h"
#include "Server.h"

void Vibranium::Client::start() {
    do_read();
}

void Vibranium::Client::do_read() {
    auto self(shared_from_this());
    socket_.async_read_some(boost::asio::buffer(data_, max_length),
    [this, self](boost::system::error_code ec, std::size_t length)
    {
        if ((boost::asio::error::eof == ec) || (boost::asio::error::connection_reset == ec))
        {
            Logger::Log("Disconnected ID: " + std::to_string(connectionId),Logger::Error, true);
            for (int i = 0; i < Server::Clients.size(); ++i) {
                if(Server::Clients[i]->connectionId == connectionId)
                    Server::Clients.erase(Server::Clients.begin()+i);
            }
        }
        else
        {
            std::cout.write(data_, length);
            std::cout << "\n";
            do_write(length);
        }
    });
}

void Vibranium::Client::do_write(std::size_t length) {
    auto self(shared_from_this());
    boost::asio::async_write(socket_, boost::asio::buffer(data_, length),
     [this, self](boost::system::error_code ec, std::size_t /*length*/)
     {
         if (!ec)
         {
             do_read();
         }
     });
}

Here is how I start the server:

#include "Config.h"
#include "Database/MySQLConnection.h"
#include "Implementation/LoginDatabase.h"
#include "Banner.h"
#include "Server/Server.h"
#include <boost/asio.hpp>

using boost::asio::ip::tcp;
using namespace std;
using namespace Vibranium;

int main() {
    //Don't mind Logger::FatalError it's just for coloring!
    Banner::Show(Logger::Error,"AuthServer");
    Config config("AuthServer");
    std::string defaultPort = "8080";
    MySQLConnectionInfo mySqlConnectionInfo(config, "LoginDatabaseInfo");
    LoginDatabaseConnection loginDatabaseConnection(mySqlConnectionInfo);
    loginDatabaseConnection.LoadDatabase();

    try
    {
        boost::asio::io_service io_service;
        Server s(io_service, std::stoi(config.GetConfigValue("AuthServerPort", defaultPort)));
        io_service.run();
    }
    catch (std::exception& e)
    {
        std::cerr << "Exception: " << e.what() << "\n";
    }

    return 0;
}

Here is how I try to make several connections at once and keep them alive:

Config config("AuthServer");
std::string defaultIP   = "127.0.0.1";
std::string defaultPort = "8080";
int connectionsNumber   = CommandQuestion<int>::AskQuestion("How many connections do you want established?");
std::cout << "Initializing " << std::to_string(connectionsNumber) << " connection/s." << std::endl;

std::cout << "Trying to connect to  " <<  defaultIP << " on port: " << config.GetConfigValue("AuthServerPort", defaultPort)  << std::endl;
for (int i = 0; i < connectionsNumber; ++i) {
   try
    {
        boost::asio::io_context io_context;
        tcp::socket s(io_context);
        tcp::resolver resolver(io_context);
        boost::asio::connect(s, resolver.resolve( defaultIP,config.GetConfigValue("AuthServerPort", defaultPort)));

        std::string message = "I am testing here!!!";
        size_t request_length = std::strlen(message.c_str());
        boost::asio::write(s, boost::asio::buffer(message, request_length));

        char reply[max_length];
        size_t reply_length = boost::asio::read(s,boost::asio::buffer(reply, request_length));
        Logger::Log(std::to_string(i) + " Connected!",Logger::Success);
        /*        std::cout << "Reply is: ";
                std::cout.write(reply, reply_length);
                std::cout << "\n";*/
    }
    catch (std::exception& e)
    {
        std::cerr << "Exception: " << e.what() << "\n";
    }

}

I am able to make a successful connections, but they got dropped directly after they are created and transferred the example echo message.

When I try to initialize for example 3 connections i see the following output on the server:

Server Started! Listening on Port(8085)
New Connection (ID: 1)
I am testing here!!!
Disconnected ID: 1
New Connection (ID: 2)
I am testing here!!!
Disconnected ID: 2
New Connection (ID: 3)
I am testing here!!!
Disconnected ID: 3

I am initializing the connections to the server from the same PC but from another C++ target executable. So I have server executable up and running and another executable which is trying to create several connections and keep them at once.

How can I make my connections to persist and not drop directly after they are created? Where is my mistake and how can I fix it?

Venelin
  • 2,905
  • 7
  • 53
  • 117

1 Answers1

2

The tcp::socket will close its connection when it goes out of scope (reference). If you want to keep the connections open, you have to keep the object alive.

You can do this by moving the io_context out of the loop in your client example and keeping some list for holding the sockets:

  boost::asio::io_context io_context;
  std::vector<tcp::socket> sockets{};

  for (int i = 0; i < connectionsNumber; ++i) {

    try {
      sockets.emplace_back(io_context);
      tcp::socket& s{sockets.back()};

      // initialize socket as before ...

The server output will then show the disconnection message after the client loop has completed:

New Connection (ID: 2)
Hello, world!
New Connection (ID: 3)
Hello, world!
New Connection (ID: 4)
Hello, world!
Disconnected ID: 2
Disconnected ID: 3
Disconnected ID: 4

One additional note: You did not show the content of do_write, but I would assume your implementation of this function in some way calls do_read again, so that it can handle the eventual EOF or connection reset.

  • Thank you! Somehow i've missed `do_write` but i've added it now. It implements `do_read` check it out ;) – Venelin Sep 17 '20 at 07:26
  • Does the connection closes when `io_context` goes out of scope as well ? – Venelin Sep 17 '20 at 07:31
  • Not in the sense you want. If the io_context destructs before the sockets registered to it, then the socket destructor will trigger a use-after-free memory error, i.e. undefined behavior. Your program may or may not run fine, but you have no guarantees for what will happen (which is very bad). You can observe this by declaring the io_context after the socket vector in example above (so that the io_context destructor will run before the socket destructors), and then compiling with address sanitization enabled (`-fsanitize=address` with clang or gcc). It should then report the use-after-free. – f9c69e9781fa194211448473495534 Sep 22 '20 at 22:18
  • For this reason, the io_context should always outlive the sockets registered to it. Also from a performance point of view, when you do async I/O you will get more benefit the more sockets are sharing the same io_context. – f9c69e9781fa194211448473495534 Sep 22 '20 at 22:37
  • When you have multiple io_context objects each controlling one socket, you will have a performance overhead from switching between their worker threads. Whereas with a single io_context, multiple sockets can be controlled by one thread. You can still assign a thread group to the io_context to balance the load over multiple threads, but that will do more efficient balancing compared to nailing each socket to a separate thread that will be forced to be idle when no data is available on the socket. So this why you should share your io_context as much as possible from a performance point of view. – f9c69e9781fa194211448473495534 Sep 22 '20 at 22:39