개요
이전 글에서 다루었던 cancel() 메서드의 경우 연산이 시작되고 난 후 해당 소켓 객체에서 호출되어야만 한다.
TCP 프로토콜로 통신하는 어떤 분산 어플리케이션은 메시지의 크기가 고정되어 있지도 않고, 특정 바이트 순열로 메시지의 끝을 나타내는 것도 아닌 경우가 있다.
따라서 소켓에서 메시지를 읽어들이는 동안 어디서 메시지가 끝나는지 메시지 자체로는 알 수 없다.
헤더 영역과 본문 영역으로 메시지를 구성해 메시지의 끝을 알릴 수도 있다.
헤더 부분은 크기가 고정되어 있고 미리 정해진 구조를 따르면서 본문 부분의 크기를 알리는 것이다.
그러면 수신하는 측에서 헤더 부분만 먼저 읽어 분석하기만 하면 메시지 본문의 크기를 알 수 있고, 나머지 메시지를 적절하게 읽을 수 있다.
이 방법은 매우 간단하며 널리 사용되고 있다, 하지만 중복되는 부분도 있고 계산 하는 데 드는 부하가 있다.
소켓 종료
또 다른 방법은 어플리케이션이 각각의 상대와 통신할 때 매번 새로운 소켓을 사용하는 경우에 쓸 수 있다.
소켓에 메시지를 쓴 후 메시지를 전송한 측에서 소켓의 전송 부분을 종료하는 방법이다.
그러면 특별한 서비스 메시지가 수신측으로 전달되고, 수신 측에서는 메시지가 끝났으며 더 이상 상대가 현재 연결로는 아무것도 전송하지 않을 것이라는 점을 알 수 있다.
소켓 닫기
두 번째 방법은 TCP 프로토콜 소프트웨어에 정의된 것을 활용하는 것이다.
소켓에서 할 수 있는 또 다른 연산으로는 닫기가 있다.
이는 종료하기와 유사해 보이지만 사실 매우 다르다.
소켓을 닫는다는 것은 소켓 및 소켓과 관련된 다른 자원들을 운영체제로 되돌려준다는 것을 의미한다.
메모리, 프로세스나 스레드, 파일 핸들, 뮤텍스 처럼 소켓도 운영체제의 자원이다.
다른 자원들처럼 할당하고 사용한 후, 쓰지 않는다면 운영체제로 되돌려줘야 한다.
그렇지 않다면 자원 누수가 일어나 결국에는 자원이 모자라게 되고, 어플리케이션이 잘못되거나 운영체제가 불안정해 진다.
소켓을 닫지 않으면 매우 심각한 문제가 일어날 수 있기 때문에 닫는 연산은 매우 중요하다.
소켓 종료와 닫기 차이점
소켓 닫기의 경우 수립된 연결을 방해하고 결국은 소켓을 할당 해지하여 운영체제로 되돌려준다.
소켓 종료의 경우 그저 소켓에서 쓰기나 읽기 또는 둘 다를 못하게 막고 상대에게 그 사실을 알리는 메시지를 보낸다.
즉, 소켓을 종료하더라도 소켓할당이 해지되지는 않는다.
shutdown 메서드를 사용해 소켓을 종료해 보자
만약 클라이언트 측에서 write를 실행한 후에 shutdown 메서드를 호출한다면 이제 해당 소켓으로 write를 할 수 없다.
다만 read는 가능한 상태로 서버 측에서 출력한 데이터를 입력받을 수 있다.
반대로 read를 실행한 후에 shutdown 메서드를 호출한다면 해당 소켓으로 read를 더 이상 할 수 없다.
다만 write는 가능한 상태가 된다. 이는 서버 및 클라이언트 모두 동일하다.
close 메서드를 사용하여 소켓을 닫을 수 있다.
소켓을 닫을 경우 해당 소켓으론 다시 통신을 할 수 없고, 양 방향 통신이 모두 종료되게 된다.
또한 소켓 통신으로 인해 사용한 자원들이 모두 운영체제로 반환된다.
클라이언트 소켓 종료
클라이언트의 목적은 소켓을 할당한 후 서버와 연결하는 것이다.
연결이 수립되고 나면 데이터를 통신하고 소켓을 종료시켜 메시지의 끝을 알리는 요청 메시지를 보낸다.
요청을 전송하고 나면 클라는 응답을 읽어야 한다, 응답의 메시지의 크기는 알 수 없다.
따라서 서버가 소켓을 종료시켜 응답의 끝을 알릴 때 까지 계속 읽어야만 한다.
#include <boost/asio.hpp>
#include <iostream>
using namespace boost;
void communicate(asio::ip::tcp::socket& sock) {
// 요청 데이터 준비
const char request_buf[] = { 0x48, 0x65, 0x0, 0x6c, 0x6c, 0x6f };
// 요청 전송
asio::write(sock, asio::buffer(request_buf));
// 소켓 종료 (전송 완료)
sock.shutdown(asio::socket_base::shutdown_send);
// 응답 버퍼
asio::streambuf response_buf;
system::error_code ec;
// 응답 읽기
asio::read(sock, response_buf, ec);
if (ec == asio::error::eof) {
// 응답 완료
}
else {
// 오류 처리
throw system::system_error(ec);
}
}
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);
// io_context 생성
asio::io_context ios;
// 소켓 생성
asio::ip::tcp::socket sock(ios, ep.protocol());
// 서버 연결
sock.connect(ep);
// 통신
communicate(sock);
}
catch (system::system_error& e) {
// 오류 출력
std::cout << "오류 발생! 오류 코드 = " << e.code()
<< ". 메시지: " << e.what() << std::endl;
return e.code().value();
}
return 0;
}
- 서버로 보낼 데이터의 경우 해당 데이터의 크기를 클라이언트 입장에서는 알고 있다.
- 따라서 write를 할 때에는 출력을 위한 버퍼를 전달해 주면 된다.
- 데이터 송신을 완료한 후 소켓을 닫아주면 이제 서버측의 응답을 들을 준비가 되어야 한다.
- 하지만 서버로부터 입력을 받을 데이터 크기를 가늠할 수 없기에 입력 버퍼는 가변적이여야 한다.
- 따라서 입력 버퍼는 streambuf를 통해 동적으로 할당할 수 있도록 한다.
- 이후 read메서드를 통해 서버측 데이터를 모두 입력 받아준다.
- eof까지 입력 받았는지를 체크 하고 모든 데이터를 받았다면 통신이 완료된다.
서버 소켓 종료
서버는 수용자 소켓을 할당한 후 클라이언트에게 연결 요청이 올때까지 리스닝 상태로 기다려야 한다.
연결 요청이 들어오면, 이를 받아들이고 클라이언트에서 소켓을 종료하기 전까지 클라이언트와 연결된 소켓을 통해 데이터를 읽어야 한다.
서버 또한 자신의 소켓을 종료할 때 메시지의 끝을 알리는 응답 메시지가 보내지도록 한다.
#include <boost/asio.hpp>
#include <iostream>
using namespace boost;
void processRequest(asio::ip::tcp::socket& sock) {
// 확장 가능한 버퍼 사용 (요청 메시지 크기 미정)
asio::streambuf request_buf;
system::error_code ec;
// 요청 받기
asio::read(sock, request_buf, ec);
if (ec != asio::error::eof)
// 오류 처리
throw system::system_error(ec);
// 요청 완료, 응답 준비
const char response_buf[] = { 0x48, 0x69, 0x21 };
// 응답 전송
asio::write(sock, asio::buffer(response_buf));
// 소켓 종료 (전송 완료)
sock.shutdown(asio::socket_base::shutdown_send);
}
int main()
{
unsigned short port_num = 3333;
try {
// 엔드포인트 생성 (로컬 주소와 포트)
asio::ip::tcp::endpoint ep(asio::ip::address_v4::any(), port_num);
// io_context 생성
asio::io_context ios;
// acceptor 생성
asio::ip::tcp::acceptor acceptor(ios, ep);
// 소켓 생성
asio::ip::tcp::socket sock(ios);
// 클라이언트 연결 수락
acceptor.accept(sock);
// 요청 처리
processRequest(sock);
}
catch (system::system_error& e) {
// 오류 출력
std::cout << "오류 발생! 오류 코드 = " << e.code()
<< ". 메시지: " << e.what() << std::endl;
return e.code().value();
}
return 0;
}
- 포트 번호를 초기화 하고 모든 IP 주소로 부터 초기화한 포트 번호로 종료점을 생성한다.
- io_context를 생성하고 종료점과 함께 acceptor를 초기화 한다.
- 이 때 boost.asio에서는 acceptor를 정의한 순간 bind와 listen 상태가 된다.
- ios를 기반으로 소켓을 생성하고 acceptor를 통해 클라이언트 연결을 수락한다.
- 클라이언트 측의 요청을 processRequest라는 별도의 함수를 통해 처리한다.
- processRequest 함수에 소켓을 매개변수로 전달한다.
- 입력 받는 데이터는 크기를 알 수 없으니 가변 버퍼인 streambuf로 받아준다.
- read메서드를 통해 소켓의 데이터를 읽어 버퍼에 저장해 준다.
- 이제 클라이언트 쪽으로 출력할 버퍼를 정의하고 write메서드를 통해 버퍼의 데이터를 출력한다.
- 입출력 연산을 마친 후 shutdown메서드를 통해 소켓 종료를 선언한다.
서버 소켓 닫기
위의 클라이언트 예제를 보면 소켓 출력 후 shutdown을 실행하였다.
그럼 더 이상 출력 연산은 할 수 없으며 오로지 입력 연산만 가능한 상태가 된다.
서버 예제를 보면 소켓 입력 후 출력 연산까지 해준 뒤에 shutdown을 실행하였다.
그럼 더 이상 입출력 연산을 할 수 없게 된다, 사실상 서버측 소켓은 닫은 상태로 볼 수 있다.
또한 클라이언트측의 shutdown을 통해 서버측 read 시 eof를 전달 받아 요청 메시지의 끝임을 알게 된다.
마찬가지로 서버측의 출력 후 shutdown을 통해 클라이언트 read시 eof를 전달 받아 요청 메시지의 끝임을 알게 된다.
이로서 클라이언트 및 서버 모두 요청 메시지가 더 이상 없다는 것을 알게 되었다.
통신이 모두 종료된 후에는 close() 메서드를 통해 소켓을 닫아 할당된 자원을 돌려받는다.
'소켓 통신 > Boost' 카테고리의 다른 글
Boost.asio 동기 TCP 클라이언트 (0) | 2024.11.18 |
---|---|
Boost.asio 클라이언트 개요 (1) | 2024.11.14 |
Boost.asio I/O 비동기 연산 취소하기 (0) | 2024.11.14 |
Boost.asio I/O TCP 소켓 비동기적 읽기 (1) | 2024.11.14 |
Boost.asio I/O TCP 소켓 비동기적 쓰기 (0) | 2024.11.14 |