네트워크 통신/Boost

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

마달랭 2024. 11. 8. 15:24
반응형

개요

TCP 소켓에 쓴다는 것은 이 소켓에 연결된 원격 어플리케이션으로 데이터를 보내는 출력 연산을 한다는 뜻이다.

Boost.asio가 제공하는 소켓 연산 중 가장 간단하게 데이터를 보내는 방법은 동기화 쓰기다.

동기적으로 소켓에 쓰기 연산을 하는 메서드와 함수는 실행 중인 스레드를 멈추게 하며, 데이터가 소켓에 쓰이거나 오류를 발생하면 멈춘 스레드를 풀어준다.

 

Boost.asio 라이브러리가 제공하는 가장 기본적은 소켓 쓰기 연산 방법은 write_some() 메서드이다.

이 메서드는 결합 버퍼를 나타내는 객체를 인자로 받으며, 이름에서 알 수 있듯이 버퍼의 데이터 일부를 소켓에 쓴다.

만약, 메서드가 성공하면 몇 바이트나 썼는지를 반환한다.

여기서 중요한 점은 이 메서드가 buffers 인자를 통해 받은 데이터 전체를 보내지 않을 수 있다는 점이다.

이 메서드는 오류가 발생하지 않는다면 적어도 한 바이트는 보낼 것이라는 점만 보장한다.

일반적으로 버퍼에 있는 모든 데이터를 소켓에 쓰려면 이 메서드를 여러 번 호출해야 한다.

 

 

write_some()

  1. 클라이언트 프로그램에서 능동 TCP 소켓을 초기화 한 뒤 열고, 서버에 연결한다.
  2. 서버 프로그램에서 수용자 소켓을 통해 연결 요청을 받아 연결시킨 능동 TCP 소켓을 얻는다.
  3. 버퍼를 할당하고 소켓으로 쓸 데이터를 채운다.
  4. 루프 내에서 버퍼에 저장된 모든 데이터를 보낼 때 까지 소켓의 write_some() 메서드를 호출한다.
#include <boost/asio.hpp>
#include <iostream>

using namespace boost;

void writeToSocket(asio::ip::tcp::socket& sock) {
	// 버퍼에 채울 값을 초기화 한다.
	std::string buf = "Hello";

	std::size_t total_bytes_written = 0;

	// 모든 데이터를 소켓에 쓸 때까지 루프를 돈다.
	while (total_bytes_written != buf.length()) {
		total_bytes_written += sock.write_some(
			asio::buffer(buf.c_str() +
				total_bytes_written,
				buf.length() - total_bytes_written));
	}
}

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);

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

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

	return 0;
}

 

해당 코드는 클라이언트 프로그램 상황일 때 소켓에 쓰는 방법이다.

 

main 함수에서는 소켓을 할당, 열고, 원격 어플리케이션에 동기적으로 연결한다.

그런 다음, wirteToSocket 함수를 호출하여 소켓에 버퍼를 쓰는 작업을 진행한다.

해당 함수의 매개변수로 소켓의 주소를 인자로 받기 때문에 소켓이 초기화된 상태여야 한다.

 

wirteToSocket 함수에서는 buf에 데이터를 초기화 해준다.

또한 total_bytes_written 변수를 0으로 초기화 했다. 해당 변수는 소켓에 쓴 데이터 바이트 수를 저장하는 변수이다.

그런 다음 buf가 빌 때까지 while루프를 진행한다. while 루프의 조건은 소켓에 쓴 데이터 바이트 수가 버퍼의 크기와 같아질때 종료된다. 따라서 버퍼 내의 모든 바이트가 소켓에 쓰여야만 끝난다는 말이다.

while루프를 도는 동안 write_some()메서드는 소켓에 쓰기 연산 후 성공한 바이트 수만큼을 반환하니 total_bytes_written 변수는 소켓에 쓰기 연산에 성공한 바이트 만큼 증가를 하게 된다.

 

write_some()이 호출될 때마다 전달되는 인자도 수정된다.

버퍼의 시작 위치는 원래 버퍼와 비교해 매번 성공한 바이트 수만큼 이동하게 된다.

여기서 total_bytes_written 인자가 buf의 위치를 계속 변경해 주는 것이다.

 

그럼 왜 굳이 while문을 사용해 준 것인가에 대한 의문이 있을 수도 있다.

데이터를 통신할 때는 항상 모든 데이터가 소켓에 씌여지고 통신에 사용될 것이라는 보장이 없다.

네트워크가 혼잡하거나 소켓이 꽉 차있다면 write_some 메서드는 버퍼의 일부분만 소켓에 쓸수도 있다.

그렇다면 버퍼에 남은 데이터들을 소켓에 모두 쓸때까지 계속 시도하는 것이다.

total_bytes_written은 해당 부분에서 버퍼에 있는 데이터를 일관되고 무결하게 보내는 것에 도움을 준다.

 

 

send()

asio::ip::tcp::socket 클래스에는 동기적으로 소켓에 데이터를 쓰는 또 다른 메서드인 send()가 있다.

이 메서드에는 세 가지 오버로딩 버전이 있다.

그 중 하나는 앞에서 설명한 write_some() 메서드와 완전히 동일하다.

 

가장 많이 쓰는 버전이 write_some()과 동일하기에 해당 부분을 통해 연산을 진행하게 된다.

 

 

write()

소켓의 write_some() 메서드를 통해 소켓에 쓰는 일은 간단한 연산이지만 코드가 복잡해 진다.

몇 바이트의 작은 메시지를 보내는 데도 루프를 돌아야 하고, 현재까지 몇 바이트나 보냈는지 기록해야 하며, 루프를 돌 때마다 버퍼를 적절히 생성해야 한다.

이러한 방식은 오류도 발생하기 쉽고, 사람이 이해하기도 어렵다.

 

Boost.asio에서 소켓에 쓰는 작업을 간단하게 해주는 asio::write() 메서드가 존재한다.

template <typename SyncWriteStream, typename ConstBufferSequence>
inline std::size_t write(SyncWriteStream& s, const ConstBufferSequence& buffers,
    constraint_t<
      is_const_buffer_sequence<ConstBufferSequence>::value
    >)

 

이 함수는 2개의 인자를 받는다, 첫 번째 인자인 s는 SyncWriteStream 개념을 만족하는 객체의 주소이다.

두번째 인자인 buffers는 버퍼를 나타내며 소켓에 쓸 데이터를 갖고 있어야 한다.

 

소켓 객체가 제공하는 write_some() 메서드는 버퍼에 있는 데이터 중 일부를 소켓에 쓰는데 반해, asio::write() 함수는 버퍼에 있는 모든 데이터를 소켓에 쓴다, 따라서 소켓에 쓰는 작업과 코드가 간단해진다.

 

void writeToSocketEnhanced(asio::ip::tcp::socket& sock) {
	// 버퍼에 채울 값을 초기화 한다.
	std::string buf = "Hello";

	// 버퍼 전체를 소켓에 쓴다.
	asio::write(sock, asio::buffer(buf));
}

 

코드가 엄청 간결해진 것을 볼 수 있다. 사실상 내부 구조는 write()와 write_some()과 유사한 방법으로 구사된다.

위의 예제 코드 뿐만 아니라 write() 함수는 더 많은 오버로딩 버전이 있다, 그 중 몇몇은 특정 상황에서 유용하다.

 

 

728x90
반응형