출처 : 열혈 TCP/IP 소켓 프로그래밍 윤성우 저
Epoll
--- select 기반의 멀티 플렉싱 서버가 느릴 수 있는 이유
1. select 함수 호출 이전에 모든 관찰 대상을 초기화하여 전달해야 한다.
char buf[BUF_SIZE];
int fd_max = serv_sock, fd_num;
struct timeval timeout;
while(1)
{
// 관찰 대상 초기화
cpy_reads = reads;
timeout.tv_sec = 5;
timeout.tv_usec = 5000;
// ... select 호출
}
2. select 함수 호출 이후 모든 관찰 대상에 대해 반복문을 통해 어떤 관찰 대상이 변했는지 체크하고 그에 따른 처리를 해줘야 한다.
// 모든 관찰 대상을 체크
for(int i=0;i<fd_max+1;++i)
{
// 변화했는 지 확인
if(FD_ISSET(i, &cpy_reads))
{
// server socket selected
if(i==serv_sock) // connection requested
{
struct sockaddr_in clnt_adr = {};
socklen_t adr_sz = sizeof(clnt_adr);
int clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_adr, &adr_sz);
// client socket ready ready
FD_SET(clnt_sock, &reads);
if(clnt_sock > fd_max)
fd_max = clnt_sock;
printf("connected client: %d\n", clnt_sock);
}
3. select 함수 호출 시 모든 파일 디스크립터를 운영체제에게 전달하는 과정이 필요하다.
select 함수를 호출할 때마다 관찰대상에 대한 정보를 매번 운영체제에게 전달해야 한다. 이 동작이 부담이 제일 크다.
epoll기반 서버
epoll의 기본원리
운영체제에게 관찰대상에 대한 정보를 딱 한 번만 알려주고서, 관찰대상의 범위 또는 내용에 변경이 있을 때 변경 사항만 알려주도록 한다.
epoll 구현에 필요한 함수와 구조체
- epoll을 사용함으로써 얻는 장점
- 기존 상태 변화를 확인하기 위한 전체 파일 디스크립터를 대상으로 하는 반복문이 필요 없다.
- select 함수에 대응하는 epoll_wait 함수 호출 시 관찰 대상의 정보를 매번 전달할 필요가 없다.
- 사용 함수
epoll_create: epoll 파일 디스크립터 저장소 생성
epoll_ctl: 저장소에 파일 디스크립터 등록 및 삭제
epoll_wait: select 함수와 같이 파일 디스크립터의 변화를 대기한다.
epoll_create
int epoll_create(int size);
- 성공 시 epoll 파일 디스크립터, 실패 시 -1
- size: epoll 인스턴스의 크기 정보
리눅스 운영체제에서 사용되는 epoll은 위 함수를 통해 인스턴스를 생성하며 운영체제로부터 관리되는 핸들 값을 얻는다.
select 방식에서는 관찰 대상을 fd_set이라는 변수를 통해 나타냈고 select 호출 시 전달해야만 했다. epoll에서는 파일 디스크립터를 저장하는 저장소를 생성하고 관찰 대상을 등록하여 사용한다.
epoll_ctl
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
- 성공 시 0, 실패 시 -1 반환
- epfd: 관찰 대상을 등록할 epoll 인스턴스의 파일 디스크립터
- op: 관찰대상 추가, 삭제 또는 변경 여부 지정
- fd: 등록할 관찰대상의 파일 디스크립터
- event: 관찰대상의 관찰 이벤트 유형
기존 select 방식에서는 fd_set 변수를 구성하는 관찰 대상에 대한 비트를 FD_SET, FD_CLR 함수로 설정하였다. epoll에서는 fd_set과 같이 관찰 대상을 운영체제에게 전달하지 않으므로 미리 등록하여 사용한다.
등록과 삭제
- EPOLL_CTL_ADD
epoll_ctl(A, EPOLL_CTL_ADD, B, C);
epoll 인스턴스 A에 C라는 특정 이벤트를 관찰할 목적으로 파일 디스크립터 B를 등록한다.
- EPOLL_CTL_DEL
epoll_ctl(A, EPOLL_CTL_DEL, B, NULL);
epoll 인스턴스 A에 파일 디스크립터 B를 삭제한다.
- EPOLL_CTL_MOD
등록된 파일 디스크립터의 이벤트 발생 상황을 변경한다.
epoll_event 구조체
관찰할 대상의 이벤트를 지정할 때 사용하거나 실제 epoll_wait 호출 시 발생한 이벤트 내용을 담을 수 있는 구조체이다.
struct epoll_event
{
__uint32_t events;
epoll_data_t data;
}
typedef union epoll_data
{
void* ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;
events에는 매크로로 정의된 event 상황을 대입하고 epoll_data 공용체의 int fd를 통해 대상 소켓을 지정할 수 있다.
등록하고자 할 때 사용한다면, 상황 인자와 파일 디스크립터를 지정한다.
struct epoll_event event;
...
event.events = EPOLLIN; // 수신할 데이터가 존재하는 상황 발생 시
event.data.fd = sockfd; // 소켓의 파일 디스크립터
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &event);
- EPOLL_IN: 수신할 데이터가 존재
- EPOLL_OUT: 출력 버퍼가 비워져서 당장 데이터를 전송할 수 있는 상황
- EPOLLPRI: OOB(긴급) 데이터가 수신된 상황
- EPOLLRDHUP: 연결 종료 혹은 Half-Close가 진행된 상황
- EPOLLERR: 에러가 발생
- EPOLLET: 이벤트의 감지를 에지 트리거 방식으로 동작시킨다.
- EPOLLONESHOT: 이벤트가 한 번 감지된 이후로는 이벤트가 발생하지 않도록 지정. 이벤트 발생 후 EPOLL_CTL_MODE를 통해 이벤트를 재설정해야 한다.
epoll_wait
가장 마지막에 호출되는 함수
int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);
- 성공 시 이벤트가 발생한 파일 디스크립터의 수, 실패 시 -1 반환
- epfd: 이벤트 발생의 관찰 영역인 epoll 인스턴스의 파일 디스크립터
- events: 이벤트가 발생한 파일 디스크립터가 채워질 버퍼의 주소 값
- maxevents: 두 번째 인자로 전달된 주소 값의 버퍼에 등록 가능한 최대 이벤트 수
- timeout: 1ms 단위의 대기시간/ -1 전달 시 이벤트 발생 시까지 무한 대기
select에서 반환된 fd_set 변수에 대해 모든 관찰 대상을 반복문으로 확인해야 했다. epoll에서는 변화가 감지되면 events 매개변수에 발생한 이벤트 내용을 채워 반환해준다. 또한 반환 값으로 이벤트가 발생한 디스크립터 수를 알 수 있기 때문에 events 결과를 개수만큼 확인할 수 있다.
int event_cnt;
struct epoll_event *ep_events;
//...
ep_events = malloc(sizeof(struct epoll_event)*EPOLL_SIZE);
//...
event_cnt = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);
epoll_wait에 전달하는 ep_events는 epoll_event 구조체를 최대 EPOLL_SIZE 개수 만큼 담을 수 있는 크기를 가진다.
epoll 기반 서버 구현
- 서버 소켓 생성 이후 epoll_crate와 epoll_ctl
epoll 인스턴스 생성과 서버 소켓에 대해 들어오는 클라이언트 요청을 감지할 수 있도록 epoll에 등록한다.
// descripter of epoll instance
epfd = epoll_create(EPOLL_SIZE);
// storage for occurring epoll event
ep_events = (epoll_event*) malloc(sizeof(struct epoll_event) * EPOLL_SIZE);
// Which Event?
struct epoll_event event;
event.events = EPOLLIN;
event.data.fd = serv_sock;
// Register file descripter [serv_sock], which kind is [event] to epoll instance [epfd]
epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event);
- 관찰 대상 변화 감지 epoll_wait
while문 안에서 epoll_wait를 통해 등록된 대상의 변화를 감지하고 event내용을 파악한다.
while(1)
{
event_cnt = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);
if(event_cnt == -1)
{
puts("epoll_wait() error");
break;
}
// ...
}
- event_cnt 만큼 이벤트 수가 발생했다면 그 수만큼만 반복문으로 이벤트를 처리한다.
1. 서버 소켓에서 클라이언트 연결 요청이 들어온 이벤트
연결 요청을 수락하고 다시 클라이언트 소켓에 대해 수신 데이터를 감지하는 이벤트를 epoll에 등록한다.
for(i = 0; i < event_cnt; ++i)
{
// 서버 소켓 이벤트 처리
if(ep_events[i].data.fd == serv_sock)
{
adr_sz = sizeof(clnt_adr);
clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_adr, &adr_sz);
event.events = EPOLLIN;
event.data.fd = clnt_sock;
epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &event);
printf("connected client: %d\n", clnt_sock);
}
// 클라이언트 데이터 송신 이벤트 처리
else
{
//...
}
}
2. 클라이언트 소켓에서 데이터 수신 이벤트 발생 시 에코 메시지를 전송한다.
메시지가 EOF(0)인 경우 epoll에 등록된 클라이언트 소켓을 제거하고 소켓을 닫는다.
// 클라이언트 데이터 송신 이벤트 처리
else
{
str_len = read(ep_events[i].data.fd, buf, BUF_SIZE);
if(str_len == 0) // close request
{
epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, NULL);
close(ep_events[i].data.fd);
printf("closed client: %d \n", ep_events[i].data.fd);
}
else
{
write(ep_events[i].data.fd, buf, str_len);
}
}
전체 코드
int main(int argc, char *argv[])
{
int serv_sock, clnt_sock;
struct sockaddr_in serv_adr, clnt_adr;
socklen_t adr_sz;
int str_len, i;
char buf[BUF_SIZE];
struct epoll_event* ep_events;
struct epoll_event event;
int epfd, event_cnt;
// create server socket
serv_sock = socket(PF_INET, SOCK_STREAM, 0);
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_adr.sin_port = htons(PORT);
// bind and listen
if(bind(serv_sock, (struct sockaddr*) &serv_adr, sizeof(serv_adr)) == -1)
error_handling("bind() error");
if(listen(serv_sock,5) == -1)
error_handling("listen() error");
// descripter of epoll instance
epfd = epoll_create(EPOLL_SIZE);
// storage for occurring epoll event
ep_events = (epoll_event*) malloc(sizeof(struct epoll_event) * EPOLL_SIZE);
event.events = EPOLLIN;
event.data.fd = serv_sock;
// Register file descripter [serv_sock], which kind is [event] to epoll instance [epfd]
epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event);
while(1)
{
event_cnt = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);
if(event_cnt == -1)
{
puts("epoll_wait() error");
break;
}
for(i = 0; i < event_cnt; ++i)
{
if(ep_events[i].data.fd == serv_sock)
{
adr_sz = sizeof(clnt_adr);
clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_adr, &adr_sz);
event.events = EPOLLIN;
event.data.fd = clnt_sock;
epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &event);
printf("connected client: %d\n", clnt_sock);
}
else
{
str_len = read(ep_events[i].data.fd, buf, BUF_SIZE);
if(str_len == 0) // close request
{
epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, NULL);
close(ep_events[i].data.fd);
printf("closed client: %d \n", ep_events[i].data.fd);
}
else
{
write(ep_events[i].data.fd, buf, str_len);
}
}
}
}
close(serv_sock);
close(epfd);
return 0;
}
'Computer Science 기본 지식 > 소켓 프로그래밍' 카테고리의 다른 글
[TCP/IP 소켓 프로그래밍] (12) 다중 접속 서버 - 쓰레드 (0) | 2021.04.06 |
---|---|
[TCP/IP 소켓 프로그래밍] (11-2) 다중 접속 서버 - 레벨 트리거/엣지 트리거 (0) | 2021.04.05 |
[TCP/IP 소켓 프로그래밍] (10) 멀티 캐스트와 브로드 캐스트 (0) | 2021.04.01 |
[TCP/IP 소켓 프로그래밍] (9) TCP 긴급 메세지 (0) | 2021.04.01 |
[TCP/IP 소켓 프로그래밍] (8) 다중 접속 서버 - 멀티 플렉싱 기반 select 서버 (0) | 2021.04.01 |