소켓 통신/Boost

Boost.asio 동기 UDP 클라이언트

마달랭 2024. 11. 18. 10:34

개요

동기 UDP 클라이언트는 다음과 같은 사항을 만족시키는 분산 어플리케이션이다.

  • 클라이언트 서버 통신 모델에서 클라이언트로 동작한다.
  • UDP 프로토콜을 사용해 서버 프로그램과 통신한다.

I/O 및 제어 연산을 하는 동안에는 연산이 끝나거나 오류가 발생할 때까지 실행 스레드를 멈춘다.

일반적인 동기 UDP 클라이언트는 다음과 같은 알고리즘에 따라 동작한다.

  1. 클라이언트 프로토콜이 통신할 각 서버 프로그램의 IP 주소와 프로토콜 포트 번호를 알아낸다.
  2. UDP 소켓을 할당한다.
  3. 서버들과 메시지를 주고 받는다.
  4. 소켓을 할당 해지한다.

 

동기 UDP 클라이언트 구현

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

using namespace boost;

class SyncUDPClient {
public:
    SyncUDPClient() :
        m_sock(m_ios) {

        m_sock.open(asio::ip::udp::v4());
    }

    std::string emulateLongComputationOp(
        unsigned int duration_sec,
        const std::string& raw_ip_address,
        unsigned short port_num) {

        std::string request = "EMULATE_LONG_COMP_OP "
            + std::to_string(duration_sec)
            + "\n";

        asio::ip::udp::endpoint ep(
            asio::ip::address::from_string(raw_ip_address),
            port_num);

        sendRequest(ep, request);
        return receiveResponse(ep);
    };

private:
    void sendRequest(const asio::ip::udp::endpoint& ep,
        const std::string& request) {

        m_sock.send_to(asio::buffer(request), ep);
    }

    std::string receiveResponse(asio::ip::udp::endpoint& ep) {
        char response[6];
        std::size_t bytes_received =
            m_sock.receive_from(asio::buffer(response), ep);

        return std::string(response, bytes_received);
    }

private:
    asio::io_context m_ios;  // 최신 버전에서는 io_context 사용

    asio::ip::udp::socket m_sock;
};

int main()
{
    const std::string server1_raw_ip_address = "127.0.0.1";
    const unsigned short server1_port_num = 3333;

    const std::string server2_raw_ip_address = "192.168.1.10";
    const unsigned short server2_port_num = 3334;

    try {
        SyncUDPClient client;

        std::cout << "Sending request to the server #1 ... "
            << std::endl;

        std::string response =
            client.emulateLongComputationOp(10,
                server1_raw_ip_address, server1_port_num);

        std::cout << "Response from the server #1 received: "
            << response << std::endl;

        std::cout << "Sending request to the server #2... "
            << std::endl;

        response =
            client.emulateLongComputationOp(10,
                server2_raw_ip_address, server2_port_num);

        std::cout << "Response from the server #2 received: "
            << response << std::endl;
    }
    catch (system::system_error& e) {
        std::cout << "Error occurred! Error code = " << e.code()
            << ". Message: " << e.what();

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

    return 0;
}

 

 

  1. 우선 SyncUDPClient의 인스턴스로 생성할때 생성자의 인자로 아무것도 전달하지 않았다.
  2. 해당 클래스의 인스턴스를 생성할 때 클래스의 생성자는 io_context와 소켓을 초기화 한다.
  3. 또한 emulateLongComputationOp 메서드에 ip주소와 포트 번호를 인자로 받는다.
  4. 따라서 소켓 통신을 할 때 해당 메서드에 ip주소와 포트 번호를 전달하면 통신이 가능하다.
  5. UDP 통신에선 상대방과 연결이 잘 됐는지, 메시지를 잘 받았는지는 상관없이 자기가 원하는 통신만 진행한다.
  6. emulateLongComputationOp 메서드에서 요청을 보낼 내용을 문자열 변수 request에 저장한다.
  7. 매개변수로 입력 받았던 ip주소와 포트 번호를 from_string메서드를 통해 종료점을 생성해 준다.
  8. sendRequest 함수에 종료점과 버퍼를 매개변수로 전달한다.
  9. sendRequest 함수에선 send_to 메서드르르 통하여 request를 버퍼로 사용하여 종료점에 데이터를 쓴다.
  10. 해당 작업 이후 emulateLongComputationOp 메서드에서 receiveResponse 함수를 호출하고 해당 리턴값을 리턴한다.
  11. receiveResponse 함수는 종료점을 인자로 받으며, 해당 종료점으로 부터 receive_from 메서드를 통해 버퍼에 데이터를 읽는다, 또한 bytes_received 변수를 통해 읽은 바이트 수를 받고, 해당 크기만큼 문자열을 반환한다.

이 처럼 UDP 통신은 연결less 프로토콜로, 상태를 유지하지 않으며, 각 패킷은 독립적으로 처리한다.

TCP 통신과 달리 UDP 통신에서 응답을 받을 때 정적 배열을 사용하는 것에 대해 의문을 가질 수 있다.

이는 UDP 통신의 특성으로 서버에서 보내는 데이터의 크기나 구조가 미리 정의된 경우가 많기 때문이다.

서버에서 보내는 응답의 크기가 일정하거나 예상되는 최대 크기가 있을 때, 정적 크기의 버퍼를 사용하여 간단히 처리할 수 있다.

만약 서버의 응답 크기가 예측할 수 없거나 동적으로 변화하는 경우라면, 동적 크기 버퍼를 사용하는 것이 좋다.

예제에서 크기를 6바이트로 설정한 이유는 응답 데이터가 "OK\n", "ERROR\n" 둘 뿐이라 가정했기 때문이다.

 

728x90