이 글은 윤성우의 TCP/IP 소켓 프로그래밍 책을 참고하였습니다.

순서

I. 레벨 트리거와 엣지 트리거 개념

II. 소켓의 Non-Blocking Mode

III. 엣지 트리거로 서버 구현

IV. 레벨 트리거와 엣지트리거의 비교


I. 레벨 트리거와 엣지 트리거 개념

epoll함수를 사용하기 위해서 꼭 이해해야 하는 개념이다.

두 차이는 다음과 같다.

레벨 트리거 : 입력 버퍼에 데이터가 남아있는 동안에 계속해서 이벤트가 새로 등록된다.
엣지 트리거 : 데이터가 수신된 상황에서 딱! 한 번만 이벤트로 등록된다.                    

만약 수신되는 버퍼의 크기가 작다고 하면
레벨트리거는 데이터를 모두 수신 못하므로 지속해서 이벤트가 등록된다.
엣지 트리거에서는 그럼에도 불구하고 단 한 번만 이벤트가 등록된다.

우리가 epoll 코드를 작성하게 된다면 기본적으로 레벨 트리거 방식으로 작동한다.

따라서 옵션을 추가해줘야하는데, 바로 이때 event 상수 중 EPOLLET이 사용되는 것이다.

따라서 userevent. events = EPOLLIN | EPOLLET; 로 정의해주게 된다.

 

엣지 트리거에는 문제점이 있다.

우리는 목표한 데이터가 다 올 때까지 read함수를 반복 호출해야 하고
readbuf가 비어있는 것을 확인하고 데이터가 다 왔다는 시그널을 날려야 한다.

하지만, read 함수는 기본적으로 readbuf가 비어있으면 무한 대기상태(Blocking) 상태를 가진다.

따라서 이 문제점을 해결하기 위해 소켓을 Non-Blocking 상태로 변경시켜주는 작업이 필요하다.

이를 다음 단락에서 확인해보자. 

 


II. 소켓의 Non-blocking Mode

여기서는 두 가지를 설명할 것이다.

1. 변수 errno를 이용한 오류의 원인을 확인하는 방법
2. Non-blocking IO를 위한 소켓의 특성 변경하는 방법

2 같은 경우는 위에서 설명했고,

1의 경우에는 readbuf가 비어있을 때 어떤 에러를 처리해야 하는지 알아야만
이후 시그널을 보낼 수 있기 때문이다.


1. 변수 errno

일반적으로 리눅스에서는 -1을 오류의 반환 값으로 설정해둔다.

따라서 오류가 발생한 것은 알 수 있으나 정확히 어떤 원인인지는 모르기 때문에
리눅스에서는 int errno;라는 전역 변수를 이용해 추가적인 정보를 제공해준다.

이 변수는 <errno.h>를 헤더로 포함해야 한다.
그 이유는 이 헤더에 errno의 extern선언이 존재하기 때문이다.

우리가 알고자 하는 read함수에 대한 에러를 설명하면, 다음과 같다.

"read함수는 readbuf가 비어서 더 이상 읽을 데이터가 없으면 -1을 반환하고, errno변수에는 EAGAIN이 저장된다"


2. Non-blocking IO 소켓으로의 변경

리눅스에서 파일의 특성을 변경 및 참조하기 위해서는 다음 함수를 사용한다.

#include <fcntl.h> // 파일 컨트롤

int fcntl(int filedes, int cmd, . . .);
    --> 성공 시 매개변수 cmd에 따른 값, 실패 시 -1 반환
    
    ㄴ. filedes : 특성 변경의 대상이 되는 파일의 fd 전달
    ㄴ. cmd     : 함수호출의 목적에 해당하는 정보 전달.
    
 
Non-blocking으로의 변경방법

int flag = fcntl(fd, F_GETFL,0); // 기존에 설정되어 있던 특성정보를 가져옴
fcntl(fd, F_SETFL, flag | O_NONBLOCK); //넌 블로킹으로 특성 재 설정함.

인자2. int cmd

이 인자에는 현재 두 가지를 사용하였다.

F_GETFL : 첫 번째 인자로 전달된 fd에 설정되어 있는 특성정보를 int형으로 얻어옴.
F_SETFL : 첫 번째 인자로 전달된 fd에 3번째 가변 인자로 전달된 특성정보를 설정함.

Non-blocking 입출력을 의미하는 매크로 : O_NONBLOCK (아웃풋 넌-블럭이라는 뜻인 것 같다.) 

 


III. 엣지 트리거로 서버 구현

앞선 레벨 트리거 코드에서 큰 차이점은 "*******" 주석처리된 부분인데,
이 부분이 엣지 트리거를 위한 구현이라고 보면 된다.

<hide/>
// 헤더 두 개만 추가로 더 선언하면 된다
#include <fcntl.h>
#include <errno.h>

// 버퍼가 4로 매우 작다
#define BUF_SIZE 4 
#define EPOLL_SIZE 50

// 논블락킹으로 해주는 함수 만들었음.
void setnonblockingmode(int fd)
{
    int flag = fcntl(fd, F_GETFL, 0);
    fcntl(fd, F_SETFL, flag|O_NONBLOCK);
}

	int main()
{
    // 기본 소켓 프로그래밍 변수들
    int serv_sock, clnt_sock;
    struct sockaddr_in serv_addr, clnt_addr;
    socklen_t addr_sz;
    int str_len, i;
    char buf[BUF_SIZE]
    
    // epoll위한 변수들
    struct epoll_event* ep_events_buf; // 변화를 담을 버퍼의 주소!
    struct epoll_event userevent; // 등록하기 위한 변수!
    int epfd, event_cnt;
    
    1. socket();
    2. bind();
    3. listen();
    
    // 여기서부터 epoll 시작
    
    epfd = epoll_create(EPOLL_SIZE);
    ep_events_buf = malloc(sizeof(struct epoll_event)* EPOLL_SIZE); // 버퍼 동적할당
    
    setnonblockingmode(serv_sock); // 여기서 Non-blocking 모드 설정**************************************
    
    userevent.events = EPOLLIN; // 수신관련한 이벤트를 보고싶다.
    userevent.data.fd = serv_sock; // 서버 fd를 등록할 것이다.
    epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &userevent); // ADD로 등록, 서버fd, 원하는 이벤트
    
    while(1)
    {
        // 무한 대기하며 epfd중에 변화가 있는 fd 확인. 확인 후 event 버퍼에 넣음.
        event_cnt = epoll_wait(epfd, ep_events_buf, EPOLL_SIZE, -1);
        
        if(event_cnt ==-1) 
        {
            puts("wait() error!\n");
            break;
        }
        
        // 확인된 변화의 갯수만큼 for문
        for(i=0; i<event_cnt; i++)
        {
            // 만약 이벤트가 생겼고 그 fd가 서버라면 -> 연결요청임!
            if(ep_events_buf[i].data.fd == serv_sock)
            {
                addr_sz = sizeof(clnt_addr);
                clnt_sock = accept(serv_sock, (struct sockaddr*) &clnt_addr, &addr_sz) // 이때 accept!!
                
                // 클라이언트의 fd를 등록해주는 과정
                // 여기서 소켓 설정 변경***************************************************************
                setnonblockingmode(clnt_sock) // Non-blocking
                userevent.events=EPOLLIN | EPOLLET; // EPOLLET설정해 엣지트리거설정
                userevent.data.fd = clnt_sock;
                epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &userevent);
                
                printf("connected client : %d\n",clnt_sock);
            }
            
            // 서버가 아니라면 -> 클라이언트의 변화
            else
            {
                while(1)  // 여기서 차이점이 생긴다 ****************************************************
                {
                	str_len = read(ep_events_buf[i].data.fd, buf, BUF_SIZE); // 디스크럽터 접근방법 집중!
                	
                    if( str_len ==0) // close socket
                	{
                    	epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events_buf[i], NULL);
                    	close(ep_events_buf[i].data.fd);
                    	printf("Close client : %d\n", ep_events[i].data.fd);
                        break;
                	}
                    
                    // 만약 -1이고 errno를 통해 readbuf 빈거 확인했으면 다 받아서 break!!****************
                    
                    else if(str_len<0) {
                        if(errno==EAGAIN) break;
                    }
                    
                	else 
                	{
                    	write(ep_events_buf[i].data.fd, buf, str_len);
                        //정상 수신되면 원하는 작업들~~
                	}
                }
            }
        }
    }
    close(serv_sock);
    close(epfd);
    return 0;
}

+ Recent posts