c++ - Boost ASIO networking server/client

13,083

Solution 1

Start with learning some of the basics. There are excellent tutorials and examples provided by the library that will walk you through concepts and examples of both synchronous and async servers.

http://www.boost.org/doc/libs/1_62_0/doc/html/boost_asio.html

Start at the beginning. Work through the basic concepts with timers and callbacks. Continue with the tutorial into the networking. Build on the core ideas over the course of a few hours by editing the tutorial programs to try out ideas on your own.

Then work your way over to the examples section. The 'Chat' example is really close to what you are looking to build. This specific example shows how to connections open by re-issuing async_reads in the read handlers. Asynchronous asio servers will service multiple clients as long as the service threads continue to run with the assigned async_accept task.

http://www.boost.org/doc/libs/1_62_0/doc/html/boost_asio/examples/cpp11_examples.html http://www.boost.org/doc/libs/1_62_0/doc/html/boost_asio/example/cpp11/chat/chat_server.cpp

Solution 2

Implementing an asynchronous TCP server

An asynchronous TCP server is a part of a distributed application that satisfies the following criteria:

  • Acts as a server in the client-server communication model
  • Communicates with client applications over TCP protocol
  • Uses the asynchronous I/O and control operations
  • May handle multiple clients simultaneously

A typical asynchronous TCP server works according to the following algorithm:

  • Allocate an acceptor socket and bind it to a particular TCP port.
  • Initiate the asynchronous accept operation.
  • Spawn one or more threads of control and add them to the pool of threads that run the Boost.Asio event loop.
  • When the asynchronous accept operation completes, initiate a new one to accept the next connection request.
  • Initiate the asynchronous reading operation to read the request from the connected client.
  • When the asynchronous reading operation completes, process the request and prepare the response message.
  • Initiate the asynchronous writing operation to send the response message to the client.
  • When the asynchronous writing operation completes, close the connection and deallocate the socket.
//responsible for handling a single client by reading the request message, processing it, and then sending back the response message.
//Each instance of the Service class is intended to handle one connected client
//by reading the request message, processing it, and then sending the response message back.
class Service
{
public:
    //The class's constructor accepts a shared pointer to an object representing a socket connected to a particular client as an argument
    // and caches this pointer. This socket will be used later to communicate with the client application.
    Service(std::shared_ptr<asio::ip::tcp::socket> sock) : m_sock(sock)

    //This method starts handling the client by initiating the asynchronous reading operation
    //to read the request message from the client specifying the onRequestReceived() method as a callback.
    void StartHandling()

private:
    void onRequestReceived(const boost::system::error_code &ec,
                           std::size_t bytes_transferred)


    void onResponseSent(const boost::system::error_code &ec,
                        std::size_t bytes_transferred)

    // Here we perform the cleanup.
    void onFinish()
    {
        delete this;
    }

    //To keep things simple,  we implement a dummy service which only emulates the execution of certain operations
    //The request processing emulation consists of performing many increment operations to emulate operations
    //that intensively consume CPU and then putting the thread of control to sleep for some time to emulate I/O operations
    std::string ProcessRequest(asio::streambuf &request)

private:
    std::shared_ptr<asio::ip::tcp::socket> m_sock;
    std::string m_response;
    asio::streambuf m_request;
};

//responsible for accepting the connection requests arriving from clients and instantiating the objects of the Service class,
// which will provide the service to connected clients.
class Acceptor
{
public:
    //Its constructor accepts a port number on which it will listen for the incoming connection requests as its input argument. 
    Acceptor(asio::io_service &ios, unsigned short port_num) : m_ios(ios),
                                                               //The object of this class contains an instance of the asio::ip::tcp::acceptor class as its member named m_acceptor,
                                                               //which is constructed in the Acceptor class's constructor.
                                                               m_acceptor(m_ios,
                                                                          asio::ip::tcp::endpoint(
                                                                              asio::ip::address_v4::any(),
                                                                              port_num)),
                                                               m_isStopped(false)

    //The Start() method is intended to instruct an object of the Acceptor class to start listening and accepting incoming connection requests.
    void Start()

    // Stop accepting incoming connection requests.
    void Stop()


private:
    void InitAccept()

    void onAccept(const boost::system::error_code &ec,
                  std::shared_ptr<asio::ip::tcp::socket> sock)

private:
    asio::io_service &m_ios;
    //used to asynchronously accept the incoming connection requests.
    asio::ip::tcp::acceptor m_acceptor;
    std::atomic<bool> m_isStopped;
};

//represents the server itself
class Server
{
public:
    Server()

    // Start the server.
    // Accepts a protocol port number on which the server should listen for the incoming connection requests
    // and the number of threads to add to the pool as input arguments and starts the server
    // Nonblocking Method
    void Start(unsigned short port_num,
               unsigned int thread_pool_size)

    // Stop the server.
    // Blocks the caller thread until the server is stopped and all the threads running the event loop exit.
    void Stop()

private:
    asio::io_service m_ios;
    std::unique_ptr<asio::io_service::work> m_work;
    std::unique_ptr<Acceptor> acc;
    std::vector<std::unique_ptr<std::thread>> m_thread_pool;
};

int main()
{
    unsigned short port_num = 3333;

    try
    {
        //it instantiates an object of the Server class named srv.
        Server srv;

        //before starting the server, the optimal size of the pool is calculated.
        // The general formula often used in parallel applications to find the optimal number of threads is the number of processors the computer has multiplied by 2.
        // We use the std::thread::hardware_concurrency() static method to obtain the number of processors. 
        unsigned int thread_pool_size =
            std::thread::hardware_concurrency() * 2;

        //because this method may fail to do its job returning 0,
        // we fall back to default value represented by the constant DEFAULT_THREAD_POOL_SIZE, which is equal to 2 in our case.
        if (thread_pool_size == 0)
            thread_pool_size = DEFAULT_THREAD_POOL_SIZE;

        srv.Start(port_num, thread_pool_size);

        std::this_thread::sleep_for(std::chrono::seconds(60));

        srv.Stop();
    }
    catch (system::system_error &e)
    {
        std::cout << "Error occured! Error code = "
                  << e.code() << ". Message: "
                  << e.what();
    }

    return 0;
}

Implementing an asynchronous TCP client

An asynchronous TCP client application supporting the asynchronous execution of the requests and request canceling functionality:

  • Input from the user should be processed in a separate thread—the user interface thread. This thread should never be blocked for a noticeable amount of time.
  • The user should be able to issue multiple requests to different servers.
  • The user should be able to issue a new request before the previously issued requests complete.
  • The user should be able to cancel the previously issued requests before they complete.
// Function pointer type that points to the callback
// function which is called when a request is complete.
// Based on the values of the parameters passed to it, it outputs information about the finished request.
typedef void (*Callback)(unsigned int request_id,        // unique identifier of the request is assigned to the request when it was initiated.
                         const std::string &response,    // the response data
                         const system::error_code &ec);  // error information

// data structure whose purpose is to keep the data related to a particular request while it is being executed
struct Session
{
    Session(asio::io_service &ios,
            const std::string &raw_ip_address,
            unsigned short port_num,
            const std::string &request,
            unsigned int id,
            Callback callback) : m_sock(ios),
                                 m_ep(asio::ip::address::from_string(raw_ip_address),
                                      port_num),
                                 m_request(request),
                                 m_id(id),
                                 m_callback(callback),
                                 m_was_cancelled(false) {}

    asio::ip::tcp::socket m_sock; // Socket used for communication
    asio::ip::tcp::endpoint m_ep; // Remote endpoint.
    std::string m_request;        // Request string.

    // streambuf where the response will be stored.
    asio::streambuf m_response_buf;
    std::string m_response; // Response represented as a string.

    // Contains the description of an error if one occurs during
    // the request lifecycle.
    system::error_code m_ec;

    unsigned int m_id; // Unique ID assigned to the request.

    // Pointer to the function to be called when the request
    // completes.
    Callback m_callback;

    bool m_was_cancelled;
    std::mutex m_cancel_guard;
};

// class that provides the asynchronous communication functionality.
class AsyncTCPClient : public boost::noncopyable
{
public:
    AsyncTCPClient(unsigned char num_of_threads)

    // initiates a request to the server
    void emulateLongComputationOp(
        unsigned int duration_sec,          //represents the request parameter according to the application layer protocol
        const std::string &raw_ip_address,  //specify the server to which the request should be sent.
        unsigned short port_num,            //specify the server to which the request should be sent.
        Callback callback,                  //callback function, which will be called when the request is complete.
        unsigned int request_id)    // unique identifier of the request


    // cancels the previously initiated request designated by the request_id argument
    void cancelRequest(unsigned int request_id) //accepts an identifier of the request to be canceled as an argument.


    // blocks the calling thread until all the currently running requests complete and deinitializes the client.
    void close()


private:
    // method is called whenever the request completes with any result.
    void onRequestComplete(std::shared_ptr<Session> session)

private:
    asio::io_service m_ios;
    std::map<int, std::shared_ptr<Session>> m_active_sessions;
    std::mutex m_active_sessions_guard;
    std::unique_ptr<boost::asio::io_service::work> m_work;
    std::list<std::unique_ptr<std::thread>> m_threads;
};

// a function that will serve as a callback, which we'll pass to the AsyncTCPClient::emulateLongComputationOp() method
// It outputs the result of the request execution and the response message to the standard output stream if the request is completed successfully
void handler(unsigned int request_id,
             const std::string &response,
             const system::error_code &ec)

int main()
{
    try
    {
        AsyncTCPClient client(4);

        // Here we emulate the user's behavior.

        // creates an instance of the AsyncTCPClient class and then calls its emulateLongComputationOp() method to initiate three asynchronous requests
        // User initiates a request with id 1.
        client.emulateLongComputationOp(10, "127.0.0.1", 3333, handler, 1);

        // Decides to exit the application.
        client.close();
    }
    catch (system::system_error &e)
    {
        std::cout << "Error occured! Error code = " << e.code()
                  << ". Message: " << e.what();

        return e.code().value();
    }

    return 0;
};

Setup environment

1. Install CMake

cd ~
wget https://github.com/Kitware/CMake/releases/download/v3.14.5/cmake-3.14.5.tar.gz
tar xf cmake-3.14.5.tar.gz
cd cmake-3.14.5
./bootstrap --parallel=10
make -j4
sudo make -j4 install

2. Install Boost

cd ~
wget https://boostorg.jfrog.io/artifactory/main/release/1.69.0/source/boost_1_69_0.tar.gz
tar xf boost_1_69_0.tar.gz
cd boost_1_69_0
./bootstrap.sh
./b2 ... cxxflags="-std=c++0x -stdlib=libc++" linkflags="-stdlib=libc++" ...
sudo ./b2 toolset=gcc -j4 install

How to build

mkdir build
cd build
cmake ..
cmake --build .

How to Run

Run server

./bin/server

we can check that server is run or not

netstat -tulpn | grep LISTEN

tcp        0      0 0.0.0.0:445             0.0.0.0:*               LISTEN      -
tcp        0      0 0.0.0.0:3333            0.0.0.0:*               LISTEN      6866/./bin/server   <===============
tcp        0      0 0.0.0.0:139             0.0.0.0:*               LISTEN      -
tcp        0      0 127.0.0.53:53           0.0.0.0:*               LISTEN      -
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN      -
tcp        0      0 127.0.0.1:631           0.0.0.0:*               LISTEN      -
tcp        0      0 127.0.0.1:5433          0.0.0.0:*               LISTEN      -
tcp6       0      0 :::445                  :::*                    LISTEN      -
tcp6       0      0 :::5000                 :::*                    LISTEN      -
tcp6       0      0 :::5001                 :::*                    LISTEN      -
tcp6       0      0 :::139                  :::*                    LISTEN      -
tcp6       0      0 :::22                   :::*                    LISTEN      -
tcp6       0      0 ::1:631                 :::*                    LISTEN      -

Run client

./bin/client

Request #1 has completed. Response: Response from server

You can find the project in Boost Asio C++ Network Programming Client Server

Share:
13,083
helix
Author by

helix

Updated on June 21, 2022

Comments

  • helix
    helix almost 2 years

    I have written a program for client and server. The program currently does the following:

    1. Server listens to an end point for a connection
    2. Client connects to the server
    3. server sends message on accepting a connection
    4. client receives the message

    I'm doing this asynchronously. But, the problem is they can only send/receive one message. After that, they just terminate. Below is my code:

    #include <iostream>
    #include <vector>
    #include<boost/asio.hpp>
    
    std::vector<char> buff(256);
    
    void SendHandler(boost::system::error_code ex){
        std::cout << " do something here" << std::endl;
    }
    
    void ReadHandler(boost::system::error_code ex){
        std::cout << " print the buffer data..." << std::endl;
        std::cout << buff.data() << std::endl;
    
    }
    
    void Server(){
        boost::asio::io_service service;
        using namespace boost::asio::ip;
        tcp::endpoint endpoint(tcp::v4(), 4000);
        tcp::acceptor acceptor(service, endpoint); 
        tcp::socket socket(service);
        std::cout << "[Server] Waiting for connection" << std::endl;
    
    
        acceptor.accept(socket);
        std::cout << "[Server] Accepted a connection from client" << std::endl;
    
        std::string msg = "Message from server";
        socket.async_send(boost::asio::buffer(msg), SendHandler);
        service.run();
    
    }
    
    void Client(){
        boost::asio::io_service service;
        using namespace boost::asio::ip;
        tcp::endpoint endpoint(address::from_string("127.0.0.1"), 4000);
        tcp::socket socket(service);
        std::cout << "[Client] Connecting to server..." << std::endl;
        socket.connect(endpoint);
        std::cout << "[Client] Connection successful" << std::endl;
    
        socket.async_read_some(boost::asio::buffer(buff), ReadHandler);
        service.run();
    }
    
    int main(int argc, char **argv) {
        if(argc == 1){
            std::cout << "Please specify s for server or c for client" << std::endl;
            return -1;
        }
        if(argv[1][0] == 's'){
            Server();
        }
        else{
            Client();
        }
        return 0;
    }
    

    I want to scale this program so that:

    1. server can listen and client can send request indefinitely. More like a one way chat system.
    2. server being able to connect to multiple clients.

    Putting the async_send() and service.run() in an infinite loop didn't help. It just prints the message over and over on the client side until the client terminates.

    I'm fairly new to boost::asio even to network programming. Please tell me where and what I should modify in my code?