C++/Socket 통신

비동기 소켓 프로그래밍 - C++

초심을 찾자 2025. 2. 26. 10:42
SMALL

포스팅 계기

 이전 포스팅에 2025.02.26 - [C++ 개발이야기/Socket 통신] - 멀티스레드 기반 소켓 프로그래밍 : 서버 성능 극대화를 위한 필수 개념 - c++에 이어 어떤 심화적인 이야기를 하면 좋을까 생각하다 해당 주제를 선택하게 되었다. 네트워크 프로그래밍에서 동기 방식은 클라이언트의 요청을 순차적으로 처리하기 때문에 성능이 제한될 수 있다. 특히, 다수의 클라이언트가 동시에 접속하는 경우 멀티스레드 방식으로 해결할 수 있지만, 스레드 관리 비용이 증가하는 문제가 있다. 이를 해결하는 대안으로 비동기 소켓 프로그래밍이 있다. 이번 글에서는 비동기 소켓 프로그래밍의 개념과 구현 방법을 다뤄보도록 하겠다.


비동기 소켓 프로그래밍 개념

 비동기 소켓 프로그래밍은 논블로킹(non-blocking) I/O와 이벤트 기반(event-driven) 모델을 활용하여 효율적으로 네트워크 요청을 처리하는 방식이다. 서버는 클라이언트의 요청을 기다리지 않고 이벤트 루프(Event Loop)를 통해 다수의 연결을 효율적으로 처리한다.

주요 개념

  1. 논블로킹 I/O: 요청을 보낸 후 즉시 반환하며, 데이터를 받을 준비가 되었을 때 알림을 받는 방식
  2. 이벤트 루프(Event Loop): 클라이언트 요청이 도착하면 콜백 함수를 실행하여 응답을 처리
  3. select/poll/epoll/kqueue:
    • select(): 모든 소켓을 검사하며, 감지된 이벤트를 기반으로 동작한다. FD(파일 디스크립터) 개수가 많아질수록 성능이 저하된다.
    • poll(): select()와 유사하지만, 감시할 소켓을 배열로 관리하여 일부 성능 개선이 이루어졌다. 하지만 여전히 모든 소켓을 순차적으로 검사해야 하는 단점이 있다.
    • epoll(): 리눅스에서 제공하는 효율적인 다중 I/O 이벤트 감지 방식으로, 이벤트가 발생한 소켓만 처리하므로 성능이 뛰어나다.
    • kqueue(): BSD 계열 운영체제에서 제공하는 이벤트 감지 방식으로, epoll()과 유사한 고성능 비동기 I/O 메커니즘을 제공한다. ( ※ BSD 계열 : 유닉스 계열의 운영체제(OS)로, Berkeley Software Distribution의 약자이며, AT&T의 Research UNIX 운영체제를 확장한 소스 코드 배포판이었던 것이 시초 )
      출처 : ChatGPT

 


작성 코드

#include <iostream>
#include <sys/epoll.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <vector>

#define PORT 8080
#define MAX_EVENTS 10

void set_nonblocking(int sock) {
    int flags = fcntl(sock, F_GETFL, 0);
    fcntl(sock, F_SETFL, flags | O_NONBLOCK);
}

int main() {
    int server_fd, client_fd, epoll_fd;
    struct sockaddr_in server_addr, client_addr;
    socklen_t client_len = sizeof(client_addr);
    epoll_event event, events[MAX_EVENTS];

    server_fd = socket(AF_INET, SOCK_STREAM, 0);
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(PORT);

    bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr));
    listen(server_fd, 5);

    set_nonblocking(server_fd);

    epoll_fd = epoll_create1(0);
    event.events = EPOLLIN;
    event.data.fd = server_fd;
    epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &event);

    while (true) {
        int event_count = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
        for (int i = 0; i < event_count; i++) {
            if (events[i].data.fd == server_fd) {
                client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &client_len);
                set_nonblocking(client_fd);
                event.events = EPOLLIN | EPOLLET;
                event.data.fd = client_fd;
                epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &event);
            } else {
                char buffer[1024] = {0};
                read(events[i].data.fd, buffer, sizeof(buffer));
                std::cout << "Received: " << buffer << std::endl;
                close(events[i].data.fd);
            }
        }
    }
    close(server_fd);
    close(epoll_fd);
    return 0;
}

 


구현할 때 주의해야 할 점

  1. 논블로킹 소켓 설정: fcntl()을 사용하여 소켓을 논블로킹 모드로 설정 필요
  2. 이벤트 루프 관리: epoll_wait()을 사용하여 적절히 이벤트를 감지하고 처리
  3. 클라이언트 연결 관리: 클라이언트가 정상적으로 종료되지 않을 경우를 대비해 타임아웃 및 예외 처리를 추가 필요  ( ※  예시 : KEEP_ALIVE )
  4. 파일 디스크립터 누수 방지: 클라이언트가 연결을 종료하면 반드시 close()를 호출하여 파일 디스크립터를 반환해야 합니다.
  5. 대규모 연결 처리 시 성능 고려: 초당 수천 개 이상의 요청이 발생하는 환경에서는 적절한 이벤트 처리 방식(epoll, kqueue)을 선택하고, 부하 분산(Load Balancing) 기법을 활용하는 것이 중요합니다.
  6. 데드락(Deadlock) 및 경쟁 상태(Race Condition) 방지: 멀티스레드 환경에서는 소켓 처리 중 데드락이 발생하지 않도록 주의해야 합니다.

포스팅 마무리

 이번 글에서는 비동기 소켓 프로그래밍의 개념과 C++을 활용한 epoll 기반의 서버 구현 방법을 살펴보았다. 실무에서도 비동기 소켓 프로그래밍을 적용하면 서버의 확장성을 높이고 리소스를 효율적으로 사용할 수 있을 거라 생각한다. 또한, 논블로킹 방식과 적절한 이벤트 감지 기법을 활용하면 높은 동시성을 유지하면서도 성능 저하를 최소화할 수 있다. 이제까지 다룬 소켓 프로그래밍 개념을 익히면 실무에서도 큰 문제없이 네트워크 애플리케이션을 개발할 수 있을 거로 생각이 된다. 하지만 아직 부족하다고 생각이 들어 다음 글에서는 보안과 암호화된 소켓 통신에 대해 다뤄보면서 지식을 더 쌓는 시간을 가지고자 한다. 다음 포스팅으로 찾아오겠다.