네트워크 통신/Boost

Boost.asio I/O TCP 소켓 동기적 읽기

마달랭 2024. 11. 8. 16:22
반응형

개요

TCP 소켓에서 읽는다는 것은 이 소켓에 연결된 원격 프로그램이 보낸 데이터를 받는다는 의미다.

Boost.asio가 제공하는 소켓을 사용해 데이터를 수신하는 가장 간단한 방법은 동기적으로 읽는 것이다.

쓰기와 마찬가지로 동기적 방식은 실행 중인 스레드를 멈추게 하고, 데이터를 모두 읽거나 오류가 발생하면 멈춘 스레드를 다시 풀어준다.

 

Boost.asio 라이브러리로 소켓에서 데이터를 읽는 방법 중 가장 기본적인 방법이다.

이 메서드는 쓸 수 있는 버퍼를 인자로 받고, 소켓에서 읽은 일부 데이터를 버퍼에 쓴다.

이 함수가 성공하면 몇 바이트나 읽었는지를 반환한다.

이 메서드가 몇 바이트를 읽을지는 제어할 수 없다.

다만 오류가 나지 않는 한 적어도 한 바이트 이상을 읽는다는 것을 보장한다.

일반적으로 소켓에서 일정량의 데이터를 읽으려면 write_some()과 동일하게 여러번 호출해 주어야 한다.

 

 

read_some()

  1. 클라이언트 프로그램에서 능동 TCP 소켓을 할당하고 서버에 연결 요청을 한다.
  2. 서버 프로그램에서 수용자 소켓을 통해 연결 요청을 받아 연결된 능동 TCP 소켓을 얻는다.
  3. 읽을 메시지의 예상 크기에 맞춰 큰 버퍼를 충분히 할당한다.
  4. 루프 내에서 메시지를 읽는 데 필요한 만큼의 소켓이 read_some() 메서드를 호출한다.
#include <boost/asio.hpp>
#include <iostream>

using namespace boost;

std::string readFromSocket(asio::ip::tcp::socket& sock) {
	const unsigned char MESSAGE_SIZE = 7;
	char buf[MESSAGE_SIZE];
	std::size_t total_bytes_read = 0;

	while (total_bytes_read != MESSAGE_SIZE) {
		total_bytes_read += sock.read_some(
			asio::buffer(buf + total_bytes_read,
				MESSAGE_SIZE - total_bytes_read));
	}

	return std::string(buf, total_bytes_read);
}

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

	try {
		asio::ip::tcp::endpoint
			ep(asio::ip::address::from_string(raw_ip_address),
				port_num);

		asio::io_service ios;

		asio::ip::tcp::socket sock(ios, ep.protocol());

		sock.connect(ep);

		readFromSocket(sock);
	}
	catch (system::system_error& e) {
		std::cout << "Error occured! Error code = " << e.code()
			<< ". Message: " << e.what();

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

	return 0;
}

 

클라이언트 프로그램에서 소켓을 읽는 방법이다.

 

  1. main 함수에서 서버의 IP 주소와 포트번호를 알고 있다고 가정한다.
  2. 종료점 ep를 IP주소 및 포트번호를 통해 초기화 해준다.
  3. io_service클래스의 인스턴스 ios를 초기화 해준다.
  4. ios, ep를 사용하여 소켓을 초기화 해준다.
  5. 초겟을 종료점에 connect 요청을 한다.
  6. 서버에서 connect 요청을 accept 했을 경우 이제 소켓 통신이 가능하다.
  7. readFromSocket 메서드에 connect에 성공한 소켓을 인자로 보낸다.
  8. 버퍼에 읽을 데이터의 사이즈를 정의해준다.
  9. 소켓이 현재까지 읽은 바이트 크기를 나타낼 변수를 0으로 초기화 해준다.
  10. 현재 소켓이 읽은 바이트와 메시지 크기가 같아질 때까지 while루프를 실행한다.
  11. read_some 함수를 호출하여 소켓에 데이터를 읽고 읽은 데이터 만큼 체크 변수에 더해준다.
  12. 버퍼에 저장된 값을 문자열 형태로 변환하여 리턴해준다.

대체적으로 wirte_some 메서드와 비슷하다, 단 write_some 메서드는 버퍼에 있는 데이터를 소켓에 쓰는 형태이며 read_some 메서드는 소켓에 있는 데이터를 버퍼에 읽는 형태이다.

write()와 마찬가지로 read()도 존재한다. 단 소켓 읽기의 경우 write에 비해 더 많은 메서드가 존재한다.

 

 

asio::read()

첫번째로 asio::read() 함수는 그 중에서도 가장 간단한 함수이다.

 

template <typename SyncReadStream, typename MutableBufferSequence>
inline std::size_t read(SyncReadStream& s, const MutableBufferSequence& buffers,
    constraint_t<is_mutable_buffer_sequence<MutableBufferSequence>::value>)

 

이 함수는 2개의 인자를 받는다.

  • 첫번째 인자 s는 SyncReadStream 개념의 요구사항을 만족시키는 객체의 주소를 받는다.
  • 두번째 인자 buffers는 소켓에서 읽은 데이터를 저장할 버퍼를 나타낸다.

 

소켓 객체가 제공하는 read_some() 메서드는 소켓의 데이터 중 일부를 읽어들일 수도 있는데에 반해 read() 함수는 한 번만 호출해도 인자로 받은 버퍼가 꽉 차거나 오류가 발생할 때까지 소켓에서 데이터를 읽는다.

while루프 및 읽은 바이트 개수를 추적할 필요가 없으니 작업이 간단해지고, 코드도 짧아진다.

 

std::string readFromSocketEnhanced(asio::ip::tcp::socket& sock) {
	const unsigned char MESSAGE_SIZE = 7;
	char buf[MESSAGE_SIZE];

	asio::read(sock, asio::buffer(buf, MESSAGE_SIZE));

	return std::string(buf, MESSAGE_SIZE);
}

 

 

asio::read_until()

소켓의 데이터에서 특정 패턴을 만나기 전까지 모든 데이터를 읽는 방법을 제공한다.

 

template <typename SyncReadStream, typename Allocator>
inline std::size_t read_until(SyncReadStream& s,
    boost::asio::basic_streambuf<Allocator>& b, char delim)

 

이 함수는 3개의 인자를 받는다.

  • 첫 번째 인자인 s는 read함수와 동일하게 SyncReadStream 개념의 요구사항을 객체에 대한 주소이다.
  • 두 번째 인자인 b는 데이터를 읽어들일 스트림 기반의 확장 가능한 버퍼를 나타낸다.
  • 세 번째 인자인 delim은 구분자 문자를 나타낸다.

 

해당 함수는 소켓 s에서 데이터를 읽어 버퍼 b에 쓰는데, 만약 읽은 데이터가 delim 인자와 같다면 읽기를 끝내고 반환한다.

이 함수는 다양한 크기의 데이터를 읽을 수 있도록 구현됐다.

이 함수가 반환됐을 때, 버퍼 b의 구분자 기호 다음에도 몇 가지 기호가 덧붙여 있을 수도 있다.

원격 프로그램이 구분자 기호 다음에 데이터를 붙여 보내면 이러한 일이 발생한다.

 

다시 말해 asio::read_until() 함수가 성공적으로 끝나면, 버퍼 b에 적어도 하나의 구분자 기호가 들어 있다는 것을 보장한다. (더 많을 수도 있음)

구분자 기호 뒤에 데이터가 더 있을 경우, 파싱하는 것은 개발자가 할 일이다.

 

예를 들어보자

현재 소켓에는 "abcdefghijklmnopqrstuvwxyz" 라는 문자열 데이터가 있다.

read_unilt 함수를 사용해 소켓에서 버퍼로 'i'를 구분자로 데이터를 읽는다면 2바이트를 읽고 반환할 것이다.

버퍼엔 "abcdefghijklmnop"가 존재하고 소켓엔 " qrstuvwxyz"가 존재하는 상태이다.

여기서 적절하게 데이터를 파싱하는 것은 개발자가 알아서 해야한다!

 

std::string readFromSocketDelim(asio::ip::tcp::socket& sock) {
	asio::streambuf buf;

	// \n 기호를 만날 때 까지 소켓에서 동기적 데이터를 읽는다.
	asio::read_until(sock, buf, '\n');

	std::string message;

	// buf에 \n 이후에도 데이터가 더 있을 수 있기 때문에 개행문자를 기준으로 파싱한다.
	// 만약 구분자가 ' '이거나 'a'라면 getline의 세번째 인자로 전달하면 되겠죠?
	std::istream input_stream(&buf);
	std::getline(input_stream, message);

	return message;
}

 

 

asio::read_at()

소켓에서 데이터를 읽을 때 원하는 오프셋에서 부터 읽을 수 있다.

이 함수는 거의 사용되지 않기 때문에 이런 함수도 있구나~ 생각하고 넘어가면 될 것 같다.

 

 

728x90
반응형