현생/TCP 소켓 프로그래밍

17-2. 레벨 트리거와 엣지 트리거 [TCP/IP][C][LINUX]

푸더기 2022. 2. 16. 22:35
반응형

 

레벨 트리거 방식은 입력버퍼에 데이터가 남아있는 동안 계속 이벤트가 등록됩니다.

엣지 트리거 방식은 입력버퍼로 데이터가 수신된 상황에서 딱 한 번만 이벤트가 등록됩니다.

 

https://pupuduck.tistory.com/88에 따르면 epoll_event 구조체의 event 변수에 EPOLLET를 지정함으로써 엣지트리거로 변경시킬 수 있습니다.  ex) event.events=EPOLLIN|EPOLLET;

 

엣지 트리거 기반의 서버 구현을 위해선 변수 errno를 이용한 오류 원인 파악과 Non-blocking IO를 위해 소켓의 특성을 변경하는 방법을 알아야합니다. 일반적으로 소켓관련 함수는 오류가 발생했을 때 -1을 반환하는데, 이로써 오류의 원인을 알 수는 없습니다. 헤더파일 errno.h를 include하면 errno 변수에 접근할 수 있습니다.

read함수로 예를 들면 입력버퍼가 비어서 더 이상 읽어 들일 데이터가 없을 때 -1을 반환하는데 이 때 errno에는 상수 EAGAIN가 저장됩니다.

#include<fcntl.h>
int fcntl(int filedes, int cmd, ...);
//success: 매개변수 cmd에 따른 값   fail: -1

fcntl함수는 파일의 특성을 변경 및 참조하는 함수인데 , filedes에는 파일 디스크립터, cmd에는 함수호출의 목적에 해당하는 정보를 전달합니다. cmd에 F_GETFL을 전달하면 파일 디스크립터의 특성정보를 int형으로 얻을 수 있고, F_SETFL 인자를 전달하면 특성정보를 변경할 수 있습니다.

소켓을 Non-blocking 모드로 변경하기 위해서는 2번째 인자에 F_SETFL, 3번째 가변인자에 flag|O_NONBLOCK을 전달하면 됩니다.

 

위 두 방법을 알아야하는 이유는, 엣지 트리거 방식에서는 데이터가 수신되면 딱 한 번 이벤트가 등록되기 떄문에 입력과 관련해서 이벤트가 발생하면 입력버퍼의 모든 데이터를 읽어들여야 하고, 따라서 입력버퍼가 비어있는지 확인하는 과정을 거쳐야 합니다.

그리고 엣지 트리거 방식의 특성상 블로킹 방식으로 동작하는 read&write 함수는 서버를 오랜 시간 멈추게 할 수 있기에 반드시 Non_blocking 소켓을 기반으로 호출해야합니다.

 

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.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 *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);
	
	//서버 소켓을 non-blocking 모드로
	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);
				// 클라이언트 소켓을 non-blocking모드, 이벤트 방식을 엣지트리거로
				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);
}

https://pupuduck.tistory.com/88에서 다룬 epoll을 이용한 echo 서버를 엣지트리거를 사용하도록 다듬은 것입니다. epoll 서버와 엣지트리거를 이해했다면 쉽게 이해할 수 있을 것입니다. 

 

엣지트리거는 레벨트리거와 달리 데이터의 수신과 데이터가 처리되는 시점을 분리할 수 있습니다. 구현모델의 특징상 엣지트리거가 레벨트리거보다 성능이 좋을 확률이 높습니다.

반응형