[Basic]Boost.Asio/Asio Tcp echo Server

๐Ÿ‘‰๐Ÿป c++์˜ Asio(๋˜๋Š” Bootst.asio) ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์ดํ•ดํ•˜๊ธฐ ์œ„ํ•œ ์ฝ”๋“œ์ž…๋‹ˆ๋‹ค.
This is code to understand the C++ Asio (or Bootst.asio) library.

๐Ÿ‘‰๐Ÿป ์•„๋ž˜์˜ ์ฝ”๋“œ๋Š” ์ด์ „์— ์—…๋กœ๋“œํ•œ mini_mediaserver_1 ์˜ ์ผ๋ถ€๋กœ์ง์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
The code below uses some of the logic from the previously uploaded mini_mediaserver_1.

๐Ÿ‘‰๐Ÿป ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์€ MacOS์ž…๋‹ˆ๋‹ค.
The test environment is MacOS.

๐Ÿ‘‰๐Ÿป mini_mediaserver_1์€ standalone๋ฐฉ์‹์˜ asio๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
mini_mediaserver_1 uses standalone asio.

๐Ÿ‘‰๐Ÿป ์—ฌ๊ธฐ์„œ๋Š” Boosta.asio๋ฅผ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
Here, we use Boosta.asio.

๐Ÿ‘‰๐Ÿป ์„ค์น˜ ๋ฐ ์‚ฌ์šฉ / Install and Usage

โœ”๏ธ Boost.Asio ์ „์ฒด ์„ค์น˜ / full installation

— ์„ค์น˜ / Installatioon

# macOS brew - boost.asio
brew install boost

— ์‚ฌ์šฉ / Usage

#include "boost/asio" 

โœ”๏ธ Asio Standalone

— ์„ค์น˜ / Installation

# macOS brew - asio standalone
brew install asio

— ์‚ฌ์šฉ / Usage

#include "asio.hpp" 

๐Ÿ‘‰๐Ÿป ์ฝ”๋“œ / Code

โœ”๏ธ์ฝ”๋“œ ์„ค๋ช… / Code Description

— ํ•จ์ˆ˜ ์‹คํ–‰ ํ๋ฆ„ / Function execution flow

main() -> Server Class- > do_accept() -> Session Class -> start { do_read {do_write()}}

— ์†Œ์ผ“,์ ‘์†์ •๋ณด ๊ณต์œ  / Socket, connection information sharing

1.์†Œ์ผ“์€ ๋ณต์‚ฌ ํ•  ์ˆ˜ ์—†๊ธฐ ๋•Œ๋ฌธ์— ์†Œ์œ ๊ถŒ ์ด๋™์„ ํ•ฉ๋‹ˆ๋‹ค.
Since sockets cannot be copied, ownership is transferred.

std::make_shared<Session>(std::move(socket))->start();

2.์ ‘์† ์ •๋ณด๋ฅผ ๊ณต์œ ํ•˜๊ธฐ ์œ„ํ•ด ๋‹ค์Œ ์ฝ”๋“œ๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
Use the following code to share connection information.

- make_shared๋Š” ํ•˜๋‚˜๋งŒ ๊ณต์œ ํ•  ์ˆ˜ ์žˆ๋Š” ํฌ์ธํ„ฐ๋ฅผ ๋งŒ๋“ญ๋‹ˆ๋‹ค.
make_shared creates a pointer that can be shared by only one person.

std::make_shared<Session>(std::move(socket))->start();

- enable_shared_from_this๋ฅผ ์ƒ์†๋ฐ›์œผ๋ฉด shared_from_this(); ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
If you inherit from enable_shared_from_this, you can use the shared_from_this(); function.

public std::enable_shared_from_this<Session>

- this(Session class) ์ •๋ณด๊ฐ€ ์‚ฌ๋ผ์งˆ ์ˆ˜ ์žˆ์–ด์„œ ์•ˆ์ „ํ•œ ์—ฐ๊ฒฐ์„ ์œ„ํ•ด self์— Session Class ์ •๋ณด๋ฅผ ๋ณตํ•˜ํ•ฉ๋‹ˆ๋‹ค. 
Since the information in this (Session class) can be lost, the Session Class information is copied to self for a secure connection.

auto self = shared_from_this();

— echo

- ์•„๋ž˜์˜ ๋žŒ๋‹ค ์ฝ”๋“œ์—์„œ ์†Œ์ผ“์—์„œ ๋ฐ›์€ data_์˜ ๋‚ด์šฉ์„ ์ถœ๋ ฅํ•˜๊ณ  ๋‹ค์‹œ ์†Œ์ผ“์— ๋ฐ์ดํ„ฐ๋ฅผ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.
In the lambda code below, the contents of data_ received from the socket are printed, and data is written back to the socket.

[this, self](boost::system::error_code ec, size_t len) {
   if (!ec) {
     std::cout << "๋ฐ›์Œ/Receive: " << std::string(data_, len) << "\n";
     do_write(len); // ์—์ฝ” / echo
   }
});

โœ”๏ธ์ „์ฒด ์ฝ”๋“œ / Full Cdoe (tcpecho.cpp)

#include <iostream>
#include <memory>
#include <boost/asio.hpp>
using boost::asio::ip::tcp;

// --
/*
 - std::make_shared<Session>(std::move(socket)) :
   ํ•œ ๊ณณ๋งŒ ํฌ์ธํ„ฐ ๊ณต์œ ์„ค์ • / Pointer sharing settings for only one location

 - : public std::enable_shared_from_this<Session> :
   shared_from_this(); ์‚ฌ์šฉ์„ ์œ„ํ•ด ์ƒ์†
   Inheritance for using shared_from_this();

 - shared_from_this :
   class Session ๊ฐ์ฒด๋ฅผ ๋ณต์‚ฌํ•ด์„œ ์ €์žฅ
   Copy and save the class Session object
   ** this๊ฐ€ ์†Œ๋ฉธ๋  ์ˆ˜ ์žˆ์–ด์„œ ์•ˆ์ „ํ•œ ์„ธ์…˜์—ฐ๊ฒฐ ์œ ์ง€๋ฅผ ์œ„ํ•ด Sessionํด๋ž˜์Šค๋ฅผ ํ•˜๋‚˜ ๋” ๋ณต์‚ฌํ•จ
      Since 'this' can be destroyed,
      an additional copy of the Session class is made to maintain a safe session connection. **
 */
class Session : public std::enable_shared_from_this<Session> {
    tcp::socket socket_;
    char data_[1024];

public:
    Session(tcp::socket socket) : socket_(std::move(socket)) {}

    void start() {
        do_read();
    }

private:
    void do_read() {

        // ๋žŒ๋‹ค ์‚ด์•„์žˆ๋Š” ๋™์•ˆ Session ์•ˆ ์ฃฝ์Œ / The Session does not die as long as Lambda is alive
        // ์ด ํด๋ž˜์Šค ๊ฐ์ฒด๋ฅผ ๋ณต์‚ฌํ•ด์„œ ์ €์žฅ / Copy and save this class object
        auto self = shared_from_this();

        /*
         * async_read_some(buffer(data_), ๋žŒ๋‹ค/rambda)
         * 1) ๋ฐ์ดํ„ฐ ์˜ค๋ฉด data_์— ์ฑ„์›Œ๋†“๊ณ  ๋žŒ๋‹ค ์‹คํ–‰ ์˜ˆ์•ฝ๋งŒ ํ•œ๋‹ค.
         * When data arrives, fill it into data_ and just schedule the Lambda execution.
         *
         * 2) OS๊ฐ€ ์†Œ์ผ“์— ๋ฐ์ดํ„ฐ ๋“ค์–ด์˜ค๋Š” ๊ฑฐ ๊ฐ์ง€ โ†’ data_ ๋ฐฐ์—ด์— len๋ฐ”์ดํŠธ๋งŒํผ ๋ณต์‚ฌํ•ด ๋‘”๋‹ค.
         * The OS detects incoming data in the socket and copies len bytes into the data_ array.
         *
         * 3) ๋ณต์‚ฌ๊ฐ€ ๋๋‚ฌ์œผ๋ฉด ๋žŒ๋‹ค ์‹คํ–‰
         * Run Lambda once copying is finished
         *
         * 4) ์ด๋ฏธ data_์— ๋“ค์–ด์™€ ์žˆ๋Š” ๋ณ€์ˆ˜๋ฅผ ๋žŒ๋‹ค์—์„œ ์‚ฌ์šฉ
         * Using variables already in data_ in Lambda
         */
        socket_.async_read_some(boost::asio::buffer(data_),
            [this, self](boost::system::error_code ec, size_t len) {
                if (!ec) {
                    std::cout << "๋ฐ›์Œ/Receive: " << std::string(data_, len) << "\n";
                    do_write(len); // ์—์ฝ” / echo
                }
            });
    }

    void do_write(size_t len) {
        auto self = shared_from_this();
        boost::asio::async_write(socket_, boost::asio::buffer(data_, len),
            [this, self](boost::system::error_code ec, size_t) {
                if (!ec) do_read(); // ์ฝ๊ธฐ / read
            });
    }
};

// --
class Server {
    // socket() + bind() + listen()์ˆ˜ํ–‰ / Execute socket() + bind() + listen()
    tcp::acceptor acceptor_; // private
public:
    Server(boost::asio::io_context& io, short port)
        : acceptor_(io, tcp::endpoint(tcp::v4(), port)) {
        do_accept();
    }

private:
    void do_accept() {
        acceptor_.async_accept(
            // ๋žŒ๋‹คํ•จ์ˆ˜ / Lambda function
            [this](boost::system::error_code ec, tcp::socket socket) {
                if (!ec) {
                    // ์—๋Ÿฌ๊ฐ€ ์—†์œผ๋ฉด ์†Œ์ผ“์„ ์„ธ์…˜์— ๋„˜๊ธฐ๊ณ  start ํ˜ธ์ถœ, ์†Œ์ผ“์€ ๋ณต์‚ฌ ์•ˆ๋จ
                    // If there are no errors, pass the socket to the session and call start;
                    // the socket is not copied
                    std::make_shared<Session>(std::move(socket))->start();
                }
                // ๋‹ค์Œ ํด๋ผ์ด์–ธํŠธ ๊ณ„์† ๋ฐ›์Œ
                // Continue accepting next client
                //
                // ๋ฌดํ•œ๋ฃจํ”„ / infinite loop
                do_accept();
            });
    }
};

int main() {
    try {
        boost::asio::io_context io;
        Server s(io, 12345);
        std::cout << "์„œ๋ฒ„ ์‹œ์ž‘. ํฌํŠธ 12345 / Start server. Port 12345\n";
        io.run();
    } catch (std::exception& e) {
        std::cerr << "์—๋Ÿฌ/Error: " << e.what() << "\n";
    }
    return 0;
}

๐Ÿ‘‰๐Ÿป์ปดํŒŒ์ผ / Compiling

— boot๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์˜ ๊ฒฝ๋กœ๋ฅผ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค.
Includes the path to the boot library.

g++ tcpecho.cpp -o server -std=c++17 -pthread \
  -I/opt/homebrew/include \
  -L/opt/homebrew/lib \
  -lboost_system

๐Ÿ‘‰๐Ÿป์‹คํ–‰ / Run

โœ”๏ธ ํ„ฐ๋ฏธ๋„์„ ๋‘๊ฐœ ์—ด์–ด์„œ ํ•˜๋‚˜๋Š” ์„œ๋ฒ„ ํ•˜๋‚˜๋Š” ํด๋ผ์ด์–ธํŠธ๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค.
Open two terminals and run the server and the client in one.

โœ”๏ธnc(netcat)๋Š” TCP/UDP ํ†ต์‹  ํ…Œ์ŠคํŠธ ๋„๊ตฌ์ž…๋‹ˆ๋‹ค.
nc(netcat) is a TCP/UDP communication test tool.

โœ”๏ธ ํด๋ผ์ด์–ธํŠธ๋Š” nc localhost 12345๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค.
The client runs nc localhost 12345.

# server
./server

# client
 nc localhost 12345

๐Ÿ‘‰๐Ÿป์‹คํ–‰ ๊ฒฐ๊ณผ / Execution result

server/nc

Leave a Reply