C++에서 Boost ASIO를 사용하여 TCP 비동기(Unblocked) 통신 프로그래밍

공대생의 팁 2019.03.07 23:04
 C++에서 소켓 통신을 할 때 동기식(Blocked, sync)과 비동기식(Unblocked, async)방식으로 TCP 통신을 구현합니다. 그러나 Java에 비교하였을 때 C에서 기본으로 제공하는 socket 프로그래밍을 사용하는 것은 여간 불편한 일이 아닙니다.

 이러한 환경에서 Boost 라이브러리에서 제공하는 ASIO 소켓 통신 프로그램을 사용하게 되면 이전보다 코딩 환경이 매우 쾌적함을 경험할 수 있습니다.

 이번 포스팅에서는 Boost 라이브러리에서 제공하는 ASIO를 사용하여 Unblock(Asynchronization)방식의 소켓 통신 프로그램을 구현해 보았습니다.


 위 그림은 Boost library에서 ASIO의 비동기(Asynchronization)방식의 소켓 통신 프로그래밍의 알고리즘을 나타낸 것입니다.

 프로그램을 실행하게 되었을 때 Socket에 IP주소와 Port 번호를 등록하고 이를 io_service(1.66 버전 이후에서는 io_context)에 이 때 handle_connect, handle_read, handle_write와 같은 handler 또한 함께 등록합니다. 이 때 등록된 handler는 server 혹은 client에서 실행되었을 때 바로 handler를 실행하는 방법으로 어느 한 쪽으로부터 데이터 전달을 기다리지 않고 있다가 요청이 들어왔을 때 실행하는 것이지요.

 시작하기에 앞서 Ubuntu 환경에서 다음과 같이 Boost 라이브러리를 설치합니다.

$ sudo apt install libboost-all-dev

자세한 내용은 소스코드의 주석을 통해 설명을 달아보았습니다.

server.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
#include <cstdlib>
#include <iostream>
#include <boost/bind.hpp>
#include <boost/asio.hpp>
 
using boost::asio::ip::tcp;
using namespace std;
 
class session
{
public:
  session(boost::asio::io_service& io_service)
  //boost 1.66이후 (Ubuntu 18.10 이후) 버전의 경우 io_context를 사용
  //session(boost::asio::io_context& io_service)
    : socket_(io_service)
  {
  }
 
  tcp::socket& socket()
  {
    return socket_;
  }
 
  void start()
  {
    //client로부터 연결됨
    cout << "connected" << endl;
    //client로부터 비동기 read 실행
    socket_.async_read_some(boost::asio::buffer(data_, max_length),
        boost::bind(&session::handle_read, this,
          boost::asio::placeholders::error,
          boost::asio::placeholders::bytes_transferred));
  }
 
private:
  void handle_read(const boost::system::error_code& error,
      size_t bytes_transferred)
  {
    if (!error)
    {
      cout << data_ << endl;
    }
    else
    {
      delete this;
    }
  }
 
  tcp::socket socket_;
  enum { max_length = 1024 };
  char data_[max_length];
};
 
class server
{
public:
  server(boost::asio::io_service& io_service, short port)
  //boost 1.66이후 (Ubuntu 18.10 이후) 버전의 경우 io_context를 사용
  //server(boost::asio::io_context& io_service, short port)
    : io_service_(io_service),
      //PORT 번호 등록
      acceptor_(io_service, tcp::endpoint(tcp::v4(), port))
  {
    start_accept();
  }
 
private:
  void start_accept()
  {
    session* new_session = new session(io_service_);
    //client로부터 접속될 때 까지 대기한다.
    acceptor_.async_accept(new_session->socket(),
        boost::bind(&server::handle_accept, this, new_session,
          boost::asio::placeholders::error));
  }
 
  //client로부터 접속이 되었을 때 해당 handler 함수를 실행한다.
  void handle_accept(session* new_session,
      const boost::system::error_code& error)
  {
    if (!error)
    {
      new_session->start();
    }
    else
    {
      delete new_session;
    }
    //client로부터 접속이 끊겼을 대 다시 대기한다.
    start_accept();
  }
 
  boost::asio::io_service& io_service_;
  //boost 1.66이후 (Ubuntu 18.10 이후) 버전의 경우 io_context를 사용
  //boost::asio::io_context &io_service_;
  tcp::acceptor acceptor_;
};
 
int main(int argc, char* argv[])
{
  try
  {
    if (argc != 2)
    {
      std::cerr << "Usage: async_tcp_echo_server <port>\n";
      return 1;
    }
 
    boost::asio::io_service io_service;
    //boost 1.66이후 (Ubuntu 18.10 이후) 버전의 경우 io_context를 사용
    //boost::asio::io_context io_service;
 
    server s(io_service, atoi(argv[1]));
    //asio 통신을 시작한다.
    io_service.run();
  }
  catch (exception& e)
  {
    cerr << "Exception: " << e.what() << "\n";
  }
 
  return 0;
}
cs

client.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
#include<cstdlib>
#include<cstring>
#include<iostream>
#include<boost/asio.hpp>
#include<boost/bind.hpp>
 
using boost::asio::ip::tcp;
using namespace std;
 
enum { max_length = 1024 };
 
class client
{
public:
  client(boost::asio::io_service& io_context,
  //client(boost::asio::io_context& io_context,
      const string& host, const string& port) : socket_(io_context)
  {
    boost::asio::ip::tcp::resolver resolver(io_context);
    boost::asio::ip::tcp::resolver::query query(host, port);
    boost::asio::ip::tcp::resolver::iterator endpoint_iterator = resolver.resolve(query);
 
    //비동기(Unblock) 상태로 서버와 접속한다.
    boost::asio::async_connect(socket_, endpoint_iterator,
        boost::bind(&client::handle_connect, this, boost::asio::placeholders::error));
  }
 
  void handle_connect(const boost::system::error_code& e){
    if(!e){
      cout << "Connected!" << endl;
      string msg = "Hello! Server!";
      //Server로부터 비동기 write를 시도한다.
      boost::asio::async_write(socket_, boost::asio::buffer(msg, msg.length()),
          boost::bind(&client::handle_write,this,boost::asio::placeholders::error)); 
    }
  }
 
  void handle_write(const boost::system::error_code& e){
    if(!e){
      cout << "Done!" << endl;
    }
  }
 
private:
  tcp::socket socket_;
  char data_[max_length];
};
 
int main(int argc, char* argv[])
{
  try
  {
    // Check command line arguments.
    if (argc != 3)
    {
      std::cerr << "Usage: client <host> <port>" << std::endl;
      return 1;
    }
 
    boost::asio::io_service io_context;
    //boost::asio:io_context io_context;
    client c(io_context, argv[1], argv[2]);
    io_context.run();
  }
  catch (std::exception& e)
  {
    cerr << e.what() << endl;
  }
 
  return 0;
}
cs

CMakeLists.txt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
cmake_minimum_required(VERSION 3.0)
 
project(asio_async)
find_package(Boost REQUIRED system)
find_package(Threads)
 
include_directories(${Boost_INCLUDE_DIR})
 
add_executable(client
    client.cpp
)
 
add_executable(server
    server.cpp
)
 
target_link_libraries(client
    ${Boost_LIBRARIES}
    ${CMAKE_THREAD_LIBS_INIT}
)
 
target_link_libraries(server
    ${Boost_LIBRARIES}
)
cs


Linux 환경에서 위의 3개의 소스코드를 한 폴더에 넣으신 후 다음과 같이 실행합니다.


$ mkdir build

$ cd build

$ cmake ..

$ make


 위와 같은 과정을 거치면 client와 server라는 실행파일이 build 폴더 안에 생성됩니다. 이 두 프로그램을 실행하면 다음과 같은 결과가 나옵니다.


1
2
3
4
$ ./server 2580
 
Connected!
Hello! Server!
cs



1
2
3
4
./client 127.0.0.1 2580
 
Connected!
Done!
cs


관련 자료

C++에서 Boost ASIO를 사용하여 TCP 동기화(Blocked) 통신 프로그래밍

https://elecs.tistory.com/332

  • Wordbe 2019.06.09 04:04 신고 ADDR 수정/삭제 답글

    도움이 되었습니다 감사합니다.

  • 나그네 2019.08.06 15:09 ADDR 수정/삭제 답글

    클라이언트는 일회성인가요? 한번읽고 닫고
    다시 연결하고 쓰고 닫고..

    • Justin T. 2019.08.06 15:34 신고 수정/삭제

      서로 연결된 소켓이 한 쪽에서 close를 하게 되면 반대편의 소켓에서 통신이 종료되었다는 signal을 보내어 종료 요청을 합니다. 즉 한 쪽에서 통신을 종료하게 되면 양쪽의 소켓은 사용이 종료됩니다. 그러므로 양측이 다시 통신을 하기 위해서는 소켓을 다시 열어야합니다.
      자세한 내용을 알고 싶으시다면 아래의 링크를 참고해주세요.
      https://sunyzero.tistory.com/m/167

  • 나그네 2019.08.06 17:18 ADDR 수정/삭제 답글

    답변 감사합니다.
    소켓이 뭔지도 잘 모르고 프로그래밍은 무리였네요.

    한가지 더 질문이 있는데,
    보통 소켓통신은 1:1접속후 클라이언트가 서버에 무엇인가 요청을 하고, 서버는 요청 데이터를 보내주는것 같은데 맞나요?

    항상 모든 예제가 클라이언트는 write만 있어서 어떤식으로 구성할지 막막하네요
    보통 핸들러에는 어떤걸 구현하나요?

  • Justin T. 2019.08.06 18:50 신고 ADDR 수정/삭제 답글

    클라이언트와 서버가 소켓으로 연결되면 상호간에 데이터를 주고 받는것이 가능합니다.
    핸들러에서는 클라이언트 혹은 서버가 연결된 상대에게 데이터를 보냈을 때 이를 메인스레드와는 독립적으로 동작시키기를 원할 때 따로 구현한 것이라고 할 수 있겠습니다.

    • 나그네 2019.08.07 13:41 수정/삭제

      감사합니다.
      그럼 한가지 질문이 있는데,
      https://www.boost.org/doc/libs/1_53_0/doc/html/boost_asio/example/chat/chat_client.cpp
      예제에서 보이듯
      클라이언트가 한번 커넥트가 되면, 그 뒤로 계속 읽기 모드로 동작합니다.
      handle_connect -> handle_read_header -> handle_read_body -> header -> body ...
      이처럼 read 는 항상 읽고 있는 상태여야, 다른 소켓에서 데이터를 보낼때 반응을 할 수 있는게 맞는건가요?

    • Justin T. 2019.08.16 15:03 신고 수정/삭제

      정확히 말씀드리자면 handler를 사용하여 상대방에서 데이터를 보냈다는 신호가 오는지 기다리다가 소켓과 연결된 상대가 데이터를 보내는 것이 감지되면 handler가 이를 수신하고 이를 read로 읽은 것이라고 할 수 있겠습니다.