개요
반복적 동기 TCP 서버는 다음과 같은 사항을 만족시키는 분산 어플리케이션이다.
- 클라이언트 서버 통신 모델에서 서버로 동작한다.
- TCP 프로토콜을 사용해 클라이언트 프로그램과 통신한다.
- I/O 및 제어 연산을 하는 동안에는 연산이 끝나거나 오류가 발생할 때까지 실행 스레드를 멈춘다.
- 클라이언트를 한 번에 하나씩 처리한다.
일반적인 동기 TCP 서버는 다음과 같은 알고리즘에 따라 동작한다.
- 수용자 소켓을 할당하고 특정 TCP 포트에 묶는다.
- 서버가 중단될 때까지 루프를 돈다.
- 클라이언트로부터 연결 요청이 오기를 기다린다.
- 연결 요청이 오면 받아들인다.
- 클라이언트로부터 요청 메시지가 오기를 기다린다.
- 요청 메시지를 읽는다.
- 요청을 처리한다.
- 클라이언트에게 응답 메시지를 보낸다.
- 클라이언트와의 연결을 닫고 소켓을 할당 해지한다.
반복 동기 TCP 서버 구현
#include <boost/asio.hpp>
#include <thread>
#include <atomic>
#include <memory>
#include <iostream>
using namespace boost;
class Service {
public:
Service() {}
void HandleClient(asio::ip::tcp::socket& sock) {
try {
asio::streambuf request;
asio::read_until(sock, request, '\n');
// 요청 처리 에뮬레이션
int i = 0;
while (i != 1000000)
i++;
std::this_thread::sleep_for(
std::chrono::milliseconds(500));
// 응답 전송
std::string response = "Response\n";
asio::write(sock, asio::buffer(response));
}
catch (system::system_error& e) {
std::cout << "오류 발생! 오류 코드 = "
<< e.code() << ". 메시지: "
<< e.what();
}
}
};
class Acceptor {
public:
Acceptor(asio::io_context& ioc, unsigned short port_num) :
m_ioc(ioc),
m_acceptor(m_ioc,
asio::ip::tcp::endpoint(
asio::ip::address_v4::any(),
port_num))
{
m_acceptor.listen();
}
void Accept() {
asio::ip::tcp::socket sock(m_ioc);
m_acceptor.accept(sock);
Service svc;
svc.HandleClient(sock);
}
private:
asio::io_context& m_ioc;
asio::ip::tcp::acceptor m_acceptor;
};
class Server {
public:
Server() : m_stop(false), m_work_guard(asio::make_work_guard(m_ioc)) {} // 생성자에서 work_guard 초기화
void Start(unsigned short port_num) {
m_thread.reset(new std::thread([this, port_num]() {
Run(port_num);
}));
}
void Stop() {
m_stop.store(true);
m_work_guard.reset(); // io_context의 작업이 없음을 알림
m_thread->join();
}
private:
void Run(unsigned short port_num) {
Acceptor acc(m_ioc, port_num);
while (!m_stop.load()) {
acc.Accept();
}
}
std::unique_ptr<std::thread> m_thread;
std::atomic<bool> m_stop;
asio::io_context m_ioc;
asio::executor_work_guard<asio::io_context::executor_type> m_work_guard; // 생성자에서 초기화된 work_guard
};
int main()
{
unsigned short port_num = 3333;
try {
Server srv;
srv.Start(port_num);
std::this_thread::sleep_for(std::chrono::seconds(60));
srv.Stop();
}
catch (system::system_error& e) {
std::cout << "오류 발생! 오류 코드 = "
<< e.code() << ". 메시지: "
<< e.what();
}
return 0;
}
main() 함수에서 Server 객체를 생성하고, Start() 메서드를 호출하여 서버를 시작한다.
서버가 시작되면 Acceptor가 클라이언트 연결 요청을 기다리고, 연결 요청이 있을 때마다 Service 클래스가 이를 처리한다.
main() 함수는 60초 동안 서버를 실행한 뒤, Stop() 메서드를 호출하여 서버를 안전하게 종료한다.
이 서버는 단순한 반복적 동기 서버이지만, 서버 전체 실행을 별도의 스레드에서 처리함으로써 메인 스레드가 서버를 중단할 수 있는 유연성을 갖고 있다.
Service 클래스
class Service {
public:
Service() {}
void HandleClient(asio::ip::tcp::socket& sock) {
try {
asio::streambuf request;
asio::read_until(sock, request, '\n');
// 요청 처리 에뮬레이션
int i = 0;
while (i != 1000000)
i++;
std::this_thread::sleep_for(
std::chrono::milliseconds(500));
// 응답 전송
std::string response = "Response\n";
asio::write(sock, asio::buffer(response));
}
catch (system::system_error& e) {
std::cout << "오류 발생! 오류 코드 = "
<< e.code() << ". 메시지: "
<< e.what();
}
}
};
클라이언트의 요청을 처리하는 역할을 담당하는 클래스
HandleClient 메서드가 호출되면 가변 버퍼인 streambuf타입의 request를 초기화 한다.
read_until 메서드를 통해 구분자인 '\n'가 입력될 때까지 소켓을 읽어 가변 버퍼 request에 저장한다.
이후 while 및 threa를 sleep하는 부분은 실제로는 데이터를 처리하는 영역이다.
데이터 처리가 끝났다면 클라이언트에 전송할 응답을 문자열 response에 저장한다.
response를 버퍼화 하여 write 메서드를 통해 소켓에 쓰기 작업을 실행한다.
Acceptor 클래스
class Acceptor {
public:
Acceptor(asio::io_context& ioc, unsigned short port_num) :
m_ioc(ioc),
m_acceptor(m_ioc,
asio::ip::tcp::endpoint(
asio::ip::address_v4::any(),
port_num))
{
m_acceptor.listen();
}
void Accept() {
asio::ip::tcp::socket sock(m_ioc);
m_acceptor.accept(sock);
Service svc;
svc.HandleClient(sock);
}
private:
asio::io_context& m_ioc;
asio::ip::tcp::acceptor m_acceptor;
};
서버가 클라이언트의 연결을 수락하도록 설정하고, 각 연결에 대해 Service 클래스를 호출하여 요청을 처리하는 클래스
생성자의 인자로 받은 ioc와 포트넘버를 통해 클래스 내부의 m_ioc, m_acceptor를 초기화 한다.
m_acceptor.listen()을 통해 해당 acceptor를 리스닝 상태로 만들어 준다.
Accept함수가 호출되면 m_ioc를 사용하여 소켓을 생성하고, m_acceptor를 통해 해당 소켓을 accept한다.
Service 클래스의 인스턴스 svc를 초기화 하고, 위에서 생성한 소켓을 HandleClient 메서드의 인자로 전달하여 호출한다.
Server 클래스
class Server {
public:
Server() : m_stop(false), m_work_guard(asio::make_work_guard(m_ioc)) {} // 생성자에서 work_guard 초기화
void Start(unsigned short port_num) {
m_thread.reset(new std::thread([this, port_num]() {
Run(port_num);
}));
}
void Stop() {
m_stop.store(true);
m_work_guard.reset(); // io_context의 작업이 없음을 알림
m_thread->join();
}
private:
void Run(unsigned short port_num) {
Acceptor acc(m_ioc, port_num);
while (!m_stop.load()) {
acc.Accept();
}
}
std::unique_ptr<std::thread> m_thread;
std::atomic<bool> m_stop;
asio::io_context m_ioc;
asio::executor_work_guard<asio::io_context::executor_type> m_work_guard; // 생성자에서 초기화된 work_guard
};
서버의 전반적인 실행 흐름을 관리하는 클래스
클래스의 생성자로 m_stop을 false로 초기화 하여 서버가 작동중임을 나타낸다.
m_work_guard에 make_work_guard(m_ioc)를 통해 m_ioc에 작업이 있음 알려 run상태를 유지하게 한다.
Start 메서드가 실행되면 새로운 스레드를 Run함수를 실행하고 있도록 초기화 해준다.
Stop 메서드가 실행되면 m_stop.store(true)를 통해 서버가 중지되었음을 나타낸다.
m_work_guard.reset()을 통해 ioc에 작업이 없음을 알리고 m_thread->join을 통해 스레드를 종료한다.
Run 메서드가 실행되면 m_ioc와 매개변수로 전달받은 포트 번호를 인자로 Acceptor 클래스의 인스턴스 acc를 생성한다.
m_stop이 true상태가 되기 전까지 acc의 Accept 메서드를 계속 호출하여 클라이언트로의 연결 요청을 받는다.
main()
int main()
{
unsigned short port_num = 3333;
try {
Server srv;
srv.Start(port_num);
std::this_thread::sleep_for(std::chrono::seconds(60));
srv.Stop();
}
catch (system::system_error& e) {
std::cout << "오류 발생! 오류 코드 = "
<< e.code() << ". 메시지: "
<< e.what();
}
return 0;
}
포트 번호로 서버를 구동하고 60초 후 서버를 닫는 함수
port_num 변수에 임의의 포트 번호를 저장한다.
Server 클래스의 인스턴스 srv를 초기화 한다.
srv인스턴스로 Server클래스의 함수 Start에 포트 번호를 인자로 전달하여 호출한다.
스레드를 60초간 sleep상태로 만들고 Server 클래스의 함수 Stop을 호출하여 서버를 종료한다.
여기서 사용한 sleep명령은 srv 인스턴스 내부에서 생성된 스레드와는 별개로 동작한다.
'네트워크 통신 > Boost' 카테고리의 다른 글
Boost.asio 비동기 TCP 서버 (0) | 2024.11.20 |
---|---|
Boost.asio 병렬 동기 TCP 서버 (0) | 2024.11.20 |
Boost.asio 서버 개요 (1) | 2024.11.18 |
Boost.asio 비동기 멀티 스레드 TCP 클라이언트 (0) | 2024.11.18 |
Boost.asio 비동기 단일 스레드 TCP 클라이언트 (1) | 2024.11.18 |