웬디의 기묘한 이야기

글 작성자: WENDYS
반응형

C++ Async Socket Server Example

server/client 프로그램을 개발하다 보면 간단하게 1:1 send, receive만 하면 편하겠지만 일반적으로 여러 클라이언트는 동시에 붙을 수 있고 서버는 동시에 여러 가지 일을 처리할 수 있는 비동기 서버가 필요한 경우가 더 많습니다.

비동기 소켓 서버를 간단히 만드는건 어찌어찌할 수 있지만 잘 만들기는 쉽지 않은데요,

이때 활용할 수 있는 검증된 라이브러리가 바로 Boost의 ASIO (Async Input Output) Library입니다.

 

Boost Library 다운로드 및 설치 - https://wendys.tistory.com/115

 

[C/C++] 윈도우10 Boost 최신버전 설치 및 사용법

Boost Library Download and Build C++ 필수 라이브러리 중 Boost Library에 대해 설치 및 사용법을 정리합니다. Boost는 공식 홈페이지에서 다운로드 가능하며, 주기적으로 업데이트가 되고 있습니다. https://ww..

wendys.tistory.com

아주아주 잘 되어있는 라이브러리이지만 비동기 소켓 프로그래밍 자체가 그리 간단하지는 않기 때문에 한번 만들어놓고 응용하여 사용할 수 있도록 클래스로 만들어서 사용 중인 것을 공유하고자 합니다.

Boost ASIO는 소켓뿐만 아니라 파이프 서버에도 응용이 가능합니다.

 

boost/asio.hpp, boost/bind.hpp 헤더를 사용합니다.

#include <boost/asio.hpp>
#include <boost/bind.hpp>

 

비동기 소켓 헤더

#pragma once

class socket_server_tcp {

public:
    static const size_t buffer_size = 8192;

public:
    socket_server_tcp(boost::asio::io_context &io_context);
    ~socket_server_tcp();

    boost::asio::ip::tcp::socket& native_object() {
        return _socket;
    }

    void start_accept();

private:

    void read_complete(
        __in const boost::system::error_code& error,
        __in size_t bytes_received
        );

    void write(
        __in const void* data,
        __in const size_t size
        );

    void write_complete(
        __in const boost::system::error_code&,
        __in size_t
        );

    void reset();

    void read();

    void accept_complete(const boost::system::error_code& error);

private:

    int _sequence_number;
    char _receive_buffer[buffer_size];

    boost::asio::ip::tcp::acceptor _acceptor;
    boost::asio::ip::tcp::socket _socket;
};

 

비동기 소켓 예제

#include "stdafx.h"

socket_server_tcp::socket_server_tcp(boost::asio::io_context &io_context) :
    _acceptor(io_context,
        boost::asio::ip::tcp::endpoint(
            boost::asio::ip::tcp::v4(),
            __socket_server_port
        )
    ), _socket(io_context) {

}

socket_server_tcp::~socket_server_tcp() {

}

void socket_server_tcp::start_accept() {
    
    _acceptor.async_accept(_socket,
        boost::bind(
            &socket_server_tcp::accept_complete,
            this,
            boost::asio::placeholders::error)
    );
}

void socket_server_tcp::accept_complete(
    const boost::system::error_code& error
    ) {

    if (!error) {
        __log_trace("a new connection was established.");
        read();
    }
    else {
        __log_trace("error occured");
    }
}

void socket_server_tcp::read() {
    memset(&_receive_buffer, '\0', sizeof(_receive_buffer));
    _socket.async_read_some(
        boost::asio::buffer(_receive_buffer),
        boost::bind(&socket_server_tcp::read_complete, 
            this,
            boost::asio::placeholders::error,
            boost::asio::placeholders::bytes_transferred)
    );
}

void socket_server_tcp::read_complete(
    __in const boost::system::error_code& error,
    __in size_t bytes_received
    ) {

    if (error) {
        if (error == boost::asio::error::eof) {
            __log_trace("client disconnection.");
        }
        else {
            __log_trace(error.message().c_str());
        }

        reset();
    }
    else {

        auto transactor = [this](
            __in const void* data,
            __in const size_t size
            ) {

            bool send_empty_reply = true;

            if (data && size) {
                
                //
                // receive packet data parsing.
                //

            }

            if (send_empty_reply) {
                basic_socket_packet<> reply_packet;
                write(&reply_packet, sizeof(reply_packet));
            }
        };

        //
        // do transaction.
        //

        transactor(_receive_buffer, bytes_received);

        //
        // read next data.
        //

        read();
    }
}

void socket_server_tcp::write(
    __in const void* data,
    __in const size_t size
    ) {
    boost::asio::async_write(_socket,
        boost::asio::buffer(data, size),
        boost::bind(&socket_server_tcp::write_complete,
            this,
            boost::asio::placeholders::error,
            boost::asio::placeholders::bytes_transferred)
    );
}

void socket_server_tcp::write_complete(
    __in const boost::system::error_code&,
    __in size_t
    ) {

}

void socket_server_tcp::reset() {
    if (_socket.is_open()) {
        _socket.close();
    }
    
    start_accept();
}

생각보다 매우 간단해 보이지 않나요??

이제 여기서 우리가 할 작업은 모든 데이터가 들어왔음을 알리는 read_complete 함수에서 transactor 내부를 구현하기만 하면 됩니다.

예를 들어 소켓 데이터를 만들어놓고 프로토콜에 맞춰 통신을 하게 되는데요 샘플을 한번 보면 이런 식입니다.

 

소켓 프로토콜 예제

struct socket_data_type {
    int data_type;
    unsigned long data_size;
    char* data;
};

auto transactor = [this](
            __in const void* data,
            __in const size_t size
            ) {

            bool send_empty_reply = true;

            if (data && size) {
                
                const socket_data_type* socket_data = reinterpret_cast<const socket_data_type*>(data);
                if (0 == socket_data->data_type) {

                    //
                    // 
                    //

                }
                else {

                    //
                    // ...
                    //

                    socket_data_type reply_packet;
                    reply_packet .data_type = 1;

                    write(&reply_packet, sizeof(reply_packet));
                    send_empty_reply = false;
                }
            }

            if (send_empty_reply) {
                basic_socket_packet<> reply_packet;
                write(&reply_packet, sizeof(reply_packet));
            }
        };

여기서 주의할 점은 _socket 값은 멤버로 구현 시 생성자에서 생성을 해주어야만 한다는 것입니다.

이렇게 소켓 클래스를 구현했다면 이제 어떻게 사용하는지 방법이 궁금하실 거라고 생각이 드네요

비동기 소켓을 사용한다 하더라도 하나의 소켓에 여러 클라이언트를 처리할 수 없기에 여러 개의 소켓을 생성해놓고 동시다발적으로 처리되는 것처럼 처리를 해야 합니다.

소켓을 몇 개를 생성할지는 각자의 환경을 고려하여 테스트 후 적용해야 되기 때문에 개수가 정해져 있지는 않습니다.

 

비동기 소켓 boost::asio 사용 예제

private:
    std::list<std::thread> _socket_threads;
//
// run as socket server.
//

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

    _socket_threads.push_back(std::thread([&]() {
        boost::asio::io_context io_context;

        socket_server_tcp socket_server(io_context);
        socket_server.start_accept();

        io_context.run();
    }));
}
클라이언트는 보통 비동기로 하지 않아도 충분하기때문에 connect > send > receive > disconnect 형태로 구현해도 상관없습니다.
간단한 소켓 예제는 MSDN을 참고하셔도 충분할 것 같습니다.

 

 

반응형