네트워크 통신/Boost

Boost.asio 동기 TCP 클라이언트

마달랭 2024. 11. 18. 10:13
반응형

개요

서버 프로그램은 아스키 문자열로 표현된 요청을 받는다, 요청 문자열의 형식은 다음과 같다.

EMULATE_LONG_COMP_OP [s]<LF>

 

여기서 [s]는 양수를, <LF>는 아스키로 개행 문자를 나타낸다.

서버는 이 문자열을 [s]초 동안 무의미한 연산을 실행해달라는 요청으로 해석한다.

 

"EMULATE_LONG_COMP_OP 10\n"

 

위와 같은 문자열이 서버에 요청으로 들어왔다고 가정한다면, 서버는 클라이언트가 10초간 무의미한 연산을 한 후 응답을 보내달라고 요청한 것으로 생각한다.

 

클라이언트의 요청과 마찬가지로, 서버가 보내는 응답도 아스키 문자열로 표현된다.

연산이 성공한다면 OK<LF>를, 실패한다면 ERROR<LF>를 보낸다.

 

 

동기 TCP 클라이언트 구현

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

  • 클라이언트 서버 통신 모델에서 클라이언트로 동작한다.
  • TCP 프로토콜을 사용해 서버 프로그램과 통신한다.
  • I/O 및 제어 연산을 하는 동안에는연산이 끝나거나 오류가 발생할 때까지 실행 스레드를 멈춘다.

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

  1. 서버 프로그램의 IP주소와 프로토콜 포트 번호를 알아낸다.
  2. 능동 소켓을 할당한다.
  3. 서버 프로그램과 연결을 수립한다.
  4. 서버와 메시지를 주고 받는다.
  5. 연결을 종료한다.
  6. 소켓을 할당 해지한다.
#include <boost/asio.hpp>
#include <iostream>

using namespace boost;

// 동기 TCP 클라이언트 클래스
class SyncTCPClient {
public:
    SyncTCPClient(const std::string& raw_ip_address,
        unsigned short port_num) :
        m_ep(asio::ip::make_address(raw_ip_address), port_num), // 최신 버전에서는 from_string 대신 make_address 사용
        m_sock(m_ios) {

        m_sock.open(m_ep.protocol());
    }

    // 서버에 연결하는 함수
    void connect() {
        m_sock.connect(m_ep);
    }

    // 연결을 종료하는 함수
    void close() {
        m_sock.shutdown(asio::ip::tcp::socket::shutdown_both);
        m_sock.close();
    }

    // 서버에 오래 걸리는 작업을 요청하고 응답을 받는 함수
    std::string emulateLongComputationOp(unsigned int duration_sec) {
        std::string request = "EMULATE_LONG_COMP_OP " + std::to_string(duration_sec) + "\n";
        sendRequest(request);
        return receiveResponse();
    }

private:
    // 서버에 요청을 보내는 함수
    void sendRequest(const std::string& request) {
        asio::write(m_sock, asio::buffer(request));
    }

    // 서버의 응답을 받는 함수
    std::string receiveResponse() {
        asio::streambuf buf;
        asio::read_until(m_sock, buf, '\n');

        std::istream input(&buf);
        std::string response;
        std::getline(input, response);

        return response;
    }

private:
    asio::io_context m_ios; // io_service 대신 io_context 사용

    asio::ip::tcp::endpoint m_ep;
    asio::ip::tcp::socket m_sock;
};

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

    try {
        SyncTCPClient client(raw_ip_address, port_num);

        // 동기 연결
        client.connect();

        std::cout << "서버에 요청을 보냅니다..." << std::endl;

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

        std::cout << "응답을 받았습니다: " << response << std::endl;

        // 연결을 종료하고 리소스를 해제합니다.
        client.close();
    }
    catch (system::system_error& e) {
        std::cout << "오류 발생! 오류 코드 = " << e.code()
            << ". 메시지: " << e.what();

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

    return 0;
}

 

모두 기존에 I/O 연산에서 사용했던 메서드가 모여있는 것을 알 수 있다.

다만 SyncTCPClient라는 클래스르 별도로 생성하여 연결부터 연산 작업, 소켓 종료와 닫기 모두 진행된다.

  1. main함수에서는 ip주소와 포트번호를 알고 있다고 가정하고 있다.
  2. SyncTCPClient 클래스의 생성자에 ip주소와 포트 번호를 전달하여 인스턴스 client를 생성한다.
  3. client를 통해 SyncTCPClient 클래스의 connect 메서드를 호출하여 연결을 시도한다.
  4. 연결이 완료되었을 경우 client를 통해 SyncTCPClient 클래스의 emulateLongComputationOp 메서드를 호출한다.
  5. emulateLongComputationOp 메서드는 양수 정수를 매개변수로 받아 해당 초간 무의미한 연산 작업을 수행하는 버퍼를 초기화 한 뒤 sendRequest 함수에 매개변수로 전달하여 실행한다.
  6. sendRequest 함수는 write를 통해 버퍼로 전달받은 문자열을 소켓에 쓴다.
  7. 작업이 완료된 후엔 receiveResponse 함수의 결괏값을 리턴해 준다.
  8. receiveResponse 함수에서는 가변 버퍼 buf에 read_until 메서드를 통해 개행 문자를 만날 때 까지 읽기 연산을 한다.
  9. 이후 버퍼를 istream 타입의 객체로 변환하고, 빈 문자열 response를 초기화 해준다.
  10. getline 메서드를 통해 input에서 개행 문자를 구분 문자로 response에 데이터를 파싱해 준다.
  11. 이후 response를 리턴하면 쵲오적으로 emulateLongComputationOp 메서드의 반환값이 된다.
  12. 이를 main함수의 response에 값을 받아들인 뒤 클라이언트가 원하는 작업을 진행하면 된다.
  13. I/O 연산이 완료되면 client를 통해 SyncTCPClient 클래스의 close 메서드를 실행해 소켓을 종료하고 닫는다.

해당 동기 TCP 클라이언트 예제에서는 각 작업이 진행되는 모든 순간동안 스레드가 멈추게 된다.

728x90
반응형