Computer Science 기본 지식/소켓 프로그래밍

[TCP/IP 소켓 프로그래밍] (11-1) 다중 접속 서버 - epoll

로파이 2021. 4. 5. 16:52

 

출처 : 열혈 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;
}