[소켓 #17] select보다 나은 epoll

이석환·2023년 6월 11일

Socket Programming

목록 보기
18/18

1. epoll의 이해와 활용

seelct는 오래 전에 개발된 멀티플렉싱 기법이다.
이를 이용하면 프로그램의 성능을 최적화시킨다고 해도 동시접속자의 수에 한계가 있다.
이러한 select 방식은 웹 기반의 서버 개발이 주를 이루는 오늘 날의 개발환경에는 적절하지 않다.
이에 대한 대안으로 리눅스에서 자주 활용되는 epoll에 대해 공부해보자

1-1. select 기반의 IO 멀티플렉싱이 느린 이유

select 기반의 멀티플렉싱 서버에는 크게 두 가지의 불합리한 점이 있다.

  • select 함수 호출 이후에 항상 등장하는, 모든 파일 디스크립터를 대상으로 하는 반복문
  • select 함수를 호출할 때마다 인자로 매번 전달해야 하는 관찰대상에 대한 정보들

    select 함수는 OS의 커널에 의해서 완성되는 기능이 아닌, 순수하게 함수에 의해 완성되는 기능이다. 따라서 select 함수의 호출을 통해서 잔달된 정보는 OS에 등록되지 않는 것이며, select 함수를 호출할 때마다 매번 관련 정보를 전달해야 한다. 그리고 이것이 select 함수가 지니는 단점의 가장 큰 원인이다.
  • 단점의 해결책
    "운영체제에서 관찰 대상에 대한 정보를 딱 한 번만 알려주고서, 관찰 대상의 범위, 또는 내용에 변경이 있을 때 변경 사항만 알려주도록 하자 !"

즉, select 함수의 단점 극복을 위해서는 OS 레벨에서 멀티플렉싱 기능을 지원해야 한다는 뜻이다.
리눅스의 epoll, 윈도우의 IOCP가 있다.

1-2. select는 필요 없는 것인가 ?

위에 설명한 보면 필요가 없는 것이라고 생각할 수도 있다.
하지만 장점이 있다.
이번 Chapter에서 설명하는 epoll 함수는 Linux에서만 지원하는 방식이다.
즉, 개선된 IO 멀티플렉싱 모델은 OS 별로 호환되지 않는다.
반면 select 함수는 대부분의 OS에서 지원을 한다.
운영체제 레벨이 아닌, 함수 레벨에서 완성되는 기능이다 보니, 호환성이 상대적으로 좋다.
따라서 다음 두 가지 조건이 만족되거나 요구되는 상황이라면, Linux 환경일지라도 epoll을 고집할 이유가 없다.

  • 서버의 접속자 수가 많지 않다.
  • 다양한 운영체제에서 운영이 가능해야 한다.

1-3. epoll의 구현에 필요한 함수와 구조체

먼저 select와 비교한 epoll의 장점에 대해 살펴보자.

  • 상태 변화의 확인을 위한, 전체 파일 디스크립터를 대상으로 하는 반복문이 필요 없다.
  • select 함수에 대응하는 epoll_wait 함수 호출 시, 관찰 대상의 정보를 매번 전달할 필요가 없다.
    즉, 상태 변화 관찰에 더 나은 방법을 제공한다.
    그리고 커널에서 상태 정보를 유지하기 때문에 관찰 대상의 정보를 매번 전달하지 않아도 된다.

지금부터 epoll 기반의 서버 구현에 필요한 세 가지 함수를 소개하겠다.

  • epoll_create : epoll 파일 디스크립터 저장소 생성
  • epoll_ctl : 저장소에 파일 디스크립터 등록 및 삭제
  • epoll_wait : select 함수와 마찬가지로 파일 디스크립터의 변화를 대기한다.
  1. epoll_create
    select 방식에서는 관찰 대상인 파일 디스크립터의 저장을 위해서 fd_set형 변수를 직접 선언했었다.
    epoll 방식에서는 관찰 대상인 파일 디스크립터의 저장을 OS가 담당한다.
    때문에 파일 디스크립터의 저장을 위한 저장소의 생성을 OS에게 요청해야 하는데, 이 때 사용되는 함수이다.
include <sys/epoll.h>

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

/*
size : epoll 인스턴스의 크기 정보
*/

epoll_create 함수 호출 시 생성되는 파일 디스크립터의 저장소를 가리켜 'epoll 인스턴스'라 한다.
이 함수가 반환하는 파일 디스크립터는 epoll 인스턴스를 구분하는 목적으로 사용되며 소멸 시 close 함수 호출을 통한 종료의 과정이 필요하다.
위의 함수 호출을 통해서 생성된 epoll 인스턴스에 관찰 대상을 저장 및 삭제하는 함수 : epoll_ctl
epoll 인스턴스에 등록된 파일 디스크립터를 대상으로 이벤트의 발생 유무룰 확인하는 함수 : epoll_wait


2. epoll_ctl
select 방식에서는 관찰 대상인 파일 디스크립터의 추가, 삭제를 위해서 FD_SET, FD_CLR 함수를 사용하지만, epoll 방식에서는 epoll_ctl 함수를 통해서 OS에게 요청하는 방식으로 이뤄진다.

#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(A, EPOLL_CTL_ADD, B, C);
: epoll 인스턴스 A에 파일 디스크립터 B를 등록하되, C를 통해 전달된 이벤트의 관찰을 목적으로 등록

epoll_ctl(A, EPOLL_CTL_DEL, B, NULL);
: epoll 인스턴스 A에서 파일 디스크립터 B를 삭제

여기서 4번 째 인자인 epoll_event는 앞에서 싱태 변화가 발생한 파일 디스크립터를 묶는 용도로 사용된다고 하였다. 하지만 파일 디스크립터를 epoll 인스턴스에 등록할 때, 이벤트의 유형을 등록하는 용도로도 사용된다.

위의 코드는 epoll 인스턴스인 epfd에 sockfd를 등록하되, 수신할 데이터가 존재하는 상황에서 이벤트가 발생하도록 등록하는 방법이다.
이번에는 epoll_event의 멤버인 events에 저장 가능한 상수와 이벤트의 유형이다.

  1. epoll_wait
    select 방식에서는 select 함수 호출 시 전달한 fd_set형 변수의 변화를 통해서 관찰대상의 상태변화를 확인하지만, epoll 방식에서는 구조체 epoll_event를 기반으로 상태변화가 발생한 파일 디스크립터가 별도로 묶인다.


    위의 구조체 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 전달 시, 이벤트가 발생할 때까지 무한 대기
*/

사용 예는 다음과 같다.

함수 호출 후에는 이벤트가 발생한 파일 디스크립터의 수가 반환되고, 두 번째 인자로 전달된 주소 값의 버퍼에는 이벤트가 발생한 파일 디스크립터의 정보가 별도로 묶이기 때문에 전체 파일 디스크립터 대상의 반복문이 필요하지 않다.

1-4. epoll 기반의 에코 서버

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.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 *buf);

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) {
        printf("Usage : %s <port>\n", argv[0]);
        exit(1);
    }

    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(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);
    ep_events=malloc(sizeof(struct epoll_event)*EPOLL_SIZE);

    event.events=EPOLLIN;
    event.data.fd=serv_sock;
    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)
                    {
                        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;
}

void error_handling(char *buf)
{
    fputs(buf, stderr);
    fputc('\n', stderr);
    exit(1);
}
  • 실행 결과

    select 방식과 똑같기 때문에 자세한 설명은 생략하겠다.

2. 레벨 트리거(Level Trigger)와 엣지 트리거(Edge Trigger)

epoll을 알면서 레벨 트리거와 엣지 트리거의 방식은 꼭 알아야 한다.

2-1. 레벨 트리거와 엣지 트리거의 차이는 이벤트가 발생하는 시점에 있다 !

  • 레벨 트리거
    입력 버퍼에 데이터가 남아있는 동안에 데이터 양의 변화에 상관없이 계속해서 이벤트를 발생시킴.
  • 엣지 트리거
    입력 버퍼에 데이터가 들어오는 순간 딱 한 번만 이벤트를 발생시킴

예를 들어서 레벨 트리거에서 서버으 입력 버퍼로 50byte의 데이터가 수신되면, 일단 서버 측 운영체제는 이를 이벤트로 등록한다.(변화가 발생한 파일 디스크립터로 등록)
그런데 서버 프로그램에서 20byte를 수신해서 입력 버퍼에 30byte만 남는다면 이 상황 역시 이벤트로 등록한다.

2-2. 레벨 트리거의 이벤트 특성 파악하기

바로 코드를 보자
앞에서 설명한 epollserv에서 조금만 수정하였다.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.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 *buf);

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) {
        printf("Usage : %s <port>\n", argv[0]);
        exit(1);
    }

    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(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);
    ep_events=malloc(sizeof(struct epoll_event)*EPOLL_SIZE);

    event.events=EPOLLIN;
    event.data.fd=serv_sock;
    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;
        }

        puts("return epoll_wait");
        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)
                    {
                        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;
}

void error_handling(char *buf)
{
    fputs(buf, stderr);
    fputc('\n', stderr);
    exit(1);
}
  • 실행 결과
    epollserv와 차이점은 다음 두 가지이다.
  • 2행 : read 함수 호출 시 사용할 버퍼의 크기를 4byte로 축소
  • 50행: epoll_wait 함수의 호출 횟수를 확인하기 위한 문장 삽입

    버퍼의 크기를 줄인 이유는 입력 버퍼에 수신된 데이터를 한 번에 읽지 못하게 하기 위함이다.
    즉, read 함수 호출 이후에도 입력 버퍼에는 여전히 읽어 들일 데이터가 존재하기 때문에 이로 인해 새로운 이벤트가 등록되어서 epoll_wait 함수를 반환하기 때문에 문자열 "return epoll_wait"가 반복 출력되는 것을 볼 수 있다.
    소켓은 기본적으로 레벨 트리거로 동작한다.
    출력 결과를 보면 알겠지만, 클라이언트로부터 메시지를 한 번 수신할 때마다 이벤트 등록이 여러 번 이뤄지고, 이로 인해서 epoll_wait 함수가 다수 호출됨을 보기오 있다.
    추가적으로 select 모델은 레벨 트리거 방식으로 동작한다.
    즉, 입력 버퍼에 데이터가 남아있으면 무조건 이벤트에 등록된다.

2-3. 엣지 트리거 기반의 서버 구현을 위해서 알아야 할 것 두 가지

  • 변수 errno를 이용한 오류의 원인을 확인하는 방법
  • 넌-블로킹(Non-blocking) IO를 위한 소켓의 특성을 변경하는 방법
  1. 변수 errno를 이용한 오류의 원인을 확인하는 방법
    일반적으로 리눅스에서 제공하는 소켓관련 함수는 -1을 반환함으로써 오류의 발생을 알린다.
    하지만 오류가 발생했다는 것만 인지할 수 있고 오류의 원인을 확인할 수 없다.
    때문에 리눅스에서는 오류 발생시 추가적인 정보를 위해 errno라는 전역 변수가 선언되어 있다.
    헤더파일 <error.h>를 포함해야한다. 대략적 의미는 다음과 같다.
    "read 함수는 입력 버퍼가 비어서 더 이상 읽어 들일 데이터가 없을 때 -1을 반환하고, 이 때 errno에는 상수 EAGAIN가 저장된다."
    즉, errno에 상수 EAGAIN이 저장되면 버퍼가 빈 상태이다.

  2. 넌-블로킹 IO로 소켓 속성 변경
    이 함수의 사용은 [소켓 #13]에서 다룬 적이 있다. 하지만 다시 한 번 다루겠다.

#include <fcntl.h>

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

/*
filedes : 특성 변경의 대상이 되는 파일의 파일 디스크립터 전달
cmd : 함수 호출의 목적에 해당하는 정보 전달
*/

파일(소켓)을 넌-블로킹 모드로 변경하기 위해서 다음 두 문장을 실행하면 된다.

int flag = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flag|O_NONBLOCK);

첫 번째 문장을 통해서 기본에 설정되어 있던 특성 정보를 얻어오고, 두 번째 문장에서는 여기에 넌-블로킹 입출력을 의미하는 O_NONBLOCK을 더해서 특성을 재설정해주고 있다.
이로써 read & write 함수 호출 시에도 데이터의 유무에 상관없이 블로킹이 되지 않는 파일(소켓)이 만들어 진다.

엣지 트리거는 데이터 수신 시 딱 한 번만 이벤트가 발생하기 때문에 이벤트가 발생했을 때 충분한 양의 버퍼를 마련한 다음에 모든 데이터를 다 읽어야 한다.
즉, 데이터의 분량에 따라서 IO로 인한 DELAY가 생길 수 있다.
그래서 엣지 트리거에서는 넌-블로킹 IO를 이용한다.
입력 함수의 호출과 다른 작업을 병행할 수 있기 때문이다.

2-4. 엣지 트리거 기반의 에코 서버 구현

서버 구현에 앞서 에러 원인의 확인 방법과 넌-블로킹 모드의 소켓 생성에 대한 것을 다시 설명하겠다.
먼저 errno를 이용한 오류의 확인 과정이 필요한 이유이다.
"엣지 트리거 방식에서는 데이터가 수신되면 딱 한 번 이벤트가 등록된다."
이러한 특성때문에 일단 입력과 관련해서 이벤트가 발생하며, 입력 버퍼에 저장된 데이터 전부를 읽어야 한다.
따라서 입력 버퍼가 비어있는 지 확인하는 과정은 필수이다.
"read 함수가 -1을 반환하고, 변수 errno에 저장된 값이 EAGAIN이면 더 이상 읽을 데이터가 없다."
소켓을 넌-블로킹 모드로 만드는 이유에 대해 설명하겠다.
엣지 트리거 방식의 특성상 블로킹 방식으로 동작하는 read & write 함수의 호출은 서버를 오랜 시간 멈추는 상황으로까지 이어지게 할 수 있다.
때문에 엣지 트리거 방식에서는 반드시 넌-블로킹 소켓을 기반으로 read & write 함수를 호출해야 한다.
이제 서버를 구현해보겠다.

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

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

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) {
        printf("Usage : %s <port>\n", argv[0]);
        exit(1);
    }

    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(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);
    ep_events=malloc(sizeof(struct epoll_event)*EPOLL_SIZE);

    setnonblockingmode(serv_sock);
    event.events=EPOLLIN;
    event.data.fd=serv_sock;
    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;
        }

        puts("return epoll_wait");
        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);
                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
            {
                    while(1)
                    {
                        str_len=read(ep_events[i].data.fd, buf, BUF_SIZE);
                        if(str_len==0)
                        {
                            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);
                            break;
                        }
                        else if(str_len<0)
                        {
                            if(errno==EAGAIN)
                                break;
                        }
                        else
                        {
                            write(ep_events[i].data.fd, buf, str_len);
                        }
                }
            }
        }
    }
    close(serv_sock);
    close(epfd);
    return 0;
}

void setnonblockingmode(int fd)
{
    int flag=fcntl(fd, F_GETFL, 0);
    fcntl(fd, F_SETFL, flag|O_NONBLOCK);
}
void error_handling(char *buf)
{
    fputs(buf, stderr);
    fputc('\n', stderr);
    exit(1);
}
  • 실행 결과
    !
  • 엣지 트리거 동작 방식을 확인하기 위해 버퍼의 길이를 4로 정했다.
  • 59행에서 accept 함수 호출에 의해 생성된 소켓을 넌-블로킹 소켓으로 변경하고 있다.
  • 60행에서는 EPOLLIN에 EPOLLET을 추가해서 소켓의 이벤트 등록 방식을 엣지 트리거 방식으로 설정하였다.
  • 레벨 트리거에서 존재하지 않던 while문이 등장하였다. 이는 이벤트 발생시 입력 버퍼에 존재하는 데이터를 모두 수신해야 하기 때문에 read를 반복해서 호출한다.
  • read 함수가 -1을 반환하고, errno에 저장된 값이 EAGAIN인 상황은 입력 버퍼에 저장된 데이터를 모두 읽었을 때 발생하기 때문에 break문을 통해서 반복문을 빠져나간다.


    위 실행결과를 보면 클라이언트의 메시지 전송횟수와 서버의 epoll_wait 함수의 호출 횟수가 동일하다는 것을 알 수 있다.

2-5. 레벨 트리거와 엣지 트리거의 비교

위에 글만 보면 엣지 트리거가 레벨 트리거에 비해 지니는 장점이 없다고 생각할 수 있다.
그러나 엣지 트리거 방식을 사용하면 다음과 같은 형태의 구현이 가능하다.
"데이터의 수신과 데이터가 처리되는 시점을 분리할 수 있다!"

다음과 같은 시나리오를 보여주겠다.

위와 같은 상황에서 Clinet A는 연결조차 되지 않았거나, A,B,C가 순서에 상관없이 데이터를 서버에 전송하거나 하는 경우가 있다.
따랗서 입력 버퍼에 데이터가 수신된 상황임에도 불구하고(이벤트가 등록된 상황), 이를 읽어 들이고처리하는 시점을 서버가 결정할 수 있도록 하는 것은 서버 구현에 엄청난 유연성을 제공한다.

즉, 위와 같은 시나리오 상에서 클라이언트가 서버에 접속 및 데이터를 전송하는 순서는 서버의 기대와 상관이 없다. 이처럼 서버측에서의 컨트롤 요소가 많은 경우에는 엣지 트리거가 유리하다.
반면, 서버의 역할이 상대적으로 단순하고 또 데이터 송수신의 상황이 다양하지 않다면, 레벨 트리거 방식을 선택할 만 하다.

3. Chat_serv

EPLT와 EPET 서버를 채팅으로 구현해보았다.

  • 서버에 접속한 모든 클라이언트들 사이에서 메시지를 주고 받는 형태의 채팅 서버
  • 레벨/엣지 트리거 방식으로 각각 구현

3-1. chat_EPLTserv

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

#define BUF_SIZE 100
#define MAX_CLNT 256
#define EPOLL_SIZE 50
void error_handling(char *buf);
void send_msg(char * msg, int len);

int clnt_cnt=0;
int clnt_socks[MAX_CLNT];

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) {
        printf("Usage : %s <port>\n", argv[0]);
        exit(1);
    }

    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(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);
    ep_events=malloc(sizeof(struct epoll_event)*EPOLL_SIZE);

    event.events=EPOLLIN;
    event.data.fd=serv_sock;
    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)
        {
            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);
                clnt_socks[clnt_cnt++]=clnt_sock;
                printf("connected client: %d \n", clnt_sock);
            }
            else
            {
                str_len=read(ep_events[i].data.fd, buf, BUF_SIZE);
                if(str_len==0)
                {
                    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);

                    for(i=0; i<clnt_cnt; i++)
                    {
                        if(clnt_sock==clnt_socks[i])
                        {
                            while(i++<clnt_cnt-1)
                                clnt_socks[i]=clnt_socks[i+1];
                            break;
                        }
                    }
                    clnt_cnt--;
                }
                else
                {
                    send_msg(buf, str_len);
                }
            }
        }
    }
    close(serv_sock);
    close(epfd);
    return 0;
}

void send_msg(char * msg, int len)
{
    int i;
    for(i=0; i<clnt_cnt; i++)
        write(clnt_socks[i], msg, len);
}

void error_handling(char *buf)
{
    fputs(buf, stderr);
    fputc('\n', stderr);
    exit(1);
}
  • 실행 결과

3-2. chat_EPET serv

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

#define BUF_SIZE 100
#define MAX_CLNT 256
#define EPOLL_SIZE 50

void setnonblockingmode(int fd);
void error_handling(char *buf);
void send_msg(char * msg, int len);

int clnt_cnt=0;
int clnt_socks[MAX_CLNT];

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) {
        printf("Usage : %s <port>\n", argv[0]);
        exit(1);
    }

    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(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);
    ep_events=malloc(sizeof(struct epoll_event)*EPOLL_SIZE);

    setnonblockingmode(serv_sock);
    event.events=EPOLLIN;
    event.data.fd=serv_sock;
    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;
        }

        puts("return epoll_wait");
        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);
                setnonblockingmode(clnt_sock);
                event.events=EPOLLIN|EPOLLET;
                event.data.fd=clnt_sock;
                epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &event);
                clnt_socks[clnt_cnt++]=clnt_sock;
                printf("connected client: %d \n", clnt_sock);
            }
            else
            {
                    while(1)
                    {
                        str_len=read(ep_events[i].data.fd, buf, BUF_SIZE);
                        if(str_len==0)
                        {
                            epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, NULL);
                            close(ep_events[i].data.fd);

                            for(i=0; i<clnt_cnt; i++)
                            {
                                if(ep_events[i].data.fd==clnt_socks[i])
                                {
                                    while(i++<clnt_cnt-1)
                                        clnt_socks[i]=clnt_socks[i+1];
                                    break;
                                }
                            }
                            clnt_cnt--;
                            printf("closed client: %d \n", ep_events[i].data.fd);
                            break;
                        }
                        else if(str_len<0)
                        {
                            if(errno==EAGAIN)
                                break;
                        }
                        else
                        {
                            send_msg(buf, str_len);
                        }
                }
            }
        }
    }
    close(serv_sock);
    close(epfd);
    return 0;
}


void send_msg(char * msg, int len)
{
    int i;
    for(i=0; i<clnt_cnt; i++)
        write(clnt_socks[i], msg, len);
}
void setnonblockingmode(int fd)
{
    int flag=fcntl(fd, F_GETFL, 0);
    fcntl(fd, F_SETFL, flag|O_NONBLOCK);
}
void error_handling(char *buf)
{
    fputs(buf, stderr);
    fputc('\n', stderr);
    exit(1);
}
  • 실행 결과

참고 : 윤성우의 열혈 TCP/IP 소켓 프로그래밍
Git : https://github.com/im2sh/Socket_Programming/tree/main/lab14

profile
반갑습니다.

0개의 댓글