본문 바로가기
Programming/열혈 TCP, IP 소켓 프로그래밍(저자 윤성우)

Ch 17. select보다 나은 epoll

by minjunkim.dev 2020. 8. 10.

   모든 내용은 [윤성우 저, "열혈강의 TCP/IP 소켓 프로그래밍", 오렌지미디어] 를 기반으로 제 나름대로 이해하여 정리한 것입니다. 다소 부정확한 내용이 있을수도 있으니 이를 유념하고 봐주세요!


# 멀티플렉싱 기법 중 하나인 select는 오래 전에 개발됨.
- 이를 이용하면 아무리 프로그램의 성능을 최적화시켜도, 허용할 수 있는 동시접속자의 수가 백을 넘기 힘듦.
(물론 하드웨어 성능에 따라 많이 좌지우지 되겠지만)
- 따라서, 웹기반의 서버개발이 주를 이루는 오늘날의 개발환경에서는 적절치 않음.
- 따라서 이에 대한 대안으로 리눅스 영역에서 주로 활용되는 epoll에 대해 공부해보자.

 


# select 기반의 IO 멀티플렉싱이 느린 이유
1) select 함수호출 이후에 항상 등장하는, "모든 파일 디스크립터를 대상"으로 하는 반복문
2) select 함수를 호출할때마다 인자로 매번 전달해야 하는 관찰대상에 대한 정보들

=> 매번 운영체제에게 이 정보를 전달해야 함.
(1보다 2가 성능에 치명적인 약점이 될 수 있음, 코드의 개선을 통해서 덜 수 있는 유형의 부담이 아니기 때문)

 

# select 함수와 epoll 함수는 파일 디스크립터(즉, 소켓)의 변화를 관찰하는 함수인데,
소켓은 운영체제에 의해 관리되는 대상이므로, 두 함수 모두 절대적으로 운영체제에 의해 기능이 완성되는 함수임.

 

# select 함수의 단점을 해결하기 위해서는?
- "운영체제에게 관찰대상에 대한 정보를 딱 한번만 알려주고서,
관찰 대상의 범위, 또는 내용에 변경이 있을때 변경사항만 알려주도록" 해야함.
- 이러면, 매번 select 함수를 호출할 때마다 관찰대상에 대한 정보를 운영체제에게 전달할 필요가 없음.
- 단, 운영체제가 이를 지원할때만 가능(운영체제마다 지원여부와 방식의 차이가 존재)
- 리눅스에서는 이를 epoll, 윈도우에서는 IOCP 라고 함.

 

# select 함수의 장점도 있긴 있다!
- 개선된 IO 멀티플렉싱 모델은 운영체제별로 호환이 되지 않음.
- 반면, select 함수는 대부분의 운영체제에서 지원함.
- 다음 두가지 유형의 조건이 만족 또는 요구된다면,
리눅스라고 할지라도 epoll을 사용할 필요가 없음
1) 서버의 접속자 수가 많지 않다.
2) 다양한 운영체제에서 운영이 가능해야 한다.


# epoll의 장점
- 장점(select 함수의 단점과 반대)
1) 상태변화의 확인을 위한, 전체 파일 디스크립터를 대상으로 하는 반복문이 필요없음
2) select 함수에 대응하는 epoll_wait 함수 호출시, 관찰대상의 정보를 매번 전달할 필요가 없다.

 

# epoll 기반의 서버 구현에 구현에 필요한 함수와 구조체
1) epoll_create : epoll 파일 디스크립터 저장소(epoll 인스턴스) 생성

#include <sys/epoll.h>

int epoll_create(int size);
-> 성공시 epoll 파일 디스크립터, 실패시 -1반환

- size : epoll 인스턴스의 크기정보

- (관찰 대상인) 파일 디스크립터의 저장소 : "epoll 인스턴스"
- size는 운영체제에 전달하는 힌트에 지나지 않음.
즉, 운영체제는 이 값을 "참고만" 하여 epoll 인스턴스를 생성.
- 리눅스 2.6.8 이후부터는 size가 완전히 무시됨.
커널 내에서 epoll 인스턴스 크기를 유동적으로 변화시키기 때문.
- epoll 인스턴스는 소켓과 마찬가지로 운영체제가 관리함.

따라서, 소켓 생성시와 마찬가지로 파일 디스크립터를 반환하고, 소멸시 close 함수를 사용함.

 

2) epoll_ctl : 저장소(epoll 인스턴스)에 파일 디스크립터 등록 및 삭제

#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
-> 성공시 0, 실패시 -1 반환

- epfd : 관찰대상을 등록할 epoll 인스턴스의 파일 디스크립터
- op : 관찰대상의 추가, 삭제 또는 변경여부 지정

- fd : 등록할 관찰대상의 파일 디스크립터
- event : 관찰대상의 관찰 이벤트 유형

 

* epoll_ctl의 두번째 인자(op)로 전달 가능한 상수와 그 의미

1) EPOLL_CTL_ADD : 파일 디스크립터를 epoll 인스턴스에 등록.
2) EPOLL_CTL_DEL : 파일 디스크립터를 epoll 인스턴스에서 삭제. 이때, 네번째 인자(event)에 NULL을 전달.
3) EPOLL_CTL_MOD : 등록된 파일 디스크립터의 이벤트 발생상황을 변경.

- 구조체 epoll_event는 이벤트가 발생한 파일 디스크립터를 묶는 용도로 사용되나,
파일 디스크립터를 epoll 인스턴스에 등록할 때, 이벤트 유형을 등록하는 용도로도 사용됨
e.g.

struct epoll_event event;
....
event.events=EPOLLIN; // 수신할 데이터가 존재하는 이벤트 발생시
event.data.fd=sockfd; // epoll 인스턴스에 sockfd 파일 디스크립터 등록을 위함임
epoll_ctl(efpd,EPOLL_CTL_ADD,sockfd,&event);
....

 

* epoll_event 구조체의 멤버인 events에 저장 가능한 상수와 이벤트의 유형
- EPOLLIN : 수신할 데이터가 존재하는 상황
- EPOLLOUT : 출력버퍼가 비워져서 당장 데이터를 전송할 수 있는 상황
- EPOLLPRI : OOB 데이터가 수신된 상황
- EPOLLRDHUP : 연결이 종료되거나 Half-close가 진행된 상황,

이는 엣지 트리거 방식에서 유용하게 사용될 수 있음
- EPOLLERR : 에러가 발생한 상황
- EPOLLET : 이벤트의 감지를 엣지 트리거 방식으로 동작시킴
- EPOLLONESHOT : 이벤트가 한번 감지되면, 해당 파일 디스크립터에서는 더이상 이벤트를 발생시키지 않음.
따라서 epoll_ctl 함수의 두번째 인자로 EPOLL_CTL_MOD를 전달해서 이벤트를 재설정해야함.


=> 위 상수들은 비트 OR 연산자(연산자 |)를 이용해서 둘 이상을 함께 등록할 수 있다.

 

3) epoll_wait : select 함수와 마찬가지로 파일 디스크립터의 변화를 대기
- select 방식에서는 관찰대상인 파일 디스크립터 저장을 위해 fd_set 변수를 직접 선언하였음.
- epoll 방식에서는 파일 디스크립터 저장을 운영체제가 담당하기에,

저장소의 생성을 운영체제에게 요청 해야함.(epoll_create)
- 관찰 대상인 파일디스크립터의 추가, 삭제 => select : FD_SET, FD_CLR, ... / epoll : epoll_ctl
- select <=> epoll_wait : 파일 디스크립터의 변화를 대기
- select 방식 : select 함수 호출시 전달한 fd_set형 변수의 변화를 통해 상태변화를 확인(이벤트 발생여부 확인)
- epoll 방식 : epoll_event 구조체를 기반으로 상태변화가 발생(이벤트가 발생)한 파일 디스크립터가 별도로 묶임

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;

- 구조체 epoll_event 기반의 배열을 넉넉하게 선언후,
epoll_wait 함수 호출시 인자로 전달하면, 상태변화(이벤트)가 발생한 파일 디스크립터의 정보가
이 배열에 별도로 묶임 => select 함수 사용시처럼 전체 관심대상 파일 디스크립터를 대상으로 한 반복문이 불필요

 

#include <sys/epoll.h>

int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);
// -> 성공시 이벤트가 발생한 파일 디스크립터 수, 실패시 -1반환

- epfd : 이벤트 발생의 관찰 영역인 epoll 인스턴스의 파일 디스크립터
- events : 이벤트가 발생한 파일 디스크립터가 채워질 버퍼의 주소값
- maxevents : 두번째 인자로 전달된 주소값의 버퍼에 등록 가능한 최대 이벤트 수 - 버퍼를 동적으로 할당해야함
- timeout : 1/1000초 단위의 대기시간. -1 전달시 이벤트가 발생할 때까지 무한대기

e.g.

int event_cnt;
struct epoll_event *ep_events;
....
ep_events=malloc(sizeof(struct epoll_event)*EPOLL_SIZE); // EPOLL_SIZE는 매크로 상수값
....
event_cnt=epoll_wait(epfd,ep_events,EPOLL_SIZE,-1);
....

# epoll 기반의 에코 서버

 

echo_epollserv.c

#define _XOPEN_SOURCE 700

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <unistd.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/epoll.h>

#define BUF_SIZE 100
#define EPOLL_SIZE 50
void error_handling(char *message);

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;

    if(argc!=2) // 실행파일 경로/PORT번호 를 입력으로 받아야 함
    {
        printf("Usage : %s <port> \n", argv[0]);
        exit(EXIT_FAILURE);
    }

    serv_sock=socket(PF_INET,SOCK_STREAM,0); // TCP 소켓 생성

    /* 서버 주소정보 초기화 */
    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(atoi(argv[1]));

    /* 서버 주소정보를 토대로 주소할당 */
    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");

    epfd=epoll_create(EPOLL_SIZE); // epoll 인스턴스 생성(관심 대상 파일 디스크립터 저장소)
    ep_events=malloc(sizeof(struct epoll_event)*EPOLL_SIZE);
    /*
    epoll 인스턴스에 있는 파일 디스크립터 중
    실제로 이벤트가 발생한 파일 디스크립터를 따로 모아놓는 동적배열
    최대 EPOLL_SIZE 만큼 이벤트가 발생할 수 있음
    */

    event.events=EPOLLIN; // 수신한 데이터가 있는 이벤트
    event.data.fd=serv_sock; // 서버소켓이 대상
    epoll_ctl(epfd,EPOLL_CTL_ADD,serv_sock,&event); // epoll 인스턴스에 이벤트 등록

    while(true)
    {
        event_cnt=epoll_wait(epfd,ep_events,EPOLL_SIZE,-1);
        /*
        epoll 인스턴스에 있는 관심대상에서
        이벤트가 발생할때까지 무한대기
        */
        
        if(event_cnt==-1)
        {
            puts("epoll_wait() error");
            break;
        }

        for(i=0;i<event_cnt;++i) // 이벤트 발생한 파일 디스크립터에 대해서만 반복문(select 기반과 다른 점)
        {
            
            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 // 클라이언트의 메시지를 실제로 수신하는 소켓에 대해(accept 함수호출로 생성된 소켓)
            {
                str_len=read(ep_events[i].data.fd,buf,BUF_SIZE); // 클라이언트로부터 데이터 수신
                if(str_len==0) // close request!(EOF 수신)
                {
                    epoll_ctl(epfd,EPOLL_CTL_DEL,ep_events[i].data.fd,NULL);
                    // 클라이언트가 종료했으므로 이 소켓 또한 관심대상에서 제외하고
                    
                    close(ep_events[i].data.fd); // 이 소켓의 연결도 종료
                    printf("close client: %d \n",ep_events[i].data.fd);
                }
                else
                    write(ep_events[i].data.fd,buf,str_len); // 수신한 문자열을 다시 클라이언트로 에코
            }
        }
    }
    close(serv_sock); // 서버소켓 소멸
    close(epfd); // epoll 인스턴스 소멸
    return 0;
}

void error_handling(char *message)
{
    fputs(message,stderr);
    fputc('\n',stderr);
    exit(EXIT_FAILURE); // -1
}

# 레벨 트리거와 엣지 트리거 : 차이점은 "이벤트가 발생하는 시점"
1) 레벨 트리거 : 입력버퍼에 데이터가 남아있는 동안에 계속해서 이벤트가 등록됨.
2) 엣지 트리거 : 입력버퍼에 데이터가 수신된 상황에 딱 한번만 이벤트가 등록됨.

- "이벤트가 등록되었다"는 의미는 운영체제가 변화가 발생한 디스크립터로 등록했다는 의미.

즉, 해당 이벤트를 struct epoll_event 배열에 추가함.
- epoll은 기본적으로 레벨 트리거 방식으로 동작한다.
- select 모델은 레벨 트리거 방식으로 동작함.

 

# 레벨 트리거의 이벤트 특성 파악하기

echo_EPLTserv.c

#define _XOPEN_SOURCE 700

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <unistd.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/epoll.h>

#define BUF_SIZE 4
#define EPOLL_SIZE 50
void error_handling(char *message);

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;

    if(argc!=2) // 실행파일 경로/PORT번호 를 입력으로 받아야 함
    {
        printf("Usage : %s <port> \n", argv[0]);
        exit(EXIT_FAILURE);
    }

    serv_sock=socket(PF_INET,SOCK_STREAM,0); // TCP 소켓 생성

    /* 서버 주소정보 초기화 */
    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(atoi(argv[1]));

    /* 서버 주소정보를 토대로 주소할당 */
    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");

    epfd=epoll_create(EPOLL_SIZE); // epoll 인스턴스 생성(관심 대상 파일 디스크립터 저장소)
    ep_events=malloc(sizeof(struct epoll_event)*EPOLL_SIZE);
    /*
    epoll 인스턴스에 있는 파일 디스크립터 중
    실제로 이벤트가 발생한 파일 디스크립터를 따로 모아놓는 동적배열
    최대 EPOLL_SIZE 만큼 이벤트가 발생할 수 있음
    */

    event.events=EPOLLIN; // 수신한 데이터가 있는 이벤트
    event.data.fd=serv_sock; // 서버소켓이 대상
    epoll_ctl(epfd,EPOLL_CTL_ADD,serv_sock,&event); // epoll 인스턴스에 이벤트 등록

    while(true)
    {
        event_cnt=epoll_wait(epfd,ep_events,EPOLL_SIZE,-1);
        /*
        epoll 인스턴스에 있는 관심대상에서
        이벤트가 발생할때까지 무한대기
        */
        
        if(event_cnt==-1)
        {
            puts("epoll_wait() error");
            break;
        }

        puts("return epoll_wait");
        for(i=0;i<event_cnt;++i) // 이벤트 발생한 파일 디스크립터에 대해서만 반복문(select 기반과 다른 점)
        {
            
            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 // 클라이언트의 메시지를 실제로 수신하는 소켓에 대해(accept 함수호출로 생성된 소켓)
            {
                str_len=read(ep_events[i].data.fd,buf,BUF_SIZE); // 클라이언트로부터 데이터 수신
                if(str_len==0) // close request!(EOF 수신)
                {
                    epoll_ctl(epfd,EPOLL_CTL_DEL,ep_events[i].data.fd,NULL);
                    // 클라이언트가 종료했으므로 이 소켓 또한 관심대상에서 제외하고
                    
                    close(ep_events[i].data.fd); // 이 소켓의 연결도 종료
                    printf("close client: %d \n",ep_events[i].data.fd);
                }
                else
                    write(ep_events[i].data.fd,buf,str_len); // 수신한 문자열을 다시 클라이언트로 에코
            }
        }
    }
    close(serv_sock); // 서버소켓 소멸
    close(epfd); // epoll 인스턴스 소멸
    return 0;
}

void error_handling(char *message)
{
    fputs(message,stderr);
    fputc('\n',stderr);
    exit(EXIT_FAILURE); // -1
}

- read 함수호출 시 사용할 버퍼의 크기를 4바이트로 축소

=> 입력버퍼에 수신된 데이터를 한번에 읽어들이지 못하게 하기 위함임

- epoll_wait 함수의 호출횟수를 확인하기 위한 문장 삽입

# 엣지 트리거 기반의 서버 구현을 위해서 알아야 할 것 두가지
1) 변수 errno을 이용한 오류의 원인을 확인하는 방법
- 일반적으로 리눅스에서는 소켓 관련 함수는 -1을 반환함으로써 오류의 발생을 알림

=> 따라서, 오류의 원인을 정확히 알수는 없음
- 리눅스에서는 오류 발생시 추가적인 정보제공을 위해 다음의 변수를 전역으로 선언 : int errno;
- errno 변수 접근을 위해서는 헤더파일 errno.h를 포함해야 함

(이 헤더파일은 errno에 extern 선언이 존재함)
- read 함수는 입력버퍼가 비어서 더이상 읽어들일 필요가 없을때 -1을 반환하고,
이때 errno에는 상수 EAGAIN가 저장됨.

2) 넌-블로킹(Non-blocking) IO를 위한 소켓의 특성을 변경하는 방법
- 소켓을 넌-블로킹 모드로 변경하는 방법

#include <fcntl.h>

int fcntl(int filedes, int cmd, ...);
// -> 성공시 매개변수 cmd에 따른값, 실패시 -1반환

- 리눅스에서 파일의 특성을 변경 및 참조하는 함수임
- filedes : 특성 변경의 대상이 되는 파일의 파일 디스크립터
- cmd : 함수호출의 목적에 해당하는 정보
- 파일(소켓)을 넌-블로킹 모드로 변경을 위해서는

int flag=fcntl(fd,F_GETTL,0); // 파일 디스크립터에 이미 설정되어있는 특성정보를 int형으로 얻음
fcntl(fd,F_SETL,flag|O_NONBLOCK); // O_NONBLOCK 특성을 더하여 재설정

=> read & write 함수 호출시에도 데이터의 유무에 상관없이 블로킹이 되지 않는 파일(소켓)이 만들어짐


# 엣지 트리거 기반의 서버 구현
- 엣지 트리거 방식은 데이터가 수신되면 딱 한 번만 이벤트가 등록되므로,
입력과 관련한 이벤트 발생시, 입력버퍼에 저장된 데이터 전부를 읽어들여야 한다.(대부분의 경우)
- 따라서, 입력버퍼가 비어있는지를 확인하는 과정을 거쳐야 함. 그 방법으로
"read 함수가 -1을 반환하고, 변수 errno에 저장된 값이 EAGAIN이라면

더이상 읽어들일 데이터가 존재하지 않는 상황"
- 소켓을 넌-블로킹 모드로 만드는 이유는 엣지 트리거 방식 특성상
블로킹 방식으로 동작하는 read & write 함수의 호출은

서버를 오랜 시간 멈추는 상황으로까지 이어지게 할 수 있기 때문.
=> 엣지 트리거 방식에서는 반드시 넌-블로킹 모드 소켓을 기반으로 read & write 함수를 호출하자.

 

ehco_EPETserv.c

#define _XOPEN_SOURCE 700

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <unistd.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <errno.h>

#define BUF_SIZE 4
#define EPOLL_SIZE 50
void setnonblockingmode(int fd);
void error_handling(char *message);

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;

    if(argc!=2) // 실행파일 경로/PORT번호 를 입력으로 받아야 함
    {
        printf("Usage : %s <port> \n", argv[0]);
        exit(EXIT_FAILURE);
    }

    serv_sock=socket(PF_INET,SOCK_STREAM,0); // TCP 소켓 생성

    /* 서버 주소정보 초기화 */
    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(atoi(argv[1]));

    /* 서버 주소정보를 토대로 주소할당 */
    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");

    epfd=epoll_create(EPOLL_SIZE); // epoll 인스턴스 생성(관심 대상 파일 디스크립터 저장소)
    ep_events=malloc(sizeof(struct epoll_event)*EPOLL_SIZE);
    /*
    epoll 인스턴스에 있는 파일 디스크립터 중
    실제로 이벤트가 발생한 파일 디스크립터를 따로 모아놓는 동적배열
    최대 EPOLL_SIZE 만큼 이벤트가 발생할 수 있음
    */

    setnonblockingmode(serv_sock);
    event.events=EPOLLIN; // 수신한 데이터가 있는 이벤트
    event.data.fd=serv_sock; // 서버소켓이 대상
    epoll_ctl(epfd,EPOLL_CTL_ADD,serv_sock,&event); // epoll 인스턴스에 이벤트 등록

    while(true)
    {
        event_cnt=epoll_wait(epfd,ep_events,EPOLL_SIZE,-1);
        /*
        epoll 인스턴스에 있는 관심대상에서
        이벤트가 발생할때까지 무한대기
        */
        
        if(event_cnt==-1)
        {
            puts("epoll_wait() error");
            break;
        }

        puts("return epoll_wait");
        for(i=0;i<event_cnt;++i) // 이벤트 발생한 파일 디스크립터에 대해서만 반복문(select 기반과 다른 점)
        {
            
            if(ep_events[i].data.fd==serv_sock)
            /*
            클라이언트의 연결요청도 데이터 전송을 통해 이루어지므로
            서버소켓에 수신된 데이터가 존재한다는 것은 클라이언트의 연결요청이 있었다는 의미
            */
            {
                adr_sz=sizeof(clnt_adr);
                clnt_sock=accept(serv_sock,(struct sockaddr*)&clnt_adr,&adr_sz); // 클라이언트의 연결요청을 수락
                setnonblockingmode(clnt_sock);
                /* 클라이언트와의 송수신을 위해 새로 생성된 소켓에 대해 이벤트 등록*/
                event.events=EPOLLIN|EPOLLET; // 이벤트 등록방식을 엣지 트리거 방식으로
                event.data.fd=clnt_sock;
                epoll_ctl(epfd,EPOLL_CTL_ADD,clnt_sock,&event);
                
                printf("connected client: %d \n",clnt_sock);
            }
            else // 클라이언트의 메시지를 실제로 수신하는 소켓에 대해(accept 함수호출로 생성된 소켓)
            {
                while(true) // 이벤트 발생시 입력버퍼에 모든 데이터를 수신하기 위함임
                {
                    str_len=read(ep_events[i].data.fd,buf,BUF_SIZE); // 클라이언트로부터 데이터 수신
                    if(str_len==0) // close request!(EOF 수신)
                    {
                        epoll_ctl(epfd,EPOLL_CTL_DEL,ep_events[i].data.fd,NULL);
                        // 클라이언트가 종료했으므로 이 소켓 또한 관심대상에서 제외하고
                        
                        close(ep_events[i].data.fd); // 이 소켓의 연결도 종료
                        printf("close client: %d \n",ep_events[i].data.fd);
                    }
                    else if(str_len<0) 
                    {
                        if(errno==EAGAIN) // 다 읽어들여 입력버퍼가 빈 경우
                            break;
                    }
                    else
                        write(ep_events[i].data.fd,buf,str_len); // 수신한 문자열을 다시 클라이언트로 에코
                }
                
            }
        }
    }
    close(serv_sock); // 서버소켓 소멸
    close(epfd); // epoll 인스턴스 소멸
    return 0;
}

void setnonblockingmode(int fd)
{
    int flag=fcntl(fd,F_GETFL,0);
    fcntl(fd,F_SETFL,flag|O_NONBLOCK);
}

void error_handling(char *message)
{
    fputs(message,stderr);
    fputc('\n',stderr);
    exit(EXIT_FAILURE); // -1
}

# 엣지 트리거 방식의 장점
- 데이터의 수신과 데이터가 처리되는 시점을 분리할 수 있다.
: 입력버퍼에 데이터가 수신된 상황(이벤트가 등록된 상황)임에도 불구하고,
이를 읽어들이고 처리하는 시점을 서버가 결정할 수 있도록 하는 것은 서버 구현에 엄청난 유연성을 제공
- 레벨 트리거 방식으로 이것이 불가능한 것은 아니나, 사실상 불가능.
(처음 이벤트가 등록된 상황에서 처리하지 않으면, epoll_wait 함수를 호출할때마다
이벤트가 발생하고, 이로 인해 발생하는 이벤트의 수가 계속 누적되므로)
- 일반적으로 엣지 트리거가 빠르지만, 상황에 따라 다를 수 있음.


[출처] : 윤성우 저, "열혈강의 TCP/IP 소켓 프로그래밍", 오렌지미디어