서버 측 네트워크 I/O 관리를 위한 리액터 패턴의 핵심 절차는 다음과 같습니다:
- 콜백 함수 정의 및 I/O 작업 핸들러 구현
typedef int(*HandlerFunc)(int fd);
int accept_handler(int fd);
int send_handler(int fd);
int recv_handler(int fd);
- 파일 디스크립터 관리 구조체 설계
struct connection {
int fd;
char recv_buf[BUF_SIZE];
int recv_len;
char send_buf[BUF_SIZE];
int send_len;
HandlerFunc send_handler;
union {
HandlerFunc accept_handler;
HandlerFunc recv_handler;
} read_action;
};
- 전역 연결 배열 `connections[MAX_CONNS]` 생성 및 epoll 인스턴스 초기화
- 서버 소켓 생성 후 연결 목록에 등록, read_action을 `accept_handler`로 설정
- epoll 인스턴스에 서버 소켓 등록(EPOLLIN 이벤트 감지)
- `epoll_wait()`로 이벤트 대기, 발생한 이벤트 유형에 따라 처리:
- EPOLLIN: 등록된 read_action 실행 (서버 소켓-accept_handler, 클라이언트-recv_handler)
- EPOLLOUT: send_handler 실행
핸들러별 동작 로직:
1. accept_handler
- 클라이언트 연결 수락
- 새 소켓을 연결 목록에 추가
- epoll 인스턴스에 EPOLLIN 이벤트 등록
2. recv_handler
- 데이터 수신 후 recv_buf 저장
- 연결 종료 시 소켓 정리
- 데이터 처리 후 응답을 send_buf에 저장
- 이벤트를 EPOLLOUT으로 변경
3. send_handler
- send_buf 데이터 전송
- 이벤트를 EPOLLIN으로 재설정
리액터 패턴 장점:
- I/O 작업을 이벤트 기반 콜백으로 추상화
- 개별 I/O 연산의 독립성 및 결합도 감소
- 네트워크 I/O와 비즈니스 로직 분리
- 명시적 send/recv 호출 대신 이벤트 제어
구현 예제:
#include <sys/epoll.h>
#include <netinet/in.h>
#define BUF_SIZE 1024
#define MAX_CONNS 1048576
int epoll_fd;
struct connection connections[MAX_CONNS];
void setup_event(int fd, int events, int op) {
struct epoll_event ev = {.events = events, .data.fd = fd};
epoll_ctl(epoll_fd, op ? EPOLL_CTL_ADD : EPOLL_CTL_MOD, fd, &ev);
}
void register_conn(int fd, int event) {
connections[fd].fd = fd;
connections[fd].read_action.recv_handler = recv_handler;
connections[fd].send_handler = send_handler;
setup_event(fd, event, 1);
}
int accept_handler(int fd) {
struct sockaddr_in client_addr;
socklen_t addr_len = sizeof(client_addr);
int client_fd = accept(fd, (struct sockaddr*)&client_addr, &addr_len);
register_conn(client_fd, EPOLLIN);
return 0;
}
int recv_handler(int fd) {
int bytes = recv(fd, connections[fd].recv_buf, BUF_SIZE, 0);
if(bytes <= 0) {
close(fd);
epoll_ctl(epoll_fd, EPOLL_CTL_DEL, fd, NULL);
return bytes;
}
connections[fd].recv_len = bytes;
// 에코 서버 구현
connections[fd].send_len = connections[fd].recv_len;
memcpy(connections[fd].send_buf, connections[fd].recv_buf, connections[fd].send_len);
setup_event(fd, EPOLLOUT, 0);
return bytes;
}
int send_handler(int fd) {
int sent = send(fd, connections[fd].send_buf, connections[fd].send_len, 0);
setup_event(fd, EPOLLIN, 0);
return sent;
}
int create_server(short port) {
int sock = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr = {
.sin_family = AF_INET,
.sin_addr.s_addr = htonl(INADDR_ANY),
.sin_port = htons(port)
};
bind(sock, (struct sockaddr*)&addr, sizeof(addr));
listen(sock, 10);
return sock;
}
int main() {
int server_fd = create_server(2000);
epoll_fd = epoll_create(1);
connections[server_fd].fd = server_fd;
connections[server_fd].read_action.accept_handler = accept_handler;
setup_event(server_fd, EPOLLIN, 1);
struct epoll_event events[1024];
while(1) {
int n = epoll_wait(epoll_fd, events, 1024, -1);
for(int i=0; i<n; i++) {
int fd = events[i].data.fd;
if(events[i].events & EPOLLIN)
connections[fd].read_action.accept_handler(fd);
if(events[i].events & EPOLLOUT)
connections[fd].send_handler(fd);
}
}
}